44
55namespace WPGraphQL \Logging \Logger \Database ;
66
7+ /**
8+ * Entity class for the custom database table for Monolog.
9+ *
10+ * This class represents a single log entry in the database and provides methods to create, save, and manage log entries.
11+ *
12+ * @package WPGraphQL\Logging
13+ *
14+ * @since 0.0.1
15+ */
716class DatabaseEntity {
817 /**
9- * Gets the name of the logging table .
18+ * The ID of the log entry. Null if the entry is not yet saved .
1019 *
11- * @return string The name of the logging table.
20+ * @var int|null
1221 */
13- public static function get_table_name (): string {
22+ protected ?int $ id = null ;
23+
24+ /**
25+ * The channel for the log entry.
26+ *
27+ * @var string
28+ */
29+ protected string $ channel = '' ;
30+
31+ /**
32+ * The logging level.
33+ *
34+ * @var int
35+ */
36+ protected int $ level = 0 ;
37+
38+ /**
39+ * The name of the logging level.
40+ *
41+ * @var string
42+ */
43+ protected string $ level_name = '' ;
44+
45+ /**
46+ * The log message.
47+ *
48+ * @var string
49+ */
50+ protected string $ message = '' ;
51+
52+ /**
53+ * Additional context for the log entry.
54+ *
55+ * @var array<mixed>
56+ */
57+ protected array $ context = [];
58+
59+ /**
60+ * Extra data for the log entry.
61+ *
62+ * @var array<mixed>
63+ */
64+ protected array $ extra = [];
65+
66+ /**
67+ * The datetime of the log entry.
68+ *
69+ * @var string
70+ */
71+ protected string $ datetime = '' ;
72+
73+ /**
74+ * The constructor is protected to encourage creation via static methods.
75+ */
76+ protected function __construct () {
77+ // Set a default datetime for new, unsaved entries.
78+ $ this ->datetime = current_time ( 'mysql ' , 1 );
79+ }
80+
81+ /**
82+ * Creates a new, unsaved log entry instance.
83+ *
84+ * @param string $channel The channel for the log entry.
85+ * @param int $level The logging level.
86+ * @param string $level_name The name of the logging level.
87+ * @param string $message The log message.
88+ * @param array<mixed> $context Additional context for the log entry.
89+ * @param array<mixed> $extra Extra data for the log entry.
90+ */
91+ public static function create (string $ channel , int $ level , string $ level_name , string $ message , array $ context = [], array $ extra = []): self {
92+ $ entity = new self ();
93+ $ entity ->channel = self ::sanitize_text_field ( $ channel );
94+ $ entity ->level = $ level ;
95+ $ entity ->level_name = self ::sanitize_text_field ( $ level_name );
96+ $ entity ->message = self ::sanitize_text_field ( $ message );
97+ $ entity ->context = self ::sanitize_array_field ( $ context );
98+ $ entity ->extra = self ::sanitize_array_field ( $ extra );
99+
100+ return $ entity ;
101+ }
102+
103+ /**
104+ * Finds a single log entry by its ID and returns it as an object.
105+ *
106+ * @param int $id The ID of the log entry to find.
107+ *
108+ * @return self|null Returns an instance of DatabaseEntity if found, or null if not found.
109+ */
110+ public static function find (int $ id ): ?self {
111+ global $ wpdb ;
112+ $ table_name = self ::get_table_name ();
113+
114+ $ query = $ wpdb ->prepare ( "SELECT * FROM {$ table_name } WHERE id = %d " , $ id ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
115+ $ row = $ wpdb ->get_row ( $ query , ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
116+
117+ if ( ! $ row ) {
118+ return null ;
119+ }
120+
121+ return self ::create_from_db_row ( $ row );
122+ }
123+
124+ /**
125+ * Saves a new logging entity to the database. This is an insert-only operation.
126+ *
127+ * @return int The ID of the newly created log entry, or 0 on failure.
128+ */
129+ public function save (): int {
14130 global $ wpdb ;
131+ $ table_name = self ::get_table_name ();
132+
133+ $ data = [
134+ 'channel ' => $ this ->channel ,
135+ 'level ' => $ this ->level ,
136+ 'level_name ' => $ this ->level_name ,
137+ 'message ' => $ this ->message ,
138+ 'context ' => wp_json_encode ( $ this ->context ),
139+ 'extra ' => wp_json_encode ( $ this ->extra ),
140+ 'datetime ' => $ this ->datetime ,
141+ ];
142+
143+ $ formats = [ '%s ' , '%d ' , '%s ' , '%s ' , '%s ' , '%s ' , '%s ' ];
144+
145+ $ result = $ wpdb ->insert ( $ table_name , $ data , $ formats ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
146+
147+ if ( $ result ) {
148+ $ this ->id = (int ) $ wpdb ->insert_id ;
149+ return $ this ->id ;
150+ }
15151
16- return (string ) apply_filters ( 'wpgraphql_logging_database_name ' , $ wpdb ->prefix . 'wpgraphql_logging ' );
152+ return 0 ;
153+ }
154+
155+ /**
156+ * Gets the name of the logging table.
157+ */
158+ public static function get_table_name (): string {
159+ global $ wpdb ;
160+ $ name = apply_filters ( 'wpgraphql_logging_database_name ' , $ wpdb ->prefix . 'wpgraphql_logging ' );
161+ return self ::sanitize_text_field ( $ name );
17162 }
18163
19164 /**
20165 * Gets the database schema for the logging table.
21- *
22- * @return string The SQL CREATE an TABLE statement.
23166 */
24167 public static function get_schema (): string {
25168 global $ wpdb ;
26169 $ table_name = self ::get_table_name ();
27170 $ charset_collate = $ wpdb ->get_charset_collate ();
28171
172+ // **IMPORTANT**: This schema format with PRIMARY KEY on its own line is the
173+ // correct and stable way to work with dbDelta.
29174 return "
30- CREATE TABLE {$ table_name } (
31- id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
32- channel VARCHAR(191) NOT NULL,
33- level SMALLINT UNSIGNED NOT NULL,
34- level_name VARCHAR(50) NOT NULL,
35- message LONGTEXT NOT NULL,
36- context JSON NULL,
37- extra JSON NULL,
38- datetime DATETIME NOT NULL,
39- INDEX channel_index (channel),
40- INDEX level_index (level),
41- INDEX datetime_index (datetime)
42- ) {$ charset_collate };
43- " ;
175+ CREATE TABLE {$ table_name } (
176+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
177+ channel VARCHAR(191) NOT NULL,
178+ level SMALLINT UNSIGNED NOT NULL,
179+ level_name VARCHAR(50) NOT NULL,
180+ message LONGTEXT NOT NULL,
181+ context JSON NULL,
182+ extra JSON NULL,
183+ datetime DATETIME NOT NULL,
184+ PRIMARY KEY (id),
185+ INDEX channel_index (channel),
186+ INDEX level_index (level),
187+ INDEX datetime_index (datetime)
188+ ) {$ charset_collate };
189+ " ;
44190 }
45191
46192 /**
47193 * Creates the logging table in the database.
48194 *
49- * @throws \RuntimeException If ABSPATH is not defined.
50195 */
51196 public static function create_table (): void {
52- if ( ! defined ( 'ABSPATH ' ) ) {
53- throw new \RuntimeException ( 'ABSPATH is not defined. ' );
54- }
55-
56- require_once ABSPATH . 'wp-admin/includes/upgrade.php ' ; // @phpstan-ignore-line
57- $ schema = self ::get_schema ();
58- dbDelta ( $ schema );
197+ require_once ABSPATH . 'wp-admin/includes/upgrade.php ' ;
198+ dbDelta ( self ::get_schema () );
59199 }
60200
61201 /**
@@ -64,8 +204,77 @@ public static function create_table(): void {
64204 public static function drop_table (): void {
65205 global $ wpdb ;
66206 $ table_name = self ::get_table_name ();
207+ $ wpdb ->query ( "DROP TABLE IF EXISTS {$ table_name }" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.NoCaching
208+ }
67209
68- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
69- $ wpdb ->query ( "DROP TABLE IF EXISTS {$ table_name }" );
210+ /**
211+ * Sanitizes a text field.
212+ *
213+ * @param string $value The value to sanitize.
214+ */
215+ protected static function sanitize_text_field (string $ value ): string {
216+ return sanitize_text_field ( $ value );
217+ }
218+
219+ /**
220+ * Sanitizes an array field recursively.
221+ *
222+ * @param array<mixed> $data The array to sanitize.
223+ *
224+ * @return array<mixed> The sanitized array.
225+ */
226+ protected static function sanitize_array_field (array $ data ): array {
227+ foreach ( $ data as &$ value ) {
228+ if ( is_string ( $ value ) ) {
229+ $ value = self ::sanitize_text_field ( $ value );
230+ continue ;
231+ }
232+
233+ if ( is_array ( $ value ) ) {
234+ $ value = self ::sanitize_array_field ( $ value );
235+ }
236+ }
237+ return $ data ;
238+ }
239+
240+ /**
241+ * Helper to populate an instance from a database row.
242+ *
243+ * @param array<string, mixed> $row The database row to populate from.
244+ *
245+ * @return self The populated instance.
246+ */
247+ private static function create_from_db_row (array $ row ): self {
248+ $ log = new self ();
249+ $ log ->id = (int ) $ row ['id ' ];
250+ $ log ->channel = $ row ['channel ' ];
251+ $ log ->level = (int ) $ row ['level ' ];
252+ $ log ->level_name = $ row ['level_name ' ];
253+ $ log ->message = $ row ['message ' ];
254+ $ log ->context = $ row ['context ' ] ? json_decode ( $ row ['context ' ], true ) : [];
255+ $ log ->extra = $ row ['extra ' ] ? json_decode ( $ row ['extra ' ], true ) : [];
256+ $ log ->datetime = $ row ['datetime ' ];
257+ return $ log ;
258+ }
259+
260+ /**
261+ * Magic method to handle dynamic getters like get_level().
262+ *
263+ * @param string $name The name of the method called.
264+ * @param array<mixed> $arguments The arguments passed to the method.
265+ *
266+ * @throws \BadMethodCallException If the method does not exist.
267+ *
268+ * @return mixed The value of the property if it exists, otherwise throws an exception.
269+ */
270+ public function __call (string $ name , array $ arguments ) {
271+ if ( strpos ( $ name , 'get_ ' ) === 0 ) {
272+ $ property = substr ( $ name , 4 );
273+ if ( property_exists ( $ this , $ property ) ) {
274+ return $ this ->$ property ;
275+ }
276+ }
277+ $ name = $ this ->sanitize_text_field ( $ name );
278+ throw new \BadMethodCallException ( sprintf ( 'Method %s does not exist. ' , esc_html ( $ name ) ) );
70279 }
71280}
0 commit comments