'Orm\\BelongsTo', 'has_one' => 'Orm\\HasOne', 'has_many' => 'Orm\\HasMany', 'many_many' => 'Orm\\ManyMany', ); /** * @var array global array to track circular references in to_array() */ protected static $to_array_references = array(); /** * Create a new model instance */ public static function forge($data = array(), $new = true, $view = null, $cache = true) { return new static($data, $new, $view, $cache); } /** * Fetch the database connection name to use * * @param bool if true return the writeable connection (if set) * @return null|string */ public static function connection($writeable = false) { $class = get_called_class(); if ($writeable and property_exists($class, '_write_connection') && static::$_write_connection !== null) { return static::$_write_connection; } return property_exists($class, '_connection') ? static::$_connection : null; } /** * Sets the connection to use for this model. * @param string $connection */ public static function set_connection($connection) { static::$_connection = $connection; } /** * Sets the write connection to use for this model. * @param string $connection */ public static function set_write_connection($connection) { static::$_write_connection = $connection; } /** * Get the table name for this class * * @return string */ public static function table() { $class = get_called_class(); // Table name unknown if ( ! array_key_exists($class, static::$_table_names_cached)) { // Table name set in Model if (property_exists($class, '_table_name')) { static::$_table_names_cached[$class] = static::$_table_name; } else { static::$_table_names_cached[$class] = \Inflector::tableize($class); } } return static::$_table_names_cached[$class]; } /** * Get a defined condition for this class * * @param string type of condition to return * @return array */ public static function condition($type = null) { $class = get_called_class(); // a specific condition requested? if (property_exists($class, '_conditions')) { if ($type !== null) { return isset(static::$_conditions[$type]) ? static::$_conditions[$type] : array(); } else { return static::$_conditions; } } else { return array(); } } /** * Attempt to retrieve an earlier loaded object * * @param array|Model $obj * @param null|string $class * @return Model|false */ public static function cached_object($obj, $class = null) { $class = $class ?: get_called_class(); $id = (is_int($obj) or is_string($obj)) ? (string) $obj : $class::implode_pk($obj); $result = ( ! empty(static::$_cached_objects[$class][$id])) ? static::$_cached_objects[$class][$id] : false; return $result; } /** * Flush the object cache * * @param null|string|object $class */ public static function flush_cache($class = null) { // determine what to flush if (func_num_args() == 0) { $class = get_called_class(); } elseif (is_object($class)) { $class = get_class($class); } elseif (is_string($class)) { $class = ltrim($class, "\\"); } // flush ... if (is_null($class)) { // the entire cache static::$_cached_objects = array(); } elseif (array_key_exists($class, static::$_cached_objects)) { // the requested object cache unset(static::$_cached_objects[$class]); } } /** * Get the primary key(s) of this class * * @return array */ public static function primary_key() { return static::$_primary_key; } /** * Implode the primary keys within the data into a unique string for the runtime * * Note: as explained below, it is not possible to use the output to match an item after runtime * - The format contains no reference to which value is destined for which key * - The format does not encode the values * * @internal Designed for internal use only * * @param array * @return string */ public static function implode_pk($data) { if (count(static::$_primary_key) == 1) { $p = reset(static::$_primary_key); return (is_object($data) ? strval($data->{$p}) : (isset($data[$p]) ? strval($data[$p]) : null)); } $pk = ''; foreach (static::$_primary_key as $p) { if (is_null((is_object($data) ? $data->{$p} : (isset($data[$p]) ? $data[$p] : null)))) { return null; } $pk .= '['.(is_object($data) ? $data->{$p} : $data[$p]).']'; } return $pk; } /** * Get the class's properties * * @throws \FuelException Listing columns failed * * @return array */ public static function properties() { $class = get_called_class(); // If already determined if (array_key_exists($class, static::$_properties_cached)) { return static::$_properties_cached[$class]; } // Try to grab the properties from the class... if (property_exists($class, '_properties')) { $properties = static::$_properties; foreach ($properties as $key => $p) { if (is_string($p)) { unset($properties[$key]); $properties[$p] = array(); } } } // ...if the above failed, run DB query to fetch properties if (empty($properties)) { try { $properties = \DB::list_columns(static::table(), null, static::connection()); } catch (\Exception $e) { throw new \FuelException('Listing columns failed, you have to set the model properties with a '. 'static $_properties setting in the model. Original exception: '.$e->getMessage()); } } // cache the properties for next usage static::$_properties_cached[$class] = $properties; return static::$_properties_cached[$class]; } /** * Fetches a property description array, or specific data from it * * @param string property or property.key * @param mixed return value when key not present * @return mixed */ public static function property($key, $default = null) { $class = get_called_class(); // If already determined if ( ! array_key_exists($class, static::$_properties_cached)) { static::properties(); } return \Arr::get(static::$_properties_cached[$class], $key, $default); } /** * Fetch the model's views * * @throws \InvalidArgumentException Database view is defined without columns * * @return array */ public static function views() { $class = get_called_class(); if ( ! isset(static::$_views_cached[$class])) { static::$_views_cached[$class] = array(); if (property_exists($class, '_views')) { $views = $class::$_views; foreach ($views as $k => $v) { if ( ! isset($v['columns'])) { throw new \InvalidArgumentException('Database view '.$k.' is defined without columns.'); } $v['columns'] = (array) $v['columns']; if ( ! isset($v['view'])) { $v['view'] = $k; } static::$_views_cached[$class][$k] = $v; } } } return static::$_views_cached[$class]; } /** * Get the class's relations * * @param bool $specific * @return HasOne|HasMany|ManyMany|Belongsto|HasOne[]|HasMany[]|ManyMany[]|Belongsto[] */ public static function relations($specific = false) { $class = get_called_class(); if ( ! array_key_exists($class, static::$_relations_cached)) { $relations = array(); foreach (static::$_valid_relations as $rel_name => $rel_class) { if (property_exists($class, '_'.$rel_name)) { foreach (static::${'_'.$rel_name} as $key => $settings) { $name = is_string($settings) ? $settings : $key; $settings = is_array($settings) ? $settings : array(); $relations[$name] = new $rel_class($class, $name, $settings); } } } static::$_relations_cached[$class] = $relations; } if ($specific === false) { return static::$_relations_cached[$class]; } else { if ( ! array_key_exists($specific, static::$_relations_cached[$class])) { return false; } return static::$_relations_cached[$class][$specific]; } } /** * Get the name of the class that defines a relation * * @param string * @return array */ public static function related_class($relation) { $class = get_called_class(); foreach (static::$_valid_relations as $rel_name => $rel_class) { if (property_exists($class, '_'.$rel_name)) { foreach (static::${'_'.$rel_name} as $key => $value) { if (is_string($value)) { if ($value === $relation) { return \Inflector::classify('model_'.$value); } } else { if ($key === $relation) { return static::${'_'.$rel_name}[$relation]['model_to']; } } } } } return null; } /** * Get the class's observers and what they observe * * @param string specific observer to retrieve info of, allows direct param access by using dot notation * @param mixed default return value when specific key wasn't found * @return array */ public static function observers($specific = null, $default = null) { $class = get_called_class(); if ( ! array_key_exists($class, static::$_observers_cached)) { $observers = array(); if (property_exists($class, '_observers')) { foreach (static::$_observers as $obs_k => $obs_v) { if (is_int($obs_k)) { $observers[$obs_v] = array(); } else { if (is_string($obs_v) or (is_array($obs_v) and is_int(key($obs_v)))) { // @TODO deprecated until v1.4 logger(\Fuel::L_WARNING, 'Passing observer events as array is deprecated, they must be inside another array under a key "events". Check the docs for more info.', __METHOD__); $observers[$obs_k] = array('events' => (array) $obs_v); } else { $observers[$obs_k] = $obs_v; } } } } static::$_observers_cached[$class] = $observers; } if ($specific) { return \Arr::get(static::$_observers_cached[$class], $specific, $default); } return static::$_observers_cached[$class]; } /** * Register an observer * * @param string class name of the observer (including namespace) * @param mixed observer options * * @return void */ public static function register_observer($name, $options = array()) { $class = get_called_class(); static::$_observers_cached[$class] = static::observers() + array($name => $options); } /** * Unregister an observer * * @param string class name of the observer (including namespace) * @return void */ public static function unregister_observer($name) { $class = get_called_class(); foreach (static::observers() as $key => $value) { if ((is_array($value) and $key == $name) or $value == $name) { unset(static::$_observers_cached[$class][$key]); } } } /** * Find one or more entries * * @param int|null $id * @param array $options * * @throws \FuelException * * @return Model|Model[] */ public static function find($id = null, array $options = array()) { // deal with null valued PK's if (is_null($id)) { // if no options are present, simply return null. a PK with a null value can exist return func_num_args() === 2 ? static::query($options) : null; } // Return all that match $options array elseif ($id === 'all') { return static::query($options)->get(); } // Return first or last row that matches $options array elseif ($id === 'first' or $id === 'last') { $query = static::query($options); foreach(static::primary_key() as $pk) { $query->order_by($pk, $id == 'first' ? 'ASC' : 'DESC'); } return $query->get_one(); } // Return specific request row by ID else { $cache_pk = $where = array(); $id = (array) $id; foreach (static::primary_key() as $pk) { $where[] = array($pk, '=', current($id)); $cache_pk[$pk] = current($id); next($id); } if (array_key_exists(get_called_class(), static::$_cached_objects) and array_key_exists(static::implode_pk($cache_pk), static::$_cached_objects[get_called_class()]) and (! isset($options['from_cache']) or $options['from_cache'] == true)) { return static::$_cached_objects[get_called_class()][static::implode_pk($cache_pk)]; } array_key_exists('where', $options) and $where = array_merge($options['where'], $where); $options['where'] = $where; return static::query($options)->get_one(); } } /** * Creates a new query with optional settings up front * * @param array * @return Query */ public static function query($options = array()) { return Query::forge(get_called_class(), array(static::connection(), static::connection(true)), $options); } /** * Count entries, optionally only those matching the $options * * @param array * @return int */ public static function count(array $options = array()) { return static::query($options)->count(); } /** * Find the maximum * * @param mixed * @param array * @return bool|int Maximum value or false */ public static function max($key = null) { return static::query()->max($key ?: static::primary_key()); } /** * Find the minimum * * @param mixed * @param array * @return object|array */ public static function min($key = null) { return static::query()->min($key ?: static::primary_key()); } public static function __callStatic($method, $args) { // storage for the type of find query $find_type = false; // Start with count_by? Get counting! if (strpos($method, 'count_by') === 0) { $find_type = 'count'; $fields = substr($method, 9); } // Otherwise, lets find stuff elseif (strpos($method, 'find_') === 0) { if ($method == 'find_by') { $find_type = 'all'; $fields = array_shift($args); } else { $find_type = strncmp($method, 'find_all_by_', 12) === 0 ? 'all' : (strncmp($method, 'find_by_', 8) === 0 ? 'first' : false); $fields = $find_type === 'first' ? substr($method, 8) : substr($method, 12); } } // bail out if an invalid find type is detected if ( ! $find_type) { throw new \FuelException('Invalid method call. Method '.$method.' does not exist.', 0); } $where = $or_where = array(); if (($and_parts = explode('_and_', $fields))) { foreach ($and_parts as $and_part) { $or_parts = explode('_or_', $and_part); if (count($or_parts) == 1) { $where[] = array($or_parts[0], array_shift($args)); } else { foreach($or_parts as $or_part) { $or_where[] = array($or_part, array_shift($args)); } } } } $options = count($args) > 0 ? array_pop($args) : array(); if ( ! empty($where)) { if ( ! array_key_exists('where', $options)) { $options['where'] = $where; } else { $options['where'] = array_merge($where, $options['where']); } } if ( ! empty($or_where)) { if ( ! array_key_exists('or_where', $options)) { $options['or_where'] = $or_where; } else { $options['or_where'] = array_merge($or_where, $options['or_where']); } } if ($find_type == 'count') { return static::count($options); } else { return static::find($find_type, $options); } // min_...($options) // max_...($options) } /* --------------------------------------------------------------------------- * Object usage * --------------------------------------------------------------------------- */ /** * @var bool keeps track of whether it's a new object */ protected $_is_new = true; /** * @var bool keeps to object frozen */ protected $_frozen = false; /** * @var bool $_sanitization_enabled If this is a records data will be sanitized on get */ protected $_sanitization_enabled = false; /** * @var array keeps the current state of the object */ protected $_data = array(); /** * @var array storage for custom properties on this object */ protected $_custom_data = array(); /** * @var array keeps a copy of the object as it was retrieved from the database */ protected $_original = array(); /** * @var array */ protected $_data_relations = array(); /** * @var array keeps a copy of the relation ids that were originally retrieved from the database */ protected $_original_relations = array(); /** * @var array keeps track of relations that need to be reset before saving the new ones */ protected $_reset_relations = array(); /** * @var array disabled observer events */ protected $_disabled_events = array(); /** * @var string view name when used */ protected $_view; /** * Constructor * * @param array * @param bool */ public function __construct($data = array(), $new = true, $view = null, $cache = true) { // Make sure we get the correct dataformat passed if ( ! is_array($data) and ! $data instanceOf \ArrayAccess) { throw new \ErrorException( 'Argument 1 passed to '.__METHOD__.'() must be of the type array or implement ArrayAccess, '.gettype($data).' given', 0, E_ERROR, __FILE__, __LINE__-7 // adjust the line to point to the function prototype, not this line! ); } // This is to deal with PHP's native hydration that happens before constructor is called // for some weird reason, for example using the DB's as_object() function if( ! empty($this->_data) or ! empty($this->_custom_data)) { // merge the injected data with the passed data $data = array_merge($this->_custom_data, $this->_data, $data); // and reset them $this->_data = array(); $this->_custom_data = array(); // and mark it as existing data $new = false; } // move the passed data to the correct container $properties = $this->properties(); foreach ($properties as $prop => $settings) { // do we have data for this this model property? if (array_key_exists($prop, $data)) { // store it in the data container $this->_data[$prop] = $data[$prop]; unset($data[$prop]); } // property not present, do we have a default value? elseif ($new and array_key_exists('default', $settings)) { $this->_data[$prop] = $settings['default']; } } // store the remainder in the custom data store $this->_custom_data = $data; // store the view, if one was passed if ($view and array_key_exists($view, $this->views())) { $this->_view = $view; } if ($new === false) { // update the original datastore and the related datastore $this->_update_original($this->_data); // update the object cache if needed $cache and static::$_cached_objects[get_class($this)][static::implode_pk($this->_data)] = $this; // mark the object as existing $this->_is_new = false; // and fire the after-load observers $this->observe('after_load'); } else { // make sure the primary keys are reset foreach (static::$_primary_key as $pk) { $this->_data[$pk] = null; } // new object, fire the after-create observers $this->observe('after_create'); } } /** * Update the original setting for this object * * @param array|null $original */ public function _update_original($original = null) { $original = is_null($original) ? $this->_data : $original; $this->_original = array_merge($this->_original, $original); $this->_update_original_relations(); } /** * Update the original relations for this object */ public function _update_original_relations($relations = null) { if (is_null($relations)) { $this->_original_relations = array(); $relations = $this->_data_relations; } else { foreach ($relations as $key => $rel) { // Unload the just fetched relation from the originals unset($this->_original_relations[$rel]); // Unset the numeric key and set the data to update by the relation name unset($relations[$key]); $relations[$rel] = $this->_data_relations[$rel]; } } foreach ($relations as $rel => $data) { if (is_array($data)) { $this->_original_relations[$rel] = array(); foreach ($data as $obj) { if ($obj and ! $obj->is_new()) { $this->_original_relations[$rel][] = $obj->implode_pk($obj); } } } else { $this->_original_relations[$rel] = null; if ($data and ! $data->is_new()) { $this->_original_relations[$rel] = $data->implode_pk($data); } } } } /** * Fetch or set relations on this object * To be used only after having fetched them from the database! * * @param array|bool|null $rels * * @throws \FuelException Invalid input for _relate(), should be an array * @throws FrozenObject No changes allowed * * @return void|array */ public function _relate($rels = false) { if ($rels === false) { return $this->_data_relations; } elseif (is_array($rels)) { if ($this->_frozen) { throw new FrozenObject('No changes allowed.'); } $this->_data_relations = $rels; } else { throw new \FuelException('Invalid input for _relate(), should be an array.'); } } /** * Fetch a property or relation * * @param string * @return mixed */ public function & __get($property) { return $this->get($property); } /** * Set a property or relation * * @param string * @param mixed * * @return Model */ public function __set($property, $value) { return $this->set($property, $value); } /** * Check whether a property exists, only return true for table columns, relations, eav and custom data * * @param string $property * @return bool */ public function __isset($property) { if (array_key_exists($property, $this->_data)) { return true; } elseif (static::relations($property)) { return true; } elseif ($this->_get_eav($property, true)) { return true; } elseif (array_key_exists($property, $this->_custom_data)) { return true; } return false; } /** * Empty a property, relation or custom data * * @param string $property */ public function __unset($property) { if (array_key_exists($property, static::properties())) { $this->_data[$property] = null; } elseif ($rel = static::relations($property)) { $this->_reset_relations[$property] = true; $this->_data_relations[$property] = $rel->singular ? null : array(); } elseif ($this->_get_eav($property, true, true)) { // no additional work needed here } elseif (array_key_exists($property, $this->_custom_data)) { unset($this->_custom_data[$property]); } } /** * Allow for getter, setter and unset methods * * @param string $method * @param array $args * @return mixed * @throws \BadMethodCallException */ public function __call($method, $args) { if (substr($method, 0, 4) == 'get_') { return $this->get(substr($method, 4)); } elseif (substr($method, 0, 4) == 'set_') { return $this->set(substr($method, 4), reset($args)); } elseif (substr($method, 0, 6) == 'unset_') { return $this->__unset(substr($method, 6)); } // Throw an exception throw new \BadMethodCallException('Call to undefined method '.get_class($this).'::'.$method.'()'); } /** * Allow object cloning to new object */ public function __clone() { // Reset primary keys foreach (static::$_primary_key as $pk) { $this->_data[$pk] = null; } // This is a new object $this->_is_new = true; $this->_original = array(); $this->_original_relations = array(); // Cleanup relations foreach ($this->relations() as $name => $rel) { // singular relations (hasone, belongsto) can't be copied, neither can HasMany if ($rel->singular or $rel instanceof HasMany) { unset($this->_data_relations[$name]); } } $this->observe('after_clone'); } /** * Get * * Gets a property or * relation from the * object * * @access public * @param string $property * @param array $conditions * @return mixed */ public function & get($property, array $conditions = array()) { // database columns if (array_key_exists($property, static::properties())) { if ( ! array_key_exists($property, $this->_data)) { $result = null; } elseif ($this->_sanitization_enabled) { // use a copy $result = $this->_data[$property]; } else { // use a reference $result =& $this->_data[$property]; } } // related models elseif ($rel = static::relations($property)) { if ( ! array_key_exists($property, $this->_data_relations)) { $this->_data_relations[$property] = $rel->get($this, $conditions); $this->_update_original_relations(array($property)); } $result =& $this->_data_relations[$property]; } // EAV properties elseif (($result = $this->_get_eav($property)) !== false) { // nothing else to do here } // database view columns elseif ($this->_view and in_array($property, static::$_views_cached[get_class($this)][$this->_view]['columns'])) { if ($this->_sanitization_enabled) { // use a copy $result = $this->_data[$property]; } else { // use a reference $result =& $this->_data[$property]; } } // stored custom data elseif (array_key_exists($property, $this->_custom_data)) { if ($this->_sanitization_enabled) { // use a copy $result = $this->_custom_data[$property]; } else { // use a reference $result =& $this->_custom_data[$property]; } } else { throw new \OutOfBoundsException('Property "'.$property.'" not found for '.get_class($this).'.'); } // do we need to clean before returning the result? if ($this->_sanitization_enabled) { $result = $this->_sanitize($property, $result); } return $result; } /** * Set * * Sets a property or * relation of the * object * * @access public * @param string|array $property * @param string $value in case $property is a string * * @throws \FuelException Primary key on model cannot be changed * @throws \InvalidArgumentException You need to pass both a property name and a value to set() * @throws FrozenObject No changes allowed * * @return Model */ public function set($property, $value = null) { if ($this->_frozen) { throw new FrozenObject('No changes allowed.'); } if (is_array($property)) { foreach ($property as $p => $v) { $this->set($p, $v); } } else { if (func_num_args() < 2) { throw new \InvalidArgumentException('You need to pass both a property name and a value to set().'); } // is it a primary key we're updating? if (in_array($property, static::primary_key()) and $this->{$property} !== null and $this->{$property} != $value) { throw new \FuelException('Primary key on model '.get_class($this).' cannot be changed.'); } // is it a model property we're updating? if (array_key_exists($property, static::properties())) { $this->_data[$property] = $value; } // or perhaps a related model? elseif ($rel = static::relations($property)) { $this->is_fetched($property) or $this->_reset_relations[$property] = true; if (isset($this->_data_relations[$property]) and ($this->_data_relations[$property] instanceof self) and is_array($value)) { $this->_data_relations[$property]->set($value); } elseif ($value === null or $value === array()) { $this->_reset_relations[$property] = true; $this->_data_relations[$property] = $rel->singular ? null : array(); } else { $this->_data_relations[$property] = $value; } } // none of the above, assume its custom data elseif ( ! $this->_set_eav($property, $value)) { $this->_custom_data[$property] = $value; } } return $this; } /** * Save the object and it's relations, create when necessary * * @param mixed $cascade * null = use default config, * bool = force/prevent cascade, * array cascades only the relations that are in the array * * @return bool */ public function save($cascade = null, $use_transaction = false) { if ($this->frozen()) { return false; } if ($use_transaction) { $db = \Database_Connection::instance(static::connection(true)); $db->start_transaction(); } try { $this->observe('before_save'); $this->freeze(); foreach($this->relations() as $rel_name => $rel) { if (array_key_exists($rel_name, $this->_reset_relations)) { if (method_exists($rel, 'delete_related')) { $rel->delete_related($this); $this->_original_relations[$rel_name] = $rel->singular ? null : array(); } else { if (empty($this->_original_relations[$rel_name])) { $data = $rel->get($this); if (is_array($data)) { $this->_original_relations[$rel_name] = array(); foreach ($data as $obj) { $this->_original_relations[$rel_name][] = $obj ? $obj->implode_pk($obj) : null; } } else { $this->_original_relations[$rel_name] = $data ? $data->implode_pk($data) : null; } } } unset($this->_reset_relations[$rel_name]); } if (array_key_exists($rel_name, $this->_data_relations)) { $rel->save($this, $this->{$rel_name}, array_key_exists($rel_name, $this->_original_relations) ? $this->_original_relations[$rel_name] : null, false, is_array($cascade) ? in_array($rel_name, $cascade) : $cascade ); } } $this->unfreeze(); // Insert or update $return = $this->_is_new ? $this->create() : $this->update(); $this->freeze(); foreach($this->relations() as $rel_name => $rel) { if (array_key_exists($rel_name, $this->_data_relations)) { $rel->save($this, $this->{$rel_name}, array_key_exists($rel_name, $this->_original_relations) ? $this->_original_relations[$rel_name] : null, true, is_array($cascade) ? in_array($rel_name, $cascade) : $cascade ); } } $this->unfreeze(); // update the original datastore and the related datastore $this->_update_original(); $this->observe('after_save'); $use_transaction and $db->commit_transaction(); } catch (\Exception $e) { $use_transaction and $db->rollback_transaction(); throw $e; } return $return; } /** * Save using INSERT */ protected function create() { // Only allow creation with new object, otherwise: clone first, create later if ( ! $this->is_new()) { return false; } $this->observe('before_insert'); // Set all current values $query = Query::forge(get_called_class(), static::connection(true)); $primary_key = static::primary_key(); $properties = array_keys(static::properties()); foreach ($properties as $p) { if ( ! (in_array($p, $primary_key) and is_null($this->{$p}))) { $query->set($p, $this->{$p}); } } // Insert! $id = $query->insert(); // when there's one PK it might be auto-incremented, get it and set it if (count($primary_key) == 1 and $id !== false) { $pk = reset($primary_key); // only set it if it hasn't been set manually is_null($this->{$pk}) and $this->{$pk} = $id; } // update the original properties on creation and cache object for future retrieval in this request $this->_is_new = false; $this->_original = $this->_data; static::$_cached_objects[get_class($this)][static::implode_pk($this)] = $this; $this->observe('after_insert'); return $id !== false; } /** * Save using UPDATE */ protected function update() { // New objects can't be updated, neither can frozen if ($this->is_new()) { return false; } // Non changed objects don't have to be saved, but return true anyway (no reason to fail) if ( ! $this->is_changed(array_keys(static::properties()))) { return true; } $this->observe('before_update'); // Create the query and limit to primary key(s) $query = Query::forge(get_called_class(), static::connection(true)); $primary_key = static::primary_key(); $properties = static::properties(); $properties_keys = array_keys($properties); //Add the primary keys to the where $this->add_primary_keys_to_where($query); // Set all current values foreach ($properties_keys as $p) { if ( ! in_array($p, $primary_key) ) { if (array_key_exists($p, $this->_original)) { if ((array_key_exists('type', $properties[$p]) and $properties[$p]['type'] == 'int') or (array_key_exists('data_type', $properties[$p]) and $properties[$p]['data_type'] == 'int')) { if ($this->{$p} != $this->_original[$p]) { $query->set($p, isset($this->_data[$p]) ? $this->_data[$p] : null); } } elseif ($this->{$p} !== $this->_original[$p]) { $query->set($p, isset($this->_data[$p]) ? $this->_data[$p] : null); } } else { array_key_exists($p, $this->_data) and $query->set($p, $this->_data[$p]); } } } // Return false when update fails if ( ! $query->update()) { return false; } $this->_original = $this->_data; static::$_cached_objects[get_class($this)][static::implode_pk($this)] = $this; // update the original property on success $this->observe('after_update'); return true; } /** * Adds the primary keys in where clauses to the given query. * * @param Query $query */ protected function add_primary_keys_to_where($query) { $primary_key = static::primary_key(); foreach ($primary_key as $pk) { $query->where($pk, '=', $this->_original[$pk]); } } /** * Delete current object * * @param mixed $cascade * null = use default config, * bool = force/prevent cascade, * array cascades only the relations that are in the array * @param bool $use_transaction * * @throws \Exception * * @return Model this instance as a new object without primary key(s) */ public function delete($cascade = null, $use_transaction = false) { // New objects can't be deleted, neither can frozen if ($this->is_new() or $this->frozen()) { return false; } if ($use_transaction) { $db = \Database_Connection::instance(static::connection(true)); $db->start_transaction(); } try { $this->observe('before_delete'); $this->freeze(); foreach($this->relations() as $rel_name => $rel) { $should_cascade = is_array($cascade) ? in_array($rel_name, $cascade) : $rel->cascade_delete; // Give model subclasses a chance to chip in. if ($should_cascade && ! $this->should_cascade_delete($rel)) { // The function returned false so something does not want this relation to be cascade deleted $should_cascade = false; } $rel->delete($this, $this->{$rel_name}, false, $should_cascade); } $this->unfreeze(); // Delete the model in question if ( ! $this->delete_self()) { return false; } $this->freeze(); foreach($this->relations() as $rel_name => $rel) { $should_cascade = is_array($cascade) ? in_array($rel_name, $cascade) : $rel->cascade_delete; // Give model subclasses a chance to chip in. if ($should_cascade && ! $this->should_cascade_delete($rel)) { // The function returned false so something does not want this relation to be cascade deleted $should_cascade = false; } $rel->delete($this, $this->{$rel_name}, true, $should_cascade); } $this->unfreeze(); // Perform cleanup: // remove from internal object cache, remove PK's, set to non saved object, remove db original values if (array_key_exists(get_called_class(), static::$_cached_objects) and array_key_exists(static::implode_pk($this), static::$_cached_objects[get_called_class()])) { unset(static::$_cached_objects[get_called_class()][static::implode_pk($this)]); } foreach ($this->primary_key() as $pk) { unset($this->_data[$pk]); } // remove original relations too foreach($this->relations() as $rel_name => $rel) { $this->_original_relations[$rel_name] = $rel->singular ? null : array(); } $this->_is_new = true; $this->_original = array(); $this->observe('after_delete'); $use_transaction and $db->commit_transaction(); } catch (\Exception $e) { $use_transaction and $db->rollback_transaction(); throw $e; } return $this; } /** * Deletes this model instance from the database. * * @return bool */ protected function delete_self() { // Create the query and limit to primary key(s) $query = Query::forge(get_called_class(), static::connection(true))->limit(1); $primary_key = static::primary_key(); foreach ($primary_key as $pk) { $query->where($pk, '=', $this->{$pk}); } // Return success of update operation return $query->delete(); } /** * Allows subclasses to more easily define if a relation can be cascade deleted or not. * * @param array $rel * * @return bool False to stop the relation from being deleted. Works the same as the cascade_delete property */ protected function should_cascade_delete($rel) { return true; } /** * Reset values to those gotten from the database */ public function reset() { foreach ($this->_original as $p => $val) { $this->_data[$p] = $val; } } /** * Disable an observer event * * @param string event to disable * @return void */ public function disable_event($event) { $this->_disabled_events[$event] = true; } /** * Enable a defined observer * * @param string class name of the observer (including namespace) * @param string event to enable, or null for all events * @return void */ public function enable_event($event) { unset($this->_disabled_events[$event]); } /** * Calls all observers for the current event * * @param string */ public function observe($event) { foreach ($this->observers() as $observer => $settings) { $events = isset($settings['events']) ? $settings['events'] : array(); if ((empty($events) or in_array($event, $events)) and empty($this->_disabled_events[$event])) { if ( ! class_exists($observer)) { $observer_class = \Inflector::get_namespace($observer).'Observer_'.\Inflector::denamespace($observer); if ( ! class_exists($observer_class)) { throw new \UnexpectedValueException($observer); } // Add the observer with the full classname for next usage unset(static::$_observers_cached[$observer]); static::$_observers_cached[$observer_class] = $events; $observer = $observer_class; } try { call_user_func(array($observer, 'orm_notify'), $this, $event); } catch (\Exception $e) { // Unfreeze before failing $this->unfreeze(); throw $e; } } } } /** * Compare current state with the retrieved state * * @param string|array $property * * @throws \OutOfBoundsException * * @return bool */ public function is_changed($property = null) { $properties = static::properties(); $relations = static::relations(); $property = (array) $property ?: array_merge(array_keys($properties), array_keys($relations)); $simple_data_types = array('int','bool'); foreach ($property as $p) { if (isset($properties[$p])) { if (array_key_exists($p, $this->_original)) { if ((array_key_exists('type', $properties[$p]) and in_array($properties[$p]['type'], $simple_data_types)) or (array_key_exists('data_type', $properties[$p]) and in_array($properties[$p]['data_type'], $simple_data_types))) { if ($this->{$p} != $this->_original[$p]) { return true; } } elseif ($this->{$p} !== $this->_original[$p]) { return true; } } else { if (array_key_exists($p, $this->_data)) { return true; } } } elseif (isset($relations[$p])) { if ($relations[$p]->singular) { if (empty($this->_original_relations[$p]) !== empty($this->_data_relations[$p]) or ( ! empty($this->_original_relations[$p]) and $this->_original_relations[$p] !== $this->_data_relations[$p]->implode_pk($this->{$p}))) { return true; } } else { if (empty($this->_original_relations[$p])) { if ( ! empty($this->_data_relations[$p])) { return true; } continue; } $orig_rels = $this->_original_relations[$p]; foreach ($this->{$p} as $rk => $r) { if ( ! in_array($r->implode_pk($r), $orig_rels)) { return true; } unset($orig_rels[array_search($rk, $orig_rels)]); } if ( ! empty($orig_rels)) { return true; } } } else { throw new \OutOfBoundsException('Unknown property or relation: '.$p); } } return false; } /** * Generates an array with keys new & old that contain ONLY the values that differ between the original and * the current unsaved model. * Note: relations are given as single or array of imploded pks * * @return array */ public function get_diff() { $diff = array(0 => array(), 1 => array()); foreach ($this->_data as $key => $val) { if ($this->is_changed($key)) { $diff[0][$key] = array_key_exists($key, $this->_original) ? $this->_original[$key] : null; $diff[1][$key] = $val; } } foreach ($this->_data_relations as $key => $val) { $rel = static::relations($key); if ($rel->singular) { $new_pk = empty($val) ? null : $val->implode_pk($val); if (empty($this->_original_relations[$key]) !== empty($val) or ( ! empty($this->_original_relations[$key]) and ! empty($val) and $this->_original_relations[$key] !== $new_pk )) { $diff[0][$key] = isset($this->_original_relations[$key]) ? $this->_original_relations[$key] : null; $diff[1][$key] = isset($val) ? $new_pk : null; } } else { $original_pks = empty($this->_original_relations[$key]) ? array() : $this->_original_relations[$key]; $new_pks = array(); if ($val) { foreach ($val as $v) { if ( ! in_array(($new_pk = $v->implode_pk($v)), $original_pks)) { $new_pks[] = $new_pk; } else { $original_pks = array_diff($original_pks, array($new_pk)); } } } if ( ! empty($original_pks) or ! empty($new_pks)) { $diff[0][$key] = empty($original_pks) ? null : $original_pks; $diff[1][$key] = empty($new_pks) ? null : $new_pks; } } } return $diff; } /*** * Returns whether the given relation is fetched. If no relation is * * @param string $relation Name of relation * * @return bool */ public function is_fetched($relation) { if (static::relations($relation)) { return array_key_exists($relation, $this->_data_relations); } return false; } /*** * Returns whether this is a saved or a new object * * @return bool */ public function is_new() { return $this->_is_new; } /** * Check whether the object was frozen * * @return boolean */ public function frozen() { return $this->_frozen; } /** * Freeze the object to disallow changing it or saving it */ public function freeze() { $this->_frozen = true; } /** * Unfreeze the object to allow changing it or saving it again */ public function unfreeze() { $this->_frozen = false; } /** * Enable sanitization mode in the object * * @return $this */ public function sanitize() { $this->_sanitization_enabled = true; return $this; } /** * Disable sanitization mode in the object * * @return $this */ public function unsanitize() { $this->_sanitization_enabled = false; return $this; } /** * Returns the current sanitization state of the object * * @return bool */ public function sanitized() { return $this->_sanitization_enabled; } /** * Sanitizatize a data value * * @param string $field Name of the property that is being sanitized * @param mixed $value Value to sanitize * * @return mixed */ protected function _sanitize($field, $value) { return \Security::clean($value, null, 'security.output_filter'); } /** * Method for use with Fieldset::add_model() * * @param Fieldset Fieldset instance to add fields to * @param array|Model Model instance or array for use to repopulate */ public static function set_form_fields($form, $instance = null) { Observer_Validation::set_fields($instance instanceof static ? $instance : get_called_class(), $form); $instance and $form->populate($instance, true); } /** * Allow populating this object from an array, and any related objects * * @param array assoc array with named values to store in the object * * @return Model this instance as a new object without primary key(s) */ public function from_array(array $values) { foreach($values as $property => $value) { if (array_key_exists($property, static::properties()) and ! in_array($property, static::primary_key())) { $this->_data[$property] = $value; } elseif (array_key_exists($property, static::relations()) and is_array($value)) { $rel = static::relations($property); if ( ! isset($this->_data_relations[$property])) { $this->_data_relations[$property] = $rel->singular ? null : array(); } foreach($value as $id => $data) { if (is_array($data)) { if (array_key_exists($id, $this->_data_relations[$property])) { foreach($data as $field => $contents) { if ($rel->singular) { $this->_data_relations[$property]->{$field} = $contents; } else { $this->_data_relations[$property][$id]->{$field} = $contents; } } } else { if ($rel->singular) { $this->_data_relations[$property] = call_user_func(static::relations($property)->model_to.'::forge', $data); } else { $this->_data_relations[$property][] = call_user_func(static::relations($property)->model_to.'::forge', $data); } } } } } elseif (property_exists($this, '_eav') and ! empty(static::$_eav)) { $this->_set_eav($property, $value); } else { $this->_custom_data[$property] = $value; } } return $this; } /** * Allow converting this object to an array * * @param bool $custom * @param bool $recurse * @param bool $eav * * @internal param \Orm\whether $bool or not to include the custom data array * * @return array */ public function to_array($custom = false, $recurse = false, $eav = false) { // storage for the result $array = array(); // reset the references array on first call $recurse or static::$to_array_references = array(get_class($this)); // make sure all data is scalar or array if ($custom) { foreach ($this->_custom_data as $key => $val) { if (is_object($val)) { if (method_exists($val, '__toString')) { $val = (string) $val; } else { $val = get_object_vars($val); } } $array[$key] = $val; } } // make sure all data is scalar or array foreach ($this->_data as $key => $val) { if (is_object($val)) { if (method_exists($val, '__toString')) { $val = (string) $val; } else { $val = get_object_vars($val); } } $array[$key] = $val; } // convert relations foreach ($this->_data_relations as $name => $rel) { if (empty($rel)) { $array[$name] = null; } elseif (is_array($rel)) { $array[$name] = array(); if ( ! in_array(get_class(reset($rel)), static::$to_array_references)) { static::$to_array_references[] = get_class(reset($rel)); foreach ($rel as $id => $r) { $array[$name][$id] = $r->to_array($custom, true, $eav); } array_pop(static::$to_array_references); } } elseif ( ! in_array(get_class($rel), static::$to_array_references)) { static::$to_array_references[] = get_class($rel); $array[$name] = $rel->to_array($custom, true, $eav); array_pop(static::$to_array_references); } } // get eav relations if ($eav and property_exists(get_called_class(), '_eav')) { // loop through the defined EAV containers foreach (static::$_eav as $rel => $settings) { // normalize the container definition, could be string or array if (is_string($settings)) { $rel = $settings; $settings = array(); } // determine attribute and value column names $attr = \Arr::get($settings, 'attribute', 'attribute'); $val = \Arr::get($settings, 'value', 'value'); // check if relation is present if (array_key_exists($rel, $array)) { // get eav properties $container = \Arr::assoc_to_keyval($array[$rel], $attr, $val); // merge eav properties to array without overwritting anything $array = array_merge($container, $array); // we don't need this relation anymore unset($array[$rel]); } } } // strip any excluded values from the array foreach (static::get_to_array_exclude() as $key) { if (array_key_exists($key, $array)) { unset($array[$key]); } } return $array; } /** * Provide the identifying details in the form of an array * * @return array */ public function get_pk_assoc() { $array = array_flip(static::primary_key()); foreach ($array as $key => &$value) { $value = $this->get($key); } return $array; } /** * Allow converting this object to a real object * * @return object */ public function to_object($custom = false, $recurse = false) { return (object) $this->to_array($custom, $recurse); } /** * EAV attribute getter. Also deals with isset() and unset() * * @param string $attribute, the attribute value to get * @param bool $isset, if true, do an exists check instead of returning the value * @param bool $unset, if true, delete the EAV attribute if it exists * * @throws \OutOfBoundsException if the defined EAV relation does not exist or of the wrong type * * @return mixed */ protected function _get_eav($attribute, $isset = false, $unset = false) { // get the current class name $class = get_called_class(); // don't do anything unless we actually have an EAV container if (property_exists($class, '_eav')) { // loop through the defined EAV containers foreach (static::$_eav as $rel => $settings) { // normalize the container definition, could be string or array if (is_string($settings)) { $rel = $settings; $settings = array(); } // fetch the relation object for this EAV container if ( ! $rel = static::relations($rel)) { throw new \OutOfBoundsException('EAV container defines a relation that does not exist in '.get_class($this).'.'); } // EAV containers must be of the "Many type" if ($rel instanceOf \Orm\HasOne or $rel instanceOf \Orm\BelongsTo ) { throw new \OutOfBoundsException('EAV containers can only be defined on "HasMany" or "ManyMany" relations in '.get_class($this).'.'); } // determine attribute and value column names $attr = isset($settings['attribute']) ? $settings['attribute'] : 'attribute'; $val = isset($settings['value']) ? $settings['value'] : 'value'; // see if we have a result if ($result = $this->{$rel->name}) { // loop over the resultset foreach ($result as $key => $record) { // check if this is the attribute we need if ($record->{$attr} === $attribute) { if ($unset) { // delete the related object if we need to unset unset($this->{$rel->name}[$key]); $record->delete(); return true; } else { // else return its existence or its value return $isset ? true : $record->{$val}; } } } } } } return false; } /** * EAV attribute setter * * @param string $attribute * @param string $value * * @throws \OutOfBoundsException * * @return mixed */ protected function _set_eav($attribute, $value) { // get the current class name $class = get_called_class(); // don't do anything unless we actually have an EAV container if (property_exists($class, '_eav')) { // loop through the defined EAV containers foreach (static::$_eav as $rel => $settings) { // normalize the container definition, could be string or array if (is_string($settings)) { $rel = $settings; $settings = array(); } // fetch the relation object for this EAV container if ( ! $relation = static::relations($rel)) { throw new \OutOfBoundsException('EAV container defines a relation that does not exist in '.get_class($this).'.'); } // EAV containers must be of the "Many type" if ($relation instanceOf \Orm\HasOne or $relation instanceOf \Orm\BelongsTo) { throw new \OutOfBoundsException('EAV containers can only be defined on "HasMany" or "ManyMany" relations in '.get_class($this).'.'); } // determine attribute and value column names $attr = isset($settings['attribute']) ? $settings['attribute'] : 'attribute'; $val = isset($settings['value']) ? $settings['value'] : 'value'; // loop over the resultset foreach ($this->{$relation->name} as $key => $record) { if ($record->{$attr} === $attribute) { $record->{$val} = $value; return true; } } // not found, we've got outselfs a new attribute, so add it if ($rel = static::related_class($rel)) { $this->{$relation->name}[] = $rel::forge(array( $attr => $attribute, $val => $value, )); return true; } } } return false; } /*************************************************************************** * Implementation of ArrayAccess **************************************************************************/ public function offsetSet($offset, $value) { try { $this->__set($offset, $value); } catch (\Exception $e) { return false; } } public function offsetExists($offset) { return $this->__isset($offset); } public function offsetUnset($offset) { $this->__unset($offset); } public function offsetGet($offset) { try { return $this->__get($offset); } catch (\Exception $e) { return false; } } /*************************************************************************** * Implementation of Iterable **************************************************************************/ protected $_iterable = array(); public function rewind() { $this->_iterable = array_merge($this->_custom_data, $this->_data, $this->_data_relations); reset($this->_iterable); } public function current() { return current($this->_iterable); } public function key() { return key($this->_iterable); } public function next() { return next($this->_iterable); } public function valid() { return key($this->_iterable) !== null; } /** * Returns a list of properties that will be excluded when to_array() is used. * @return array */ public static function get_to_array_exclude() { return static::$_to_array_exclude; } /** * Returns a list of properties and their information with _to_array_exclude * properties removed. * * @return array */ public static function get_filtered_properties() { $array = static::properties(); // strip any excluded values from the array foreach (static::get_to_array_exclude() as $key) { if (array_key_exists($key, $array)) { unset($array[$key]); } } return $array; } }