Skip to content

Commit 2b24ee4

Browse files
committed
Updated Database entity with CRUD operations. Added tests.
1 parent d9e9edf commit 2b24ee4

File tree

4 files changed

+404
-32
lines changed

4 files changed

+404
-32
lines changed

plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php

Lines changed: 239 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,198 @@
44

55
namespace 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+
*/
716
class 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
}

plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*/
1818
class PluginTest extends WPTestCase {
1919

20-
public function test_instance_from_function_in_hwp_previews() {
20+
public function test_instance_from_function_in_wpgraphql_logging_plugin_init() {
2121
$instance = wpgraphql_logging_plugin_init();
2222
$this->assertTrue( $instance instanceof Plugin );
2323
}

plugins/wpgraphql-logging/tests/wpunit/Hooks/PluginHooksTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function drop_table(): void {
2626
DatabaseEntity::drop_table();
2727
}
2828

29-
public function test_instance_from_function_in_hwp_previews() {
29+
public function test_instance_from_plugin_instance() {
3030
$instance = PluginHooks::init();
3131
$this->assertTrue( $instance instanceof PluginHooks );
3232
}

0 commit comments

Comments
 (0)