You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
PlexShare/fuel/packages/orm/classes/model.php

2518 lines
57 KiB

<?php
/**
* Fuel is a fast, lightweight, community driven PHP 5.4+ framework.
*
* @package Fuel
* @version 1.8.1
* @author Fuel Development Team
* @license MIT License
* @copyright 2010 - 2018 Fuel Development Team
* @link http://fuelphp.com
*/
namespace Orm;
/**
* Record Not Found Exception
*/
class RecordNotFound extends \OutOfBoundsException {}
/**
* Frozen Object Exception
*/
class FrozenObject extends \RuntimeException {}
class Model implements \ArrayAccess, \Iterator, \Sanitization
{
/* ---------------------------------------------------------------------------
* Static usage
* --------------------------------------------------------------------------- */
/**
* @var string connection to use
*/
// protected static $_connection = null;
/**
* @var string write connection to use
*/
// protected static $_write_connection = null;
/**
* @var string table name to overwrite assumption
*/
// protected static $_table_name;
/**
* @var array array of object properties
*/
// protected static $_properties;
/**
* @var array array of views with additional properties
*/
// protected static $_views;
/**
* @var array array of observer classes to use
*/
// protected static $_observers;
/**
* @var array relationship properties
*/
// protected static $_has_one;
// protected static $_belongs_to;
// protected static $_has_many;
// protected static $_many_many;
// protected static $_eav;
/**
* @var array name or names of the primary keys
*/
protected static $_primary_key = array('id');
/**
* @var array name or columns that need to be excluded from any to_array() result
*/
protected static $_to_array_exclude = array();
/**
* @var array cached tables
*/
protected static $_table_names_cached = array();
/**
* @var array cached properties
*/
protected static $_properties_cached = array();
/**
* @var array cached properties
*/
protected static $_views_cached = array();
/**
* @var string relationships
*/
protected static $_relations_cached = array();
/**
* @var array cached observers
*/
protected static $_observers_cached = array();
/**
* @var array array of fetched objects
*/
protected static $_cached_objects = array();
/**
* @var string Name of DB connection to use
*/
protected static $_connection = null;
/**
* @var string Name of the DB connection to use when writing
*/
protected static $_write_connection = null;
/**
* @var array array of valid relation types
*/
protected static $_valid_relations = array(
'belongs_to' => '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;
}
}