<?php
namespace Yoast\WP\Lib;
use Exception;
use JsonSerializable;
use ReturnTypeWillChange;
/**
* Make Model compatible with WordPress.
*
* Model base class. Your model objects should extend
* this class. A minimal subclass would look like:
*
* class Widget extends Model {
* }
*/
class Model implements JsonSerializable {
/**
* Default ID column for all models. Can be overridden by adding
* a public static $id_column property to your model classes.
*
* @var string
*/
public const DEFAULT_ID_COLUMN = 'id';
/**
* Default foreign key suffix used by relationship methods.
*
* @var string
*/
public const DEFAULT_FOREIGN_KEY_SUFFIX = '_id';
/**
* Set a prefix for model names. This can be a namespace or any other
* abitrary prefix such as the PEAR naming convention.
*
* @example Model::$auto_prefix_models = 'MyProject_MyModels_'; //PEAR
* @example Model::$auto_prefix_models = '\MyProject\MyModels\'; //Namespaces
*
* @var string
*/
public static $auto_prefix_models = '\Yoast\WP\SEO\Models\\';
/**
* Set true to to ignore namespace information when computing table names
* from class names.
*
* @example Model::$short_table_names = true;
* @example Model::$short_table_names = false; // default
*
* @var bool
*/
public static $short_table_names = false;
/**
* The ORM instance used by this model instance to communicate with the database.
*
* @var ORM
*/
public $orm;
/**
* The table name for the implemented Model.
*
* @var string
*/
public static $table;
/**
* Whether or not this model uses timestamps.
*
* @var bool
*/
protected $uses_timestamps = false;
/**
* Which columns contain boolean values.
*
* @var array
*/
protected $boolean_columns = [];
/**
* Which columns contain int values.
*
* @var array
*/
protected $int_columns = [];
/**
* Which columns contain float values.
*
* @var array
*/
protected $float_columns = [];
/**
* Hacks around the Model to provide WordPress prefix to tables.
*
* @param string $class_name Type of Model to load.
* @param bool $yoast_prefix Optional. True to prefix the table name with the Yoast prefix.
*
* @return ORM Wrapper to use.
*/
public static function of_type( $class_name, $yoast_prefix = true ) {
// Prepend namespace to the class name.
$class = static::$auto_prefix_models . $class_name;
// Set the class variable to the custom value based on the WPDB prefix.
$class::$table = static::get_table_name( $class_name, $yoast_prefix );
return static::factory( $class_name, null );
}
/**
* Creates a model without the Yoast prefix.
*
* @param string $class_name Type of Model to load.
*
* @return ORM
*/
public static function of_wp_type( $class_name ) {
return static::of_type( $class_name, false );
}
/**
* Exposes method to get the table name to use.
*
* @param string $table_name Simple table name.
* @param bool $yoast_prefix Optional. True to prefix the table name with the Yoast prefix.
*
* @return string Prepared full table name.
*/
public static function get_table_name( $table_name, $yoast_prefix = true ) {
global $wpdb;
// Allow the use of WordPress internal tables.
if ( $yoast_prefix ) {
$table_name = 'yoast_' . $table_name;
}
return $wpdb->prefix . \strtolower( $table_name );
}
/**
* Sets the table name for the given class name.
*
* @param string $class_name The class to set the table name for.
*
* @return void
*/
protected function set_table_name( $class_name ) {
// Prepend namespace to the class name.
$class = static::$auto_prefix_models . $class_name;
$class::$table = static::get_table_name( $class_name );
}
/**
* Retrieve the value of a static property on a class. If the
* class or the property does not exist, returns the default
* value supplied as the third argument (which defaults to null).
*
* @param string $class_name The target class name.
* @param string $property The property to get the value for.
* @param mixed|null $default_value Default value when property does not exist.
*
* @return mixed|null The value of the property.
*/
protected static function get_static_property( $class_name, $property, $default_value = null ) {
if ( ! \class_exists( $class_name ) || ! \property_exists( $class_name, $property ) ) {
return $default_value;
}
if ( ! isset( $class_name::${$property} ) ) {
return $default_value;
}
return $class_name::${$property};
}
/**
* Static method to get a table name given a class name.
* If the supplied class has a public static property
* named $table, the value of this property will be
* returned.
*
* If not, the class name will be converted using
* the class_name_to_table_name() method.
*
* If Model::$short_table_names == true or public static
* property $table_use_short_name == true then $class_name passed
* to class_name_to_table_name() is stripped of namespace information.
*
* @param string $class_name The class name to get the table name for.
*
* @return string The table name.
*/
protected static function get_table_name_for_class( $class_name ) {
$specified_table_name = static::get_static_property( $class_name, 'table' );
$use_short_class_name = static::use_short_table_name( $class_name );
if ( $use_short_class_name ) {
$exploded_class_name = \explode( '\\', $class_name );
$class_name = \end( $exploded_class_name );
}
if ( $specified_table_name === null ) {
return static::class_name_to_table_name( $class_name );
}
return $specified_table_name;
}
/**
* Should short table names, disregarding class namespaces, be computed?
*
* $class_property overrides $global_option, unless $class_property is null.
*
* @param string $class_name The class name to get short name for.
*
* @return bool True when short table name should be used.
*/
protected static function use_short_table_name( $class_name ) {
$class_property = static::get_static_property( $class_name, 'table_use_short_name' );
if ( $class_property === null ) {
return static::$short_table_names;
}
return $class_property;
}
/**
* Convert a namespace to the standard PEAR underscore format.
*
* Then convert a class name in CapWords to a table name in
* lowercase_with_underscores.
*
* Finally strip doubled up underscores.
*
* For example, CarTyre would be converted to car_tyre. And
* Project\Models\CarTyre would be project_models_car_tyre.
*
* @param string $class_name The class name to get the table name for.
*
* @return string The table name.
*/
protected static function class_name_to_table_name( $class_name ) {
$find = [
'/\\\\/',
'/(?<=[a-z])([A-Z])/',
'/__/',
];
$replacements = [
'_',
'_$1',
'_',
];
$class_name = \ltrim( $class_name, '\\' );
$class_name = \preg_replace( $find, $replacements, $class_name );
return \strtolower( $class_name );
}
/**
* Return the ID column name to use for this class. If it is
* not set on the class, returns null.
*
* @param string $class_name The class name to get the ID column for.
*
* @return string|null The ID column name.
*/
protected static function get_id_column_name( $class_name ) {
return static::get_static_property( $class_name, 'id_column', static::DEFAULT_ID_COLUMN );
}
/**
* Build a foreign key based on a table name. If the first argument
* (the specified foreign key column name) is null, returns the second
* argument (the name of the table) with the default foreign key column
* suffix appended.
*
* @param string $specified_foreign_key_name The keyname to build.
* @param string $table_name The table name to build the key name for.
*
* @return string The built foreign key name.
*/
protected static function build_foreign_key_name( $specified_foreign_key_name, $table_name ) {
if ( $specified_foreign_key_name !== null ) {
return $specified_foreign_key_name;
}
return $table_name . static::DEFAULT_FOREIGN_KEY_SUFFIX;
}
/**
* Factory method used to acquire instances