PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.4
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.4
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 19.1 All 128 releases
wordpress-seo / lib / orm.php

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

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