model = sanitize_key((new \ReflectionClass($this))->getShortName()); if (is_array($object)) { $object = (object) $object; } // end if; if (!is_object($object)) { return; } // end if; $this->setup_model($object); } // end __construct; /** * Get the value of slug * * @return mixed */ public function get_slug() { return $this->slug; } // end get_slug; /** * Set the value of slug * * @param mixed $slug The slug. */ public function set_slug($slug) { $this->slug = $slug; } // end set_slug; /** * Returns a hashed version of the id. Useful for displaying data publicly. * * @param string $field Field to use to generate the hash. * @since 2.0.0 * @return string|false */ public function get_hash($field = 'id') { $value = call_user_func(array($this, "get_{$field}")); if (!is_numeric($value)) { _doing_it_wrong(__METHOD__, __('You can only use numeric fields to generate hashes.', 'wp-ultimo'), '2.0.0'); return false; } // end if; return Hash::encode($value, $this->model); } // end get_hash; /** * Setup properties. * * @param object $object Row from the database. * * @access private * @since 2.0.0 * @return bool */ private function setup_model($object) { if (!is_object($object)) { return false; } // end if; $vars = get_object_vars($object); $this->attributes($vars); if (empty($this->id)) { return false; } // end if; return true; } // end setup_model; /** * Sets the attributes of the model using the setters available. * * @since 2.0.0 * * @param array $atts Key-value pairs of model attributes. * @return \WP_Ultimo\Models\Base_Model */ public function attributes($atts) { foreach ($atts as $key => $value) { if ($key === 'meta' && is_array($value)) { $this->meta = is_array($this->meta) ? array_merge($this->meta, $value) : $value; } // end if; if (method_exists($this, "set_$key")) { call_user_func(array($this, "set_$key"), $value); } // end if; $mapping = wu_get_isset($this->_mappings, $key); if ($mapping && method_exists($this, "set_$mapping")) { call_user_func(array($this, "set_$mapping"), $value); } // end if; } // end foreach; /* * Keeps the original. */ if ($this->_original === null) { $original = get_object_vars($this); unset($original['_original']); $this->_original = $original; } // end if; return $this; } // end attributes; /** * Return the model schema. useful to list all models fields. * * @since 2.0.0 * @return Schema */ public static function get_schema() { $instance = new static(); $query_class = new $instance->query_class(); $reflector = new \ReflectionObject($query_class); $method = $reflector->getMethod('get_columns'); $method->setAccessible(true); $columns = $method->invoke($query_class); return array_map( fn($column) => $column->to_array(), $columns ); } // end get_schema; /** * Checks if this model was already saved to the database. * * @since 2.0.0 * @return bool */ public function exists() { return !empty($this->id); } // end exists; /** * Gets a single database row by the primary column ID, possibly from cache. * * @since 2.0.0 * * @param int $item_id The item id. * * @return object|false Base_Model */ public static function get_by_id($item_id) { if (empty($item_id)) { return false; } // end if; $instance = new static(); $query_class = new $instance->query_class(); return $query_class->get_item($item_id); } // end get_by_id; /** * Gets a single database row by the hash, possibly from cache. * * @since 2.0.0 * * @param string $item_hash The item hash. * * @return Base_Model|false */ public static function get_by_hash($item_hash) { $instance = new static(); $item_id = Hash::decode($item_hash, sanitize_key((new \ReflectionClass(static::class))->getShortName())); $query_class = new $instance->query_class(); return $query_class->get_item($item_id); } // end get_by_hash; /** * Gets a model instance by a column value. * * @since 2.0.0 * * @param string $column The name of the column to query for. * @param string $value Value to search for. * @return Base_Model|false */ public static function get_by($column, $value) { $instance = new static(); $query_class = new $instance->query_class(); return $query_class->get_item_by($column, $value); } // end get_by; /** * Wrapper for a Query call. * * @since 2.0.0 * * @param array $query Arguments for the query. * @return array|int List of items, or number of items when 'count' is passed as a query var. */ public static function get_items($query) { $instance = new static(); return (new $instance->query_class($query))->query(); } // end get_items; /** * Wrapper for a Query call, but returns the list as arrays. * * @since 2.0.0 * * @param array $query Arguments for the query. * @return array|int List of items, or number of items when 'count' is passed as a query var. */ public static function get_items_as_array($query = array()) { $instance = new static(); $list = (new $instance->query_class($query))->query(); return array_map(fn($item) => $item->to_array(), $list); } // end get_items_as_array; /** * Get the ID of the model. * * @access public * @since 2.0.0 * @return int */ public function get_id() { return absint($this->id); } // end get_id; /** * Check if this model has a job running. * * @since 2.0.0 * @return mixed */ public function has_running_jobs() { $jobs = wu_get_scheduled_actions(array( 'status' => \ActionScheduler_Store::STATUS_RUNNING, 'args' => array( "{$this->model}_id" => $this->get_id(), ), )); return $jobs; } // end has_running_jobs; /** * Set iD of the object. * * @since 2.0.0 * @param integer $id ID of the object. * @return void */ private function set_id($id) { $this->id = $id; } // end set_id; /** * Set the validation rules for this particular model. * * To see how to setup rules, check the documentation of the * validation library we are using: https://github.com/rakit/validation * * @since 2.0.0 * @link https://github.com/rakit/validation * @return array */ public function validation_rules() { return array(); } // end validation_rules; /** * Validates the rules and make sure we only save models when necessary. * * @since 2.0.0 * @return true|\WP_Error */ public function validate() { if ($this->skip_validation) { return true; } // end if; $validator = new \WP_Ultimo\Helpers\Validator; $validator->validate($this->to_array(), $this->validation_rules()); if ($validator->fails()) { return $validator->get_errors(); } // end if; foreach ($validator->get_validation()->getValidData() as $key => $value) { $this->{$key} = $value; } // end foreach; return true; } // end validate; /** * Save (create or update) the model on the database. * * @since 2.0.0 * * @return bool|\WP_Error */ public function save() { $query_class = new $this->query_class(); $data = get_object_vars($this); if (isset($data['id']) && empty($data['id'])) { unset($data['id']); } // end if; unset($data['_original']); $data_unserialized = $data; $meta = wu_get_isset($data, 'meta', array()); $new = !$this->exists(); /** * Filters the data meta before it is serialized to be stored into the database. * * @since 2.0.0 * * @param array $meta The meta data that will be stored, unserializedserialized. * @param array $data_unserialized The object data that will be stored. * @param Base_Model $this The object instance. */ $meta = apply_filters("wu_{$this->model}_meta_pre_save", $meta, $data_unserialized, $this); $blocked_attributes = array( 'query_class', 'meta', ); foreach ($blocked_attributes as $attribute) { unset($data[$attribute]); } // end foreach; $this->validate(); $data = array_map('maybe_serialize', $data); $data = array_map(function($_data) { if (is_serialized($_data)) { $_data = addslashes($_data); } // end if; return $_data; }, $data); /** * Filters the object data before it is stored into the database. * * @since 2.0.0 * * @param array $data The object data that will be stored, serialized. * @param array $data_unserialized The object data that will be stored. * @param Base_Model $this The object instance. */ $data = apply_filters("wu_{$this->model}_pre_save", $data, $data_unserialized, $this); $is_valid_data = $this->validate(); if (is_wp_error($is_valid_data) && !$this->skip_validation) { return $is_valid_data; } // end if; $saved = false; if (!$this->get_id()) { $new_id = $query_class->add_item($data); if ($new_id) { $this->id = $new_id; $saved = true; } // end if; } else { $saved = $query_class->update_item($this->get_id(), $data); } // end if; if (!empty($meta)) { $this->update_meta_batch($meta); $saved = true; } // end if; /** * Delete object cache to prevent errors. * As BerlinDB groups are protected, we try * guess the group name with model name. */ wp_cache_delete($this->get_id(), "wu-{$this->model}s"); /** * Fires after an object is stored into the database. * * @since 2.0.0 * * @param string $model The model slug. * @param array $data The object data that will be stored, serialized. * @param array $data_unserialized The object data that will be stored. * @param Base_Model $this The object instance. */ do_action('wu_model_post_save', $this->model, $data, $data_unserialized, $this); /** * Fires after an object is stored into the database. * * @since 2.0.0 * * @param array $data The object data that will be stored. * @param Base_Model $this The object instance. * @param bool $new True if the object is new. */ do_action("wu_{$this->model}_post_save", $data, $this, $new); return $saved; } // end save; /** * Delete the model from the database. * * @since 2.0.0 * * @return \WP_Error|bool */ public function delete() { if (!$this->get_id()) { return new \WP_Error("wu_{$this->model}_delete_unsaved_item", __('Item not found.', 'wp-ultimo')); } // end if; /** * Fires after an object is stored into the database. * * @since 2.0.0 * * @param Base_Model $this The object instance. */ do_action("wu_{$this->model}_pre_delete", $this); $query_class = new $this->query_class(); $result = $query_class->delete_item($this->get_id()); /** * Fires after an object is stored into the database. * * @since 2.0.0 * * @param bool $result True if the object was successfully deleted. * @param Base_Model $this The object instance. */ do_action("wu_{$this->model}_post_delete", $result, $this); return $result; } // end delete; /** * Returns the meta type name. * * @since 2.0.0 * * @return mixed Meta type name */ private function get_meta_type_name() { $query_class = new $this->query_class(); // Maybe apply table prefix $table = !empty($query_class->prefix) ? "{$query_class->prefix}_{$query_class->item_name}" : $query_class->item_name; // Return table if exists, or false if not return $table; } // end get_meta_type_name; /** * Returns the meta table name. * * @since 2.0.0 * * @return string|false Table name if exists, False if not */ private function get_meta_table_name() { $table = $this->get_meta_type_name(); return _get_meta_table($table); } // end get_meta_table_name; /** * Checks if metadata handling is available, i.e., if there is a meta table * for this model and if the object already has an ID set. * * @since 2.0.0 * @return boolean */ protected function is_meta_available() { if (!$this->get_meta_table_name()) { // _doing_it_wrong(__METHOD__, __('This model does not support metadata.', 'wp-ultimo'), '2.0.0'); return false; } // end if; if (!$this->get_id() && !$this->_mocked) { // _doing_it_wrong(__METHOD__, __('Model metadata only works for already saved models.', 'wp-ultimo'), '2.0.0'); return false; } // end if; return true; } // end is_meta_available; /** * Returns the meta data, if set. Otherwise, returns the default. * * @since 2.0.0 * * @param string $key The meta key. * @param mixed $default The default value to be passed. * @param bool $single To return single values or not. * @return mixed */ public function get_meta($key, $default = false, $single = true) { if (!$this->is_meta_available()) { return $default; } // end if; $meta_type = $this->get_meta_type_name(); if (metadata_exists($meta_type, $this->get_id(), $key)) { return get_metadata($meta_type, $this->get_id(), $key, $single); } // end if; return $default; } // end get_meta; /** * Adds or updates meta data in batch. * * @since 2.0.0 * * @param array $meta An array of meta data in `'key' => 'value'` format. * @return bool True on successful update, false on failure. */ public function update_meta_batch($meta) { if (!$this->is_meta_available()) { return false; } // end if; if (!is_array($meta)) { _doing_it_wrong(__METHOD__, __('This method expects an array as argument.', 'wp-ultimo'), '2.0.0'); return false; } // end if; $meta_type = $this->get_meta_type_name(); $success = true; foreach ($meta as $key => $value) { update_metadata($meta_type, $this->get_id(), $key, $value); } // end foreach; return $success; } // end update_meta_batch; /** * Adds or updates the meta data. * * @since 2.0.0 * * @param string $key The meta key. * @param mixed $value The new meta value. * @return int|bool The new meta field ID if a field with the given key didn't exist and was * therefore added, true on successful update, false on failure. */ public function update_meta($key, $value) { if (!$this->is_meta_available()) { return false; } // end if; $meta_type = $this->get_meta_type_name(); return update_metadata($meta_type, $this->get_id(), $key, $value); } // end update_meta; /** * Deletes the meta data. * * @since 2.0.0 * * @param string $key The meta key. * @return bool True on successful delete, false on failure. */ public function delete_meta($key) { if (!$this->is_meta_available()) { return false; } // end if; $meta_type = $this->get_meta_type_name(); return delete_metadata($meta_type, $this->get_id(), $key); } // end delete_meta; /** * Queries object in the database. * * @since 2.0.0 * * @param array $args Query arguments. * @return array|int List of items, or number of items when 'count' is passed as a query var. */ public static function query($args = array()) { $instance = new static(); $query_class = new $instance->query_class(); $items = $query_class->query($args); return $items; } // end query; /** * Transform the object into an assoc array. * * @since 2.0.0 * @return array */ public function to_array() { $array = get_object_vars($this); unset($array['query_class']); unset($array['skip_validation']); unset($array['meta']); unset($array['meta_fields']); unset($array['_original']); unset($array['_mappings']); unset($array['_mocked']); foreach ($array as $key => $value) { if (strncmp('_', $key, strlen($key)) === 0) { unset($array[$key]); } // end if; } // end foreach; return $array; } // end to_array; /** * Convert data to Mapping instance * * Allows use as a callback, such as in `array_map` * * @param stdClass $data Raw mapping data. * @return Mapping */ protected static function to_instance($data) { return new static($data); } // end to_instance; /** * Convert list of data to Mapping instances * * @param stdClass[] $data Raw mapping rows. * @return Domain[] */ protected static function to_instances($data) { return array_map(array(get_called_class(), 'to_instance'), $data); } // end to_instances; /** * By default, we just use the to_array method, but you can rewrite this. * * @since 2.0.0 * @return array */ public function to_search_results() { return $this->to_array(); } // end to_search_results; /** * Defines how we should encode this. * * @since 2.0.0 * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { if (wp_doing_ajax() && wu_request('action') === 'wu_search') { return $this->to_search_results(); } // end if; return $this->to_array(); } // end jsonSerialize; /** * Get the date when this model was created. * * @since 2.0.0 * @return string */ public function get_date_created() { if (!wu_validate_date($this->date_created)) { return wu_get_current_time('mysql'); } // end if; return $this->date_created; } // end get_date_created; /** * Get the date when this model was last modified. * * @since 2.0.0 * @return string */ public function get_date_modified() { if (!wu_validate_date($this->date_modified)) { return wu_get_current_time('mysql'); } // end if; return $this->date_modified; } // end get_date_modified; /** * Set model creation date. * * @since 2.0.0 * @param string $date_created Model creation date. * @return void */ public function set_date_created($date_created) { $this->date_created = $date_created; } // end set_date_created; /** * Set model last modification date. * * @since 2.0.0 * @param string $date_modified Model last modification date. * @return void */ public function set_date_modified($date_modified) { $this->date_modified = $date_modified; } // end set_date_modified; /** * Get the id of the original 1.X model that was used to generate this item on migration. * * @since 2.0.0 * @return int */ public function get_migrated_from_id() { return $this->migrated_from_id; } // end get_migrated_from_id; /** * Set the id of the original 1.X model that was used to generate this item on migration. * * @since 2.0.0 * @param int $migrated_from_id The ID of the original 1.X model that was used to generate this item on migration. * @return void */ public function set_migrated_from_id($migrated_from_id) { $this->migrated_from_id = absint($migrated_from_id); } // end set_migrated_from_id; /** * Checks if this model is a migration from 1.X. * * @since 2.0.0 * @return boolean */ public function is_migrated() { return !empty($this->get_migrated_from_id()); } // end is_migrated; /** * Helper method to return formatted values. * * Deals with: * - currency values; * * @since 2.0.0 * * @param string $key The key to return. * @return mixed */ public function get_formatted_amount($key = 'amount') { $value = (float) $this->{"get_{$key}"}(); if (is_numeric($value)) { return wu_format_currency($value); } // end if; return $value; } // end get_formatted_amount; /** * Helper method to return formatted dates. * * Deals with: * - dates * * @since 2.0.0 * * @param string $key The key to return. * @return mixed */ public function get_formatted_date($key = 'date_created') { $value = $this->{"get_{$key}"}(); if (wu_validate_date($value)) { return date_i18n(get_option('date_format'), wu_date($value)->format('U')); } // end if; return $value; } // end get_formatted_date; /** * Get all items. * * @since 2.0.0 * * @param array $query_args If you need to select a type to get all. * @return array With all items requested. */ public static function get_all($query_args = array()) { $instance = new static(); $query_class = new $instance->query_class(); $items = $query_class->query($query_args); return $items; } // end get_all; /** * Creates a copy of the given model adn resets it's id to a 'new' state. * * @since 2.0.0 * @return \WP_Ultimo\Model\Base_Model */ public function duplicate() { $this->hydrate(); $clone = clone $this; $clone->set_id(0); if (method_exists($clone, 'set_date_created')) { $clone->set_date_created(wu_get_current_time('mysql')); } // end if; return $clone; } // end duplicate; /** * Populate the data the resides on meta tables. * * @since 2.0.0 * @return void */ public function hydrate() { $attributes = get_object_vars($this); $attributes = array_filter($attributes, fn ($value) => $value === null); unset($attributes['meta']); foreach ($attributes as $attribute => $maybe_null) { $possible_setters = array( "get_{$attribute}", "is_{$attribute}", ); foreach ($possible_setters as $setter) { $setter = method_exists($this, $setter) ? $setter : ''; if (!$setter || !method_exists($this, "set_{$attribute}")) { continue; } // end if; $value = $this->{$setter}(); $this->{"set_{$attribute}"}($value); } // end foreach; } // end foreach; } // end hydrate; /** * Set set this to true to skip validations when saving.. * * @since 2.0.0 * @param boolean $skip_validation Set true to have field information validation bypassed when saving this event. * @return void */ public function set_skip_validation($skip_validation = false) { $this->skip_validation = $skip_validation; } // end set_skip_validation; /** * Returns the original parameters of the object. * * @since 2.0.0 * @return array */ public function _get_original() { return $this->_original; } // end _get_original; /** * Locks this model. * * @since 2.0.0 * @return bool */ public function lock() { return $this->update_meta('wu_lock', true); } // end lock; /** * Check ths lock status of the model. * * @since 2.0.0 * @return boolean */ public function is_locked() { return $this->get_meta('wu_lock', false); } // end is_locked; /** * Unlocks the model. * * @since 2.0.0 * @return bool */ public function unlock() { return $this->delete_meta('wu_lock'); } // end unlock; } // end class Base_Model;