PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.10.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.10.0
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
sureforms / inc / database / base.php
base.php
908 lines 28.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SureForms Database Tables Base Class.
4 *
5 * @link https://sureforms.com
6 * @since 0.0.10
7 * @package SureForms
8 * @author SureForms <https://sureforms.com/>
9 */
10
11 namespace SRFM\Inc\Database;
12
13 use SRFM\Inc\Helper;
14
15 // Exit if accessed directly.
16 defined( 'ABSPATH' ) || exit;
17
18 /**
19 * SureForms Database Tables Base Class
20 *
21 * @since 0.0.10
22 */
23 abstract class Base {
24 /**
25 * WordPress Database class instance.
26 *
27 * @var \wpdb
28 * @since 0.0.10
29 */
30 protected $wpdb;
31
32 /**
33 * Current database table prefix mixed with 'srfm_' as ending.
34 *
35 * @var string
36 * @since 0.0.10
37 */
38 protected $table_prefix;
39
40 /**
41 * Custom table suffix without any prefix. This needs to be overridden from child class.
42 * Eg: For entries table, suffix will be 'entries' which will be prefixed and finally named as 'wp_srfm_entries'.
43 *
44 * @var string
45 * @since 0.0.10
46 * @override
47 */
48 protected $table_suffix;
49
50 /**
51 * Version for current custom table. Default is 1.
52 * Unlike semantic versioning [eg: 1.0.0, 1.0.1] we use natural integer like 1, 2, 3... and so on.
53 * Update the table version from child class when any DB upgrade or alteration related changes are made.
54 *
55 * @var int
56 * @since 0.0.13
57 * @override
58 */
59 protected $table_version = 1;
60
61 /**
62 * Full table name mixed with table prefix and table suffix.
63 *
64 * @var string
65 * @since 0.0.10
66 */
67 private $table_name;
68
69 /**
70 * Whether or not the current database table is upgradable.
71 * Determines on the basis of the table version.
72 *
73 * @var bool
74 * @since 0.0.13
75 */
76 private $db_upgradable;
77
78 /**
79 * Current table database result caches.
80 *
81 * @var array<mixed>
82 * @since 0.0.10
83 */
84 private $caches = [];
85
86 /**
87 * Allowed operators for the database.
88 *
89 * @var array<string>
90 * @since 1.8.0
91 */
92 private $allowed_where_operators = [ 'LIKE', 'IN', '=', '!=', '>', '<', '>=', '<=' ];
93
94 /**
95 * Init class.
96 *
97 * @since 0.0.10
98 * @return void
99 */
100 public function __construct() {
101 global $wpdb;
102
103 $this->wpdb = $wpdb;
104 $this->table_prefix = $this->wpdb->prefix . 'srfm_';
105 $this->table_name = $this->table_prefix . $this->table_suffix;
106 }
107
108 /**
109 * Actions to initialize during object unload.
110 *
111 * @since 0.0.13
112 * @return void
113 */
114 public function __destruct() {
115 /**
116 * Just incase if any developer forgets to stop the db upgrade after starting.
117 * This fallback handling will take care of such scenarios.
118 */
119 $this->stop_db_upgrade();
120 }
121
122 /**
123 * Returns the current table schema.
124 *
125 * @since 0.0.10
126 * @return array<string,array<mixed>>
127 */
128 abstract public function get_schema();
129
130 /**
131 * Current table columns definition to create table. These definitions will be used by the create() method.
132 *
133 * @since 0.0.13
134 * @return array<string>
135 */
136 abstract public function get_columns_definition();
137
138 /**
139 * Any columns that needs to be added if the current table already exists. These definitions will be used by maybe_add_new_columns() method.
140 * Override this from child class if needed.
141 *
142 * @since 0.0.13
143 * @return array<string>
144 * @override
145 */
146 public function get_new_columns_definition() {
147 return [];
148 }
149
150 /**
151 * Array of columns that needs to be renamed to new column name. It will be used by maybe_rename_columns() method.
152 * Format:
153 * [
154 * [
155 * 'from' => 'old_column_name',
156 * 'to' => 'new_column_name',
157 * 'type' => 'column type definition eg: LONGTEXT', // Optional.
158 * ],
159 * ]
160 *
161 * @since 0.0.13
162 * @return array<array<string,string>>
163 */
164 public function get_columns_to_rename() {
165 return [];
166 }
167
168 /**
169 * Start the database upgrade process.
170 *
171 * @since 0.0.13
172 * @return void
173 */
174 public function start_db_upgrade() {
175 $versions = Helper::get_array_value( get_option( 'srfm_database_table_versions', [] ) );
176 $prev_version = ! empty( $versions[ $this->table_suffix ] ) ? absint( $versions[ $this->table_suffix ] ) : false;
177
178 if ( ! $prev_version ) {
179 /**
180 * If we are here then there is the chance that
181 * this site is the new site or fresh setup.
182 */
183 $this->db_upgradable = true;
184 return;
185 }
186
187 $this->db_upgradable = $this->table_version > $prev_version;
188 }
189
190 /**
191 * Stop the database upgrade process.
192 *
193 * @since 0.0.13
194 * @return bool Returns true on success.
195 */
196 public function stop_db_upgrade() {
197 if ( ! $this->db_upgradable ) {
198 // Only upgrade when it is needed.
199 return false;
200 }
201
202 $versions = Helper::get_array_value( get_option( 'srfm_database_table_versions', [] ) );
203
204 $versions[ $this->table_suffix ] = $this->table_version;
205
206 update_option( 'srfm_database_table_versions', $versions );
207
208 return true;
209 }
210
211 /**
212 * Check if current table's DB is upgradable or not.
213 *
214 * @since 0.0.13
215 * @return bool True or false depending if DB is upgradable or not.
216 */
217 public function is_db_upgradable() {
218 return $this->db_upgradable;
219 }
220
221 /**
222 * Returns full table name.
223 *
224 * @since 0.0.10
225 * @return string
226 */
227 public function get_tablename() {
228 return $this->table_name;
229 }
230
231 /**
232 * Conditionally returns current database charset or collate.
233 *
234 * @since 0.0.10
235 * @return string
236 */
237 public function get_charset_collate() {
238 $charset_collate = '';
239
240 if ( $this->wpdb->has_cap( 'collation' ) ) {
241 if ( ! empty( $this->wpdb->charset ) ) {
242 $charset_collate = "DEFAULT CHARACTER SET {$this->wpdb->charset}";
243 }
244 if ( ! empty( $this->wpdb->collate ) ) {
245 $charset_collate .= " COLLATE {$this->wpdb->collate}";
246 }
247 }
248
249 return $charset_collate;
250 }
251
252 /**
253 * Create table.
254 *
255 * @param array<string> $columns Array of columns.
256 * @since 0.0.10
257 * @return int|bool
258 */
259 public function create( $columns = [] ) {
260 if ( ! $this->db_upgradable ) {
261 // Only upgrade when it is needed.
262 return false;
263 }
264
265 if ( empty( $columns ) ) {
266 return false; // It's better to return a boolean for failure.
267 }
268
269 // Prepare columns list.
270 $columns_list = implode(
271 ', ',
272 $columns
273 );
274
275 $wpdb = $this->wpdb;
276
277 // Execute the query.
278 $query = $wpdb->prepare( 'CREATE TABLE IF NOT EXISTS %1s ( %2s ) %3s', $this->get_tablename(), $columns_list, $this->get_charset_collate() ); // phpcs:ignore -- It is okay to use complex placeholder here for the table name, column list and character set because we don't want to quote these variables.
279
280 if ( ! $query ) {
281 // If we are here, then we probably have bad query to work with and prepare method has returned null-ish value.
282 return false;
283 }
284
285 $result = $wpdb->query( $query ); // phpcs:ignore -- We are already using prepare above.
286
287 if ( false === $result ) {
288 // Stop DB alteration if we have any error.
289 $this->db_upgradable = false;
290 }
291
292 return $result;
293 }
294
295 /**
296 * Rename the column of the current table conditionally.
297 *
298 * @param array<array<string,string>> $rename_columns Array of columns to rename.
299 * @since 0.0.13
300 * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows affected/selected for all other queries. Boolean false on error.
301 */
302 public function maybe_rename_columns( $rename_columns = [] ) {
303 if ( ! $rename_columns ) {
304 return false;
305 }
306
307 if ( ! $this->db_upgradable ) {
308 // Only upgrade when it is needed.
309 return false;
310 }
311
312 $existing_columns = $this->get_columns();
313
314 if ( ! $existing_columns ) {
315 // Table does not exists or is new table.
316 return false;
317 }
318
319 $wpdb = $this->wpdb;
320
321 $query_parts = [];
322 foreach ( $rename_columns as $column ) {
323 if ( empty( $existing_columns[ $column['from'] ] ) ) {
324 // Bail if column is already renamed or does not exists.
325 continue;
326 }
327
328 $query_part = $wpdb->prepare(
329 'CHANGE %1s %2s %3s', // phpcs:ignore -- It is okay to use complex placeholders as we don't want values to be quoted.
330 $column['from'],
331 $column['to'],
332 ! empty( $column['type'] ) ? $column['type'] : $existing_columns[ $column['from'] ]['Type'] // This is column type i.e LONGTEXT, BIGINT etc.
333 );
334
335 if ( is_string( $query_part ) && $query_part ) {
336 $query_parts[] = trim( $query_part );
337 }
338 }
339
340 if ( empty( $query_parts ) ) {
341 // No renaming required.
342 return false;
343 }
344
345 $result = $wpdb->query( $wpdb->prepare( 'ALTER TABLE %1s ', $this->get_tablename() ) . implode( ', ', $query_parts ) . ';' ); // phpcs:ignore -- It is okay to use query directly here.
346
347 if ( false === $result ) {
348 // Stop DB alteration if we have any error.
349 $this->db_upgradable = false;
350 }
351
352 return $result;
353 }
354
355 /**
356 * Adds the new columns to the current table conditionally.
357 *
358 * @param array<string> $new_columns The array of new columns to add. Same as the create method.
359 * @since 0.0.13
360 * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows affected/selected for all other queries. Boolean false on error.
361 */
362 public function maybe_add_new_columns( $new_columns = [] ) {
363 if ( ! $new_columns ) {
364 return false;
365 }
366
367 if ( ! $this->db_upgradable ) {
368 // Only upgrade when it is needed.
369 return false;
370 }
371
372 $existing_columns = $this->get_columns();
373
374 if ( ! $existing_columns ) {
375 // Table does not exists or is new table.
376 return false;
377 }
378
379 $existing_indexes = $this->get_indexes();
380
381 $alter_queries = [];
382
383 $wpdb = $this->wpdb;
384
385 // Check and add each column if it does not exist.
386 foreach ( $new_columns as $column_definition ) {
387 preg_match( '/INDEX\s+(.*?)\s+\(/', $column_definition, $index_matches );
388
389 if ( ! empty( $index_matches[1] ) ) {
390 if ( isset( $existing_indexes[ $index_matches[1] ] ) ) {
391 // Move to next element if current index already exists.
392 continue;
393 }
394 // Stack and move to next if we are indexing.
395 $alter_queries[] = $wpdb->prepare( 'ADD %1s', $column_definition ); // phpcs:ignore -- We don't need quote here.
396 continue;
397 }
398
399 preg_match( '/(\w+)\s/', $column_definition, $column_matches );
400 $column_name = $column_matches[1] ?? '';
401
402 // If the column does not exist, add it.
403 if ( ! isset( $existing_columns[ $column_name ] ) ) {
404 $alter_queries[] = $wpdb->prepare( 'ADD COLUMN %1s', $column_definition ); // phpcs:ignore -- We don't need quote here.
405 }
406 }
407
408 if ( $alter_queries ) {
409 $query = $wpdb->prepare(
410 'ALTER TABLE %1s %2s', // phpcs:ignore -- We don't want to quote the value strings for the query.
411 $this->get_tablename(),
412 implode( ', ', $alter_queries )
413 );
414
415 if ( ! $query ) {
416 // If we are here then we probably have bad query and prepare method has returned null.
417 return false;
418 }
419
420 // Execute the query.
421 $result = $wpdb->query( $query ); // phpcs:ignore -- It is okay. We are already using prepare above and we need to do DB query directly here.
422
423 if ( false === $result ) {
424 // Stop DB alteration if we have any error.
425 $this->db_upgradable = false;
426 }
427
428 return $result;
429 }
430
431 return false;
432 }
433
434 /**
435 * Returns an array columns of current table.
436 *
437 * @since 0.0.13
438 * @return array<string,array<string,mixed>>
439 */
440 public function get_columns() {
441 $wpdb = $this->wpdb;
442
443 $columns = $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM %1s', $this->get_tablename() ), ARRAY_A ); // phpcs:ignore -- It is okay to use query db directly here.
444
445 if ( empty( $columns ) ) {
446 return [];
447 }
448
449 $_columns = [];
450 if ( is_array( $columns ) ) {
451 foreach ( $columns as $column ) {
452 if ( ! is_string( $column['Field'] ) ) {
453 continue;
454 }
455
456 $_columns[ $column['Field'] ] = $column;
457 }
458 }
459 return $_columns;
460 }
461
462 /**
463 * Returns an array indexes of current table.
464 *
465 * @since 0.0.13
466 * @return array<mixed>
467 */
468 public function get_indexes() {
469 $wpdb = $this->wpdb;
470
471 $indexes = $wpdb->get_results( $wpdb->prepare( 'SHOW INDEX FROM %1s', $this->get_tablename() ), ARRAY_A ); // phpcs:ignore -- We don't need quote here so this is fine.
472
473 if ( empty( $indexes ) ) {
474 return [];
475 }
476
477 $_indexes = [];
478 if ( is_array( $indexes ) ) {
479 foreach ( $indexes as $index ) {
480 $_indexes[ $index['Key_name'] ] = $index; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
481 }
482 }
483 return $_indexes;
484 }
485
486 /**
487 * Insert data. Basically, a wrapper method for wpdb::insert.
488 *
489 * @param array<mixed> $data Data to insert (in column => value pairs).
490 * Both `$data` columns and `$data` values should be "raw" (neither should be SQL escaped).
491 * Sending a null value will cause the column to be set to NULL - the corresponding
492 * format is ignored in this case.
493 * @param array<string>|string|null $format Optional. An array of formats to be mapped to each of the value in `$data`.
494 * If string, that format will be used for all of the values in `$data`.
495 * A format is one of '%d', '%f', '%s' (integer, float, string).
496 * If omitted, all values in `$data` will be treated as strings unless otherwise
497 * specified in wpdb::$field_types. Default null.
498 * @since 0.0.10
499 * @return int|false The id of the inserted entry, or false on error.
500 */
501 public function use_insert( $data, $format = null ) {
502 $prepared_data = $this->prepare_data( $data );
503
504 if ( is_null( $format ) ) {
505 /**
506 * Use formats from schema if not provided explicitly.
507 *
508 * @var array<string>|string|null $format Format specifier for the data.
509 */
510 $format = $prepared_data['format'];
511 }
512
513 $result = $this->wpdb->insert( $this->get_tablename(), $prepared_data['data'], $format );
514 return $result ? $this->wpdb->insert_id : false;
515 }
516
517 /**
518 * Update a row data of current table. Basically, a wrapper method for wpdb::update.
519 *
520 * @param array<string,mixed> $data Data to update (in column => value pairs).
521 * Both $data columns and $data values should be "raw" (neither should be SQL escaped).
522 * Sending a null value will cause the column to be set to NULL - the corresponding
523 * format is ignored in this case.
524 * @param array<string,mixed> $where A named array of WHERE clauses (in column => value pairs).
525 * Multiple clauses will be joined with ANDs.
526 * Both $where columns and $where values should be "raw".
527 * Sending a null value will create an IS NULL comparison - the corresponding
528 * format will be ignored in this case.
529 * @since 0.0.13
530 * @return int|false The number of rows updated, or false on error.
531 */
532 public function use_update( $data, $where ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- It is okay. This is our wrapper method.
533 $prepared_data = $this->prepare_data( $data, true );
534
535 /**
536 * Data format specifier.
537 *
538 * @var array<string>|string|null $format Format specifier for the data.
539 */
540 $format = $prepared_data['format'];
541
542 // Reset the cache on update.
543 $this->cache_reset();
544
545 return $this->wpdb->update(
546 $this->get_tablename(),
547 $prepared_data['data'],
548 $where,
549 $format
550 );
551 }
552
553 /**
554 * Delete a row data of current table. Basically, a wrapper method for wpdb::delete.
555 *
556 * @param array<string,mixed> $where A named array of WHERE clauses (in column => value pairs).
557 * Multiple clauses will be joined with ANDs.
558 * Both $where columns and $where values should be "raw".
559 * Sending a null value will create an IS NULL comparison - the corresponding
560 * format will be ignored in this case.
561 * @param array<string>|string $where_format Optional. An array of formats to be mapped to each of the values in $where.
562 * If string, that format will be used for all of the items in $where.
563 * A format is one of '%d', '%f', '%s' (integer, float, string).
564 * If omitted, all values in $data will be treated as strings unless otherwise
565 * specified in wpdb::$field_types. Default null.
566 * @since 0.0.13
567 * @return int|false The number of rows deleted, or false on error.
568 */
569 public function use_delete( $where, $where_format = null ) {
570 return $this->wpdb->delete( $this->get_tablename(), $where, $where_format );
571 }
572
573 /**
574 * Retrieve results from the database based on the given WHERE clauses and selected columns.
575 *
576 * This method builds a SQL SELECT query with optional WHERE clauses and retrieves the results
577 * from the database. The results are cached to improve performance on subsequent requests.
578 *
579 * @param array<mixed> $where_clauses Optional. An associative array of WHERE clauses for the SQL query.
580 * Each key represents a column name, and each value is the value
581 * to match. If the value is an array, it will be used in an IN clause.
582 * Example: ['column1' => 'value1', 'column2' => ['value2', 'value3']].
583 * Default is an empty array.
584 * @param string $columns Optional. A string specifying which columns to select. Defaults to '*' (all columns).
585 * @param array<string> $extra_queries Optional. Array of extra queries to append at the end of main query.
586 * @param bool $decode Optional. Whether to decode the results by datatype. Default is true.
587 * @since 0.0.10
588 * @return array<mixed> An associative array of results where each element represents a row, or an empty array if no results are found.
589 */
590 public function get_results( $where_clauses = [], $columns = '*', $extra_queries = [], $decode = true ) {
591 $wpdb = $this->wpdb;
592
593 $table_name = $this->get_tablename();
594
595 // Start building the query.
596 $query = "SELECT {$columns} FROM {$table_name}";
597
598 // If there are WHERE clauses, prepare and append them to the query.
599 $query .= $this->prepare_where_clauses( $where_clauses );
600
601 if ( ! empty( $extra_queries ) ) {
602 $query .= ' ' . implode( ' ', array_map( 'trim', $extra_queries ) );
603 }
604
605 // Add a semicolon at the end of the query.
606 $query = rtrim( trim( $query ), ';' ) . ';';
607
608 $cached_results = $this->cache_get( $query );
609 if ( $cached_results ) {
610 // Return the cached data if exists.
611 return Helper::get_array_value( $cached_results );
612 }
613
614 // phpcs:ignore
615 $results = $wpdb->get_results( $query, ARRAY_A );
616
617 if ( $decode && ! empty( $results ) && is_array( $results ) ) {
618 foreach ( $results as &$result ) {
619 $result = $this->decode_by_datatype( $result );
620 }
621 }
622
623 // Execute the query and return results.
624 return Helper::get_array_value( $this->cache_set( $query, $results ) );
625 }
626
627 /**
628 * Get the total number of rows in the table.
629 *
630 * @param array<mixed> $where_clauses Optional. An associative array of WHERE clauses for the SQL query.
631 * @since 0.0.13
632 * @return int The total number of rows in the table.
633 */
634 public function get_total_count( $where_clauses = [] ) {
635 $wpdb = $this->wpdb;
636
637 $table_name = $this->get_tablename();
638
639 // Start building the query.
640 $query = "SELECT COUNT(*) FROM {$table_name}";
641
642 // If there are WHERE clauses, prepare and append them to the query.
643 $query .= $this->prepare_where_clauses( $where_clauses );
644
645 // Add a semicolon at the end of the query.
646 $query = rtrim( trim( $query ), ';' ) . ';';
647
648 $cached_results = $this->cache_get( $query );
649 if ( $cached_results ) {
650 // Return the cached data if exists.
651 return Helper::get_integer_value( $cached_results );
652 }
653
654 // phpcs:ignore
655 $results = Helper::get_integer_value( $wpdb->get_var( $query ) );
656
657 // Execute the query and return the integer count.
658 return Helper::get_integer_value( $this->cache_set( $query, $results ) );
659 }
660
661 /**
662 * Retrieve a cached value by its key.
663 *
664 * @param string $key The cache key.
665 * @since 0.0.10
666 * @return mixed|null The cached value if it exists, or null if the key does not exist in the cache.
667 */
668 protected function cache_get( $key ) {
669 $key = md5( $key );
670 if ( ! isset( $this->caches[ $key ] ) ) {
671 return null;
672 }
673 return $this->caches[ $key ];
674 }
675
676 /**
677 * Store a value in the cache with the specified key.
678 *
679 * @param string $key The cache key.
680 * @param mixed $value The value to store in the cache.
681 * @since 0.0.10
682 * @return mixed The stored value.
683 */
684 protected function cache_set( $key, $value ) {
685 $key = md5( $key );
686 $this->caches[ $key ] = $value;
687 return $value;
688 }
689
690 /**
691 * Reset the cache by clearing all stored values.
692 *
693 * @since 0.0.10
694 * @return void
695 */
696 protected function cache_reset() {
697 $this->caches = [];
698 }
699
700 /**
701 * Prepares WHERE clauses for a SQL query based on the provided conditions.
702 *
703 * This method constructs a WHERE statement by iterating through the
704 * specified conditions, appending them with the appropriate SQL syntax.
705 * It supports both single key-value pairs and arrays of conditions.
706 *
707 * @param array<mixed> $where_clauses {
708 * An associative array of conditions to include in the WHERE clause.
709 *
710 * @type string|array $key The column name or an array of conditions.
711 * @type array $value {
712 * An associative array of comparison data.
713 *
714 * @type string $key The column name for comparison.
715 * @type string $compare The comparison operator (e.g., '=', 'LIKE').
716 * @type mixed $value The value to compare against.
717 * @type string $RELATION Optional. The logical relation ('AND' or 'OR').
718 * }
719 * }
720 *
721 * @since 1.1.1 -- Added support for "IN" compare.
722 * @since 0.0.13
723 * @return string The prepared SQL WHERE clause with placeholders, or an empty string if no clauses were provided.
724 */
725 protected function prepare_where_clauses( $where_clauses = [] ) {
726 if ( empty( $where_clauses ) ) {
727 return '';
728 }
729
730 $wpdb = $this->wpdb;
731
732 // If there are WHERE clauses, prepare and append them to the query.
733 if ( is_array( $where_clauses ) ) {
734 $where = '';
735 $values = [];
736 $schema = $this->get_schema();
737
738 foreach ( $where_clauses as $key => $value ) {
739
740 $relation = ! empty( $value['RELATION'] ) ? trim( $value['RELATION'] ) : 'AND';
741
742 if ( is_int( $key ) ) {
743 foreach ( $value as $_key => $_value ) {
744 if ( is_int( $_key ) ) {
745 // Check if the operator is allowed.
746 if ( ! in_array( $_value['compare'], $this->allowed_where_operators, true ) ) {
747 continue;
748 }
749
750 switch ( $_value['compare'] ) {
751 case 'LIKE':
752 $where .= ' ' . $_value['key'] . ' ' . $_value['compare'] . ' "%%' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ) . '%%" ' . $relation;
753 $values[] = $_value['value'];
754 break;
755
756 case 'IN':
757 // Based on the number of values and datatype, it will create WHERE clause for $wpdb::prepare method. Eg: for ID with three values column: ID IN (%d, %d, %d).
758 $datatype = $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) );
759 $where .= ' ' . $_value['key'] . ' ' . $_value['compare'] . ' (' . implode( ', ', array_fill( 0, count( $_value['value'] ), $datatype ) ) . ') ' . $relation;
760 $values = array_merge( $values, $_value['value'] );
761 break;
762
763 default:
764 $where .= ' ' . $_value['key'] . ' ' . $_value['compare'] . ' ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ) . ' ' . $relation;
765 $values[] = $_value['value'];
766 break;
767 }
768 }
769 }
770 continue;
771 }
772
773 if ( ! isset( $schema[ $key ] ) ) {
774 // Skip strictly if current key is not in our schema.
775 continue;
776 }
777
778 $where .= ' ' . $key . ' = ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $key ]['type'] ) ) . ' ' . $relation;
779 $values[] = $value;
780 }
781
782 if ( ! $where ) {
783 return '';
784 }
785
786 $where = ' WHERE ' . trim( trim( $where, $relation ) );
787
788 // Prepare the query with placeholders.
789 // @phpstan-ignore-next-line -- We are already assigning non-literal string above using "get_format_by_datatype" methods.
790 return $wpdb->prepare( $where, ...$values ); // phpcs:ignore -- We are returning prepared sql query here. We are already using necessary placeholders in $where variable.
791 }
792
793 return '';
794 }
795
796 /**
797 * Prepare and format data based on the schema.
798 *
799 * @param array<mixed> $data An associative array of data where the key is the column name and the value is the data to process.
800 * Missing values will be replaced with default values specified in the schema.
801 * @param bool $skip_defaults Whether or not to skip the defaults values. Pass true if updating the data.
802 * @since 0.0.10
803 * @return array<array<mixed>> An associative array containing:
804 * - 'data': Prepared data with values encoded according to their data types.
805 * - 'format': An array of format specifiers corresponding to the data values.
806 */
807 protected function prepare_data( $data, $skip_defaults = false ) {
808 $_data = [];
809 $format = [];
810 foreach ( $this->get_schema() as $key => $value ) {
811 // Process defaults.
812 if ( ! isset( $data[ $key ] ) ) {
813 if ( $skip_defaults || ! isset( $value['default'] ) ) {
814 continue;
815 }
816 $data[ $key ] = $value['default'];
817 }
818
819 $format[] = $this->get_format_by_datatype( $value['type'] ); // Format for the WP database methods.
820 $_data[ $key ] = $this->encode_by_datatype( $data[ $key ], $value['type'] );
821 }
822 return [
823 'data' => $_data,
824 'format' => $format,
825 ];
826 }
827
828 /**
829 * Get the SQL format specifier based on the provided data type.
830 *
831 * @param string $type The data type for which to get the SQL format specifier.
832 * Possible values: 'string', 'array', 'number', 'boolean'.
833 * @since 0.0.10
834 * @return string The SQL format specifier. One of '%s' for string or array (converted to JSON), '%d' for number or boolean.
835 */
836 protected function get_format_by_datatype( $type ) {
837 $format = '%s';
838 switch ( $type ) {
839 case 'string':
840 case 'array': // Because array will be converted to json string.
841 $format = '%s';
842 break;
843
844 case 'number':
845 case 'boolean':
846 $format = '%d';
847 break;
848 }
849
850 return $format;
851 }
852
853 /**
854 * Decode data based on the schema data types.
855 *
856 * @param array<mixed> $data An associative array of data where the key is the column name and the value is the data to decode.
857 * The data will be decoded if the column type in the schema is 'array' (JSON string).
858 * @since 0.0.10
859 * @return array<mixed> An associative array of decoded data based on the schema.
860 */
861 protected function decode_by_datatype( $data ) {
862 $_data = [];
863 foreach ( $this->get_schema() as $key => $schema ) {
864 if ( ! array_key_exists( $key, $data ) ) {
865 continue;
866 }
867
868 // Lets decode from JSON to Array for the results.
869 $_data[ $key ] = 'array' === $schema['type'] ? Helper::get_array_value( json_decode( Helper::get_string_value( $data[ $key ] ), true ) ) : $data[ $key ];
870 }
871 return $_data;
872 }
873
874 /**
875 * Encode a value based on the specified data type.
876 *
877 * @param mixed $value The value to encode. The encoding will depend on the data type specified.
878 * @param string $type The data type for encoding. Possible values: 'string', 'number', 'boolean', 'array'.
879 * @since 0.0.10
880 * @return mixed The encoded value. The type of the return value depends on the specified type:
881 * - 'string': Encoded as a string.
882 * - 'number': Encoded as an integer.
883 * - 'boolean': Encoded as a boolean.
884 * - 'array': Encoded as a JSON string.
885 * @since 1.8.0 - 'datetime': Returns the value as it is, assuming it is already in SQL DATETIME format.
886 */
887 protected function encode_by_datatype( $value, $type ) {
888 switch ( $type ) {
889 case 'string':
890 return Helper::get_string_value( $value );
891
892 case 'number':
893 return Helper::get_integer_value( $value );
894
895 case 'boolean':
896 return boolval( $value );
897
898 case 'array':
899 // Lets json_encode array values instead of serializing it.
900 return Helper::encode_json( Helper::get_array_value( $value ) );
901
902 case 'datetime':
903 // For datetime, we will return the value as it is because we are using sql DATETIME format.
904 return $value;
905 }
906 }
907 }
908