PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.1
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.1
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 / model.php

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

726 lines 22.8 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 JsonSerializable;
6 use ReturnTypeWillChange;
7
8 /**
9 * Make Model compatible with WordPress.
10 *
11 * Model base class. Your model objects should extend
12 * this class. A minimal subclass would look like:
13 *
14 * class Widget extends Model {
15 * }
16 */
17 class Model implements JsonSerializable {
18
19 /**
20 * Default ID column for all models. Can be overridden by adding
21 * a public static $id_column property to your model classes.
22 *
23 * @var string
24 */
25 const DEFAULT_ID_COLUMN = 'id';
26
27 /**
28 * Default foreign key suffix used by relationship methods.
29 *
30 * @var string
31 */
32 const DEFAULT_FOREIGN_KEY_SUFFIX = '_id';
33
34 /**
35 * Set a prefix for model names. This can be a namespace or any other
36 * abitrary prefix such as the PEAR naming convention.
37 *
38 * @example Model::$auto_prefix_models = 'MyProject_MyModels_'; //PEAR
39 * @example Model::$auto_prefix_models = '\MyProject\MyModels\'; //Namespaces
40 *
41 * @var string
42 */
43 public static $auto_prefix_models = '\Yoast\WP\SEO\Models\\';
44
45 /**
46 * Set true to to ignore namespace information when computing table names
47 * from class names.
48 *
49 * @example Model::$short_table_names = true;
50 * @example Model::$short_table_names = false; // default
51 *
52 * @var bool
53 */
54 public static $short_table_names = false;
55
56 /**
57 * The ORM instance used by this model instance to communicate with the database.
58 *
59 * @var ORM
60 */
61 public $orm;
62
63 /**
64 * The table name for the implemented Model.
65 *
66 * @var string
67 */
68 public static $table;
69
70 /**
71 * Whether or not this model uses timestamps.
72 *
73 * @var bool
74 */
75 protected $uses_timestamps = false;
76
77 /**
78 * Which columns contain boolean values.
79 *
80 * @var array
81 */
82 protected $boolean_columns = [];
83
84 /**
85 * Which columns contain int values.
86 *
87 * @var array
88 */
89 protected $int_columns = [];
90
91 /**
92 * Which columns contain float values.
93 *
94 * @var array
95 */
96 protected $float_columns = [];
97
98 /**
99 * Hacks around the Model to provide WordPress prefix to tables.
100 *
101 * @param string $class_name Type of Model to load.
102 * @param bool $yoast_prefix Optional. True to prefix the table name with the Yoast prefix.
103 *
104 * @return ORM Wrapper to use.
105 */
106 public static function of_type( $class_name, $yoast_prefix = true ) {
107 // Prepend namespace to the class name.
108 $class = static::$auto_prefix_models . $class_name;
109
110 // Set the class variable to the custom value based on the WPDB prefix.
111 $class::$table = static::get_table_name( $class_name, $yoast_prefix );
112
113 return static::factory( $class_name, null );
114 }
115
116 /**
117 * Creates a model without the Yoast prefix.
118 *
119 * @param string $class_name Type of Model to load.
120 *
121 * @return ORM
122 */
123 public static function of_wp_type( $class_name ) {
124 return static::of_type( $class_name, false );
125 }
126
127 /**
128 * Exposes method to get the table name to use.
129 *
130 * @param string $table_name Simple table name.
131 * @param bool $yoast_prefix Optional. True to prefix the table name with the Yoast prefix.
132 *
133 * @return string Prepared full table name.
134 */
135 public static function get_table_name( $table_name, $yoast_prefix = true ) {
136 global $wpdb;
137
138 // Allow the use of WordPress internal tables.
139 if ( $yoast_prefix ) {
140 $table_name = 'yoast_' . $table_name;
141 }
142
143 return $wpdb->prefix . \strtolower( $table_name );
144 }
145
146 /**
147 * Sets the table name for the given class name.
148 *
149 * @param string $class_name The class to set the table name for.
150 *
151 * @return void
152 */
153 protected function set_table_name( $class_name ) {
154 // Prepend namespace to the class name.
155 $class = static::$auto_prefix_models . $class_name;
156
157 $class::$table = static::get_table_name( $class_name );
158 }
159
160 /**
161 * Retrieve the value of a static property on a class. If the
162 * class or the property does not exist, returns the default
163 * value supplied as the third argument (which defaults to null).
164 *
165 * @param string $class_name The target class name.
166 * @param string $property The property to get the value for.
167 * @param mixed|null $default_value Default value when property does not exist.
168 *
169 * @return mixed|null The value of the property.
170 */
171 protected static function get_static_property( $class_name, $property, $default_value = null ) {
172 if ( ! \class_exists( $class_name ) || ! \property_exists( $class_name, $property ) ) {
173 return $default_value;
174 }
175
176 if ( ! isset( $class_name::${$property} ) ) {
177 return $default_value;
178 }
179
180 return $class_name::${$property};
181 }
182
183 /**
184 * Static method to get a table name given a class name.
185 * If the supplied class has a public static property
186 * named $table, the value of this property will be
187 * returned.
188 *
189 * If not, the class name will be converted using
190 * the class_name_to_table_name() method.
191 *
192 * If Model::$short_table_names == true or public static
193 * property $table_use_short_name == true then $class_name passed
194 * to class_name_to_table_name() is stripped of namespace information.
195 *
196 * @param string $class_name The class name to get the table name for.
197 *
198 * @return string The table name.
199 */
200 protected static function get_table_name_for_class( $class_name ) {
201 $specified_table_name = static::get_static_property( $class_name, 'table' );
202 $use_short_class_name = static::use_short_table_name( $class_name );
203 if ( $use_short_class_name ) {
204 $exploded_class_name = \explode( '\\', $class_name );
205 $class_name = \end( $exploded_class_name );
206 }
207
208 if ( $specified_table_name === null ) {
209 return static::class_name_to_table_name( $class_name );
210 }
211
212 return $specified_table_name;
213 }
214
215 /**
216 * Should short table names, disregarding class namespaces, be computed?
217 *
218 * $class_property overrides $global_option, unless $class_property is null.
219 *
220 * @param string $class_name The class name to get short name for.
221 *
222 * @return bool True when short table name should be used.
223 */
224 protected static function use_short_table_name( $class_name ) {
225 $class_property = static::get_static_property( $class_name, 'table_use_short_name' );
226
227 if ( $class_property === null ) {
228 return static::$short_table_names;
229 }
230
231 return $class_property;
232 }
233
234 /**
235 * Convert a namespace to the standard PEAR underscore format.
236 *
237 * Then convert a class name in CapWords to a table name in
238 * lowercase_with_underscores.
239 *
240 * Finally strip doubled up underscores.
241 *
242 * For example, CarTyre would be converted to car_tyre. And
243 * Project\Models\CarTyre would be project_models_car_tyre.
244 *
245 * @param string $class_name The class name to get the table name for.
246 *
247 * @return string The table name.
248 */
249 protected static function class_name_to_table_name( $class_name ) {
250 $find = [
251 '/\\\\/',
252 '/(?<=[a-z])([A-Z])/',
253 '/__/',
254 ];
255 $replacements = [
256 '_',
257 '_$1',
258 '_',
259 ];
260
261 $class_name = \ltrim( $class_name, '\\' );
262 $class_name = \preg_replace( $find, $replacements, $class_name );
263
264 return \strtolower( $class_name );
265 }
266
267 /**
268 * Return the ID column name to use for this class. If it is
269 * not set on the class, returns null.
270 *
271 * @param string $class_name The class name to get the ID column for.
272 *
273 * @return string|null The ID column name.
274 */
275 protected static function get_id_column_name( $class_name ) {
276 return static::get_static_property( $class_name, 'id_column', static::DEFAULT_ID_COLUMN );
277 }
278
279 /**
280 * Build a foreign key based on a table name. If the first argument
281 * (the specified foreign key column name) is null, returns the second
282 * argument (the name of the table) with the default foreign key column
283 * suffix appended.
284 *
285 * @param string $specified_foreign_key_name The keyname to build.
286 * @param string $table_name The table name to build the key name for.
287 *
288 * @return string The built foreign key name.
289 */
290 protected static function build_foreign_key_name( $specified_foreign_key_name, $table_name ) {
291 if ( $specified_foreign_key_name !== null ) {
292 return $specified_foreign_key_name;
293 }
294
295 return $table_name . static::DEFAULT_FOREIGN_KEY_SUFFIX;
296 }
297
298 /**
299 * Factory method used to acquire instances of the given class.
300 * The class name should be supplied as a string, and the class
301 * should already have been loaded by PHP (or a suitable autoloader
302 * should exist). This method actually returns a wrapped ORM object
303 * which allows a database query to be built. The wrapped ORM object is
304 * responsible for returning instances of the correct class when
305 * its find_one or find_many methods are called.
306 *
307 * @param string $class_name The target class name.
308 *
309 * @return ORM Instance of the ORM wrapper.
310 */
311 public static function factory( $class_name ) {
312 $class_name = static::$auto_prefix_models . $class_name;
313 $table_name = static::get_table_name_for_class( $class_name );
314 $wrapper = ORM::for_table( $table_name );
315 $wrapper->set_class_name( $class_name );
316 $wrapper->use_id_column( static::get_id_column_name( $class_name ) );
317
318 return $wrapper;
319 }
320
321 /**
322 * Internal method to construct the queries for both the has_one and
323 * has_many methods. These two types of association are identical; the
324 * only difference is whether find_one or find_many is used to complete
325 * the method chain.
326 *
327 * @param string $associated_class_name The associated class name.
328 * @param string|null $foreign_key_name The foreign key name in the associated table.
329 * @param string|null $foreign_key_name_in_current_models_table The foreign key in the current models table.
330 *
331 * @return ORM Instance of the ORM.
332 *
333 * @throws \Exception When ID of current model has a null value.
334 */
335 protected function has_one_or_many( $associated_class_name, $foreign_key_name = null, $foreign_key_name_in_current_models_table = null ) {
336 $base_table_name = static::get_table_name_for_class( \get_class( $this ) );
337 $foreign_key_name = static::build_foreign_key_name( $foreign_key_name, $base_table_name );
338
339 /*
340 * Value of foreign_table.{$foreign_key_name} we're looking for. Where foreign_table is the actual
341 * database table in the associated model.
342 */
343 if ( $foreign_key_name_in_current_models_table === null ) {
344 // Matches foreign_table.{$foreign_key_name} with the value of "{$this->table}.{$this->id()}".
345 $where_value = $this->id();
346 }
347 else {
348 // Matches foreign_table.{$foreign_key_name} with "{$this->table}.{$foreign_key_name_in_current_models_table}".
349 $where_value = $this->{$foreign_key_name_in_current_models_table};
350 }
351
352 return static::factory( $associated_class_name )->where( $foreign_key_name, $where_value );
353 }
354
355 /**
356 * Helper method to manage one-to-one relations where the foreign
357 * key is on the associated table.
358 *
359 * @param string $associated_class_name The associated class name.
360 * @param string|null $foreign_key_name The foreign key name in the associated table.
361 * @param string|null $foreign_key_name_in_current_models_table The foreign key in the current models table.
362 *
363 * @return ORM Instance of the ORM.
364 *
365 * @throws \Exception When ID of current model has a null value.
366 */
367 protected function has_one( $associated_class_name, $foreign_key_name = null, $foreign_key_name_in_current_models_table = null ) {
368 return $this->has_one_or_many( $associated_class_name, $foreign_key_name, $foreign_key_name_in_current_models_table );
369 }
370
371 /**
372 * Helper method to manage one-to-many relations where the foreign
373 * key is on the associated table.
374 *
375 * @param string $associated_class_name The associated class name.
376 * @param string|null $foreign_key_name The foreign key name in the associated table.
377 * @param string|null $foreign_key_name_in_current_models_table The foreign key in the current models table.
378 *
379 * @return ORM Instance of the ORM.
380 *
381 * @throws \Exception When ID has a null value.
382 */
383 protected function has_many( $associated_class_name, $foreign_key_name = null, $foreign_key_name_in_current_models_table = null ) {
384 $this->set_table_name( $associated_class_name );
385
386 return $this->has_one_or_many( $associated_class_name, $foreign_key_name, $foreign_key_name_in_current_models_table );
387 }
388
389 /**
390 * Helper method to manage one-to-one and one-to-many relations where
391 * the foreign key is on the base table.
392 *
393 * @param string $associated_class_name The associated class name.
394 * @param string|null $foreign_key_name The foreign key in the current models table.
395 * @param string|null $foreign_key_name_in_associated_models_table The foreign key in the associated table.
396 *
397 * @return $this|null Instance of the foreign model.
398 */
399 protected function belongs_to( $associated_class_name, $foreign_key_name = null, $foreign_key_name_in_associated_models_table = null ) {
400 $this->set_table_name( $associated_class_name );
401
402 $associated_table_name = static::get_table_name_for_class( static::$auto_prefix_models . $associated_class_name );
403 $foreign_key_name = static::build_foreign_key_name( $foreign_key_name, $associated_table_name );
404 $associated_object_id = $this->{$foreign_key_name};
405 $desired_record = null;
406 if ( $foreign_key_name_in_associated_models_table === null ) {
407 /*
408 * Comparison: "{$associated_table_name}.primary_key = {$associated_object_id}".
409 *
410 * NOTE: primary_key is a placeholder for the actual primary key column's name in $associated_table_name.
411 */
412 return static::factory( $associated_class_name )->where_id_is( $associated_object_id );
413 }
414
415 // Comparison: "{$associated_table_name}.{$foreign_key_name_in_associated_models_table} = {$associated_object_id}".
416 return static::factory( $associated_class_name )
417 ->where( $foreign_key_name_in_associated_models_table, $associated_object_id );
418 }
419
420 /**
421 * Helper method to manage many-to-many relationships via an intermediate model. See
422 * README for a full explanation of the parameters.
423 *
424 * @param string $associated_class_name The associated class name.
425 * @param string|null $join_class_name The class name to join.
426 * @param string|null $key_to_base_table The key to the the current models table.
427 * @param string|null $key_to_associated_table The key to the associated table.
428 * @param string|null $key_in_base_table The key in the current models table.
429 * @param string|null $key_in_associated_table The key in the associated table.
430 *
431 * @return ORM Instance of the ORM.
432 */
433 protected function has_many_through( $associated_class_name, $join_class_name = null, $key_to_base_table = null, $key_to_associated_table = null, $key_in_base_table = null, $key_in_associated_table = null ) {
434 $base_class_name = \get_class( $this );
435
436 /*
437 * The class name of the join model, if not supplied, is formed by
438 * concatenating the names of the base class and the associated class,
439 * in alphabetical order.
440 */
441 if ( $join_class_name === null ) {
442 $base_model = \explode( '\\', $base_class_name );
443 $base_model_name = \end( $base_model );
444 if ( \strpos( $base_model_name, static::$auto_prefix_models ) === 0 ) {
445 $base_model_name = \substr( $base_model_name, \strlen( static::$auto_prefix_models ), \strlen( $base_model_name ) );
446 }
447 // Paris wasn't checking the name settings for the associated class.
448 $associated_model = \explode( '\\', $associated_class_name );
449 $associated_model_name = \end( $associated_model );
450 if ( \strpos( $associated_model_name, static::$auto_prefix_models ) === 0 ) {
451 $associated_model_name = \substr( $associated_model_name, \strlen( static::$auto_prefix_models ), \strlen( $associated_model_name ) );
452 }
453 $class_names = [ $base_model_name, $associated_model_name ];
454 \sort( $class_names, \SORT_STRING );
455 $join_class_name = \implode( '', $class_names );
456 }
457
458 // Get table names for each class.
459 $base_table_name = static::get_table_name_for_class( $base_class_name );
460 $associated_table_name = static::get_table_name_for_class( static::$auto_prefix_models . $associated_class_name );
461 $join_table_name = static::get_table_name_for_class( static::$auto_prefix_models . $join_class_name );
462
463 // Get ID column names.
464 $base_table_id_column = ( $key_in_base_table === null ) ? static::get_id_column_name( $base_class_name ) : $key_in_base_table;
465 $associated_table_id_column = ( $key_in_associated_table === null ) ? static::get_id_column_name( static::$auto_prefix_models . $associated_class_name ) : $key_in_associated_table;
466
467 // Get the column names for each side of the join table.
468 $key_to_base_table = static::build_foreign_key_name( $key_to_base_table, $base_table_name );
469 $key_to_associated_table = static::build_foreign_key_name( $key_to_associated_table, $associated_table_name );
470
471 /* phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- Reason: This is commented out code.
472 " SELECT {$associated_table_name}.*
473 FROM {$associated_table_name} JOIN {$join_table_name}
474 ON {$associated_table_name}.{$associated_table_id_column} = {$join_table_name}.{$key_to_associated_table}
475 WHERE {$join_table_name}.{$key_to_base_table} = {$this->$base_table_id_column} ;"
476 */
477
478 return static::factory( $associated_class_name )
479 ->select( "{$associated_table_name}.*" )
480 ->join(
481 $join_table_name,
482 [
483 "{$associated_table_name}.{$associated_table_id_column}",
484 '=',
485 "{$join_table_name}.{$key_to_associated_table}",
486 ]
487 )
488 ->where( "{$join_table_name}.{$key_to_base_table}", $this->{$base_table_id_column} );
489 }
490
491 /**
492 * Set the wrapped ORM instance associated with this Model instance.
493 *
494 * @param ORM $orm The ORM instance to set.
495 *
496 * @return void
497 */
498 public function set_orm( $orm ) {
499 $this->orm = $orm;
500 }
501
502 /**
503 * Magic getter method, allows $model->property access to data.
504 *
505 * @param string $property The property to get.
506 *
507 * @return mixed The value of the property
508 */
509 public function __get( $property ) {
510 $value = $this->orm->get( $property );
511
512 if ( $value !== null && \in_array( $property, $this->boolean_columns, true ) ) {
513 return (bool) $value;
514 }
515 if ( $value !== null && \in_array( $property, $this->int_columns, true ) ) {
516 return (int) $value;
517 }
518 if ( $value !== null && \in_array( $property, $this->float_columns, true ) ) {
519 return (float) $value;
520 }
521
522 return $value;
523 }
524
525 /**
526 * Magic setter method, allows $model->property = 'value' access to data.
527 *
528 * @param string $property The property to set.
529 * @param string $value The value to set.
530 *
531 * @return void
532 */
533 public function __set( $property, $value ) {
534 if ( $value !== null && \in_array( $property, $this->boolean_columns, true ) ) {
535 $value = ( $value ) ? '1' : '0';
536 }
537 if ( $value !== null && \in_array( $property, $this->int_columns, true ) ) {
538 $value = (string) $value;
539 }
540 if ( $value !== null && \in_array( $property, $this->float_columns, true ) ) {
541 $value = (string) $value;
542 }
543
544 $this->orm->set( $property, $value );
545 }
546
547 /**
548 * Magic unset method, allows unset($model->property)
549 *
550 * @param string $property The property to unset.
551 *
552 * @return void
553 */
554 public function __unset( $property ) {
555 $this->orm->__unset( $property );
556 }
557
558 /**
559 * JSON serializer.
560 *
561 * @return array The data of this object.
562 */
563 #[ReturnTypeWillChange]
564 public function jsonSerialize() {
565 return $this->orm->as_array();
566 }
567
568 /**
569 * Strips all nested dependencies from the debug info.
570 *
571 * @return array
572 */
573 public function __debugInfo() {
574 return $this->orm->as_array();
575 }
576
577 /**
578 * Magic isset method, allows isset($model->property) to work correctly.
579 *
580 * @param string $property The property to check.
581 *
582 * @return bool True when value is set.
583 */
584 public function __isset( $property ) {
585 return $this->orm->__isset( $property );
586 }
587
588 /**
589 * Getter method, allows $model->get('property') access to data
590 *
591 * @param string $property The property to get.
592 *
593 * @return string The value of a property.
594 */
595 public function get( $property ) {
596 return $this->orm->get( $property );
597 }
598
599 /**
600 * Setter method, allows $model->set('property', 'value') access to data.
601 *
602 * @param string|array $property The property to set.
603 * @param string|null $value The value to give.
604 *
605 * @return static Current object.
606 */
607 public function set( $property, $value = null ) {
608 $this->orm->set( $property, $value );
609
610 return $this;
611 }
612
613 /**
614 * Setter method, allows $model->set_expr('property', 'value') access to data.
615 *
616 * @param string|array $property The property to set.
617 * @param string|null $value The value to give.
618 *
619 * @return static Current object.
620 */
621 public function set_expr( $property, $value = null ) {
622 $this->orm->set_expr( $property, $value );
623
624 return $this;
625 }
626
627 /**
628 * Check whether the given property has changed since the object was created or saved.
629 *
630 * @param string $property The property to check.
631 *
632 * @return bool True when field is changed.
633 */
634 public function is_dirty( $property ) {
635 return $this->orm->is_dirty( $property );
636 }
637
638 /**
639 * Check whether the model was the result of a call to create() or not.
640 *
641 * @return bool True when is new.
642 */
643 public function is_new() {
644 return $this->orm->is_new();
645 }
646
647 /**
648 * Wrapper for Idiorm's as_array method.
649 *
650 * @return array The models data as array.
651 */
652 public function as_array() {
653 $args = \func_get_args();
654
655 return \call_user_func_array( [ $this->orm, 'as_array' ], $args );
656 }
657
658 /**
659 * Save the data associated with this model instance to the database.
660 *
661 * @return null Nothing.
662 */
663 public function save() {
664 if ( $this->uses_timestamps ) {
665 if ( ! $this->created_at ) {
666 $this->created_at = \gmdate( 'Y-m-d H:i:s' );
667 }
668 $this->updated_at = \gmdate( 'Y-m-d H:i:s' );
669 }
670
671 return $this->orm->save();
672 }
673
674 /**
675 * Delete the database row associated with this model instance.
676 *
677 * @return null Nothing.
678 */
679 public function delete() {
680 return $this->orm->delete();
681 }
682
683 /**
684 * Get the database ID of this model instance.
685 *
686 * @return int The database ID of the models instance.
687 *
688 * @throws \Exception When the ID is a null value.
689 */
690 public function id() {
691 return $this->orm->id();
692 }
693
694 /**
695 * Hydrate this model instance with an associative array of data.
696 * WARNING: The keys in the array MUST match with columns in the
697 * corresponding database table. If any keys are supplied which
698 * do not match up with columns, the database will throw an error.
699 *
700 * @param array $data The data to pass to the ORM.
701 *
702 * @return void
703 */
704 public function hydrate( $data ) {
705 $this->orm->hydrate( $data )->force_all_dirty();
706 }
707
708 /**
709 * Calls static methods directly on the ORM
710 *
711 * @param string $method The method to call.
712 * @param array $arguments The arguments to use.
713 *
714 * @return array Result of the static call.
715 */
716 public static function __callStatic( $method, $arguments ) {
717 if ( ! \function_exists( 'get_called_class' ) ) {
718 return [];
719 }
720
721 $model = static::factory( \get_called_class() );
722
723 return \call_user_func_array( [ $model, $method ], $arguments );
724 }
725 }
726