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

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

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