PluginProbe
Tutor LMS – eLearning and online course solution / 3.9.2
Tutor LMS – eLearning and online course solution v3.9.2
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 3.9.15 3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 All 191 releases
tutor / helpers / QueryHelper.php

QueryHelper.php in Tutor LMS – eLearning and online course solution 3.9.2, at helpers/QueryHelper.php

1,356 lines 36.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Query helper class contains static helper methods to perform basic
4 * operations
5 *
6 * @package Tutor\Helper
7 * @since 2.0.7
8 */
9
10 namespace Tutor\Helpers;
11
12 /**
13 * Do the common db operations through helper
14 * methods
15 */
16 class QueryHelper {
17
18 /**
19 * Insert data in the table
20 *
21 * @since 2.0.7
22 * @since 3.2.0 sanitize_mapping param added to override sanitize function to specific keys.
23 *
24 * @param string $table table name.
25 * @param array $data | data to insert in the table.
26 * @param array $sanitize_mapping sanitize mapping.
27 *
28 * @return int inserted id.
29 *
30 * @throws \Exception Database error if occur.
31 */
32 public static function insert( string $table, array $data, array $sanitize_mapping = array() ): int {
33 global $wpdb;
34
35 $table = self::prepare_table_name( $table );
36 $data = \TUTOR\Input::sanitize_array( $data, $sanitize_mapping );
37
38 $insert = $wpdb->insert(
39 $table,
40 $data
41 );
42
43 if ( $wpdb->last_error ) {
44 throw new \Exception( $wpdb->last_error );
45 }
46
47 return $insert ? $wpdb->insert_id : 0;
48 }
49
50 /**
51 * Update data
52 *
53 * @since 2.0.7
54 * @since 3.2.0 IN clause support added.
55 *
56 * @param string $table table name.
57 * @param array $data | data to update in the table.
58 * @param array $where | condition array.
59 *
60 * @return bool true on success false on failure
61 */
62 public static function update( string $table, array $data, array $where ): bool {
63 global $wpdb;
64
65 $table = self::prepare_table_name( $table );
66 $set_clause = self::prepare_set_clause( $data );
67 $where_clause = self::prepare_where_clause( $where );
68
69 // phpcs:ignore
70 $query = $wpdb->prepare( "UPDATE {$table} {$set_clause} WHERE {$where_clause} AND 1 = %d", 1 );
71
72 // phpcs:ignore
73 $wpdb->query( $query );
74
75 if ( $wpdb->last_error ) {
76 error_log( $wpdb->last_error );
77 return false;
78 }
79
80 return true;
81 }
82
83 /**
84 * Delete a row from table with where clause.
85 * Limitation: It can only delete one row by wpdb::delete
86 *
87 * @param string $table table name.
88 * @param array $where key value pairs.Where key is the name of
89 * column & value is the value to match.
90 * For ex: [ 'id' => 1 ].
91 *
92 * @since v2.0.7
93 */
94 public static function delete( string $table, array $where ): bool {
95 global $wpdb;
96
97 $table = self::prepare_table_name( $table );
98 $delete = $wpdb->delete(
99 $table,
100 $where
101 );
102 return $delete ? true : false;
103 }
104
105 /**
106 * Bulk record delete by where clause.
107 *
108 * @since 3.7.0
109 *
110 * @param string $table table name.
111 * @param array $where where clause.
112 *
113 * @return int|boolean
114 */
115 public static function bulk_delete( $table, array $where ): bool {
116 global $wpdb;
117
118 $table = self::prepare_table_name( $table );
119 $where_clause = self::prepare_where_clause( $where );
120
121 return $wpdb->query( "DELETE FROM {$table} WHERE {$where_clause}" ); //phpcs:ignore --$where clause sanitized.
122 }
123
124 /**
125 * Delete rows from table
126 *
127 * @since 3.0.0
128 *
129 * @param string $table table name.
130 * @param array $ids array of ids.
131 *
132 * @see prepare_in_clause
133 *
134 * @throws \Exception Throw database error if occurred.
135 *
136 * @return true on success
137 */
138 public static function bulk_delete_by_ids( string $table, array $ids ): bool {
139 global $wpdb;
140
141 $table = self::prepare_table_name( $table );
142 $ids = self::prepare_in_clause( $ids );
143 //phpcs:ignore --ids already sanitized.
144 $wpdb->query( "DELETE FROM {$table} WHERE id IN ( $ids )");
145
146 if ( $wpdb->last_error ) {
147 throw new \Exception( $wpdb->last_error );
148 }
149
150 return true;
151 }
152
153 /**
154 * Clean everything from table
155 *
156 * @since v2.0.7
157 *
158 * @param string $table table name.
159 *
160 * @return bool
161 */
162 public static function table_clean( string $table ): bool {
163 global $wpdb;
164
165 $table = self::prepare_table_name( $table );
166 $delete = $wpdb->query(
167 //phpcs:ignore
168 $wpdb->prepare( "DELETE FROM {$table} WHERE 1 = %d", 1 )
169 );
170 return $delete ? true : false;
171 }
172
173 /**
174 * Insert multiple rows without knowing key value
175 *
176 * @since v2.0.7
177 * @since 3.6.0 param $return_ids added.
178 *
179 * @param string $table table name.
180 * @param array $request two dimensional array
181 * for ex: [ [id => 1], [id => 2] ].
182 * @param bool $return_ids if true returns the last inserted data ids.
183 * @param bool $do_sanitize sanitize data or not.
184 *
185 * @return mixed wpdb response true or int on success, false on failure.
186 * @throws \Exception If error occur.
187 */
188 public static function insert_multiple_rows( $table, $request, $return_ids = false, $do_sanitize = true ) {
189 global $wpdb;
190
191 $table = self::prepare_table_name( $table );
192 $column_keys = '';
193 $column_values = '';
194 $sql = '';
195 $last_key = array_key_last( $request );
196 $first_key = array_key_first( $request );
197 foreach ( $request as $k => $value ) {
198 $keys = array_keys( $value );
199
200 // Prepare column keys & values.
201 foreach ( $keys as $v ) {
202 $column_keys .= sanitize_key( $v ) . ',';
203 $sanitize_value = $value[ $v ];
204 if ( $sanitize_value && $do_sanitize ) {
205 $sanitize_value = sanitize_text_field( $sanitize_value );
206 }
207 $column_values .= is_numeric( $sanitize_value ) ? $sanitize_value . ',' : "'$sanitize_value'" . ',';
208 }
209 // Trim trailing comma.
210 $column_keys = rtrim( $column_keys, ',' );
211 $column_values = rtrim( $column_values, ',' );
212 if ( $first_key === $k ) {
213 $sql .= "INSERT INTO {$table} ($column_keys) VALUES ($column_values)";
214 if ( count( $request ) > 1 ) {
215 $sql .= ',';
216 }
217 } elseif ( $last_key == $k ) {
218 $sql .= "($column_values)";
219 } else {
220 $sql .= "($column_values),";
221 }
222
223 // Reset keys & values to avoid duplication.
224 $column_keys = '';
225 $column_values = '';
226 }
227
228 $wpdb->query( $sql );//phpcs:ignore
229
230 // If error occurred then throw new exception.
231 if ( $wpdb->last_error ) {
232 throw new \Exception( $wpdb->last_error );
233 }
234
235 if ( $return_ids ) {
236 $query_ids = $wpdb->get_results(
237 //phpcs:ignore
238 "SELECT ID FROM {$table} WHERE ID >= LAST_INSERT_ID()",
239 'ARRAY_N'
240 );
241
242 return $query_ids;
243 }
244
245 return true;
246 }
247
248 /**
249 * Make tge where clause base on its column operator and values.
250 *
251 * If the operator is IN then make the clause like `WHERE column_name IN (value1, value2, ...)`
252 * Otherwise the clause would be `WHERE column_name = 'value'`
253 *
254 * @since 3.0.0
255 *
256 * @param array $where The where clause array. e.g. array( 'id', 'IN', array(1, 2, 3) ) or array( 'id', '=', 1 ).
257 *
258 * @return string
259 */
260 public static function make_clause( array $where ) {
261 list ( $field, $operator, $value ) = $where;
262
263 $upper_operator = strtoupper( $operator );
264 if ( in_array( $upper_operator, array( 'IN', 'NOT IN' ), true ) ) {
265 $value = '(' . self::prepare_in_clause( $value ) . ')';
266 }
267
268 return "{$field} {$upper_operator} {$value}";
269 }
270
271 /**
272 * Check operator is supported.
273 *
274 * @since 3.5.0
275 *
276 * @param string $operator operator like =, !=, > , < etc.
277 *
278 * @return boolean
279 */
280 public static function is_support_operator( $operator ) {
281 $operator = strtoupper( $operator );
282
283 return in_array(
284 $operator,
285 array(
286 '=',
287 '!=',
288 '<>',
289 '>',
290 '<',
291 '>=',
292 '<=',
293 'LIKE',
294 'NOT LIKE',
295 'IN',
296 'NOT IN',
297 'IS',
298 'IS NOT',
299 'BETWEEN',
300 'NOT BETWEEN',
301 'RAW',
302 ),
303 true
304 );
305 }
306
307 /**
308 * Prepare where clause string
309 *
310 * @since 2.0.9
311 * @since 3.0.0 Null value support added, if need to check with null: [name => 'null']
312 * @since 3.5.0 All common SQL comparison operators support added.
313 * $where = array(
314 * 'id' => ['BETWEEN', [10, 20]],
315 * 'status' => ['!=', 'draft'],
316 * 'email' => ['LIKE', '%@gmail.com'],
317 * 'type' => ['NOT IN', ['test', 'sample']],
318 * 'age' => ['>=', 18],
319 * 'active' => true,
320 * 'deleted_at' => 'null',
321 * 'role' => 'editor',
322 * )
323 * @since 3.6.0 Added raw query support. Make sure the query written is not sql injectable.
324 * $where = array(
325 * 'username = %s' => [ 'RAW' , array( 'test' ) ]
326 * )
327 * @param array $where assoc array with field and value.
328 *
329 * @return string
330 */
331 public static function prepare_where_clause( array $where ) {
332 $arr = array();
333 foreach ( $where as $field => $value ) {
334 $operator = null;
335 if ( is_array( $value ) && isset( $value[0] ) && is_string( $value[0] ) && self::is_support_operator( $value[0] ) ) {
336 $operator = strtoupper( $value[0] );
337 $val = $value[1];
338 switch ( $operator ) {
339 case 'IN':
340 case 'NOT IN':
341 if ( is_array( $val ) ) {
342 $clause = array( $field, $operator, $val );
343 }
344 break;
345
346 case 'BETWEEN':
347 case 'NOT BETWEEN':
348 if ( is_array( $val ) && count( $val ) === 2 ) {
349 $val1 = is_numeric( $val[0] ) ? $val[0] : "'" . $val[0] . "'";
350 $val2 = is_numeric( $val[1] ) ? $val[1] : "'" . $val[1] . "'";
351 $clause = array( $field, $operator, "{$val1} AND {$val2}" );
352 }
353 break;
354
355 case 'IS':
356 case 'IS NOT':
357 $val = strtoupper( $val ) === 'NULL' ? 'NULL' : "'" . $val . "'";
358 $clause = array( $field, $operator, $val );
359 break;
360 case 'RAW':
361 $final_query = '';
362 if ( ! empty( $field ) && is_array( $val ) ) {
363 $final_query = self::prepare_raw_query( $field, $val );
364 }
365 $clause = $final_query;
366 break;
367 default: // =, !=, <, >, <=, >=, LIKE, NOT LIKE, <>
368 $val = is_numeric( $val ) ? $val : "'" . $val . "'";
369 $clause = array( $field, $operator, $val );
370 break;
371 }
372 } elseif ( is_array( $value ) ) {
373 $clause = array( $field, 'IN', $value );
374 } elseif ( 'null' === strtolower( $value ) ) {
375 $clause = array( $field, 'IS', 'NULL' );
376 } else {
377 $value = is_numeric( $value ) ? $value : "'" . $value . "'";
378 $clause = array( $field, '=', $value );
379 }
380
381 $arr[] = ( 'RAW' === $operator ) ? $clause : self::make_clause( $clause );
382 }
383
384 return implode( ' AND ', $arr );
385 }
386
387 /**
388 * Prepare raw query for query helper.
389 *
390 * @since 3.6.0
391 *
392 * @param string $raw_query the query to execute.
393 * @param array $parameters the parameters to pass to the query.
394 *
395 * @return string
396 */
397 public static function prepare_raw_query( $raw_query, $parameters ) {
398 /**
399 * Not allowed unsafe SQL control characters [;, --, /*]
400 * Allowed safe SQL control characters only.
401 */
402 $is_safe = preg_match( '/^[a-zA-Z0-9_%\.=\s\'"<>\(\)\-\[\],]+$/', $raw_query );
403 if ( ! $is_safe ) {
404 return '';
405 }
406
407 if ( ! count( $parameters ) ) {
408 return $raw_query;
409 }
410
411 global $wpdb;
412
413 $final_query = $wpdb->prepare( $raw_query, $parameters ); //phpcs:ignore
414
415 return $final_query;
416 }
417
418 /**
419 * Prepare like clause string with or
420 *
421 * @since 1.0.0
422 *
423 * @param array $where assoc array with field and value.
424 * @param string $relation default is OR.
425 *
426 * @return string
427 */
428 public static function prepare_like_clause( array $where, $relation = 'OR' ) {
429 global $wpdb;
430
431 $like_conditions = array();
432
433 foreach ( $where as $column_name => $term ) {
434 //phpcs:ignore
435 $like_conditions[] = $wpdb->prepare( "$column_name LIKE %s", '%' . $wpdb->esc_like( $term ) . '%' );
436 }
437
438 $where_clause = implode( ' OR ', $like_conditions );
439
440 return $where_clause;
441 }
442
443 /**
444 * Sanitize assoc array
445 *
446 * @param array $array an assoc array.
447 * @return array
448 *
449 * @since 2.0.9
450 */
451 private static function sanitize_assoc_array( array $array ) {
452 return array_map(
453 function ( $value ) {
454 return sanitize_text_field( $value );
455 },
456 $array
457 );
458 }
459
460 /**
461 * Delete comment with associate meta data
462 *
463 * @param array $where associative array with field and value.
464 * Example: array( 'comment_type' => 'comment', 'comment_id' => 1 ).
465 * @return bool
466 *
467 * @since 2.0.9
468 */
469 public static function delete_comment_with_meta( array $where ) {
470 if ( count( $where ) === 0 || ! tutor_utils()->is_assoc( $where ) ) {
471 return false;
472 }
473
474 $where = self::prepare_where_clause( self::sanitize_assoc_array( $where ) );
475
476 global $wpdb;
477 $ids = $wpdb->get_col( "SELECT comment_id FROM {$wpdb->comments} WHERE {$where}" );//phpcs:ignore
478
479 if ( is_array( $ids ) && count( $ids ) ) {
480 $ids_str = "'" . implode( "','", $ids ) . "'";
481 // delete comment metas.
482 $wpdb->query( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN({$ids_str}) " );//phpcs:ignore
483 // delete comment.
484 $wpdb->query( "DELETE FROM {$wpdb->comments} WHERE {$where}" );//phpcs:ignore
485
486 return true;
487 }
488
489 return false;
490 }
491
492 /**
493 * Delete post with associate meta data
494 *
495 * @param array $where associative array with field and value.
496 * Example: array( 'post_type' => 'post', 'id' => 1 ).
497 * @return bool
498 *
499 * @since 2.0.9
500 */
501 public static function delete_post_with_meta( array $where ) {
502 if ( count( $where ) === 0 || ! tutor_utils()->is_assoc( $where ) ) {
503 return false;
504 }
505
506 $where = self::prepare_where_clause( self::sanitize_assoc_array( $where ) );
507
508 global $wpdb;
509 $ids = $wpdb->get_col( "SELECT id FROM {$wpdb->posts} WHERE {$where}" );//phpcs:ignore
510
511 if ( is_array( $ids ) && count( $ids ) ) {
512 $ids_str = "'" . implode( "','", $ids ) . "'";
513 // delete post metas.
514 $wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE post_id IN({$ids_str}) " );//phpcs:ignore
515 // delete post.
516 $wpdb->query( "DELETE FROM {$wpdb->posts} WHERE {$where}" );//phpcs:ignore
517
518 return true;
519 }
520
521 return false;
522 }
523
524 /**
525 * Prepare SELECT clause.
526 *
527 * @since 3.8.0
528 *
529 * @param mixed $columns Column name or list of columns.
530 *
531 * @return string
532 */
533 protected static function prepare_select_clause( $columns = '' ) {
534 if ( empty( $columns ) ) {
535 return '*';
536 }
537
538 if ( is_array( $columns ) ) {
539 return implode( ',', $columns );
540 }
541
542 return $columns;
543 }
544
545 /**
546 * Prepare JOIN clause.
547 *
548 * @since 3.8.0
549 *
550 * @param array $joins Array of joins, each item:
551 * - type: join type (LEFT, INNER, RIGHT etc).
552 * - table: table name.
553 * - on: join condition.
554 *
555 * @return string
556 */
557 protected static function prepare_join_clause( $joins = array() ) {
558 if ( empty( $joins ) || ! is_array( $joins ) ) {
559 return '';
560 }
561
562 $clause = '';
563 foreach ( $joins as $join ) {
564 $type = strtoupper( $join['type'] ?? 'LEFT' );
565 $table = self::prepare_table_name( $join['table'] );
566 $on = $join['on'];
567 if ( $table && $on ) {
568 $clause .= " {$type} JOIN {$table} ON {$on} ";
569 }
570 }
571
572 return $clause;
573 }
574
575 /**
576 * Prepare WHERE + SEARCH clause together.
577 *
578 * @since 3.8.0
579 *
580 * @param array $where Array of key => value pairs.
581 * @param array $search Array of key => search string pairs.
582 * @param string $search_operator Operator for search conditions (AND/OR).
583 *
584 * @return string
585 */
586 protected static function prepare_where_search_clause( $where = array(), $search = array(), $search_operator = 'OR' ) {
587 $clauses = array();
588
589 // Handle WHERE conditions.
590 if ( ! empty( $where ) && is_array( $where ) ) {
591 $clauses[] = self::prepare_where_clause( $where );
592 }
593
594 // Handle SEARCH conditions.
595 if ( ! empty( $search ) && is_array( $search ) ) {
596 $clauses[] = self::prepare_like_clause( $search, $search_operator );
597 }
598
599 if ( empty( $clauses ) ) {
600 return '';
601 }
602
603 return 'WHERE ' . implode( ' AND ', $clauses );
604 }
605
606 /**
607 * Prepare order by clause.
608 *
609 * @since 3.8.0
610 *
611 * @param string $orderby order by column.
612 * @param string $order order ASC|DESC.
613 *
614 * @return string
615 */
616 protected static function prepare_order_clause( $orderby = '', $order = 'DESC' ) {
617 if ( empty( $orderby ) ) {
618 return '';
619 }
620
621 // Allowed: foo, foo_bar, _foo, foo.bar etc.
622 if ( ! preg_match( '/^[A-Za-z_][A-Za-z0-9._]*$/', $orderby ) ) {
623 return '';
624 }
625
626 $order = strtoupper( $order ) === 'ASC' ? 'ASC' : 'DESC';
627 return "ORDER BY {$orderby} {$order}";
628 }
629
630 /**
631 * Prepare LIMIT clause.
632 *
633 * @since 3.8.0
634 *
635 * @param int $limit limit.
636 * @param int $offset offset.
637 *
638 * @return string
639 */
640 protected static function prepare_limit_clause( $limit = 0, $offset = 0 ) {
641 if ( $limit < 1 || $offset < 0 ) {
642 return '';
643 }
644
645 return sprintf( 'LIMIT %d OFFSET %d', $limit, $offset );
646 }
647
648 /**
649 * Run a database query with flexible arguments.
650 *
651 * Supports SELECT, JOIN, WHERE, SEARCH, GROUP BY, HAVING, ORDER BY,
652 * LIMIT (pagination), and can return count, single row or full result set.
653 *
654 * @since 3.8.0
655 *
656 * @param string $table table name.
657 * @param array $args {
658 * Query arguments.
659 *
660 * @type string|array $select Columns to select, defaults to "*".
661 * @type string $alias Table alias.
662 * @type array $where WHERE conditions [ 'col' => 'val', ... ].
663 * @type array $search LIKE conditions [ 'col' => 'keyword', ... ].
664 * @type array $joins JOIN clauses [ [ 'type' => 'LEFT', 'table' => '...', 'on' => '...' ], ... ].
665 * @type string $groupby GROUP BY clause.
666 * @type string $having HAVING clause.
667 * @type string $orderby Column to order by.
668 * @type string $order ASC|DESC, default DESC.
669 * @type int $limit Limit.
670 * @type int $offset Offset.
671 * @type int $per_page Results per page for pagination.
672 * @type int $page Current page number for pagination.
673 * @type bool $count If true, return only total count.
674 * @type bool $single If true, return only single row.
675 * @type string $output OBJECT|ARRAY_A default is OBJECT.
676 * }
677 *
678 * @return mixed Result set, count or single row.
679 */
680 public static function query( $table, $args = array() ) {
681 // Flags.
682 $count = isset( $args['count'] ) && $args['count'];
683 $single = isset( $args['single'] ) && $args['single'];
684 $pagination = isset( $args['per_page'], $args['page'] );
685 $output = $args['output'] ?? 'OBJECT';
686
687 // Primary table.
688 $table = self::prepare_table_name( $table );
689 $alias = $args['alias'] ?? 'main';
690 $table_with_alias = "{$table} AS {$alias}";
691
692 // Build clauses.
693 $select_clause = self::prepare_select_clause( $args['select'] ?? '' );
694 $join_clause = self::prepare_join_clause( $args['joins'] ?? array() );
695 $where_clause = self::prepare_where_search_clause( $args['where'] ?? array(), $args['search'] ?? array() );
696 $groupby_clause = empty( $args['groupby'] ) ? '' : 'GROUP BY ' . $args['groupby'];
697 $having_clause = empty( $args['having'] ) ? '' : 'HAVING ' . $args['having'];
698 $order_by_clause = self::prepare_order_clause( $args['orderby'] ?? '', $args['order'] ?? 'DESC' );
699
700 global $wpdb;
701
702 // Count only.
703 if ( $count ) {
704 $sql_query = "SELECT COUNT(*)
705 FROM {$table_with_alias}
706 {$join_clause}
707 {$where_clause}
708 {$groupby_clause}
709 {$having_clause}";
710
711 return (int) $wpdb->get_var( $sql_query ); //phpcs:ignore
712 }
713
714 // Single record.
715 if ( $single ) {
716 $sql_query = "SELECT {$select_clause}
717 FROM {$table_with_alias}
718 {$join_clause}
719 {$where_clause}
720 {$groupby_clause}
721 {$having_clause}
722 {$order_by_clause}
723 LIMIT 1";
724
725 return $wpdb->get_row( $sql_query, $output ); //phpcs:ignore
726 }
727
728 $calc_found_rows = $pagination ? 'SQL_CALC_FOUND_ROWS' : '';
729 $limit = isset( $args['limit'] ) ? (int) $args['limit'] : 0;
730 $offset = isset( $args['offset'] ) ? (int) $args['offset'] : 0;
731
732 if ( $pagination ) {
733 $limit = (int) $args['per_page'];
734 $offset = (int) ( $args['page'] - 1 ) * $limit;
735 }
736
737 $limit_clause = self::prepare_limit_clause( $limit, $offset );
738
739 $sql_query = "SELECT {$calc_found_rows} {$select_clause}
740 FROM {$table_with_alias}
741 {$join_clause}
742 {$where_clause}
743 {$groupby_clause}
744 {$having_clause}
745 {$order_by_clause}
746 {$limit_clause}";
747
748 $rows = $wpdb->get_results( $sql_query, $output ); //phpcs:ignore
749
750 if ( $pagination ) {
751 $has_records = is_array( $rows ) && count( $rows );
752 $page = (int) $args['page'];
753 $per_page = (int) $args['per_page'];
754 $total_record = (int) $has_records ? $wpdb->get_var( 'SELECT FOUND_ROWS()' ) : 0;
755 $total_page = (int) ceil( $total_record / $per_page );
756
757 return array(
758 'total_record' => (int) $total_record,
759 'per_page' => $per_page,
760 'current_page' => $page,
761 'total_page' => $total_page,
762 'data' => $rows,
763 );
764
765 }
766
767 return $rows;
768 }
769
770 /**
771 * Get a single row from any table with where clause
772 *
773 * @param string $table table name with prefix.
774 *
775 * @param array $where assoc_array. For ex: [col_name => value ].
776 * @param string $order_by order by column name.
777 * @param string $order DESC or ASC, default is DESC.
778 * @param string $output expected output type, default is object.
779 *
780 * @return mixed based on output param, default object
781 */
782 public static function get_row( string $table, array $where, string $order_by, string $order = 'DESC', string $output = 'OBJECT' ) {
783 global $wpdb;
784
785 $table = self::prepare_table_name( $table );
786 $where_clause = self::prepare_where_clause( $where );
787
788 //phpcs:disable
789 $query = $wpdb->prepare(
790 "SELECT *
791 FROM {$table}
792 WHERE {$where_clause}
793 ORDER BY {$order_by} {$order}
794 LIMIT %d
795 ",
796 1
797 );
798
799 return $wpdb->get_row(
800 $query,
801 $output
802 );
803 //phpcs:enable
804 }
805
806 /**
807 * Get all row from any table with where clause
808 *
809 * @since 2.2.1
810 * @since 3.0.0 added support for -1 value in the limit parameter.
811 *
812 * @param string $table table name with prefix.
813 *
814 * @param array $where assoc_array. For ex: [col_name => value ].
815 * @param string $order_by order by column name.
816 * @param int $limit default is 1000, -1 for no limit.
817 * @param string $order DESC or ASC, default is DESC.
818 * @param string $output expected output type, default is object.
819 *
820 * @return mixed based on output param, default object
821 */
822 public static function get_all( string $table, array $where, string $order_by, $limit = 1000, string $order = 'DESC', string $output = 'OBJECT' ) {
823 global $wpdb;
824
825 $table = self::prepare_table_name( $table );
826 $where_clause = self::prepare_where_clause( $where );
827 $limit = (int) sanitize_text_field( $limit );
828 $limit_clause = ( -1 === $limit ) ? '' : 'LIMIT ' . $limit;
829
830 //phpcs:disable
831 $query = "SELECT *
832 FROM {$table}
833 WHERE {$where_clause}
834 ORDER BY {$order_by} {$order}
835 {$limit_clause}";
836
837 return $wpdb->get_results(
838 $query,
839 $output
840 );
841 //phpcs:enable
842 }
843
844 /**
845 * Update multiple rows by using where in
846 * clause
847 *
848 * @since v2.1.0
849 *
850 * @param string $table table name.
851 * @param array $data assoc_array data to update
852 * ex: [id => 2, name => 'john' ].
853 * @param string $where_in comma separated values, ex: 1,2,3.
854 * @param string $where_col default is ID but could be other.
855 *
856 * @return bool true on success, false on failure
857 */
858 public static function update_where_in( string $table, array $data, string $where_in, string $where_col = 'ID' ) {
859 global $wpdb;
860
861 $table = self::prepare_table_name( $table );
862 if ( empty( $where_in ) || empty( $where_col ) ) {
863 return false;
864 }
865 $set_clause = self::prepare_set_clause( $data );
866 if ( '' === $set_clause ) {
867 return false;
868 }
869 // @codingStandardsIgnoreStart
870 $query = $wpdb->prepare(
871 "UPDATE {$table}
872 {$set_clause}
873 WHERE $where_col IN ( $where_in )
874 AND 1 = %d
875 ",
876 1
877 );
878 return $wpdb->query( $query ) ? true : false;
879 }
880
881 /**
882 * Prepare MySQL SET clause for update query
883 *
884 * @since v2.1.0
885 *
886 * @param array $data single dimension assoc_array.
887 *
888 * @return string
889 */
890 public static function prepare_set_clause( array $data ) {
891 $set = '';
892 foreach ( $data as $key => $value ) {
893 if ( $key === array_key_first ( $data ) ) {
894 $set .= "SET ";
895 }
896 // Multi dimension not allowed.
897 if ( is_array( $value ) ) {
898 continue;
899 }
900
901 if ( is_null( $value ) ) {
902 $set .= "$key = null";
903 } else {
904 $value = esc_sql( sanitize_text_field( $value ) );
905 $set .= is_numeric( $value ) ? "$key = $value" : "$key = '" . $value ."'";
906 }
907
908 $set .= ",";
909 }
910 return rtrim( $set, ',' );
911 }
912
913 /**
914 * Make sanitized SQL IN clause value from an array
915 *
916 * @param array $arr a sequential array.
917 * @return string
918 * @since 2.1.1
919 */
920 public static function prepare_in_clause( array $arr ) {
921 $escaped = array_map(
922 function( $value ) {
923 global $wpdb;
924 $escaped_value = null;
925 if ( is_int( $value ) ) {
926 $escaped_value = $wpdb->prepare( '%d', $value );
927 } else if( is_float( $value ) ) {
928 list( $whole, $decimal ) = explode( '.', $value );
929 $expression = '%.'. strlen( $decimal ) . 'f';
930 $escaped_value = $wpdb->prepare( $expression, $value );
931 } else {
932 $escaped_value = $wpdb->prepare( '%s', $value );
933 }
934 return $escaped_value;
935 },
936 $arr
937 );
938
939 return implode( ',', $escaped );
940 }
941
942 /**
943 * Check table exist in database.
944 *
945 * @since 2.5.0
946 *
947 * @param string $table table name.
948 *
949 * @return bool
950 */
951 public static function table_exists( $table ) {
952 global $wpdb;
953
954 $table = self::prepare_table_name( $table );
955 $sql = "SHOW TABLES LIKE '{$table}'";
956 return $wpdb->get_var( $sql ) === $table;
957 }
958
959 /**
960 * Check column exist in a table
961 *
962 * @since 3.0.0
963 *
964 * @param string $table table name.
965 * @param string $column column name.
966 *
967 * @return bool
968 */
969 public static function column_exist( $table, $column ) {
970 global $wpdb;
971
972 $table = self::prepare_table_name( $table );
973 $sql = "SHOW COLUMNS FROM {$table} LIKE '{$column}'";
974 return $wpdb->get_var( $sql ) === $column;
975 }
976
977 /**
978 * Get data by joining multiple tables with specified join relations.
979 *
980 * Argument should be SQL escaped.
981 *
982 * @since 3.0.0
983 * @since 3.8.2 param $get_row added.
984 *
985 * @param string $primary_table The primary table name with prefix.
986 * @param array $joining_tables An array of join relations. Each relation should be an array with keys 'type', 'table', 'on'.
987 * @param array $select_columns An array of columns to select.
988 * @param array $where An associative array for the WHERE clause. For example: [col_name => value]. Without sql esc.
989 * @param array $search An associative array for the search clause. For example: [col_name => value]. Without sql esc.
990 * @param string $order_by Order by column name.
991 * @param int $limit Maximum number of rows to return.
992 * @param int $offset Offset for pagination.
993 * @param string $order DESC or ASC, default is DESC.
994 * @param string $output Expected output type, default is OBJECT.
995 * @param bool $get_row Get a single row.
996 *
997 * @throws \Exception If an error occurred during the query execution.
998 *
999 * @return mixed Based on output param, default OBJECT.
1000 */
1001 public static function get_joined_data(
1002 string $primary_table,
1003 array $joining_tables,
1004 array $select_columns,
1005 array $where = [],
1006 array $search = [],
1007 string $order_by = '',
1008 $limit = 10,
1009 $offset = 0,
1010 string $order = 'DESC',
1011 string $output = 'OBJECT',
1012 bool $get_row = false
1013 ) {
1014 global $wpdb;
1015
1016 $select_clause = implode( ', ', $select_columns );
1017 $from_clause = self::prepare_table_name( $primary_table );
1018 $join_clauses = self::prepare_join_clause( $joining_tables );
1019 $where_clause = self::prepare_where_search_clause( $where, $search );
1020 $order_by_clause = self::prepare_order_clause( $order_by, $order );
1021 $limit_clause = self::prepare_limit_clause( $limit, $offset );
1022
1023 $query = "SELECT SQL_CALC_FOUND_ROWS
1024 {$select_clause}
1025 FROM {$from_clause}
1026 {$join_clauses}
1027 {$where_clause}
1028 {$order_by_clause}
1029 {$limit_clause}";
1030
1031 if ( $get_row ) {
1032 return $wpdb->get_row( $query, $output );
1033 }
1034
1035 $results = $wpdb->get_results( $query, $output );
1036 $has_records = is_array( $results ) && count( $results );
1037 $total_count = $has_records ? (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' ) : 0;
1038
1039 // Throw exception if error occurred.
1040 if ( $wpdb->last_error ) {
1041 throw new \Exception( $wpdb->last_error );
1042 }
1043
1044 // Prepare response array.
1045 $response = array(
1046 'total_count' => $total_count,
1047 'results' => $results,
1048 );
1049
1050 return $response;
1051 }
1052
1053 /**
1054 * Get count var
1055 *
1056 * Argument should be SQL escaped.
1057 *
1058 * @since 1.0.0
1059 *
1060 * @param string $table table name with prefix.
1061 * @param array $where array of where condition.
1062 * @param array $search array of search conditions for LIKE operator.
1063 * @param string $count_column column name to count, default id.
1064 *
1065 * @return int
1066 */
1067 public static function get_count( $table, $where = [], $search = [], $count_column = 'id' ): int {
1068 global $wpdb;
1069
1070 $table = self::prepare_table_name( $table );
1071 $where_clause = self::prepare_where_search_clause( $where, $search, 'AND' );
1072
1073 $count = $wpdb->get_var(
1074 "SELECT COUNT($count_column)
1075 FROM $table
1076 {$where_clause}"
1077 );
1078
1079 // If error occurred then throw new exception.
1080 if ( $wpdb->last_error ) {
1081 throw new \Exception( $wpdb->last_error );
1082 }
1083
1084 return (int) $count;
1085 }
1086
1087 /**
1088 * Get count by joining multiple tables with specified join relations.
1089 *
1090 * Argument should be SQL escaped.
1091 *
1092 * @since 3.0.0
1093 *
1094 * @param string $primary_table The primary table name with prefix.
1095 * @param array $joining_tables An array of join relations. Each relation should be an array with keys 'type', 'table', 'on'.
1096 * @param array $where array of where conditions.
1097 * @param array $search array of search conditions for LIKE operator.
1098 * @param string $count_column column name to count, default id.
1099 *
1100 * @return int
1101 */
1102 public static function get_joined_count(string $primary_table, array $joining_tables, array $where = [], array $search = [], string $count_column = '*'): int {
1103 global $wpdb;
1104
1105 $from_clause = self::prepare_table_name( $primary_table );
1106 $join_clauses = self::prepare_join_clause( $joining_tables );
1107 $where_clause = self::prepare_where_search_clause( $where, $search, 'AND' );
1108
1109 $count_query = "
1110 SELECT COUNT($count_column) as total_count
1111 FROM {$from_clause}
1112 {$join_clauses}
1113 {$where_clause}
1114 ";
1115
1116 $total_count = $wpdb->get_var( $count_query );
1117
1118 // If error occurred then throw new exception.
1119 if ( $wpdb->last_error ) {
1120 throw new \Exception( $wpdb->last_error );
1121 }
1122
1123 return (int) $total_count;
1124 }
1125
1126 /**
1127 * Get all rows from any table with where and search clauses.
1128 *
1129 * @since 3.0.0
1130 *
1131 * @param string $table Table name with prefix.
1132 * @param array $where Assoc array for exact match. For example: [col_name => value]. Without sql esc.
1133 * @param array $search Assoc array for LIKE match. For example: [col_name => search_term]. Without sql esc.
1134 * @param string $order_by Order by column name.
1135 * @param int $limit Maximum number of rows to return, default is 10.
1136 * @param int $offset Offset for pagination, default is 0.
1137 * @param string $order DESC or ASC, default is DESC.
1138 * @param string $output Expected output type, default is OBJECT.
1139 *
1140 * @throws \Exception Throw exception if error occurred during query execution.
1141 *
1142 * @return mixed Based on output param, default OBJECT.
1143 */
1144 public static function get_all_with_search( string $table, array $where, array $search, string $order_by, $limit = 10, $offset = 0, string $order = 'DESC', string $output = 'OBJECT' ): array {
1145 global $wpdb;
1146
1147 $table = self::prepare_table_name( $table );
1148 $where_clause = self::prepare_where_search_clause( $where, $search, 'AND' );
1149 $order_by_clause = self::prepare_order_clause( $order_by, $order );
1150 $limit_clause = self::prepare_limit_clause( $limit, $offset );
1151
1152 // If error occurred then throw new exception.
1153 if ( $wpdb->last_error ) {
1154 throw new \Exception( $wpdb->last_error );
1155 }
1156
1157 $query = "SELECT SQL_CALC_FOUND_ROWS *
1158 FROM {$table}
1159 {$where_clause}
1160 {$order_by_clause}
1161 {$limit_clause}";
1162
1163 $results = $wpdb->get_results( $query, $output );
1164 $has_records = is_array( $results ) && count( $results );
1165 $total_count = $has_records ? (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' ) : 0;
1166
1167 // If error occurred then throw new exception.
1168 if ( $wpdb->last_error ) {
1169 throw new \Exception( $wpdb->last_error );
1170 }
1171
1172 // Prepare response array.
1173 $response = array(
1174 'results' => $results,
1175 'total_count' => $total_count,
1176 );
1177
1178 return $response;
1179 }
1180
1181 /**
1182 * Get period clause based on the provided period.
1183 *
1184 * @since 3.0.0
1185 *
1186 * @param string $column Table.column name, ex: table.created_at.
1187 * @param string $period Period for filter refund data.
1188 *
1189 * @return string
1190 */
1191 public static function get_period_clause( string $column, string $period = '' ) {
1192 $period_clause = '';
1193 switch ( $period ) {
1194 case 'today':
1195 $period_clause = "AND DATE($column) = CURDATE()";
1196 break;
1197 case 'monthly':
1198 $period_clause = "AND MONTH($column) = MONTH(CURDATE())";
1199 break;
1200 case 'yearly':
1201 $period_clause = "AND YEAR($column) = YEAR(CURDATE())";
1202 break;
1203 case 'last30days':
1204 $period_clause = "AND DATE($column) BETWEEN DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND CURDATE()";
1205 break;
1206 case 'last90days':
1207 $period_clause = "AND DATE($column) BETWEEN DATE_SUB(CURDATE(), INTERVAL 90 DAY) AND CURDATE()";
1208 break;
1209 case 'last365days':
1210 $period_clause = "AND DATE($column) BETWEEN DATE_SUB(CURDATE(), INTERVAL 365 DAY) AND CURDATE()";
1211 break;
1212 default:
1213 break;
1214 }
1215
1216 return $period_clause;
1217 }
1218
1219 /**
1220 * Get last executed SQL query.
1221 *
1222 * @since 3.6.0
1223 *
1224 * @return string
1225 */
1226 public static function get_last_query(){
1227 global $wpdb;
1228 return $wpdb->last_query;
1229 }
1230
1231 /**
1232 * Get table prefix.
1233 *
1234 * @since 3.7.0
1235 *
1236 * @return string
1237 */
1238 public static function get_table_prefix() {
1239 global $wpdb;
1240 return $wpdb->prefix;
1241 }
1242
1243 /**
1244 * Prepare table name with prefix.
1245 *
1246 * @since 3.7.0
1247 *
1248 * @param string $table_name table name.
1249 *
1250 * @return string
1251 */
1252 public static function prepare_table_name( string $table_name ) {
1253 $table_prefix = self::get_table_prefix();
1254 if ( strpos( $table_name,$table_prefix ) !== 0 ) {
1255 $table_name = $table_prefix . $table_name;
1256 }
1257
1258 return $table_name;
1259 }
1260
1261 /**
1262 * Duplicate a row with modification callback support.
1263 *
1264 * @since 3.7.0
1265 *
1266 * @param string $table_name name of the database table (with prefix if needed).
1267 * @param array $where associative array of WHERE conditions.
1268 * @param callable|null $modifier optional callback to modify or exclude fields before insertion.
1269 *
1270 * @return int|WP_Error New row ID on success, or WP_Error on failure.
1271 */
1272 public static function duplicate_row( $table_name, array $where, ?callable $modifier = null ) {
1273 global $wpdb;
1274
1275 $table_name = self::prepare_table_name( $table_name );
1276 if ( empty( $where ) ) {
1277 return new \WP_Error( 'missing_where', 'No WHERE condition provided.' );
1278 }
1279
1280 $where_clause = self::prepare_where_clause( $where );
1281 $sql = $wpdb->prepare( "SELECT * FROM `$table_name` WHERE {$where_clause} LIMIT %d", 1 );
1282 $row = $wpdb->get_row( $sql, ARRAY_A );
1283
1284 if ( ! $row ) {
1285 return new \WP_Error( 'not_found', 'No matching row found to duplicate.' );
1286 }
1287
1288 // Apply user-defined modifications (ex: remove ID, change field value)
1289 if ( is_callable( $modifier ) ) {
1290 $row = call_user_func( $modifier, $row );
1291
1292 if ( ! is_array( $row ) || empty( $row ) ) {
1293 return new \WP_Error( 'invalid_modified_row', 'Modified row is invalid or empty.' );
1294 }
1295 }
1296
1297 // Prepare insert
1298 $columns = array_keys( $row );
1299 $placeholders = array_fill( 0, count( $columns ), '%s' );
1300 $values = array_values( $row );
1301
1302 $insert_sql = $wpdb->prepare(
1303 "INSERT INTO `$table_name` (`" . implode( '`, `', $columns ) . "`)
1304 VALUES (" . implode( ', ', $placeholders ) . ")",
1305 ...$values
1306 );
1307
1308 $result = $wpdb->query( $insert_sql );
1309
1310 if ( false === $result ) {
1311 return new \WP_Error( 'insert_failed', 'Failed to insert duplicate row.' );
1312 }
1313
1314 return $wpdb->insert_id;
1315 }
1316
1317 /**
1318 * Get valid sort order.
1319 *
1320 * @since 3.7.1
1321 *
1322 * @param string $order order.
1323 *
1324 * @return string
1325 */
1326 public static function get_valid_sort_order( $order ) {
1327 return 'ASC' === strtoupper( $order ) ? 'ASC' : 'DESC';
1328 }
1329
1330 /**
1331 * Get the schema of a database table.
1332 *
1333 * @since 3.8.1
1334 *
1335 * @param string $table_name The name of the database table.
1336 *
1337 * @throws \Exception Throws an exception if there is a database error.
1338 *
1339 * @return array Returns an array of table columns and their details.
1340 */
1341 public static function get_table_schema( $table_name) {
1342
1343 global $wpdb;
1344
1345 $result = $wpdb->get_results( "DESCRIBE {$table_name}", ARRAY_A );
1346
1347 // If error occurred then throw new exception.
1348 if ($wpdb->last_error) {
1349 throw new \Exception($wpdb->last_error);
1350 }
1351
1352 return $result;
1353 }
1354
1355 }
1356