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