PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.8
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.8
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / lib / orm.php

orm.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 18.8, at lib/orm.php

2,545 lines 67.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\Lib;
4
5 use ReturnTypeWillChange;
6 use wpdb;
7 use Yoast\WP\SEO\Config\Migration_Status;
8
9 /**
10 * Yoast ORM class.
11 *
12 * Based on Idiorm
13 *
14 * URL: http://github.com/j4mie/idiorm/
15 *
16 * A single-class super-simple database abstraction layer for PHP.
17 * Provides (nearly) zero-configuration object-relational mapping
18 * and a fluent interface for building basic, commonly-used queries.
19 *
20 * BSD Licensed.
21 *
22 * Copyright (c) 2010, Jamie Matthews
23 * All rights reserved.
24 *
25 * Redistribution and use in source and binary forms, with or without
26 * modification, are permitted provided that the following conditions are met:
27 *
28 * * Redistributions of source code must retain the above copyright notice, this
29 * list of conditions and the following disclaimer.
30 *
31 * * Redistributions in binary form must reproduce the above copyright notice,
32 * this list of conditions and the following disclaimer in the documentation
33 * and/or other materials provided with the distribution.
34 *
35 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
37 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
39 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
40 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
41 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
42 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
43 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
44 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45 *
46 * The methods documented below are magic methods that conform to PSR-1.
47 * This documentation exposes these methods to doc generators and IDEs.
48 *
49 * @see http://www.php-fig.org/psr/psr-1/
50 */
51 class ORM implements \ArrayAccess {
52
53 /*
54 * --- CLASS CONSTANTS ---
55 */
56
57 const CONDITION_FRAGMENT = 0;
58
59 const CONDITION_VALUES = 1;
60
61 /*
62 * --- INSTANCE PROPERTIES ---
63 */
64
65 /**
66 * Holds the class name. Wrapped find_one and find_many classes will return an instance or instances of this class.
67 *
68 * @var string
69 */
70 protected $class_name;
71
72 /**
73 * Holds the name of the table the current ORM instance is associated with.
74 *
75 * @var string
76 */
77 protected $table_name;
78
79 /**
80 * Holds the alias for the table to be used in SELECT queries.
81 *
82 * @var string
83 */
84 protected $table_alias = null;
85
86 /**
87 * Values to be bound to the query.
88 *
89 * @var array
90 */
91 protected $values = [];
92
93 /**
94 * Columns to select in the result.
95 *
96 * @var array
97 */
98 protected $result_columns = [ '*' ];
99
100 /**
101 * Are we using the default result column or have these been manually changed?
102 *
103 * @var bool
104 */
105 protected $using_default_result_columns = true;
106
107 /**
108 * Holds the join sources.
109 *
110 * @var array
111 */
112 protected $join_sources = [];
113
114 /**
115 * Should the query include a DISTINCT keyword?
116 *
117 * @var bool
118 */
119 protected $distinct = false;
120
121 /**
122 * Is this a raw query?
123 *
124 * @var bool
125 */
126 protected $is_raw_query = false;
127
128 /**
129 * The raw query.
130 *
131 * @var string
132 */
133 protected $raw_query = '';
134
135 /**
136 * The raw query parameters.
137 *
138 * @var array
139 */
140 protected $raw_parameters = [];
141
142 /**
143 * Array of WHERE clauses.
144 *
145 * @var array
146 */
147 protected $where_conditions = [];
148
149 /**
150 * LIMIT.
151 *
152 * @var int
153 */
154 protected $limit = null;
155
156 /**
157 * OFFSET.
158 *
159 * @var int
160 */
161 protected $offset = null;
162
163 /**
164 * ORDER BY.
165 *
166 * @var array
167 */
168 protected $order_by = [];
169
170 /**
171 * GROUP BY.
172 *
173 * @var array
174 */
175 protected $group_by = [];
176
177 /**
178 * HAVING.
179 *
180 * @var array
181 */
182 protected $having_conditions = [];
183
184 /**
185 * The data for a hydrated instance of the class.
186 *
187 * @var array
188 */
189 protected $data = [];
190
191 /**
192 * Lifetime of the object.
193 *
194 * @var array
195 */
196 protected $dirty_fields = [];
197
198 /**
199 * Fields that are to be inserted in the DB raw.
200 *
201 * @var array
202 */
203 protected $expr_fields = [];
204
205 /**
206 * Is this a new object (has create() been called)?
207 *
208 * @var bool
209 */
210 protected $is_new = false;
211
212 /**
213 * Name of the column to use as the primary key for
214 * this instance only. Overrides the config settings.
215 *
216 * @var string
217 */
218 protected $instance_id_column = null;
219
220 /*
221 * --- STATIC METHODS ---
222 */
223
224 /**
225 * Factory method, return an instance of this class bound to the supplied
226 * table name.
227 *
228 * A repeat of content in parent::for_table, so that created class is ORM.
229 *
230 * @param string $table_name The table to create instance for.
231 *
232 * @return ORM Instance of the ORM.
233 */
234 public static function for_table( $table_name ) {
235 return new static( $table_name, [] );
236 }
237
238 /**
239 * Executes a raw query as a wrapper for wpdb::query.
240 * Useful for queries that can't be accomplished through Idiorm,
241 * particularly those using engine-specific features.
242 *
243 * @example raw_execute('INSERT OR REPLACE INTO `widget` (`id`, `name`) SELECT `id`, `name` FROM `other_table`')
244 * @example raw_execute('SELECT `name`, AVG(`order`) FROM `customer` GROUP BY `name` HAVING AVG(`order`) > 10')
245 *
246 * @param string $query The raw SQL query.
247 * @param array $parameters Optional bound parameters.
248 *
249 * @return bool Success.
250 */
251 public static function raw_execute( $query, $parameters = [] ) {
252 return self::execute( $query, $parameters );
253 }
254
255 /**
256 * Internal helper method for executing statements.
257 *
258 * @param string $query The query.
259 * @param array $parameters An array of parameters to be bound in to the query.
260 *
261 * @return bool|int Response of wpdb::query
262 */
263 protected static function execute( $query, $parameters = [] ) {
264 /**
265 * The global WordPress database variable.
266 *
267 * @var wpdb
268 */
269 global $wpdb;
270
271 $show_errors = $wpdb->show_errors;
272
273 if ( YoastSEO()->classes->get( Migration_Status::class )->get_error( 'free' ) ) {
274 $wpdb->show_errors = false;
275 }
276
277 $parameters = \array_filter(
278 $parameters,
279 static function( $parameter ) {
280 return $parameter !== null;
281 }
282 );
283 if ( ! empty( $parameters ) ) {
284 $query = $wpdb->prepare( $query, $parameters );
285 }
286
287 $result = $wpdb->query( $query );
288
289 $wpdb->show_errors = $show_errors;
290
291 return $result;
292 }
293
294 /*
295 * --- INSTANCE METHODS ---
296 */
297
298 /**
299 * "Private" constructor; shouldn't be called directly.
300 * Use the ORM::for_table factory method instead.
301 *
302 * @param string $table_name Table name.
303 * @param array $data Data to populate table.
304 */
305 protected function __construct( $table_name, $data = [] ) {
306 $this->table_name = $table_name;
307 $this->data = $data;
308 }
309
310 /**
311 * Sets the name of the class which the wrapped methods should return instances of.
312 *
313 * @param string $class_name The classname to set.
314 *
315 * @return void
316 */
317 public function set_class_name( $class_name ) {
318 $this->class_name = $class_name;
319 }
320
321 /**
322 * Creates a new, empty instance of the class. Used to add a new row to your database. May optionally be passed an
323 * associative array of data to populate the instance. If so, all fields will be flagged as dirty so all will be
324 * saved to the database when save() is called.
325 *
326 * @param array|null $data Data to populate table.
327 *
328 * @return bool|Model|ORM
329 */
330 public function create( $data = null ) {
331 $this->is_new = true;
332 if ( ! \is_null( $data ) ) {
333 $this->hydrate( $data )->force_all_dirty();
334 }
335
336 return $this->create_model_instance( $this );
337 }
338
339 /**
340 * Specifies the ID column to use for this instance or array of instances only.
341 * This overrides the id_column and id_column_overrides settings.
342 *
343 * This is mostly useful for libraries built on top of Idiorm, and will not normally be used in manually built
344 * queries. If you don't know why you would want to use this, you should probably just ignore it.
345 *
346 * @param string $id_column The ID column.
347 *
348 * @return ORM
349 */
350 public function use_id_column( $id_column ) {
351 $this->instance_id_column = $id_column;
352
353 return $this;
354 }
355
356 /**
357 * Creates an ORM instance from the given row (an associative array of data fetched from the database).
358 *
359 * @param array $row A row from the database.
360 *
361 * @return bool|Model
362 */
363 protected function create_instance_from_row( $row ) {
364 $instance = self::for_table( $this->table_name );
365 $instance->use_id_column( $this->instance_id_column );
366 $instance->hydrate( $row );
367
368 return $this->create_model_instance( $instance );
369 }
370
371 /**
372 * Tells the ORM that you are expecting a single result back from your query, and execute it. Will return a single
373 * instance of the ORM class, or false if no rows were returned. As a shortcut, you may supply an ID as a parameter
374 * to this method. This will perform a primary key lookup on the table.
375 *
376 * @param int|null $id An (optional) ID.
377 *
378 * @return bool|Model
379 */
380 public function find_one( $id = null ) {
381 if ( ! \is_null( $id ) ) {
382 $this->where_id_is( $id );
383 }
384 $this->limit( 1 );
385 $rows = $this->run();
386 if ( empty( $rows ) ) {
387 return false;
388 }
389
390 return $this->create_instance_from_row( $rows[0] );
391 }
392
393 /**
394 * Tells the ORM that you are expecting multiple results from your query, and execute it. Will return an array of
395 * instances of the ORM class, or an empty array if no rows were returned.
396 *
397 * @return array
398 */
399 public function find_many() {
400 $rows = $this->run();
401
402 if ( $rows === false ) {
403 return [];
404 }
405
406 return \array_map( [ $this, 'create_instance_from_row' ], $rows );
407 }
408
409 /**
410 * Creates an instance of the model class associated with this wrapper and populate it with the supplied Idiorm
411 * instance.
412 *
413 * @param ORM $orm The ORM used by model.
414 *
415 * @return bool|Model Instance of the model class.
416 */
417 protected function create_model_instance( $orm ) {
418 if ( $orm === false ) {
419 return false;
420 }
421
422 /**
423 * An instance of Model is being made.
424 *
425 * @var Model $model
426 */
427 $model = new $this->class_name();
428 $model->set_orm( $orm );
429
430 return $model;
431 }
432
433 /**
434 * Tells the ORM that you are expecting multiple results from your query, and execute it. Will return an array, or
435 * an empty array if no rows were returned.
436 *
437 * @return array The query results.
438 */
439 public function find_array() {
440 return $this->run();
441 }
442
443 /**
444 * Tells the ORM that you wish to execute a COUNT query.
445 *
446 * @param string $column The table column.
447 *
448 * @return float|int An integer representing the number of rows returned.
449 */
450 public function count( $column = '*' ) {
451 return $this->call_aggregate_db_function( __FUNCTION__, $column );
452 }
453
454 /**
455 * Tells the ORM that you wish to execute a MAX query.
456 *
457 * @param string $column The table column.
458 *
459 * @return float|int The max value of the chosen column.
460 */
461 public function max( $column ) {
462 return $this->call_aggregate_db_function( __FUNCTION__, $column );
463 }
464
465 /**
466 * Tells the ORM that you wish to execute a MIN query.
467 *
468 * @param string $column The table column.
469 *
470 * @return float|int The min value of the chosen column.
471 */
472 public function min( $column ) {
473 return $this->call_aggregate_db_function( __FUNCTION__, $column );
474 }
475
476 /**
477 * Tells the ORM that you wish to execute a AVG query.
478 *
479 * @param string $column The table column.
480 *
481 * @return float|int The average value of the chosen column.
482 */
483 public function avg( $column ) {
484 return $this->call_aggregate_db_function( __FUNCTION__, $column );
485 }
486
487 /**
488 * Tells the ORM that you wish to execute a SUM query.
489 *
490 * @param string $column The table column.
491 *
492 * @return float|int The sum of the chosen column.
493 */
494 public function sum( $column ) {
495 return $this->call_aggregate_db_function( __FUNCTION__, $column );
496 }
497
498 /**
499 * Returns the select query as SQL.
500 *
501 * @return string The select query in SQL.
502 */
503 public function get_sql() {
504 return $this->build_select();
505 }
506
507 /**
508 * Returns the update query as SQL.
509 *
510 * @return string The update query in SQL.
511 */
512 public function get_update_sql() {
513 return $this->build_update();
514 }
515
516 /**
517 * Executes an aggregate query on the current connection.
518 *
519 * @param string $sql_function The aggregate function to call eg. MIN, COUNT, etc.
520 * @param string $column The column to execute the aggregate query against.
521 *
522 * @return int
523 */
524 protected function call_aggregate_db_function( $sql_function, $column ) {
525 $alias = \strtolower( $sql_function );
526 $sql_function = \strtoupper( $sql_function );
527 if ( $column !== '*' ) {
528 $column = $this->quote_identifier( $column );
529 }
530 $result_columns = $this->result_columns;
531 $this->result_columns = [];
532 $this->select_expr( "{$sql_function}({$column})", $alias );
533 $result = $this->find_one();
534 $this->result_columns = $result_columns;
535 $return_value = 0;
536 if ( $result !== false && isset( $result->{$alias} ) ) {
537 if ( ! \is_numeric( $result->{$alias} ) ) {
538 $return_value = $result->{$alias};
539 }
540 // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison -- Reason: This loose comparison seems intended.
541 elseif ( (int) $result->{$alias} == (float) $result->{$alias} ) {
542 $return_value = (int) $result->{$alias};
543 }
544 else {
545 $return_value = (float) $result->{$alias};
546 }
547 }
548
549 return $return_value;
550 }
551
552 /**
553 * Hydrates (populate) this instance of the class from an associative array of data. This will usually be called
554 * only from inside the class, but it's public in case you need to call it directly.
555 *
556 * @param array $data Data to populate table.
557 *
558 * @return ORM
559 */
560 public function hydrate( $data = [] ) {
561 $this->data = $data;
562
563 return $this;
564 }
565
566 /**
567 * Forces the ORM to flag all the fields in the $data array as "dirty" and therefore update them when save() is
568 * called.
569 *
570 * @return ORM
571 */
572 public function force_all_dirty() {
573 $this->dirty_fields = $this->data;
574
575 return $this;
576 }
577
578 /**
579 * Performs a raw query. The query can contain placeholders in either named or question mark style. If placeholders
580 * are used, the parameters should be an array of values which will be bound to the placeholders in the query.
581 * If this method is called, all other query building methods will be ignored.
582 *
583 * @param array $query The query.
584 * @param array $parameters The parameters. Defaults to an empty array.
585 *
586 * @return ORM
587 */
588 public function raw_query( $query, $parameters = [] ) {
589 $this->is_raw_query = true;
590 $this->raw_query = $query;
591 $this->raw_parameters = $parameters;
592
593 return $this;
594 }
595
596 /**
597 * Adds an alias for the main table to be used in SELECT queries.
598 *
599 * @param string $alias The alias.
600 *
601 * @return ORM
602 */
603 public function table_alias( $alias ) {
604 $this->table_alias = $alias;
605
606 return $this;
607 }
608
609 /**
610 * Adds an unquoted expression to the set of columns returned by the SELECT query. Internal method.
611 *
612 * @param string $expr The expression.
613 * @param string|null $alias The alias to return the expression as. Defaults to null.
614 *
615 * @return ORM
616 */
617 protected function add_result_column( $expr, $alias = null ) {
618 if ( ! \is_null( $alias ) ) {
619 $expr .= ' AS ' . $this->quote_identifier( $alias );
620 }
621 if ( $this->using_default_result_columns ) {
622 $this->result_columns = [ $expr ];
623 $this->using_default_result_columns = false;
624 }
625 else {
626 $this->result_columns[] = $expr;
627 }
628
629 return $this;
630 }
631
632 /**
633 * Counts the number of columns that belong to the primary key and their value is null.
634 *
635 * @return int The amount of null columns.
636 *
637 * @throws \Exception Primary key ID contains null value(s).
638 * @throws \Exception Primary key ID missing from row or is null.
639 */
640 public function count_null_id_columns() {
641 if ( \is_array( $this->get_id_column_name() ) ) {
642 return \count( \array_filter( $this->id(), 'is_null' ) );
643 }
644 else {
645 return \is_null( $this->id() ) ? 1 : 0;
646 }
647 }
648
649 /**
650 * Adds a column to the list of columns returned by the SELECT query.
651 *
652 * @param string $column The column. Defaults to '*'.
653 * @param string|null $alias The alias to return the column as. Defaults to null.
654 *
655 * @return ORM
656 */
657 public function select( $column, $alias = null ) {
658 $column = $this->quote_identifier( $column );
659
660 return $this->add_result_column( $column, $alias );
661 }
662
663 /**
664 * Adds an unquoted expression to the list of columns returned by the SELECT query.
665 *
666 * @param string $expr The expression.
667 * @param string|null $alias The alias to return the column as. Defaults to null.
668 *
669 * @return ORM
670 */
671 public function select_expr( $expr, $alias = null ) {
672 return $this->add_result_column( $expr, $alias );
673 }
674
675 /**
676 * Adds columns to the list of columns returned by the SELECT query.
677 *
678 * This defaults to '*'.
679 * Many columns can be supplied as either an array or as a list of parameters to the method.
680 * Note that the alias must not be numeric - if you want a numeric alias then prepend it with some alpha chars. eg.
681 * a1.
682 *
683 * @example select_many(array('column', 'column2', 'column3'), 'column4', 'column5');
684 * @example select_many(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5');
685 * @example select_many('column', 'column2', 'column3');
686 *
687 * @return ORM
688 */
689 public function select_many() {
690 $columns = \func_get_args();
691 if ( ! empty( $columns ) ) {
692 $columns = $this->normalise_select_many_columns( $columns );
693 foreach ( $columns as $alias => $column ) {
694 if ( \is_numeric( $alias ) ) {
695 $alias = null;
696 }
697 $this->select( $column, $alias );
698 }
699 }
700
701 return $this;
702 }
703
704 /**
705 * Adds an unquoted expression to the list of columns returned by the SELECT query.
706 *
707 * Many columns can be supplied as either an array or as a list of parameters to the method.
708 * Note that the alias must not be numeric - if you want a numeric alias then prepend it with some alpha chars. eg.
709 * a1
710 *
711 * @example select_many_expr(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5')
712 * @example select_many_expr('column', 'column2', 'column3')
713 * @example select_many_expr(array('column', 'column2', 'column3'), 'column4', 'column5')
714 *
715 * @return ORM
716 */
717 public function select_many_expr() {
718 $columns = \func_get_args();
719 if ( ! empty( $columns ) ) {
720 $columns = $this->normalise_select_many_columns( $columns );
721 foreach ( $columns as $alias => $column ) {
722 if ( \is_numeric( $alias ) ) {
723 $alias = null;
724 }
725 $this->select_expr( $column, $alias );
726 }
727 }
728
729 return $this;
730 }
731
732 /**
733 * Takes a column specification for the select many methods and convert it into a normalised array of columns and
734 * aliases.
735 *
736 * It is designed to turn the following styles into a normalised array:
737 * array(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5'))
738 *
739 * @param array $columns The columns.
740 *
741 * @return array
742 */
743 protected function normalise_select_many_columns( $columns ) {
744 $return = [];
745 foreach ( $columns as $column ) {
746 if ( \is_array( $column ) ) {
747 foreach ( $column as $key => $value ) {
748 if ( ! \is_numeric( $key ) ) {
749 $return[ $key ] = $value;
750 }
751 else {
752 $return[] = $value;
753 }
754 }
755 }
756 else {
757 $return[] = $column;
758 }
759 }
760
761 return $return;
762 }
763
764 /**
765 * Adds a DISTINCT keyword before the list of columns in the SELECT query.
766 *
767 * @return ORM
768 */
769 public function distinct() {
770 $this->distinct = true;
771
772 return $this;
773 }
774
775 /**
776 * Add a JOIN source to the query. Internal method.
777 *
778 * The join_operator should be one of INNER, LEFT OUTER, CROSS etc - this
779 * will be prepended to JOIN.
780 *
781 * The table should be the name of the table to join to.
782 *
783 * The constraint may be either a string or an array with three elements. If it
784 * is a string, it will be compiled into the query as-is, with no escaping. The
785 * recommended way to supply the constraint is as an array with three elements:
786 *
787 * first_column, operator, second_column
788 *
789 * Example: array('user.id', '=', 'profile.user_id')
790 *
791 * will compile to
792 *
793 * ON `user`.`id` = `profile`.`user_id`
794 *
795 * The final (optional) argument specifies an alias for the joined table.
796 *
797 * @param string $join_operator The join_operator should be one of INNER, LEFT OUTER, CROSS etc - this will be
798 * prepended to JOIN.
799 * @param string $table The table should be the name of the table to join to.
800 * @param string $constraint The constraint.
801 * @param string|null $table_alias The alias for the joined table. Defaults to null.
802 *
803 * @return ORM
804 */
805 protected function add_join_source( $join_operator, $table, $constraint, $table_alias = null ) {
806 $join_operator = \trim( "{$join_operator} JOIN" );
807 $table = $this->quote_identifier( $table );
808 // Add table alias if present.
809 if ( ! \is_null( $table_alias ) ) {
810 $table_alias = $this->quote_identifier( $table_alias );
811 $table .= " {$table_alias}";
812 }
813 // Build the constraint.
814 if ( \is_array( $constraint ) ) {
815 list( $first_column, $operator, $second_column ) = $constraint;
816
817 $first_column = $this->quote_identifier( $first_column );
818 $second_column = $this->quote_identifier( $second_column );
819 $constraint = "{$first_column} {$operator} {$second_column}";
820 }
821 $this->join_sources[] = "{$join_operator} {$table} ON {$constraint}";
822
823 return $this;
824 }
825
826 /**
827 * Adds a RAW JOIN source to the query.
828 *
829 * @param string $table The table name.
830 * @param string $constraint The constraint.
831 * @param string $table_alias The table alias.
832 * @param array $parameters The parameters. Defaults to an empty array.
833 *
834 * @return ORM
835 */
836 public function raw_join( $table, $constraint, $table_alias, $parameters = [] ) {
837 // Add table alias if present.
838 if ( ! \is_null( $table_alias ) ) {
839 $table_alias = $this->quote_identifier( $table_alias );
840 $table .= " {$table_alias}";
841 }
842 $this->values = \array_merge( $this->values, $parameters );
843 // Build the constraint.
844 if ( \is_array( $constraint ) ) {
845 list( $first_column, $operator, $second_column ) = $constraint;
846
847 $first_column = $this->quote_identifier( $first_column );
848 $second_column = $this->quote_identifier( $second_column );
849 $constraint = "{$first_column} {$operator} {$second_column}";
850 }
851 $this->join_sources[] = "{$table} ON {$constraint}";
852
853 return $this;
854 }
855
856 /**
857 * Adds a simple JOIN source to the query.
858 *
859 * @param string $table The table name.
860 * @param string $constraint The constraint.
861 * @param string|null $table_alias The table alias. Defaults to null.
862 *
863 * @return ORM
864 */
865 public function join( $table, $constraint, $table_alias = null ) {
866 return $this->add_join_source( '', $table, $constraint, $table_alias );
867 }
868
869 /**
870 * Adds an INNER JOIN source to the query.
871 *
872 * @param string $table The table name.
873 * @param string $constraint The constraint.
874 * @param string|null $table_alias The table alias. Defaults to null.
875 *
876 * @return ORM
877 */
878 public function inner_join( $table, $constraint, $table_alias = null ) {
879 return $this->add_join_source( 'INNER', $table, $constraint, $table_alias );
880 }
881
882 /**
883 * Adds a LEFT OUTER JOIN source to the query.
884 *
885 * @param string $table The table name.
886 * @param string $constraint The constraint.
887 * @param string|null $table_alias The table alias. Defaults to null.
888 *
889 * @return ORM
890 */
891 public function left_outer_join( $table, $constraint, $table_alias = null ) {
892 return $this->add_join_source( 'LEFT OUTER', $table, $constraint, $table_alias );
893 }
894
895 /**
896 * Adds a RIGHT OUTER JOIN source to the query.
897 *
898 * @param string $table The table name.
899 * @param string $constraint The constraint.
900 * @param string|null $table_alias The table alias. Defaults to null.
901 *
902 * @return ORM
903 */
904 public function right_outer_join( $table, $constraint, $table_alias = null ) {
905 return $this->add_join_source( 'RIGHT OUTER', $table, $constraint, $table_alias );
906 }
907
908 /**
909 * Adds a FULL OUTER JOIN source to the query.
910 *
911 * @param string $table The table name.
912 * @param string $constraint The constraint.
913 * @param string|null $table_alias The table alias. Defaults to null.
914 *
915 * @return ORM
916 */
917 public function full_outer_join( $table, $constraint, $table_alias = null ) {
918 return $this->add_join_source( 'FULL OUTER', $table, $constraint, $table_alias );
919 }
920
921 /**
922 * Adds a HAVING condition to the query. Internal method.
923 *
924 * @param string $fragment The fragment.
925 * @param array $values The values. Defaults to an empty array.
926 *
927 * @return ORM
928 */
929 protected function add_having( $fragment, $values = [] ) {
930 return $this->add_condition( 'having', $fragment, $values );
931 }
932
933 /**
934 * Adds a HAVING condition to the query. Internal method.
935 *
936 * @param string $column_name The table column.
937 * @param string $separator The separator.
938 * @param mixed $value The value.
939 *
940 * @return ORM
941 */
942 protected function add_simple_having( $column_name, $separator, $value ) {
943 return $this->add_simple_condition( 'having', $column_name, $separator, $value );
944 }
945
946 /**
947 * Adds a HAVING clause with multiple values (like IN and NOT IN). Internal method.
948 *
949 * @param string|array $column_name The table column.
950 * @param string $separator The separator.
951 * @param array $values The values.
952 *
953 * @return ORM
954 */
955 public function add_having_placeholder( $column_name, $separator, $values ) {
956 if ( ! \is_array( $column_name ) ) {
957 $data = [ $column_name => $values ];
958 }
959 else {
960 $data = $column_name;
961 }
962 $result = $this;
963 foreach ( $data as $key => $val ) {
964 $column = $result->quote_identifier( $key );
965 $placeholders = $result->create_placeholders( $val );
966 $result = $result->add_having( "{$column} {$separator} ({$placeholders})", $val );
967 }
968
969 return $result;
970 }
971
972 /**
973 * Adds a HAVING clause with no parameters(like IS NULL and IS NOT NULL). Internal method.
974 *
975 * @param string $column_name The column name.
976 * @param string $operator The operator.
977 *
978 * @return ORM
979 */
980 public function add_having_no_value( $column_name, $operator ) {
981 $conditions = \is_array( $column_name ) ? $column_name : [ $column_name ];
982 $result = $this;
983 foreach ( $conditions as $column ) {
984 $column = $this->quote_identifier( $column );
985 $result = $result->add_having( "{$column} {$operator}" );
986 }
987
988 return $result;
989 }
990
991 /**
992 * Adds a WHERE condition to the query. Internal method.
993 *
994 * @param string $fragment The fragment.
995 * @param array $values The values. Defaults to an empty array.
996 *
997 * @return ORM
998 */
999 protected function add_where( $fragment, $values = [] ) {
1000 return $this->add_condition( 'where', $fragment, $values );
1001 }
1002
1003 /**
1004 * Adds a WHERE condition to the query. Internal method.
1005 *
1006 * @param string|array $column_name The table column.
1007 * @param string $separator The separator.
1008 * @param mixed $value The value.
1009 *
1010 * @return ORM
1011 */
1012 protected function add_simple_where( $column_name, $separator, $value ) {
1013 return $this->add_simple_condition( 'where', $column_name, $separator, $value );
1014 }
1015
1016 /**
1017 * Adds a WHERE clause with multiple values (like IN and NOT IN).
1018 *
1019 * @param string|array $column_name The table column.
1020 * @param string $separator The separator.
1021 * @param array $values The values.
1022 *
1023 * @return ORM
1024 */
1025 public function add_where_placeholder( $column_name, $separator, $values ) {
1026 if ( ! \is_array( $column_name ) ) {
1027 $data = [ $column_name => $values ];
1028 }
1029 else {
1030 $data = $column_name;
1031 }
1032 $result = $this;
1033 foreach ( $data as $key => $val ) {
1034 $column = $result->quote_identifier( $key );
1035 $placeholders = $result->create_placeholders( $val );
1036 $result = $result->add_where( "{$column} {$separator} ({$placeholders})", $val );
1037 }
1038
1039 return $result;
1040 }
1041
1042 /**
1043 * Adds a WHERE clause with no parameters(like IS NULL and IS NOT NULL).
1044 *
1045 * @param string $column_name The column name.
1046 * @param string $operator The operator.
1047 *
1048 * @return ORM
1049 */
1050 public function add_where_no_value( $column_name, $operator ) {
1051 $conditions = \is_array( $column_name ) ? $column_name : [ $column_name ];
1052 $result = $this;
1053 foreach ( $conditions as $column ) {
1054 $column = $this->quote_identifier( $column );
1055 $result = $result->add_where( "{$column} {$operator}" );
1056 }
1057
1058 return $result;
1059 }
1060
1061 /**
1062 * Adds a HAVING or WHERE condition to the query. Internal method.
1063 *
1064 * @param string $type The type.
1065 * @param string $fragment The fragment.
1066 * @param array $values The values. Defaults to empty array.
1067 *
1068 * @return ORM
1069 */
1070 protected function add_condition( $type, $fragment, $values = [] ) {
1071 $conditions_class_property_name = "{$type}_conditions";
1072 if ( ! \is_array( $values ) ) {
1073 $values = [ $values ];
1074 }
1075 \array_push(
1076 $this->{$conditions_class_property_name},
1077 [
1078 self::CONDITION_FRAGMENT => $fragment,
1079 self::CONDITION_VALUES => $values,
1080 ]
1081 );
1082
1083 return $this;
1084 }
1085
1086 /**
1087 * Compiles a simple COLUMN SEPARATOR VALUE style HAVING or WHERE condition into a string and value ready to be
1088 * passed to the add_condition method.
1089 *
1090 * Avoids duplication of the call to quote_identifier.
1091 * If column_name is an associative array, it will add a condition for each column.
1092 *
1093 * @param string $type The type.
1094 * @param string|array $column_name The table column.
1095 * @param string $separator The separator.
1096 * @param mixed $value The value.
1097 *
1098 * @return ORM
1099 */
1100 protected function add_simple_condition( $type, $column_name, $separator, $value ) {
1101 $multiple = \is_array( $column_name ) ? $column_name : [ $column_name => $value ];
1102 $result = $this;
1103 foreach ( $multiple as $key => $val ) {
1104 // Add the table name in case of ambiguous columns.
1105 if ( \count( $result->join_sources ) > 0 && \strpos( $key, '.' ) === false ) {
1106 $table = $result->table_name;
1107 if ( ! \is_null( $result->table_alias ) ) {
1108 $table = $result->table_alias;
1109 }
1110 $key = "{$table}.{$key}";
1111 }
1112 $key = $result->quote_identifier( $key );
1113 $placeholder = ( $val === null ) ? 'NULL' : '%s';
1114 $result = $result->add_condition( $type, "{$key} {$separator} {$placeholder}", $val );
1115 }
1116
1117 return $result;
1118 }
1119
1120 /**
1121 * Returns a string containing the given number of question marks, separated by commas. Eg "?, ?, ?".
1122 *
1123 * @param array $fields Fields to create placeholder for.
1124 *
1125 * @return string
1126 */
1127 protected function create_placeholders( $fields ) {
1128 if ( ! empty( $fields ) ) {
1129 $db_fields = [];
1130 foreach ( $fields as $key => $value ) {
1131 // Process expression fields directly into the query.
1132 if ( \array_key_exists( $key, $this->expr_fields ) ) {
1133 $db_fields[] = $value;
1134 }
1135 else {
1136 $db_fields[] = ( $value === null ) ? 'NULL' : '%s';
1137 }
1138 }
1139
1140 return \implode( ', ', $db_fields );
1141 }
1142
1143 return '';
1144 }
1145
1146 /**
1147 * Filters a column/value array returning only those columns that belong to a compound primary key.
1148 *
1149 * If the key contains a column that does not exist in the given array, a null value will be returned for it.
1150 *
1151 * @param mixed $value The value.
1152 *
1153 * @return array
1154 */
1155 protected function get_compound_id_column_values( $value ) {
1156 $filtered = [];
1157 foreach ( $this->get_id_column_name() as $key ) {
1158 $filtered[ $key ] = isset( $value[ $key ] ) ? $value[ $key ] : null;
1159 }
1160
1161 return $filtered;
1162 }
1163
1164 /**
1165 * Filters an array containing compound column/value arrays.
1166 *
1167 * @param array $values The values.
1168 *
1169 * @return array
1170 */
1171 protected function get_compound_id_column_values_array( $values ) {
1172 $filtered = [];
1173 foreach ( $values as $value ) {
1174 $filtered[] = $this->get_compound_id_column_values( $value );
1175 }
1176
1177 return $filtered;
1178 }
1179
1180 /**
1181 * Add a WHERE column = value clause to your query. Each time this is called in the chain, an additional WHERE will
1182 * be added, and these will be ANDed together when the final query is built.
1183 *
1184 * If you use an array in $column_name, a new clause will be added for each element. In this case, $value is
1185 * ignored.
1186 *
1187 * @param string|array $column_name The table column.
1188 * @param mixed|null $value The value. Defaults to null.
1189 *
1190 * @return ORM
1191 */
1192 public function where( $column_name, $value = null ) {
1193 return $this->where_equal( $column_name, $value );
1194 }
1195
1196 /**
1197 * More explicitly named version of for the where() method. Can be used if preferred.
1198 *
1199 * @param string|array $column_name The table column.
1200 * @param mixed|null $value The value. Defaults to null.
1201 *
1202 * @return ORM
1203 */
1204 public function where_equal( $column_name, $value = null ) {
1205 return $this->add_simple_where( $column_name, '=', $value );
1206 }
1207
1208 /**
1209 * Add a WHERE column != value clause to your query.
1210 *
1211 * @param string|array $column_name The table column.
1212 * @param mixed|null $value The value. Defaults to null.
1213 *
1214 * @return ORM
1215 */
1216 public function where_not_equal( $column_name, $value = null ) {
1217 return $this->add_simple_where( $column_name, '!=', $value );
1218 }
1219
1220 /**
1221 * Queries the table by its primary key. Special method.
1222 *
1223 * If primary key is compound, only the columns that belong to they key will be used for the query.
1224 *
1225 * @param string $id The ID.
1226 *
1227 * @return ORM
1228 */
1229 public function where_id_is( $id ) {
1230 return \is_array( $this->get_id_column_name() ) ? $this->where( $this->get_compound_id_column_values( $id ), null ) : $this->where( $this->get_id_column_name(), $id );
1231 }
1232
1233 /**
1234 * Allows adding a WHERE clause that matches any of the conditions specified in the array. Each element in the
1235 * associative array will be a different condition, where the key will be the column name.
1236 *
1237 * By default, an equal operator will be used against all columns, but it can be overriden for any or every column
1238 * using the second parameter.
1239 *
1240 * Each condition will be ORed together when added to the final query.
1241 *
1242 * @param array $values The values.
1243 * @param string $operator The operator.
1244 *
1245 * @return ORM
1246 */
1247 public function where_any_is( $values, $operator = '=' ) {
1248 $data = [];
1249 $query = [ '((' ];
1250 $first = true;
1251 foreach ( $values as $value ) {
1252 if ( $first ) {
1253 $first = false;
1254 }
1255 else {
1256 $query[] = ') OR (';
1257 }
1258 $firstsub = true;
1259 foreach ( $value as $key => $item ) {
1260 $op = \is_string( $operator ) ? $operator : ( isset( $operator[ $key ] ) ? $operator[ $key ] : '=' );
1261 if ( $op === '=' && $item === null ) {
1262 $op = 'IS';
1263 }
1264 if ( $firstsub ) {
1265 $firstsub = false;
1266 }
1267 else {
1268 $query[] = 'AND';
1269 }
1270 $query[] = $this->quote_identifier( $key );
1271 $data[] = $item;
1272 $query[] = $op;
1273 $query[] = ( ( $item === null ) ? 'NULL' : '%s' );
1274 }
1275 }
1276 $query[] = '))';
1277
1278 return $this->where_raw( \implode( ' ', $query ), $data );
1279 }
1280
1281 /**
1282 * Queries the table by its primary key.
1283 *
1284 * Similar to where_id_is() but allowing multiple primary keys.
1285 * If primary key is compound, only the columns that belong to they key will be used for the query.
1286 *
1287 * @param string[] $ids The IDs.
1288 *
1289 * @return ORM
1290 */
1291 public function where_id_in( $ids ) {
1292 return \is_array( $this->get_id_column_name() ) ? $this->where_any_is( $this->get_compound_id_column_values_array( $ids ) ) : $this->where_in( $this->get_id_column_name(), $ids );
1293 }
1294
1295 /**
1296 * Adds a WHERE ... LIKE clause to your query.
1297 *
1298 * @param string|array $column_name The table column.
1299 * @param mixed|null $value The value. Defaults to null.
1300 *
1301 * @return ORM
1302 */
1303 public function where_like( $column_name, $value = null ) {
1304 return $this->add_simple_where( $column_name, 'LIKE', $value );
1305 }
1306
1307 /**
1308 * Adds where WHERE ... NOT LIKE clause to your query.
1309 *
1310 * @param string|array $column_name The table column.
1311 * @param mixed|null $value The value. Defaults to null.
1312 *
1313 * @return ORM
1314 */
1315 public function where_not_like( $column_name, $value = null ) {
1316 return $this->add_simple_where( $column_name, 'NOT LIKE', $value );
1317 }
1318
1319 /**
1320 * Adds a WHERE ... > clause to your query.
1321 *
1322 * @param string|array $column_name The table column.
1323 * @param mixed|null $value The value. Defaults to null.
1324 *
1325 * @return ORM
1326 */
1327 public function where_gt( $column_name, $value = null ) {
1328 return $this->add_simple_where( $column_name, '>', $value );
1329 }
1330
1331 /**
1332 * Adds a WHERE ... < clause to your query.
1333 *
1334 * @param string|array $column_name The table column.
1335 * @param mixed|null $value The value. Defaults to null.
1336 *
1337 * @return ORM
1338 */
1339 public function where_lt( $column_name, $value = null ) {
1340 return $this->add_simple_where( $column_name, '<', $value );
1341 }
1342
1343 /**
1344 * Adds a WHERE ... >= clause to your query.
1345 *
1346 * @param string|array $column_name The table column.
1347 * @param mixed|null $value The value. Defaults to null.
1348 *
1349 * @return ORM
1350 */
1351 public function where_gte( $column_name, $value = null ) {
1352 return $this->add_simple_where( $column_name, '>=', $value );
1353 }
1354
1355 /**
1356 * Adds a WHERE ... <= clause to your query.
1357 *
1358 * @param string|array $column_name The table column.
1359 * @param mixed|null $value The value. Defaults to null.
1360 *
1361 * @return ORM
1362 */
1363 public function where_lte( $column_name, $value = null ) {
1364 return $this->add_simple_where( $column_name, '<=', $value );
1365 }
1366
1367 /**
1368 * Adds a WHERE ... IN clause to your query.
1369 *
1370 * @param string|array $column_name The table column.
1371 * @param array $values The values.
1372 *
1373 * @return ORM
1374 */
1375 public function where_in( $column_name, $values ) {
1376 return $this->add_where_placeholder( $column_name, 'IN', $values );
1377 }
1378
1379 /**
1380 * Adds a WHERE ... NOT IN clause to your query.
1381 *
1382 * @param string|array $column_name The table column.
1383 * @param array $values The values.
1384 *
1385 * @return ORM
1386 */
1387 public function where_not_in( $column_name, $values ) {
1388 return $this->add_where_placeholder( $column_name, 'NOT IN', $values );
1389 }
1390
1391 /**
1392 * Adds a WHERE column IS NULL clause to your query.
1393 *
1394 * @param string|array $column_name The table column.
1395 *
1396 * @return ORM
1397 */
1398 public function where_null( $column_name ) {
1399 return $this->add_where_no_value( $column_name, 'IS NULL' );
1400 }
1401
1402 /**
1403 * Adds a WHERE column IS NOT NULL clause to your query.
1404 *
1405 * @param string|array $column_name The table column.
1406 *
1407 * @return ORM
1408 */
1409 public function where_not_null( $column_name ) {
1410 return $this->add_where_no_value( $column_name, 'IS NOT NULL' );
1411 }
1412
1413 /**
1414 * Adds a raw WHERE clause to the query. The clause should contain question mark placeholders, which will be bound
1415 * to the parameters supplied in the second argument.
1416 *
1417 * @param string $clause The clause that should contain question mark placeholders.
1418 * @param array $parameters The parameters to include in the query.
1419 *
1420 * @return ORM
1421 */
1422 public function where_raw( $clause, $parameters = [] ) {
1423 return $this->add_where( $clause, $parameters );
1424 }
1425
1426 /**
1427 * Adds a LIMIT to the query.
1428 *
1429 * @param int $limit The limit.
1430 *
1431 * @return ORM
1432 */
1433 public function limit( $limit ) {
1434 $this->limit = $limit;
1435
1436 return $this;
1437 }
1438
1439 /**
1440 * Adds an OFFSET to the query.
1441 *
1442 * @param int $offset The offset.
1443 *
1444 * @return ORM
1445 */
1446 public function offset( $offset ) {
1447 $this->offset = $offset;
1448
1449 return $this;
1450 }
1451
1452 /**
1453 * Adds an ORDER BY clause to the query.
1454 *
1455 * @param string $column_name The column name.
1456 * @param string $ordering The ordering. DESC or ASC.
1457 *
1458 * @return ORM
1459 */
1460 protected function add_order_by( $column_name, $ordering ) {
1461 $column_name = $this->quote_identifier( $column_name );
1462 $this->order_by[] = "{$column_name} {$ordering}";
1463
1464 return $this;
1465 }
1466
1467 /**
1468 * Adds an ORDER BY column DESC clause.
1469 *
1470 * @param string|array $column_name The table column.
1471 *
1472 * @return ORM
1473 */
1474 public function order_by_desc( $column_name ) {
1475 return $this->add_order_by( $column_name, 'DESC' );
1476 }
1477
1478 /**
1479 * Adds an ORDER BY column ASC clause.
1480 *
1481 * @param string|array $column_name The table column.
1482 *
1483 * @return ORM
1484 */
1485 public function order_by_asc( $column_name ) {
1486 return $this->add_order_by( $column_name, 'ASC' );
1487 }
1488
1489 /**
1490 * Adds an unquoted expression as an ORDER BY clause.
1491 *
1492 * @param string $clause The clause.
1493 *
1494 * @return ORM
1495 */
1496 public function order_by_expr( $clause ) {
1497 $this->order_by[] = $clause;
1498
1499 return $this;
1500 }
1501
1502 /**
1503 * Adds a column to the list of columns to GROUP BY.
1504 *
1505 * @param string|array $column_name The table column.
1506 *
1507 * @return ORM
1508 */
1509 public function group_by( $column_name ) {
1510 $column_name = $this->quote_identifier( $column_name );
1511 $this->group_by[] = $column_name;
1512
1513 return $this;
1514 }
1515
1516 /**
1517 * Adds an unquoted expression to the list of columns to GROUP BY.
1518 *
1519 * @param string $expr The expression.
1520 *
1521 * @return ORM
1522 */
1523 public function group_by_expr( $expr ) {
1524 $this->group_by[] = $expr;
1525
1526 return $this;
1527 }
1528
1529 /**
1530 * Adds a HAVING column = value clause to your query.
1531 *
1532 * Each time this is called in the chain, an additional HAVING will be added, and these will be ANDed together when
1533 * the final query is built.
1534 *
1535 * If you use an array in $column_name, a new clause will be added for each element. In this case, $value is
1536 * ignored.
1537 *
1538 * @param string|array $column_name The table column.
1539 * @param mixed|null $value The value.
1540 *
1541 * @return ORM
1542 */
1543 public function having( $column_name, $value = null ) {
1544 return $this->having_equal( $column_name, $value );
1545 }
1546
1547 /**
1548 * Adds a having equal to your query.
1549 *
1550 * More explicitly named version of for the having() method. Can be used if preferred.
1551 *
1552 * @param string|array $column_name The table column.
1553 * @param mixed|null $value The value.
1554 *
1555 * @return ORM
1556 */
1557 public function having_equal( $column_name, $value = null ) {
1558 return $this->add_simple_having( $column_name, '=', $value );
1559 }
1560
1561 /**
1562 * Adds a HAVING column != value clause to your query.
1563 *
1564 * @param string|array $column_name The table column.
1565 * @param mixed|null $value The value.
1566 *
1567 * @return ORM
1568 */
1569 public function having_not_equal( $column_name, $value = null ) {
1570 return $this->add_simple_having( $column_name, '!=', $value );
1571 }
1572
1573 /**
1574 * Queries the table by its primary key. Special method.
1575 *
1576 * If primary key is compound, only the columns that belong to they key will be used for the query.
1577 *
1578 * @param string $id The ID.
1579 *
1580 * @return ORM
1581 */
1582 public function having_id_is( $id ) {
1583 return \is_array( $this->get_id_column_name() ) ? $this->having( $this->get_compound_id_column_values( $id ), null ) : $this->having( $this->get_id_column_name(), $id );
1584 }
1585
1586 /**
1587 * Adds a HAVING ... LIKE clause to your query.
1588 *
1589 * @param string|array $column_name The table column.
1590 * @param string|null $value The value.
1591 *
1592 * @return ORM
1593 */
1594 public function having_like( $column_name, $value = null ) {
1595 return $this->add_simple_having( $column_name, 'LIKE', $value );
1596 }
1597
1598 /**
1599 * Adds where HAVING ... NOT LIKE clause to your query.
1600 *
1601 * @param string|array $column_name The table column.
1602 * @param string|null $value The value.
1603 *
1604 * @return ORM
1605 */
1606 public function having_not_like( $column_name, $value = null ) {
1607 return $this->add_simple_having( $column_name, 'NOT LIKE', $value );
1608 }
1609
1610 /**
1611 * Adds a HAVING ... > clause to your query.
1612 *
1613 * @param string|array $column_name The table column.
1614 * @param mixed $value The value.
1615 *
1616 * @return ORM
1617 */
1618 public function having_gt( $column_name, $value = null ) {
1619 return $this->add_simple_having( $column_name, '>', $value );
1620 }
1621
1622 /**
1623 * Adds a HAVING ... < clause to your query.
1624 *
1625 * @param string|array $column_name The table column.
1626 * @param mixed $value The value.
1627 *
1628 * @return ORM
1629 */
1630 public function having_lt( $column_name, $value = null ) {
1631 return $this->add_simple_having( $column_name, '<', $value );
1632 }
1633
1634 /**
1635 * Adds a HAVING ... >= clause to your query.
1636 *
1637 * @param string|array $column_name The table column.
1638 * @param mixed $value The value. Defaults to null.
1639 *
1640 * @return ORM
1641 */
1642 public function having_gte( $column_name, $value = null ) {
1643 return $this->add_simple_having( $column_name, '>=', $value );
1644 }
1645
1646 /**
1647 * Adds a HAVING ... <= clause to your query.
1648 *
1649 * @param string|array $column_name The table column.
1650 * @param mixed $value The value.
1651 *
1652 * @return ORM
1653 */
1654 public function having_lte( $column_name, $value = null ) {
1655 return $this->add_simple_having( $column_name, '<=', $value );
1656 }
1657
1658 /**
1659 * Adds a HAVING ... IN clause to your query.
1660 *
1661 * @param string|array $column_name The table column.
1662 * @param array|null $values The values. Defaults to null.
1663 *
1664 * @return ORM
1665 */
1666 public function having_in( $column_name, $values = null ) {
1667 return $this->add_having_placeholder( $column_name, 'IN', $values );
1668 }
1669
1670 /**
1671 * Adds a HAVING ... NOT IN clause to your query.
1672 *
1673 * @param string|array $column_name The table column.
1674 * @param array|null $values The values. Defaults to null.
1675 *
1676 * @return ORM
1677 */
1678 public function having_not_in( $column_name, $values = null ) {
1679 return $this->add_having_placeholder( $column_name, 'NOT IN', $values );
1680 }
1681
1682 /**
1683 * Adds a HAVING column IS NULL clause to your query.
1684 *
1685 * @param string|array $column_name The table column.
1686 *
1687 * @return ORM
1688 */
1689 public function having_null( $column_name ) {
1690 return $this->add_having_no_value( $column_name, 'IS NULL' );
1691 }
1692
1693 /**
1694 * Adds a HAVING column IS NOT NULL clause to your query.
1695 *
1696 * @param string|array $column_name The table column.
1697 *
1698 * @return ORM
1699 */
1700 public function having_not_null( $column_name ) {
1701 return $this->add_having_no_value( $column_name, 'IS NOT NULL' );
1702 }
1703
1704 /**
1705 * Adds a raw HAVING clause to the query. The clause should contain question mark placeholders, which will be bound
1706 * to the parameters supplied in the second argument.
1707 *
1708 * @param string $clause The clause that should contain question mark placeholders.
1709 * @param array $parameters The parameters to include in the query.
1710 *
1711 * @return ORM
1712 */
1713 public function having_raw( $clause, $parameters = [] ) {
1714 return $this->add_having( $clause, $parameters );
1715 }
1716
1717 /**
1718 * Builds a SELECT statement based on the clauses that have been passed to this instance by chaining method calls.
1719 *
1720 * @return string
1721 */
1722 protected function build_select() {
1723 // If the query is raw, just set the $this->values to be the raw query parameters and return the raw query.
1724 if ( $this->is_raw_query ) {
1725 $this->values = $this->raw_parameters;
1726
1727 return $this->raw_query;
1728 }
1729
1730 // Build and return the full SELECT statement by concatenating the results of calling each separate builder method.
1731 return $this->join_if_not_empty(
1732 ' ',
1733 [
1734 $this->build_select_start(),
1735 $this->build_join(),
1736 $this->build_where(),
1737 $this->build_group_by(),
1738 $this->build_having(),
1739 $this->build_order_by(),
1740 $this->build_limit(),
1741 $this->build_offset(),
1742 ]
1743 );
1744 }
1745
1746 /**
1747 * Builds the start of the SELECT statement.
1748 *
1749 * @return string
1750 */
1751 protected function build_select_start() {
1752 $fragment = 'SELECT ';
1753 $result_columns = \implode( ', ', $this->result_columns );
1754 if ( $this->distinct ) {
1755 $result_columns = 'DISTINCT ' . $result_columns;
1756 }
1757 $fragment .= "{$result_columns} FROM " . $this->quote_identifier( $this->table_name );
1758 if ( ! \is_null( $this->table_alias ) ) {
1759 $fragment .= ' ' . $this->quote_identifier( $this->table_alias );
1760 }
1761
1762 return $fragment;
1763 }
1764
1765 /**
1766 * Builds the JOIN sources.
1767 *
1768 * @return string
1769 */
1770 protected function build_join() {
1771 if ( \count( $this->join_sources ) === 0 ) {
1772 return '';
1773 }
1774
1775 return \implode( ' ', $this->join_sources );
1776 }
1777
1778 /**
1779 * Builds the WHERE clause(s).
1780 *
1781 * @return string
1782 */
1783 protected function build_where() {
1784 return $this->build_conditions( 'where' );
1785 }
1786
1787 /**
1788 * Build the HAVING clause(s)
1789 *
1790 * @return string
1791 */
1792 protected function build_having() {
1793 return $this->build_conditions( 'having' );
1794 }
1795
1796 /**
1797 * Builds GROUP BY.
1798 *
1799 * @return string
1800 */
1801 protected function build_group_by() {
1802 if ( \count( $this->group_by ) === 0 ) {
1803 return '';
1804 }
1805
1806 return 'GROUP BY ' . \implode( ', ', $this->group_by );
1807 }
1808
1809 /**
1810 * Builds a WHERE or HAVING clause.
1811 *
1812 * @param string $type Where or having.
1813 *
1814 * @return string
1815 */
1816 protected function build_conditions( $type ) {
1817 $conditions_class_property_name = "{$type}_conditions";
1818 // If there are no clauses, return empty string.
1819 if ( \count( $this->{$conditions_class_property_name} ) === 0 ) {
1820 return '';
1821 }
1822 $conditions = [];
1823 foreach ( $this->{$conditions_class_property_name} as $condition ) {
1824 $conditions[] = $condition[ self::CONDITION_FRAGMENT ];
1825 $this->values = \array_merge( $this->values, $condition[ self::CONDITION_VALUES ] );
1826 }
1827
1828 return \strtoupper( $type ) . ' ' . \implode( ' AND ', $conditions );
1829 }
1830
1831 /**
1832 * Builds ORDER BY.
1833 *
1834 * @return string
1835 */
1836 protected function build_order_by() {
1837 if ( \count( $this->order_by ) === 0 ) {
1838 return '';
1839 }
1840
1841 return 'ORDER BY ' . \implode( ', ', $this->order_by );
1842 }
1843
1844 /**
1845 * Builds LIMIT.
1846 *
1847 * @return string
1848 */
1849 protected function build_limit() {
1850 if ( ! \is_null( $this->limit ) ) {
1851 return "LIMIT {$this->limit}";
1852 }
1853
1854 return '';
1855 }
1856
1857 /**
1858 * Builds OFFSET.
1859 *
1860 * @return string
1861 */
1862 protected function build_offset() {
1863 if ( ! \is_null( $this->offset ) ) {
1864 return 'OFFSET ' . $this->offset;
1865 }
1866
1867 return '';
1868 }
1869
1870 /**
1871 * Joins strings if they are not empty.
1872 *
1873 * @param string $glue Glue.
1874 * @param string[] $pieces Pieces to join.
1875 *
1876 * @return string
1877 */
1878 protected function join_if_not_empty( $glue, $pieces ) {
1879 $filtered_pieces = [];
1880 foreach ( $pieces as $piece ) {
1881 if ( \is_string( $piece ) ) {
1882 $piece = \trim( $piece );
1883 }
1884 if ( ! empty( $piece ) ) {
1885 $filtered_pieces[] = $piece;
1886 }
1887 }
1888
1889 return \implode( $glue, $filtered_pieces );
1890 }
1891
1892 /**
1893 * Quotes a string that is used as an identifier (table names, column names etc).
1894 * This method can also deal with dot-separated identifiers eg table.column.
1895 *
1896 * @param string|string[] $identifier One or more identifiers.
1897 *
1898 * @return string
1899 */
1900 protected function quote_one_identifier( $identifier ) {
1901 $parts = \explode( '.', $identifier );
1902 $parts = \array_map( [ $this, 'quote_identifier_part' ], $parts );
1903
1904 return \implode( '.', $parts );
1905 }
1906
1907 /**
1908 * Quotes a string that is used as an identifier (table names, column names etc) or an array containing multiple
1909 * identifiers. This method can also deal with dot-separated identifiers eg table.column.
1910 *
1911 * @param string|string[] $identifier One or more identifiers.
1912 *
1913 * @return string
1914 */
1915 protected function quote_identifier( $identifier ) {
1916 if ( \is_array( $identifier ) ) {
1917 $result = \array_map( [ $this, 'quote_one_identifier' ], $identifier );
1918
1919 return \implode( ', ', $result );
1920 }
1921 else {
1922 return $this->quote_one_identifier( $identifier );
1923 }
1924 }
1925
1926 /**
1927 * Quotes a single part of an identifier, using the identifier quote character specified in the config
1928 * (or autodetected).
1929 *
1930 * @param string $part The part to quote.
1931 *
1932 * @return string
1933 */
1934 protected function quote_identifier_part( $part ) {
1935 if ( $part === '*' ) {
1936 return $part;
1937 }
1938 $quote_character = '`';
1939
1940 // Double up any identifier quotes to escape them.
1941 return $quote_character . \str_replace( $quote_character, $quote_character . $quote_character, $part ) . $quote_character;
1942 }
1943
1944 /**
1945 * Executes the SELECT query that has been built up by chaining methods on this class.
1946 * Return an array of rows as associative arrays.
1947 *
1948 * @return array|false The result rows. False if the query failed.
1949 */
1950 protected function run() {
1951 global $wpdb;
1952
1953 $query = $this->build_select();
1954 $success = self::execute( $query, $this->values );
1955
1956 if ( $success === false ) {
1957 // If the query fails run the migrations and try again.
1958 // Action is intentionally undocumented and should not be used by third-parties.
1959 \do_action( '_yoast_run_migrations' );
1960 $success = self::execute( $query, $this->values );
1961 }
1962
1963 $this->reset_idiorm_state();
1964
1965 if ( $success === false ) {
1966 return false;
1967 }
1968
1969 $rows = [];
1970 foreach ( $wpdb->last_result as $row ) {
1971 $rows[] = \get_object_vars( $row );
1972 }
1973
1974 return $rows;
1975 }
1976
1977 /**
1978 * Resets the Idiorm instance state.
1979 */
1980 private function reset_idiorm_state() {
1981 $this->values = [];
1982 $this->result_columns = [ '*' ];
1983 $this->using_default_result_columns = true;
1984 }
1985
1986 /**
1987 * Returns the raw data wrapped by this ORM instance as an associative array. Column names may optionally be
1988 * supplied as arguments, if so, only those keys will be returned.
1989 *
1990 * @return array Associative array of the raw data.
1991 */
1992 public function as_array() {
1993 if ( \func_num_args() === 0 ) {
1994 return $this->data;
1995 }
1996 $args = \func_get_args();
1997
1998 return \array_intersect_key( $this->data, \array_flip( $args ) );
1999 }
2000
2001 /**
2002 * Returns the value of a property of this object (database row) or null if not present.
2003 *
2004 * If a column-names array is passed, it will return a associative array with the value of each column or null if
2005 * it is not present.
2006 *
2007 * @param string|array $key Key.
2008 *
2009 * @return array|mixed|null
2010 */
2011 public function get( $key ) {
2012 if ( \is_array( $key ) ) {
2013 $result = [];
2014 foreach ( $key as $column ) {
2015 $result[ $column ] = isset( $this->data[ $column ] ) ? $this->data[ $column ] : null;
2016 }
2017
2018 return $result;
2019 }
2020 else {
2021 return isset( $this->data[ $key ] ) ? $this->data[ $key ] : null;
2022 }
2023 }
2024
2025 /**
2026 * Returns the name of the column in the database table which contains the primary key ID of the row.
2027 *
2028 * @return string The primary key ID of the row.
2029 */
2030 protected function get_id_column_name() {
2031 if ( ! \is_null( $this->instance_id_column ) ) {
2032 return $this->instance_id_column;
2033 }
2034
2035 return 'id';
2036 }
2037
2038 /**
2039 * Gets the primary key ID of this object.
2040 *
2041 * @param bool $disallow_null Whether to allow null IDs.
2042 *
2043 * @return array|mixed|null
2044 *
2045 * @throws \Exception Primary key ID contains null value(s).
2046 * @throws \Exception Primary key ID missing from row or is null.
2047 */
2048 public function id( $disallow_null = false ) {
2049 $id = $this->get( $this->get_id_column_name() );
2050 if ( $disallow_null ) {
2051 if ( \is_array( $id ) ) {
2052 foreach ( $id as $id_part ) {
2053 if ( $id_part === null ) {
2054 throw new \Exception( 'Primary key ID contains null value(s)' );
2055 }
2056 }
2057 }
2058 else {
2059 if ( $id === null ) {
2060 throw new \Exception( 'Primary key ID missing from row or is null' );
2061 }
2062 }
2063 }
2064
2065 return $id;
2066 }
2067
2068 /**
2069 * Sets a property to a particular value on this object.
2070 *
2071 * To set multiple properties at once, pass an associative array as the first parameter and leave out the second
2072 * parameter. Flags the properties as 'dirty' so they will be saved to the database when save() is called.
2073 *
2074 * @param string|array $key Key.
2075 * @param string|null $value Value.
2076 *
2077 * @return ORM
2078 */
2079 public function set( $key, $value = null ) {
2080 return $this->set_orm_property( $key, $value );
2081 }
2082
2083 /**
2084 * Set a property to a particular value on this object as expression.
2085 *
2086 * To set multiple properties at once, pass an associative array as the first parameter and leave out the second
2087 * parameter. Flags the properties as 'dirty' so they will be saved to the database when save() is called.
2088 *
2089 * @param string|array $key Key.
2090 * @param string|null $value Value.
2091 *
2092 * @return ORM
2093 */
2094 public function set_expr( $key, $value = null ) {
2095 return $this->set_orm_property( $key, $value, true );
2096 }
2097
2098 /**
2099 * Sets a property on the ORM object.
2100 *
2101 * @param string|array $key Key.
2102 * @param string|null $value Value.
2103 * @param bool $expr Expression.
2104 *
2105 * @return ORM
2106 */
2107 protected function set_orm_property( $key, $value = null, $expr = false ) {
2108 if ( ! \is_array( $key ) ) {
2109 $key = [ $key => $value ];
2110 }
2111 foreach ( $key as $field => $value ) {
2112 $this->data[ $field ] = $value;
2113 $this->dirty_fields[ $field ] = $value;
2114 if ( $expr === false && isset( $this->expr_fields[ $field ] ) ) {
2115 unset( $this->expr_fields[ $field ] );
2116 }
2117 else {
2118 if ( $expr === true ) {
2119 $this->expr_fields[ $field ] = true;
2120 }
2121 }
2122 }
2123
2124 return $this;
2125 }
2126
2127 /**
2128 * Checks whether the given field has been changed since this object was saved.
2129 *
2130 * @param mixed $key Key.
2131 *
2132 * @return bool
2133 */
2134 public function is_dirty( $key ) {
2135 return \array_key_exists( $key, $this->dirty_fields );
2136 }
2137
2138 /**
2139 * Checks whether the model was the result of a call to create() or not.
2140 *
2141 * @return bool
2142 */
2143 public function is_new() {
2144 return $this->is_new;
2145 }
2146
2147 /**
2148 * Saves any fields which have been modified on this object to the database.
2149 *
2150 * @return bool True on success.
2151 *
2152 * @throws \Exception Primary key ID contains null value(s).
2153 * @throws \Exception Primary key ID missing from row or is null.
2154 */
2155 public function save() {
2156 global $wpdb;
2157
2158 // Remove any expression fields as they are already baked into the query.
2159 $values = \array_values( \array_diff_key( $this->dirty_fields, $this->expr_fields ) );
2160 if ( ! $this->is_new ) {
2161 // UPDATE.
2162 // If there are no dirty values, do nothing.
2163 if ( empty( $values ) && empty( $this->expr_fields ) ) {
2164 return true;
2165 }
2166 $query = \implode( ' ', [ $this->build_update(), $this->add_id_column_conditions() ] );
2167
2168 $id = $this->id( true );
2169 if ( \is_array( $id ) ) {
2170 $values = \array_merge( $values, \array_values( $id ) );
2171 }
2172 else {
2173 $values[] = $id;
2174 }
2175 }
2176 else {
2177 // INSERT.
2178 $query = $this->build_insert();
2179 }
2180 $success = self::execute( $query, $values );
2181 // If we've just inserted a new record, set the ID of this object.
2182 if ( $this->is_new ) {
2183 $this->is_new = false;
2184 if ( $this->count_null_id_columns() !== 0 ) {
2185 $column = $this->get_id_column_name();
2186 // If the primary key is compound, assign the last inserted id to the first column.
2187 if ( \is_array( $column ) ) {
2188 $column = \reset( $column );
2189 }
2190 // Explicitly cast to int to make dealing with Id's simpler.
2191 $this->data[ $column ] = (int) $wpdb->insert_id;
2192 }
2193 }
2194 $this->dirty_fields = [];
2195 $this->expr_fields = [];
2196
2197 return $success;
2198 }
2199
2200 /**
2201 * Extracts and gathers all dirty column names from the given model instances.
2202 *
2203 * @param array $models Array of model instances to be inserted.
2204 *
2205 * @return array The distinct set of columns that are dirty in at least one of the models.
2206 *
2207 * @throws \InvalidArgumentException Instance to be inserted is not a new one.
2208 */
2209 public function get_dirty_column_names( $models ) {
2210 $dirty_column_names = [];
2211
2212 foreach ( $models as $model ) {
2213 if ( ! $model->orm->is_new() ) {
2214 throw new \InvalidArgumentException( 'Instance to be inserted is not a new one' );
2215 }
2216
2217 // Remove any expression fields as they are already baked into the query.
2218 $dirty_fields = \array_diff_key( $model->orm->dirty_fields, $model->orm->expr_fields );
2219 $dirty_column_names = \array_merge( $dirty_column_names, $dirty_fields );
2220 }
2221
2222 $dirty_column_names = \array_keys( $dirty_column_names );
2223
2224 return $dirty_column_names;
2225 }
2226
2227 /**
2228 * Inserts multiple rows in a single query. Expects new rows as it's a strictly insert function, not an update one.
2229 *
2230 * @example From the Indexable_Link_Builder class: $this->seo_links_repository->query()->insert_many( $links );
2231 *
2232 * @param array $models Array of model instances to be inserted.
2233 *
2234 * @return bool True for successful insert, false for failed.
2235 *
2236 * @throws \InvalidArgumentException Invalid instances to be inserted.
2237 * @throws \InvalidArgumentException Instance to be inserted is not a new one.
2238 */
2239 public function insert_many( $models ) {
2240 // Validate the input first.
2241 if ( ! \is_array( $models ) ) {
2242 throw new \InvalidArgumentException( 'Invalid instances to be inserted' );
2243 }
2244
2245 if ( empty( $models ) ) {
2246 return true;
2247 }
2248
2249 $success = true;
2250
2251 /**
2252 * Filter: 'wpseo_chunk_bulked_insert_queries' - Allow filtering the chunk size of each bulked INSERT query.
2253 *
2254 * @api int The chunk size of the bulked INSERT queries.
2255 */
2256 $chunk = \apply_filters( 'wpseo_chunk_bulk_insert_queries', 100 );
2257 $chunk = ! \is_int( $chunk ) ? 100 : $chunk;
2258 $chunk = ( $chunk <= 0 ) ? 100 : $chunk;
2259
2260 $chunked_models = \array_chunk( $models, $chunk );
2261 foreach ( $chunked_models as $models_chunk ) {
2262 $values = [];
2263
2264 // First, we'll gather all the dirty fields throughout the models to be inserted.
2265 $dirty_column_names = $this->get_dirty_column_names( $models_chunk );
2266
2267 // Now, we're creating all dirty fields throughout the models and
2268 // setting them to null if they don't exist in each model.
2269 foreach ( $models_chunk as $model ) {
2270 $model_values = [];
2271
2272 foreach ( $dirty_column_names as $dirty_column ) {
2273 // Set the value to null if it hasn't been set already.
2274 if ( ! isset( $model->orm->dirty_fields[ $dirty_column ] ) ) {
2275 $model->orm->dirty_fields[ $dirty_column ] = null;
2276 }
2277
2278 // Only register the value if it is not null.
2279 if ( ! is_null( $model->orm->dirty_fields[ $dirty_column ] ) ) {
2280 $model_values[] = $model->orm->dirty_fields[ $dirty_column ];
2281 }
2282 }
2283 $values = \array_merge( $values, $model_values );
2284 }
2285
2286 // We now have the same set of dirty columns in all our models and also gathered all values.
2287 $query = $this->build_insert_many( $models_chunk, $dirty_column_names );
2288 $success = $success && (bool) self::execute( $query, $values );
2289 }
2290
2291 return $success;
2292 }
2293
2294 /**
2295 * Updates many records in the database.
2296 *
2297 * @return int|bool The number of rows changed if the query was succesful. False otherwise.
2298 */
2299 public function update_many() {
2300 // Remove any expression fields as they are already baked into the query.
2301 $values = \array_values( \array_diff_key( $this->dirty_fields, $this->expr_fields ) );
2302
2303 // UPDATE.
2304 // If there are no dirty values, do nothing.
2305 if ( empty( $values ) && empty( $this->expr_fields ) ) {
2306 return true;
2307 }
2308
2309 $query = $this->join_if_not_empty( ' ', [ $this->build_update(), $this->build_where() ] );
2310
2311 $success = self::execute( $query, \array_merge( $values, $this->values ) );
2312 $this->dirty_fields = [];
2313 $this->expr_fields = [];
2314
2315 return $success;
2316 }
2317
2318 /**
2319 * Adds a WHERE clause for every column that belongs to the primary key.
2320 *
2321 * @return string The where part of the query.
2322 */
2323 public function add_id_column_conditions() {
2324 $query = [];
2325 $query[] = 'WHERE';
2326 $keys = \is_array( $this->get_id_column_name() ) ? $this->get_id_column_name() : [ $this->get_id_column_name() ];
2327 $first = true;
2328 foreach ( $keys as $key ) {
2329 if ( $first ) {
2330 $first = false;
2331 }
2332 else {
2333 $query[] = 'AND';
2334 }
2335 $query[] = $this->quote_identifier( $key );
2336 $query[] = '= %s';
2337 }
2338
2339 return \implode( ' ', $query );
2340 }
2341
2342 /**
2343 * Builds an UPDATE query.
2344 *
2345 * @return string The update query.
2346 */
2347 protected function build_update() {
2348 $query = [];
2349 $query[] = "UPDATE {$this->quote_identifier($this->table_name)} SET";
2350 $field_list = [];
2351 foreach ( $this->dirty_fields as $key => $value ) {
2352 if ( ! \array_key_exists( $key, $this->expr_fields ) ) {
2353 $value = ( $value === null ) ? 'NULL' : '%s';
2354 }
2355 $field_list[] = "{$this->quote_identifier($key)} = {$value}";
2356 }
2357 $query[] = \implode( ', ', $field_list );
2358
2359 return \implode( ' ', $query );
2360 }
2361
2362 /**
2363 * Builds an INSERT query.
2364 *
2365 * @return string The insert query.
2366 */
2367 protected function build_insert() {
2368 $query = [];
2369 $query[] = 'INSERT INTO';
2370 $query[] = $this->quote_identifier( $this->table_name );
2371 $field_list = \array_map( [ $this, 'quote_identifier' ], \array_keys( $this->dirty_fields ) );
2372 $query[] = '(' . \implode( ', ', $field_list ) . ')';
2373 $query[] = 'VALUES';
2374 $placeholders = $this->create_placeholders( $this->dirty_fields );
2375 $query[] = "({$placeholders})";
2376
2377 return \implode( ' ', $query );
2378 }
2379
2380 /**
2381 * Builds a bulk INSERT query.
2382 *
2383 * @param array $models Array of model instances to be inserted.
2384 * @param array $dirty_column_names Array of dirty fields to be used in INSERT.
2385 *
2386 * @return string The insert query.
2387 */
2388 protected function build_insert_many( $models, $dirty_column_names ) {
2389 $example_model = $models[0];
2390 $total_placeholders = '';
2391
2392 $query = [];
2393 $query[] = 'INSERT INTO';
2394 $query[] = $this->quote_identifier( $example_model->orm->table_name );
2395 $field_list = \array_map( [ $this, 'quote_identifier' ], $dirty_column_names );
2396 $query[] = '(' . \implode( ', ', $field_list ) . ')';
2397 $query[] = 'VALUES';
2398
2399 // We assign placeholders per model for dirty fields that have values and NULL for dirty fields that don't.
2400 foreach ( $models as $model ) {
2401 $placeholder = [];
2402 foreach ( $dirty_column_names as $dirty_field ) {
2403 $placeholder[] = ( $model->orm->dirty_fields[ $dirty_field ] === null ) ? 'NULL' : '%s';
2404 }
2405 $placeholders = \implode( ', ', $placeholder );
2406 $total_placeholders .= "({$placeholders}),";
2407 }
2408
2409 $query[] = \rtrim( $total_placeholders, ',' );
2410 return \implode( ' ', $query );
2411 }
2412
2413 /**
2414 * Deletes this record from the database.
2415 *
2416 * @return string The delete query.
2417 *
2418 * @throws \Exception Primary key ID contains null value(s).
2419 * @throws \Exception Primary key ID missing from row or is null.
2420 */
2421 public function delete() {
2422 $query = [ 'DELETE FROM', $this->quote_identifier( $this->table_name ), $this->add_id_column_conditions() ];
2423
2424 return self::execute( \implode( ' ', $query ), \is_array( $this->id( true ) ) ? \array_values( $this->id( true ) ) : [ $this->id( true ) ] );
2425 }
2426
2427 /**
2428 * Deletes many records from the database.
2429 *
2430 * @return bool|int Response of wpdb::query.
2431 */
2432 public function delete_many() {
2433 // Build and return the full DELETE statement by concatenating
2434 // the results of calling each separate builder method.
2435 $query = $this->join_if_not_empty(
2436 ' ',
2437 [
2438 'DELETE FROM',
2439 $this->quote_identifier( $this->table_name ),
2440 $this->build_where(),
2441 ]
2442 );
2443
2444 return self::execute( $query, $this->values );
2445 }
2446
2447 /*
2448 * --- ArrayAccess ---
2449 */
2450
2451 /**
2452 * Checks whether the data has the key.
2453 *
2454 * @param mixed $offset Key.
2455 *
2456 * @return bool Whether the data has the key.
2457 */
2458 #[ReturnTypeWillChange]
2459 public function offsetExists( $offset ) {
2460 return \array_key_exists( $offset, $this->data );
2461 }
2462
2463 /**
2464 * Retrieves the value of the key.
2465 *
2466 * @param mixed $offset Key.
2467 *
2468 * @return array|mixed|null The value.
2469 */
2470 #[ReturnTypeWillChange]
2471 public function offsetGet( $offset ) {
2472 return $this->get( $offset );
2473 }
2474
2475 /**
2476 * Sets the value of the key.
2477 *
2478 * @param string|int $offset Key.
2479 * @param mixed $value Value.
2480 */
2481 #[ReturnTypeWillChange]
2482 public function offsetSet( $offset, $value ) {
2483 if ( \is_null( $offset ) ) {
2484 return;
2485 }
2486 $this->set( $offset, $value );
2487 }
2488
2489 /**
2490 * Removes the given key from the data.
2491 *
2492 * @param mixed $offset Key.
2493 */
2494 #[ReturnTypeWillChange]
2495 public function offsetUnset( $offset ) {
2496 unset( $this->data[ $offset ] );
2497 unset( $this->dirty_fields[ $offset ] );
2498 }
2499
2500 /*
2501 * --- MAGIC METHODS ---
2502 */
2503
2504 /**
2505 * Handles magic get via offset.
2506 *
2507 * @param mixed $key Key.
2508 *
2509 * @return array|mixed|null The value in the offset.
2510 */
2511 public function __get( $key ) {
2512 return $this->offsetGet( $key );
2513 }
2514
2515 /**
2516 * Handles magic set via offset.
2517 *
2518 * @param string|int $key Key.
2519 * @param mixed $value Value.
2520 */
2521 public function __set( $key, $value ) {
2522 $this->offsetSet( $key, $value );
2523 }
2524
2525 /**
2526 * Handles magic unset via offset.
2527 *
2528 * @param mixed $key Key.
2529 */
2530 public function __unset( $key ) {
2531 $this->offsetUnset( $key );
2532 }
2533
2534 /**
2535 * Handles magic isset via offset.
2536 *
2537 * @param mixed $key Key.
2538 *
2539 * @return bool Whether the offset has the key.
2540 */
2541 public function __isset( $key ) {
2542 return $this->offsetExists( $key );
2543 }
2544 }
2545