PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 0.0.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v0.0.1
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / database / base.php

base.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 0.0.1, at inc/database/base.php

516 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SureDonation Database Tables Base Class.
4 *
5 * @package SureDonation
6 */
7
8 namespace SureDonation\Inc\Database;
9
10 use SureDonation\Inc\Helper;
11
12 // Exit if accessed directly.
13 defined( 'ABSPATH' ) || exit;
14
15 /**
16 * SureDonation Database Tables Base Class
17 *
18 * Provides basic database operations using $wpdb methods.
19 * All queries in child classes use $wpdb->prepare() with explicit placeholders.
20 *
21 * @since 0.0.1
22 */
23 abstract class Base {
24 /**
25 * Option key for database table versions within consolidated options.
26 *
27 * @since 0.0.1
28 */
29 public const VERSION_OPTION_KEY = 'database_table_versions';
30
31 /**
32 * WordPress Database class instance.
33 *
34 * @var \wpdb
35 * @since 0.0.1
36 */
37 protected $wpdb;
38
39 /**
40 * Current database table prefix mixed with 'suredonation_' as ending.
41 *
42 * @var string
43 * @since 0.0.1
44 */
45 protected $table_prefix;
46
47 /**
48 * Custom table suffix without any prefix. This needs to be overridden from child class.
49 *
50 * @var string
51 * @since 0.0.1
52 */
53 protected $table_suffix;
54
55 /**
56 * Version for current custom table.
57 *
58 * @var int
59 * @since 0.0.1
60 */
61 protected $table_version = 1;
62
63 /**
64 * Full table name mixed with table prefix and table suffix.
65 *
66 * @var string
67 * @since 0.0.1
68 */
69 private $table_name;
70
71 /**
72 * Whether or not the current database table is upgradable.
73 *
74 * @var bool
75 * @since 0.0.1
76 */
77 private $db_upgradable;
78
79 /**
80 * Current table database result caches.
81 *
82 * @var array<mixed>
83 * @since 0.0.1
84 */
85 private $caches = [];
86
87 /**
88 * Init class.
89 *
90 * @return void
91 * @since 0.0.1
92 */
93 public function __construct() {
94 global $wpdb;
95
96 $this->wpdb = $wpdb;
97 $this->table_prefix = $this->wpdb->prefix . 'suredonation_';
98 $this->table_name = $this->table_prefix . $this->table_suffix;
99 }
100
101 /**
102 * Actions to initialize during object unload.
103 *
104 * @return void
105 * @since 0.0.1
106 */
107 public function __destruct() {
108 $this->stop_db_upgrade();
109 }
110
111 /**
112 * Returns the current table schema.
113 *
114 * @return array<string,array<mixed>>
115 * @since 0.0.1
116 */
117 abstract public function get_schema();
118
119 /**
120 * Current table columns definition to create table.
121 *
122 * @return array<string>
123 * @since 0.0.1
124 */
125 abstract public function get_columns_definition();
126
127 /**
128 * Start the database upgrade process.
129 *
130 * @return void
131 * @since 0.0.1
132 */
133 public function start_db_upgrade() {
134 $versions = Helper::get_suredonation_option( self::VERSION_OPTION_KEY, [] );
135 $versions = is_array( $versions ) ? $versions : [];
136 $prev_version = ! empty( $versions[ $this->table_suffix ] ) ? absint( $versions[ $this->table_suffix ] ) : false;
137
138 if ( ! $prev_version ) {
139 $this->db_upgradable = true;
140 return;
141 }
142
143 $this->db_upgradable = $this->table_version > $prev_version;
144 }
145
146 /**
147 * Stop the database upgrade process.
148 *
149 * @return bool Returns true on success.
150 * @since 0.0.1
151 */
152 public function stop_db_upgrade() {
153 if ( ! $this->db_upgradable ) {
154 return false;
155 }
156
157 $versions = Helper::get_suredonation_option( self::VERSION_OPTION_KEY, [] );
158 $versions = is_array( $versions ) ? $versions : [];
159
160 $versions[ $this->table_suffix ] = $this->table_version;
161
162 Helper::update_suredonation_option( self::VERSION_OPTION_KEY, $versions );
163
164 return true;
165 }
166
167 /**
168 * Check if current table's DB is upgradable or not.
169 *
170 * @return bool True or false depending if DB is upgradable or not.
171 * @since 0.0.1
172 */
173 public function is_db_upgradable() {
174 return $this->db_upgradable;
175 }
176
177 /**
178 * Returns full table name.
179 *
180 * @return string
181 * @since 0.0.1
182 */
183 public function get_tablename() {
184 return $this->table_name;
185 }
186
187 /**
188 * Conditionally returns current database charset or collate.
189 *
190 * @return string
191 * @since 0.0.1
192 */
193 public function get_charset_collate() {
194 $charset_collate = '';
195
196 if ( $this->wpdb->has_cap( 'collation' ) ) {
197 if ( ! empty( $this->wpdb->charset ) ) {
198 $charset_collate = "DEFAULT CHARACTER SET {$this->wpdb->charset}";
199 }
200 if ( ! empty( $this->wpdb->collate ) ) {
201 $charset_collate .= " COLLATE {$this->wpdb->collate}";
202 }
203 }
204
205 return $charset_collate;
206 }
207
208 /**
209 * Create table.
210 *
211 * @param array<string> $columns Array of columns.
212 * @return int|bool
213 * @since 0.0.1
214 */
215 public function create( $columns = [] ) {
216 if ( ! $this->db_upgradable ) {
217 return false;
218 }
219
220 if ( empty( $columns ) ) {
221 return false;
222 }
223
224 $columns_list = implode( ', ', $columns );
225 $wpdb = $this->wpdb;
226
227 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder -- Column definitions and charset are hardcoded DDL, not user input.
228 $query = $wpdb->prepare( 'CREATE TABLE IF NOT EXISTS %i ( %1s ) %2s', $this->get_tablename(), $columns_list, $this->get_charset_collate() );
229
230 if ( ! $query ) {
231 return false;
232 }
233
234 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared
235 $result = $wpdb->query( $query );
236
237 if ( false === $result ) {
238 $this->db_upgradable = false;
239 }
240
241 return $result;
242 }
243
244 /**
245 * Returns an array columns of current table.
246 *
247 * @return array<string,array<string,mixed>>
248 * @since 0.0.1
249 */
250 public function get_columns() {
251 $wpdb = $this->wpdb;
252
253 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
254 $columns = $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM %i', $this->get_tablename() ), ARRAY_A );
255
256 if ( empty( $columns ) ) {
257 return [];
258 }
259
260 $_columns = [];
261 if ( is_array( $columns ) ) {
262 foreach ( $columns as $column ) {
263 if ( ! is_string( $column['Field'] ) ) {
264 continue;
265 }
266
267 $_columns[ $column['Field'] ] = $column;
268 }
269 }
270 return $_columns;
271 }
272
273 /**
274 * Returns an array indexes of current table.
275 *
276 * @return array<mixed>
277 * @since 0.0.1
278 */
279 public function get_indexes() {
280 $wpdb = $this->wpdb;
281
282 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
283 $indexes = $wpdb->get_results( $wpdb->prepare( 'SHOW INDEX FROM %i', $this->get_tablename() ), ARRAY_A );
284
285 if ( empty( $indexes ) ) {
286 return [];
287 }
288
289 $_indexes = [];
290 if ( is_array( $indexes ) ) {
291 foreach ( $indexes as $index ) {
292 $_indexes[ $index['Key_name'] ] = $index; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
293 }
294 }
295 return $_indexes;
296 }
297
298 /**
299 * Insert data. Wrapper method for wpdb::insert.
300 *
301 * @param array<mixed> $data Data to insert.
302 * @param array<string>|string|null $format Optional format specifiers.
303 * @return int|false The id of the inserted entry, or false on error.
304 * @since 0.0.1
305 */
306 public function use_insert( $data, $format = null ) {
307 $prepared_data = $this->prepare_data( $data );
308
309 if ( is_null( $format ) ) {
310 $format = $prepared_data['format'];
311 }
312
313 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
314 $result = $this->wpdb->insert( $this->get_tablename(), $prepared_data['data'], $format ); // @phpstan-ignore argument.type
315 return $result ? $this->wpdb->insert_id : false;
316 }
317
318 /**
319 * Update a row data of current table. Wrapper method for wpdb::update.
320 *
321 * @param array<string,mixed> $data Data to update.
322 * @param array<string,mixed> $where WHERE clauses.
323 * @return int|false The number of rows updated, or false on error.
324 * @since 0.0.1
325 */
326 public function use_update( $data, $where ) {
327 $prepared_data = $this->prepare_data( $data, true );
328 $format = $prepared_data['format'];
329
330 $this->cache_reset();
331
332 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
333 return $this->wpdb->update(
334 $this->get_tablename(),
335 $prepared_data['data'],
336 $where,
337 $format // @phpstan-ignore argument.type
338 );
339 }
340
341 /**
342 * Delete a row data of current table. Wrapper method for wpdb::delete.
343 *
344 * @param array<string,mixed> $where WHERE clauses.
345 * @param array<string>|string $where_format Optional format specifiers.
346 * @return int|false The number of rows deleted, or false on error.
347 * @since 0.0.1
348 */
349 public function use_delete( $where, $where_format = null ) {
350 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
351 return $this->wpdb->delete( $this->get_tablename(), $where, $where_format );
352 }
353
354 /**
355 * Retrieve a cached value by its key.
356 *
357 * @param string $key The cache key.
358 * @return mixed|null The cached value or null.
359 * @since 0.0.1
360 */
361 protected function cache_get( $key ) {
362 $key = md5( $key );
363 if ( ! isset( $this->caches[ $key ] ) ) {
364 return null;
365 }
366 return $this->caches[ $key ];
367 }
368
369 /**
370 * Store a value in the cache.
371 *
372 * @param string $key The cache key.
373 * @param mixed $value The value to store.
374 * @return mixed The stored value.
375 * @since 0.0.1
376 */
377 protected function cache_set( $key, $value ) {
378 $key = md5( $key );
379 $this->caches[ $key ] = $value;
380 return $value;
381 }
382
383 /**
384 * Reset the cache.
385 *
386 * @return void
387 * @since 0.0.1
388 */
389 protected function cache_reset() {
390 $this->caches = [];
391 }
392
393 /**
394 * Prepare and format data based on the schema.
395 *
396 * @param array<mixed> $data Data to prepare.
397 * @param bool $skip_defaults Whether to skip defaults.
398 * @return array<array<mixed>> Prepared data with format specifiers.
399 * @since 0.0.1
400 */
401 protected function prepare_data( $data, $skip_defaults = false ) {
402 $_data = [];
403 $format = [];
404
405 foreach ( $this->get_schema() as $key => $value ) {
406 if ( ! isset( $data[ $key ] ) ) {
407 if ( $skip_defaults || ! isset( $value['default'] ) ) {
408 continue;
409 }
410 $data[ $key ] = $value['default'];
411 }
412
413 $value_type = isset( $value['type'] ) && is_string( $value['type'] ) ? $value['type'] : 'string';
414 $format[] = $this->get_format_by_datatype( $value_type );
415 $_data[ $key ] = $this->encode_by_datatype( $data[ $key ], $value_type );
416 }
417
418 return [
419 'data' => $_data,
420 'format' => $format,
421 ];
422 }
423
424 /**
425 * Get the SQL format specifier based on the provided data type.
426 *
427 * @param string $type The data type.
428 * @return string The SQL format specifier.
429 * @since 0.0.1
430 */
431 protected function get_format_by_datatype( $type ) {
432 $format = '%s';
433
434 switch ( $type ) {
435 case 'string':
436 case 'array':
437 case 'datetime':
438 $format = '%s';
439 break;
440
441 case 'number':
442 case 'boolean':
443 $format = '%d';
444 break;
445
446 case 'decimal':
447 $format = '%f';
448 break;
449 }
450
451 return $format;
452 }
453
454 /**
455 * Decode data based on the schema data types.
456 *
457 * @param array<mixed> $data Data to decode.
458 * @return array<mixed> Decoded data.
459 * @since 0.0.1
460 */
461 protected function decode_by_datatype( $data ) {
462 $_data = [];
463
464 foreach ( $this->get_schema() as $key => $schema ) {
465 if ( ! array_key_exists( $key, $data ) ) {
466 continue;
467 }
468
469 $value = $data[ $key ];
470 if ( isset( $schema['type'] ) && 'array' === $schema['type'] ) {
471 $json_string = is_scalar( $value ) ? (string) $value : '';
472 $_data[ $key ] = json_decode( $json_string, true );
473 if ( ! is_array( $_data[ $key ] ) ) {
474 $_data[ $key ] = [];
475 }
476 } else {
477 $_data[ $key ] = $value;
478 }
479 }
480
481 return $_data;
482 }
483
484 /**
485 * Encode a value based on the specified data type.
486 *
487 * @param mixed $value The value to encode.
488 * @param string $type The data type.
489 * @return mixed The encoded value.
490 * @since 0.0.1
491 */
492 protected function encode_by_datatype( $value, $type ) {
493 switch ( $type ) {
494 case 'string':
495 return is_scalar( $value ) ? (string) $value : '';
496
497 case 'number':
498 return is_numeric( $value ) ? (int) $value : 0;
499
500 case 'boolean':
501 return (bool) $value;
502
503 case 'array':
504 return wp_json_encode( is_array( $value ) ? $value : [] );
505
506 case 'datetime':
507 return $value;
508
509 case 'decimal':
510 return is_numeric( $value ) ? (float) $value : 0.0;
511 }
512
513 return $value;
514 }
515 }
516