<?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