/vendor/Mad/Model/Base.php
PHP | 3474 lines | 1761 code | 261 blank | 1452 comment | 141 complexity | 3a4a4c4ec80ad293ddd106ed696fcb3f MD5 | raw file
- <?php
- /**
- * @category Mad
- * @package Mad_Model
- * @copyright (c) 2007-2009 Maintainable Software, LLC
- * @license http://opensource.org/licenses/bsd-license.php BSD
- */
- /**
- * Object Relation Mapper (ORM) Layer. Tables are represented as classes, rows in
- * the table correspond to objects from that class, and columns map to the object
- * attributes. Handles all basic CRUD operations (Create, Read, Update, Delete).
- *
- * Model subclasses should always be created with the generator to ensure creation of
- * all correct components (including data objects, unit tests, and fixtures):
- *
- * <code>
- * php ./script/generate.php model {ModelName} {table_name}
- * </code>
- *
- * @category Mad
- * @package Mad_Model
- * @copyright (c) 2007-2009 Maintainable Software, LLC
- * @license http://opensource.org/licenses/bsd-license.php BSD
- */
- abstract class Mad_Model_Base extends Mad_Support_Object
- {
- /*##########################################################################
- # Configuration options
- ##########################################################################*/
- /**
- * Should the table introspection data be cached
- * - true: Cache table introspection data to /tmp/cache/tables
- * - false: Introspect database table on every request
- */
- public static $cacheTables = true;
- /**
- * Include the root level in json serialization
- */
- public static $includeRootInJson = false;
- /*##########################################################################
- # Connection
- ##########################################################################*/
- /**
- * @var object
- */
- protected static $_connectionSpec;
- /**
- * @var array
- */
- protected static $_activeConnection;
- /**
- * @var Logger
- */
- protected static $_logger;
- /**
- * Database adapter instance
- * @var Mad_Model_ConnectionAdapter_Abstract
- */
- public $connection;
- /*##########################################################################
- # Attributes
- ##########################################################################*/
- /**
- * List of attributes excluded from mass assignment
- * @var array
- */
- protected $_attrProtected = array();
- /**
- * List of attribute name=>value pairs
- * @var array
- */
- protected $_attributes = array();
- /**
- * Name of this class
- * @var string
- */
- protected $_className = null;
- /**
- * Name of the database table
- * @var string
- */
- protected $_tableName = null;
- /**
- * Name of the primary key db column
- * @var string
- */
- protected $_primaryKey = null;
- /**
- * Has subclasses through a types table with class_name column
- * @var boolean
- */
- protected $_inheritanceColumn = 'type';
- /**
- * @var array
- */
- protected $_columns = array();
-
- /**
- * @var array
- */
- protected $_columnsHash = array();
-
- /**
- * @var array
- */
- protected $_columnNames = array();
-
- /**
- * An object cannot allow attribute access once it has been destroyed
- * @var boolean
- */
- protected $_frozen = false;
- /**
- * Is this a new record to be inserted?
- * @var boolean
- */
- protected $_newRecord = true;
- /*##########################################################################
- # Associations
- ##########################################################################*/
- /**
- * Has the association changed (even though the actual model might not have)
- * @var boolean
- */
- protected $_assocChanged = false;
- /**
- * A list of associations for this model define in concrete _initialize()
- * Lazy initialized if an unknown property/method is called
- *
- * @var array
- */
- protected $_associationList;
- /**
- * The list of association objects for this model
- * Lazy initialized if an unknown property/method is called
- *
- * @var array
- */
- protected $_associations;
- /**
- * The list of methods that are available for the associations of this model
- * $_associationMethods['createDocument'] = $hasOneAssociationObject;
- * This is lazy initialized if an unknown propery/method is called
- *
- * @var array
- */
- protected $_associationMethods;
- /*##########################################################################
- # Validations
- ##########################################################################*/
- /**
- * The list of validations that thie model enforces before an update/insert
- * @var array
- */
- protected $_validations = array();
-
- /**
- * Should we throw exceptions when validations fail
- * @var array
- */
- protected $_throw = false;
- /**
- * An array of messages stored when validations fail
- * @var array
- */
- public $errors;
- /*##########################################################################
- # Construct/Destruct
- ##########################################################################*/
- /**
- * Initialize any values given for the model.
- *
- * Load the model by attributes
- * <code>
- * <?php
- * ...
- * $attributes = array('documentname' => 'My Folder',
- * 'description' => 'My Description');
- * $folder = new Folder($attributes);
- * ...
- * ?>
- * </code>
- *
- * @param array $attributes construct by attribute list
- * @param array $options 'include' associations
- * @throws Mad_Model_Exception
- */
- public function __construct($attributes=null, $options=null)
- {
- $this->_className = get_class($this);
- // establish connection to db
- $this->connection = $this->retrieveConnection();
- $this->errors = new Mad_Model_Errors($this);
- // Initialize relationships/data-validation from subclass
- $this->_initialize();
- // init table/fields
- $this->_tableName = $this->tableName();
- $this->_primaryKey = $this->primaryKey();
- $this->_attributes = $this->_attributesFromColumnDefinition();
- // set values by attribute list
- if (isset($attributes)) {
- $this->setAttributes($attributes);
- }
- }
- /**
- * Clone the object without the values. All objects need to be explicitly
- * copied or we get them referencing the same data
- */
- public function __clone()
- {
- // reset attributes, errors, and associations
- $this->_attributes = $this->_attributesFromColumnDefinition();
- $this->errors->clear();
- $this->_resetAssociations();
- // only need to clone validations if they exist
- if (isset($this->_validations)) {
- foreach ($this->_validations as &$validation) {
- $validation = clone $validation;
- }
- }
- }
- /**
- * Initialize relationships and Data validation from subclass
- */
- abstract protected function _initialize();
- /*##########################################################################
- # Magic Accessor methods
- ##########################################################################*/
- /**
- * Dynamically get value for a attribute. Attributes cannot be retrieved once
- * an object has been destroyed.
- *
- * @param string $name
- * @return string
- * @throws Mad_Model_Exception
- */
- public function _get($name)
- {
- // active-record primary key value
- if ($name == 'id') { $name = $this->primaryKey(); }
- // active-record || attribute-reader value
- if (array_key_exists($name, $this->_attributes)) {
- return $this->readAttribute($name);
- }
- // dynamic attribute added by an association
- $this->_initAssociations();
- if (isset($this->_associationMethods[$name])) {
- return $this->_associationMethods[$name]
- ->callMethod($name, array());
- // unknown attribute
- } else {
- throw new Mad_Model_Exception("Unrecognized attribute '$name'");
- }
- }
- /**
- * Dynamically set value for a attribute. Attributes cannot be set once an
- * object has been destroyed. Primary Key cannot be changed if the data was
- * loaded from a database row
- *
- * @param string $name
- * @param mixed $value
- * @throws Mad_Model_Exception
- */
- public function _set($name, $value)
- {
- if ($this->_frozen) {
- $msg = "You cannot set attributes of a destroyed object";
- throw new Mad_Model_Exception($msg);
- }
- // active-record primary key value
- if ($name == 'id') { $name = $this->primaryKey(); }
- // cannot change pk if it's already set
- if (($name == $this->primaryKey()) && !$this->isNewRecord()) {
- // ignore assignment of pk so that this works with activeresource
- return;
- }
- // active-record || attribute-reader value
- if (array_key_exists($name, $this->_attributes)) {
- return $this->writeAttribute($name, $value);
- }
- // dynamic attribute added by an association
- $this->_initAssociations();
- if (isset($this->_associationMethods[$name.'='])) {
- return $this->_associationMethods[$name.'=']
- ->callMethod($name.'=', array($value));
- // unknown attribute
- } else {
- throw new Mad_Model_Exception("Unrecognized attribute '$name'");
- }
- }
- /**
- * Allows testing with empty() and isset() to work inside templates
- *
- * @param string $key
- * @return boolean
- */
- public function _isset($name)
- {
- // association methods
- $this->_initAssociations();
- if (isset($this->_associationMethods[$name])) {
- return count($this->_get($name)) > 0;
- // active-record attribue
- } else {
- return isset($this->_attributes[$name]);
- }
- return isset($this->_attributes[$name]);
- }
- /**
- * Association methods are added at runtime and use dynamic methods.
- *
- * @param string $name
- * @param array $args
- */
- public function __call($name, $args)
- {
- // dynamic attribute added by an association
- $this->_initAssociations();
- if (isset($this->_associationMethods[$name])) {
- return $this->_associationMethods[$name]->callMethod($name, $args);
- // unknown method
- } else {
- throw new Mad_Model_Exception("Unrecognized method '$name'");
- }
- }
- /**
- * Print out a string describing this object's attributes
- *
- * @return string
- */
- public function __toString()
- {
- foreach ($this->_attributes as $name => $value) {
- $str[] = "$name => ".(isset($value) ? "'$value'" : 'null');
- }
- return isset($str) ? "\n".$this->_className." Object: \n".join(", \n", $str) : null;
- }
-
- /*##########################################################################
- # Serialization
- ##########################################################################*/
-
- /**
- * Serialize only needs attributes
- */
- public function __sleep()
- {
- return array('_attributes', '_attrReaders',
- '_attrWriters', '_attrValues');
- }
- /**
- * Enables models to be used as URL parameters for routes automatically.
- *
- * @return null|string
- */
- public function toParam()
- {
- $pk = $this->primaryKey();
- if ($pk && isset($this->_attributes[$pk])) {
- return (string)$this->_attributes[$pk];
- }
- }
- /*##########################################################################
- # Logger
- ##########################################################################*/
- /**
- * Set a logger object, defaulting to mad_default_logger. This needs to
- * reset connection so that the correct log is passed to the connection
- * adapter.
- *
- * @param object $logger
- */
- public static function setLogger($logger=null)
- {
- self::$_logger = isset($logger) ? $logger : $GLOBALS['MAD_DEFAULT_LOGGER'];
- self::establishConnection(self::removeConnection());
- }
- /**
- * Returns the logger object.
- *
- * @return object
- */
- public static function logger()
- {
- // set default logger
- if (!isset(self::$_logger)) {
- self::setLogger();
- }
- return self::$_logger;
- }
- /*##########################################################################
- # Connection Management
- ##########################################################################*/
- /**
- * Establishes the connection to the database. Accepts a hash as input where
- * the :adapter key must be specified with the name of a database adapter (in lower-case)
- *
- * Example for regular databases (MySQL, Postgresql, etc):
- * <code>
- * Mad_Model_Base::establishConnection(array(
- * "adapter" => "mysql",
- * "host" => "localhost",
- * "username" => "myuser",
- * "password" => "mypass",
- * "database" => "somedatabase"
- * ));
- * </code>
- *
- * Example for SQLite database:
- * <code>
- * Mad_Model_Base::establishConnection(array(
- * "adapter" => "sqlite",
- * "database" => "path/to/dbfile"
- * ));
- * </code>
- *
- * The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
- * may be returned on an error.
- *
- * @param array $spec
- * @return Connection
- */
- public static function establishConnection($spec=null)
- {
- // $spec is empty: $spec defaults to MAD_ENV string like "development"
- // keep going to read YAML for this environment string
- if (empty($spec)) {
- if ( !defined('MAD_ENV') || !MAD_ENV ) {
- throw new Mad_Model_Exception('Adapter Not Specified');
- }
- $spec = MAD_ENV;
- }
- // $spec is string: read YAML config for environment named by string
- // keep going to process the resulting array
- if (is_string($spec)) {
- $config = Horde_Yaml::loadFile(MAD_ROOT.'/config/database.yml');
- $spec = $config[$spec];
- }
- // $spec is an associative array
- if (is_array($spec)) {
-
- // validation of array is handled by horde_db
- self::$_connectionSpec = $spec;
- } else {
- throw new Mad_Model_Exception("Invalid Connection Specification");
- }
- }
- /**
- * Returns true if a connection that's accessible to this class have already
- * been opened.
- *
- * @return boolean
- */
- public static function isConnected()
- {
- return isset(self::$_activeConnection);
- }
- /**
- * Locate/Activate the connection
- *
- * @return Mad_Model_ConnectionAdapter_Abstract
- */
- public static function retrieveConnection()
- {
- // already have active connection
- if (self::$_activeConnection) {
- $conn = self::$_activeConnection;
- // connection based on spec
- } elseif ($spec = self::$_connectionSpec) {
- if (empty($spec['logger'])) {
- $spec['logger'] = self::logger();
- }
- $adapter = Horde_Db_Adapter::getInstance($spec);
- $conn = self::$_activeConnection = $adapter;
- }
- if (empty($conn)) {
- throw new Mad_Model_Exception("Connection Not Established");
- }
- return $conn;
- }
- /**
- * Remove the connection for this class. This will close the active
- * connection and the defined connection (if they exist). The result
- * can be used as argument for establishConnection, for easy
- */
- public static function removeConnection()
- {
- $spec = self::$_connectionSpec;
- $conn = self::$_activeConnection;
- self::$_connectionSpec = null;
- self::$_activeConnection = null;
- if ($conn) { $conn->disconnect(); }
- return $spec ? $spec : '';
- }
- /**
- * Returns the connection currently associated with the class. This can
- * also be used to "borrow" the connection to do database work unrelated
- * to any of the specific Active Records.
- *
- * @return Mad_Model_ConnectionAdapter_Abstract
- */
- public static function connection()
- {
- if (self::$_activeConnection) {
- return self::$_activeConnection;
- } else {
- return self::$_activeConnection = self::retrieveConnection();
- }
- }
- /*##########################################################################
- # DB Table column/keys
- ##########################################################################*/
- /**
- * Get the name of the table
- * @return string
- */
- public function tableName()
- {
- if (isset($this->_tableName)) {
- return $this->_tableName;
- } else {
- return $this->resetTableName();
- }
- }
-
- /**
- * Reset the table name based on conventions
- *
- */
- public function resetTableName()
- {
- return $this->_tableName =
- Mad_Support_Inflector::tableize($this->baseClass());
- }
- /**
- * Get the name of the primary key column
- * @return string
- */
- public function primaryKey()
- {
- if (isset($this->_primaryKey)) {
- return $this->_primaryKey;
- } else {
- return $this->resetPrimaryKey();
- }
- }
-
- /**
- * Rest primary key name based on conventions.
- */
- public function resetPrimaryKey()
- {
- return $this->_primaryKey = 'id';
- }
- /**
- * Get class name column used for single-table inheritance
- *
- * @return string
- */
- public function inheritanceColumn()
- {
- return $this->_inheritanceColumn;
- }
- /**
- * Set the name of the table for the model
- * @param string $table
- */
- public function setTableName($value)
- {
- $this->_tableName = $value;
- }
- /**
- * Set the name of the table for the model
- * @param string $value
- */
- public function setPrimaryKey($value)
- {
- $this->_primaryKey = $value;
- }
- /**
- * Change the default column used for single-table inheritance
- * @param string $col
- */
- public function setInheritanceColumn($col)
- {
- $this->_inheritanceColumn = $col;
- }
- /**
- * Returns an array of column objects for the table associated
- * with this class.
- *
- * @return array
- */
- public function columns()
- {
- if (empty($this->_columns)) {
- $this->_columns = $this->connection->columns($this->tableName(),
- "$this->_className Columns");
- foreach ($this->_columns as $col) {
- $col->setPrimary($col->getName() == $this->_primaryKey);
- }
- }
- return $this->_columns;
- }
- /**
- * Returns a hash of column objects for the table associated with
- * this class.
- *
- * @return array
- */
- public function columnsHash()
- {
- if (empty($this->_columnsHash)) {
- foreach ($this->columns() as $col) {
- $this->_columnsHash[$col->getName()] = $col;
- }
- }
- return $this->_columnsHash;
- }
- /**
- * Returns an array of column names as strings.
- *
- * @return array
- */
- public function columnNames()
- {
- if (empty($this->_columnNames)) {
- foreach ($this->columns() as $col) {
- $this->_columnNames[] = $col->getName();
- }
- }
- return $this->_columnNames;
- }
- /**
- * Reset the column info
- */
- public function resetColumnInformation()
- {
- $this->_columns = $this->_columnsHash =
- $this->_columnNames = $this->_inheritanceColumn = null;
- }
- /**
- * Get the base class for this model. Defined by subclass
- *
- * @return string
- */
- public function baseClass()
- {
- // go up single hierarchy if this is an STI model
- $parentClass = get_parent_class($this);
- if ($parentClass != 'Mad_Model_Base') {
- return $parentClass;
- }
- return $this->_className;
- }
- /*##########################################################################
- # Attributes
- ##########################################################################*/
- /**
- * Set list of attributes protected from mass assignment
- *
- * @todo implement this in save statements
- * @param string $attribute
- */
- public function attrProtected($attributes)
- {
- $names = func_get_args();
- $this->_attrProtected = array_unique(
- array_merge($this->_attrProtected, $names));
- }
- /**
- * Get the value for an attribute in this model
- *
- * @param string $name
- * @return string
- */
- public function readAttribute($name)
- {
- // active-record attributes
- if (array_key_exists($name, $this->_attributes)) {
- return $this->_attributes[$name];
- // no value set yet
- } else {
- return null;
- }
- }
- /**
- * Set the value for an attribute in this model
- *
- * @param string $name
- * @param mixed $value
- */
- public function writeAttribute($name, $value)
- {
- // active-record attributes
- if (array_key_exists($name, $this->_attributes)) {
- $this->_attributes[$name] = $value;
- }
- }
- /**
- * Get the human attribute name for a given attribute
- *
- * @return string
- */
- public function humanAttributeName($attr)
- {
- $col = $this->columnForAttribute($attr);
- return Mad_Support_Inflector::humanize($col->getName());
- }
- /**
- * Get the array of attribute fields
- * @return array
- */
- public function getAttributes()
- {
- return $this->_attributes;
- }
-
- /**
- * Mass assign attributes for this model
- * @param array $attributes
- */
- public function setAttributes($attributes = array())
- {
- // Set attributes by array
- if (is_array($attributes)) {
- foreach ($attributes as $attribute => $value) {
- $this->$attribute = $value;
- }
- // Set primary key (Beware this does not instantiate other properties)
- } elseif (is_numeric($attributes)) {
- $this->{$this->primaryKey()} = $attributes;
- }
- }
- /**
- * Finder methods must instantiate through this method to work with the
- * single-table inheritance model that makes it possible to create
- * objects of different types from the same table.
- *
- * @param array $record
- */
- public function instantiate($record)
- {
- // single table inheritance
- $column = $this->inheritanceColumn();
- if (isset($record[$column]) && $className = $record[$column]) {
- if (!class_exists($className)) {
- $msg = "The single-table inheritance mechanism failed to ".
- "locate the subclass: '$className'. This error is raised ".
- "because the column '$column' is reserved for storing the ".
- "class in case of inheritance. Please rename this column ".
- "if you didn't intend it to be used for storing the ".
- "inheritance class.";
- throw new Mad_Model_Exception($msg);
- }
- $model = new $className;
- } else {
- $model = clone $this;
- }
- return $model->setValuesByRow($record);
- }
- /**
- * Set the values for this object using a db result set.
- *
- * <code>
- * <?php
- * ...
- * $folder = new Folder();
- * $row = $result->fetchRow();
- * $folder->setValuesByRow($row)
- * ...
- * ?>
- * </code>
- *
- * @param array $dbValues
- * @return Mad_Model_Base
- */
- public function setValuesByRow($values)
- {
- // active-record attributes
- foreach ($this->_attributes as $name => $value) {
- if (array_key_exists($name, $values)) {
- $this->writeAttribute($name, $values[$name]);
- }
- }
- // attr-writers
- foreach ($this->_attrWriters as $name) {
- if (array_key_exists($name, $values)) {
- $this->$name = $values[$name];
- }
- }
- // this isn't a new record if we've loaded it from the db
- $this->_newRecord = false;
- return $this;
- }
- /**
- * Returns an array of names for the attributes available on this
- * object sorted alphabetically.
- *
- * @return array
- */
- public function attributeNames()
- {
- $attrs = array_keys($this->_attributes);
- sort($attrs);
- return $attrs;
- }
- /**
- * Returns the column object for the named attribute
- *
- * @param string $name
- * @return object
- */
- public function columnForAttribute($name)
- {
- $colHash = $this->columnsHash();
- return $colHash[$name];
- }
- /*##########################################################################
- # Deprecated column accessors
- ##########################################################################*/
- /**
- * Get an array of columns
- * @deprecated
- * @param string $tblAlias prepend table alias to columns
- * @param boolean $colAlias Generate column aliases for TO_CHAR()s
- * @return array
- */
- public function getColumns($tblAlias=null, $colAlias=true)
- {
- $tblAlias = isset($tblAlias) ? "$tblAlias." : null;
- foreach ($this->_attributes as $name => $value) {
- $cols[] = $tblAlias.($name);
- }
- return isset($cols) ? $cols : array();
- }
- /**
- * Construct the column string from the columns. Convert timestamps to string (TO_CHAR)
- * @deprecated
- * @param string $tblAlias prepend table alias to columns
- * @param boolean $colAlias Generate column aliases for TO_CHAR()s
- * @return string
- */
- public function getColumnStr($tblAlias=null, $colAlias=true)
- {
- foreach ($this->getColumns($tblAlias, $colAlias) as $col) {
- $parts = explode('.', $col);
- // has table alias
- if (isset($parts[1])) {
- $quoted[] = $this->connection->quoteColumnName($parts[0]).'.'.
- $this->connection->quoteColumnName($parts[1]);
- // column only
- } else {
- $quoted[] = $this->connection->quoteColumnName($parts[0]);
- }
- }
- return join(', ', $quoted);
- }
- /**
- * Get the insert values string from the columns.
- * @deprecated
- * @return string
- */
- public function getInsertValuesStr()
- {
- $vals = array();
- foreach ($this->_attributes as $name => $value) {
- $vals[] = $this->_quoteValue($value);
- }
- return join(', ', $vals);
- }
- /*##########################################################################
- # Associations
- ##########################################################################*/
- /**
- * Returns the Association object for the named association
- *
- * @param string $name
- * @return Mad_Model_Association_Base
- */
- public function reflectOnAssociation($name)
- {
- $this->_initAssociations();
- if (! isset($this->_associations[$name])) {
- throw new Mad_Model_Exception("Association $name does not exist for ".get_class($this));
- }
- return $this->_associations[$name];
- }
- /**
- * Since the value associated with the association has change, force it to
- * reload
- */
- public function reloadAssociation($name)
- {
- if (isset($this->_associationMethods)) {
- $this->_associationMethods = null;
- $this->_associations = null;
- }
- }
- /**
- * Set as association as being loaded
- * @param string $name
- */
- public function setAssociationLoaded($name)
- {
- $this->_initAssociations();
- if (isset($this->_associations[$name])) {
- $this->_associations[$name]->setLoaded();
- }
- }
- /*##########################################################################
- # CRUD Class methods
- ##########################################################################*/
- /**
- * <b>FIND BY PRIMARY KEY.</b>
- *
- * <code>
- * $binder = Binder::find(123);
- * $binders = Binder::find(array(123, 234));
- * </code>
- *
- *
- * <b>FIND ALL</b>
- *
- * Retrieve using WHERE conditions using SQL:
- * <code>
- * $binders = Binder::find('all', array(
- * 'conditions' => "name = 'Stubbed Images'")
- * );
- * </code>
- *
- * Retrieve using WHERE conditions and LIMIT:
- * <code>
- * $binders = Binder::find('all', array('conditions' => 'name = :name',
- * 'order' => 'name DESC'
- * 'limit' => 10),
- * array(':name' => 'Stubbed Images'));
- * </code>
- *
- * Retrieve using WHERE conditions and OFFSET (same as mysql LIMIT 20, 10):
- * <code>
- * $binders = Binder::find('all', array('conditions' => 'name = :name',
- * 'order' => 'name DESC'
- * 'offset' => 20,
- * 'limit' => 10),
- * array(':name' => 'Stubbed Images'));
- * </code>
- *
- * Retrieve using WHERE conditions and FROM tables:
- * <code>
- * $folders = Folder::find('all', array('conditions' => 'f.folderid=d.parent_folderid',
- * 'from' => 'folders f, documents d'));
- * </code>
- *
- *
- * <b>FIND FIRST</b>
- *
- * Find the first record that matches the given criteria. (same options as find('all')
- * <code>
- * $binder = Binder::find('first', array('conditions' => 'f.folderid=d.parent_folderid',
- * 'from' => 'folders f, documents d'));
- * </code>
- *
- *
- * @param mixed $type (pk/pks/all/first/count)
- * @param array $options
- * @param array $bindVars
- * @throws Mad_Model_Exception_RecordNotFound
- */
- public static function find($type, $options=null, $bindVars=null)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_find($type, $options, $bindVars);
- }
- /**
- * A convenience wrapper for find('first'). You can pass in all the
- * same arguments to this method as you can to find('first').
- *
- * @see Mad_Model_Base::find()
- *
- * @param array $options
- * @param array $bindVars
- */
- public static function first($options=null, $bindVars=null)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_find('first', $options, $bindVars);
- }
- /**
- * Count how many records match the given criteria
- * <code>
- * $binderCnt = Binder::count(array('name' => 'Stubbed Images'));
- * </code>
- */
- public static function count($options=null, $bindVars=null)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_count($options, $bindVars);
- }
- /**
- * This method provides an interface for finding records using direct sql instead of
- * the componentized api of find(). This is however not always desired as find() does
- * some magic that this method cannot do.
- *
- * <b>FIND ALL RECORDS BY SQL</b>
- *
- * <code>
- * $sql = 'SELECT *
- * FROM briefcases
- * WHERE name=:name';
- * $collection = Binder::findBySql('all', $sql, array(':name'=>'Stubbed Images'));
- * </code>
- *
- *
- * <b>FIND FIRST RECORD BY SQL</b>
- *
- * <code>
- * $sql = 'SELECT *
- * FROM briefcases
- * WHERE name=:name';
- * $binder = Binder::findBySql('first', $sql, array(':name'=>'Stubbed Images'));
- * </code>
- *
- *
- * @param string $type
- * @param string $sql
- * @param array $bindVars
- */
- protected static function findBySql($type, $sql, $bindVars=null)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_findBySql($type, $sql, $bindVars);
- }
- /**
- * This method provides an interface for counting records using direct sql
- * instead of the componentized api of find(). This is however not always
- * desired as find() does some magic that this method cannot do.
- *
- * <b>COUNT RECORDS BY SQL</b>
- *
- * <code>
- * $sql = 'SELECT COUNT(1)
- * FROM briefcases
- * WHERE name=:name';
- * $binder = Binder::countBySql($sql, array(':name'=>'Stubbed Images'));
- * </code>
- *
- * @param string $sql
- * @param array $bindVars
- */
- protected static function countBySql($sql, $bindVars=null)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_countBySql($sql, $bindVars);
- }
- /**
- * Paginate records for find()
- *
- * @param array $options
- * @param array $bindVars
- * @return Mad_Model_Collection
- */
- protected static function paginate($options=null, $bindVars=null)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_paginate($options, $bindVars);
- }
- /**
- * Check if this record exists.
- *
- * <code>
- * $folderExists = Folder::exists(123);
- * </code>
- *
- * @param int $id
- * @return boolean
- */
- public static function exists($id)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_exists($id);
- }
- /**
- * Create a new record in the db from the attributes of the model
- *
- * Create single record
- * <code>
- * $binder = Binder::create(array('name' => "derek's binder"));
- * </code>
- *
- * Create multiple records
- * <code>
- * $binders = Binder::create(array(array('name' => "derek's binder"),
- * array('name' => "dallas' binder")));
- * </code>
- *
- * @param array $attributes
- * @return mixed single model object OR array of model objects
- */
- public static function create($attributes)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_create($attributes);
- }
- /**
- * Update record in the db directly by pk or array of pks
- *
- * Single record update
- * <code>
- * $binder = Binder::update(123, array('name' => 'My new name'));
- * </code>
- *
- * Multiple record update
- * <code>
- * $binders = Binder::update(array(123, 456), array('name' => 'My new name'));
- * </code>
- *
- * @param int $id
- * @param array $attributes
- * @return void
- */
- public static function update($id, $attributes=null)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_update($id, $attributes);
- }
- /**
- * Delete record(s) from the database by primary key
- *
- * Delete single record
- * <code>
- * Binder::delete(123);
- * </code>
- *
- * Delete multiple records
- * <code>
- * Binder::delete(array(123, 234));
- * </code>
- *
- * @param mixed $id (int or array of ints)
- * @return void
- */
- public static function delete($id)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_delete($id);
- }
- /**
- * Update multiple records that match the given conditions
- *
- * <code>
- * Binder::update("description = 'my tests'", 'name = :name',
- * array(':name' => 'My test binder'));
- * </code>
- *
- * @param string $set
- * @param string $conditions
- * @param array $bindVars
- * @return void
- */
- public static function updateAll($set, $conditions=null, $bindVars=null)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_updateAll($set, $conditions, $bindVars);
- }
- /**
- * Delete multiple records that match the given conditions
- *
- * <code>
- * Binder::delete('name = :name', array(':name' => 'My test binder'));
- * </code>
- *
- * @param string $conditions
- * @param array $bindVars
- */
- public static function deleteAll($conditions=null, $bindVars=null)
- {
- // hack to get name of this class (because of static)
- $bt = debug_backtrace();
- $m = new $bt[1]['class'];
- return $m->_deleteAll($conditions, $bindVars);
- }
- /*##########################################################################
- # CRUD Instance methods
- ##########################################################################*/
- /**
- * Save data stored in memory (the object) back into the database. Performs either
- * an insert or an update depending on if this is a new record
- *
- * Insert a row
- * <code>
- * $binder = new Binder(array('name' => "Derek's binder"));
- * $binder->save();
- * </code>
- *
- * Update a row
- * <code>
- * $binder = Binder::find(123);
- * $binder->name = "Derek's updated binder";
- * $binder->save();
- * </code>
- *
- * @return mixed boolean or Mad_Model_Base
- * @throws Mad_Model_Exception_Validation
- */
- public function save()
- {
- // All saves are atomic - only start transaction if one hasn't been
- $started = $this->connection->transactionStarted();
- if (!$started) { $this->connection->beginDbTransaction(); }
- try {
- // save associated models this model depends on & validate data
- $this->_saveAssociations('before');
- $this->_validateData();
- $this->_createOrUpdate();
- $this->_saveAssociations('after');
- $this->_newRecord = false;
- if (!$started) { $this->connection->commitDbTransaction(); }
- $this->_throw = false;
- return $this;
- } catch (Exception $e) {
- $this->connection->rollbackDbTransaction();
- if ($this->_throw) {
- $this->_throw = false;
- throw $e;
- }
- return false;
- }
- }
- /**
- * Attempts to save the record, but instead of just returning false if it
- * couldn't happen, it throws a Mad_Model_Exception_Validation
- *
- * @see Mad_Model_Base::save()
- *
- * @return object
- * @throws Mad_Model_Exception_Validation
- */
- public function saveEx()
- {
- $this->_throw = true;
- $this->save();
- }
- /**
- * Update specific attributes for the current object
- *
- * Update single attribute
- * <code>
- * $binder = Binder::find(123);
- * $binder->updateAttributes('name', 'My New Briefcase');
- * </code>
- *
- * @param string $name
- * @param string $value
- * @return void
- */
- public function updateAttribute($name, $value)
- {
- $this->$name = $value;
- return $this->save();
- }
- /**
- * Update multiple attributes for the current object
- *
- * Update multiple attributes
- * <code>
- * $binder = Binder::find(123);
- * $binder->updateAttributes(array('name' => 'The new name',
- * 'description' => 'The new description'));
- * </code>
- *
- * @param array|Traversable $attributes
- * @return void
- */
- public function updateAttributes($attributes)
- {
- if (! is_array($attributes)) {
- if (! $attributes instanceof Traversable) {
- return false;
- }
- }
- foreach ($attributes as $attribute => $value) {
- $this->$attribute = $value;
- }
- return $this->save();
- }
- /**
- * Destroy a record (delete from db)
- *
- * A custom implementation of destroy() can be written for a model by overriding
- * the _destroy() method. This will ensure that all callbacks are still executed
- *
- * <code>
- * $binder = Binder::find(123);
- * $binder->destroy();
- * </code>
- *
- * @return boolean
- */
- public function destroy()
- {
- // All deletes are atomic
- $started = $this->connection->transactionStarted();
- if (!$started) { $this->connection->beginDbTransaction(); }
- try {
- $this->_beforeDestroy();
- $this->_destroy();
- $this->_afterDestroy();
- if (!$started) { $this->connection->commitDbTransaction(); }
- return true;
- } catch (Exception $e) {
- $this->connection->rollbackDbTransaction(false);
- return false;
- }
- }
- /**
- * Replace bind variables in the sql string.
- *
- * @param string $sql
- * @param array $bindVars
- */
- public function sanitizeSql($sql, $bindVars)
- {
- preg_match_all("/(:\w+)/", $sql, $matches);
- if (!isset($matches[1])) return;
- foreach ($matches[1] as $replacement) {
- if (!array_key_exists($replacement, $bindVars)) {
- $msg = "missing value for $replacement in $sql";
- throw new Mad_Model_Exception($msg);
- }
- $sql = str_replace(
- $replacement,
- $this->_quoteValue($bindVars[$replacement]),
- $sql
- );
- }
- return $sql;
- }
- /**
- * Reload values from the database
- */
- public function reload()
- {
- $model = $this->find($this->id);
- foreach ($model->getAttributes() as $name => $value) {
- $this->writeAttribute($name, $value);
- }
- // reset associations
- if (isset($this->_associations)) {
- foreach ($this->_associations as $assoc) {
- $assoc->setLoaded(false);
- }
- }
-
- return $this;
- }
- /**
- * Check if this is a record that hasn't been inserted yet
- *
- * @return boolean
- */
- public function isNewRecord()
- {
- return $this->_newRecord;
- }
- /**
- * This flag allows us to set explicitly that the association has changed and needs
- * to be saved even if the object itself hasn't been changed
- *
- * @param boolean $assocSaved
- */
- public function setIsAssocChanged($assocChanged=true)
- {
- $this->_assocChanged = $assocChanged;
- }
- /**
- * Check if the association has changed
- *
- * @return boolean
- */
- public function isAssocChanged()
- {
- return $this->_assocChanged;
- }
- /**
- * Check if this object is frozen for modification
- *
- * @return boolean
- */
- public function isDestroyed()
- {
- return $this->_frozen;
- }
- /*##########################################################################
- # Associations - These are set in _initialize() method of subclass
- ##########################################################################*/
- /**
- * This defines a one-to-one relationship with another model class. It declares
- * that the given class has a parent relationship to this model.
- *
- * The foreign key must be specified in the options of the declaration
- * using 'foreignKey'
- *
- * For Document model
- * <code>
- * <?php
- * ...
- * protected function _initialize()
- * {
- * // specify that the Document has a parent Folder
- * $this->belongsTo('Folder', array('foreignKey' => 'parent_folderid'));
- * }
- * ...
- * ?>
- * </code>
- *
- * When we specify this relationship, special attributes and methods are dynamically
- * added to the Document model.
- *
- *
- * Access the parent folder. This performs a query to get the parent folder
- * object of the document.
- *
- * <code>
- * <?php
- * ...
- * // the very verbose..
- * $folderId = $document->parent_folderid;
- * $parentFolder = Folder::find($folderId);
- * $folderName = $parentFolder->folder_name;
- *
- * // can now be simply written as
- * $folderName = $document->folder->folder_name;
- * ...
- * ?>
- * </code>
- *
- * The parent class name is assumed to be the mixed-case singular form of the
- * class name. The association name however can be defined as any name you wish
- * by specifying 'className' option.
- *
- * For Document model
- * <code>
- * <?php
- * ...
- * protected function _initialize()
- * {
- * // specify that the Document has a parent Folder
- * $this->belongsTo('Parent', array('foreignKey' => 'parent_folderid',
- * array('className' => 'Folder')));
- * }
- * ...
- * // now we can access the property using the name 'parent'
- * $parentFolder = $document->parent;
- * ...
- * ?>
- * </code>
- *
- * @param string $associationId
- * @param array $options
- */
- protected function belongsTo($associationId, $options=null)
- {
- $this->_addAssociation('belongsTo', $associationId, $options);
- }
- /**
- * This defines a one-to-one relationship with another model class. It declares
- * that a given class is a child of this model.
- *
- * The foreign key must be specified in the options of the declaration using
- * 'foreignKey'. This declaration defines the same set of methods in the model
- * object as belongsTo, So given the MdMetadata class example..
- *
- * Any given metadata can have a single icon associated with it
- *
- * For MdMetadata model
- * <code>
- * <?php
- * ...
- * protected function _initialize()
- * {
- * // specify that the Metadata has an associated metadata icon
- * $this->hasOne('MdIcon');
- * }
- * ...
- * ?>
- * </code>
- *
- * Now we can refer to the new object through the association
- * <code>
- * <?php
- * ...
- * // the very verbose..
- * $metadataId = $metadata->metadataid;
- * $mdIcon = MdIcon::find($metadataId);
- * $altText = $mdIcon->alt_text;
- *
- * // can now be simply written as
- * $altText = $metadata->mdIcon->alt_text;
- * ...
- * ?>
- * </code>
- *
- * The child class name is assumed to be the mixed-case singular form of the
- * class name. The association name however can be defined as any name you wish
- * by specifying 'className' option similar to belongsTo().
- *
- * Another options available to hasOne is 'dependent'. You can define if the associated
- * object is dependent on this object existing. This can be one of two options,
- * 1. destroy (the default)
- * 2. nullify
- *
- * A metadata Icon can't exist without it's associated metadata. Because of this, we
- * can tell metadata to destroy all metadata icons before
- *
- * @see Mad_Model_Base::belongsTo()
- *
- * @param string $associationId
- * @param array $options
- */
- protected function hasOne($associationId, $options=null)
- {
- $this->_addAssociation('hasOne', $associationId, $options);
- }
- /**
- * This defines a one-to-many relationship with another model class.
- * Define an attribute that behaves like a collection of the child objects.
- *
- * The foreign key must be specified in the options of the declaration using
- * 'foreignKey'. Ordering of children objects can also be specified using the
- * 'order' option.
- *
- * The child class name is assumed to be the mixed-case plural form of the
- * class name. The association name however can be defined as any name you wish
- * by specifying 'className' option similar to belongsTo()
- *
- * For Folder model with multiple documents
- * <code>
- * <?php
- * ...
- * protected function _initialize()
- * {
- * // specify that the Document has a parent Folder
- * $this->hasMany('Documents', array('foreignKey' => 'parent_folderid',
- * 'order' => 'document_path'));
- * }
- * ...
- * ?>
- * </code>
- *
- * Now we can refer to the new object through the association
- * <code>
- * <?php
- * ...
- * // the very verbose..
- * $folderId = $folder->folderid;
- * $documents = Document::find('all',
- * array('conditions' => 'parent_folderid=:id'),
- * array(':id' => $folderId));
- * foreach ($documents as $document) {
- * print $document->document_name;
- * }
- *
- * // can now be simply written as
- * foreach ($folder->documents as $document) {
- * print $document->document_name;
- * }
- * ...
- * ?>
- * </code>
- *
- * @see Mad_Model_Base::belongsTo()
- * @param string $associationId
- * @param array $options
- */
- protected function hasMany($associationId, $options=null)
- {
- $this->_addAssociation('hasMany', $associationId, $options);
- }
- /**
- * This defines a many-to-many relationship with another model class. It acts
- * in many ways similar to hasMany(), but allows us to specify an association table
- * between the two associated classes.
- *
- * The join table must be specified using the 'joinTable' option. The foreign keys
- * in the join table will be assumed to be the same name as the primary key from
- * the two respective tables. If this is not the case, the foreign key columns can
- * be specified using the 'foreignKey' or 'associationForeignKey' options. Ordering
- * of children objects can also be specified using the 'order' option.
- *
- * The child class name is assumed to be the mixed-case plural form of the
- * class name. The association name however can be defined as any name you wish
- * by specifying 'className' option similar to belongsTo()
- *
- * For Folder model with multiple documents
- * <code>
- * <?php
- * ...
- * protected function _initialize()
- * {
- * // specify that a briefcase has many documents,
- * // and also belongs to many documents
- * $this->hasAndBelongsToMany('Documents',
- * array('joinTable' => 'briefcase_documents',
- * 'order' => 'briefcase_documents.ordering'));
- *
- * }
- * ...
- * ?>
- * </code>
- *
- * If the foreign key names didn't match our convention, we'd have to specify them
- * as follows:
- *
- * <code>
- * <?php
- * ...
- * protected function _initialize()
- * {
- * // specify that a briefcase has many documents,
- * // and also belongs to many documents
- * $this->hasAndBelongsToMany('Documents',
- * array('joinTable' => 'briefcase_documents',
- * 'foreignKey' => 'briefcaseid',
- * 'associationForeignKey' => 'documentid',
- * 'order' => 'briefcase_documents.ordering'));
- * }
- * ...
- * ?>
- * </code>
- *
- * Now we can refer to the new object through the association
- * <code>
- * <?php
- * ...
- * // the very verbose..
- * $id = $binder->briefcaseid;
- * $documents = Document::find('all',
- * array('select' => 'd.*',
- * 'from' => 'documents d, briefcase_documents bd',
- * 'conditions' => 'd.documentid=bd.documentid
- * AND bd.briefcaseid=:id'),
- * array(':id' => $id));
- * foreach ($documents as $document) {
- * print $document->document_name;
- * }
- *
- * // can now be simply written as
- * foreach ($binder->documents as $document) {
- * print $document->document_name;
- * }
- * ...
- * ?>
- * </code>
- *
- * @see Mad_Model_Base::belongsTo()
- * @param string $associationId
- * @param array $options
- */
- protected function hasAndBelongsToMany($associationId, $options=null)
- {
- $this->_addAssociation('hasAndBelongsToMany', $associationId, $options);
- }
- /*##########################################################################
- # Validation - These are set in _initialize() method of subclass
- ##########################################################################*/
- /**
- * Check for errors, and throw exception if found
- * @throws Mad_Model_Exception_Validation
- */
- protected function checkErrors()
- {
- if (!$this->errors->isEmpty()) {
- throw new Mad_Model_Exception_Validation($this->errors->fullMessages());
- }
- }
- /**
- * Check if the data for this method is valid. This will also
- * populate the errors property
- * @return boolean
- */
- public function isValid()
- {
- return $this->_validateData();
- }
- /**
- * This method is invoked on every save() operation. Override
- * this in concrete subclasses to implement your own insert/update validation
- */
- protected function validate(){}
- /**
- * This method is invoked when a record is being inserted. Override
- * this in concrete subclasses to implement your own insert validation
- */
- protected function validateOnCreate(){}
- /**
- * This method is invoked when a record is bieng updated. Override
- * this in concrete subclasses to implement your own update validation.
- */
- protected function validateOnUpdate(){}
- /**
- * Validate the format of the data using ctype or regex.
- * Options:
- * - on: string save, create, or update. Defaults to: save
- * - with: string The ctype/regex to validate against
- * [alpha], [digit], [alnum], or /regex/
- * - message: string Custom error message (default is: "is invalid")
- *
- * <code>
- * <?php
- * ...
- * // make sure parent_id attribute is a digit only on inserts
- * $this->validatesFormatOf('parent_id', array('on' => 'insert', 'with' => '[digit]');
- *
- * // make sure length attribute matches regexp
- * $this->validatesFormatOf('length', array('with' => '/\d+(in|cm)/i');
- * ...
- * ?>
- * </code>
- *
- * @param mixed $attributes
- * @param array $options
- */
- protected function validatesFormatOf($attributes, $options=array())
- {
- $attributes = func_get_args();
- $last = end($attributes);
- $options = is_array($last) ? array_pop($attributes) : array();
- $this->_addValidation('format', $attributes, $options);
- }
- /**
- * Validate the length of the data.
- * Options:
- * - on: string save, create, or update. Defaults to: save
- * - minimum: int Value may not be greater than this int
- * - maximum: int Value may not be less than this int
- * - is: int Value must be specific length
- * - within: array The length of value must be in range: eg. array(3, 5)
- * - allowNull: bool Allow null values through
- *
- * - tooLong: string Message when 'maximum' is violated
- * (default is: "%s is too long (maximum is %d characters)")
- * - tooShort: string Message when 'minimum' is violated
- * (default is: "%s is too short (minimum is %d characters)")
- * - wrongLength: string Message when 'is' is invalid.
- * (default is: "%s is the wrong length (should be %d characters)")
- * - message: string Message to use for a 'minimum', 'maximum', or 'is violation.
- * An alias of the appropriate tooLong/tooShort/wrongLength msg
- *
- * <code>
- * <?php
- * ...
- * // validate name is between 20 and 255 chars
- * $this->validatesLengthOf('name', array('within' => '20..255');
- *
- * // validate is_locked is 1 char
- * $this->validatesLengthOf('is_locked', array('is' => 1);
- *
- * // validate password is more than or equal to 8 chars
- * $this->validatesLengthOf('password', array('minimum' => 8);
- * ...
- * ?>
- * </code>
- *
- * @param mixed $attributes
- * @param int $minLength
- * @param int $maxLength
- */
- protected function validatesLengthOf($attributes, $options=array())
- {
- $attributes = func_get_args();
- $last = end($attributes);
- $options = is_array($last) ? array_pop($attributes) : array();
- $this->_addValidation('length', $attributes, $options);
- }
- /**
- * Validate that the data is numeric. (Yes I'm aware numericality is not a real word)
- * Options:
- * - on: string save, create, or update. Defaults to: save
- * - onlyInteger: bool Don't allow floats
- * - allowNull: bool Are null values valid. Defaults to: false
- * - message: string Defaults to: "%s is not a number."
- *
- * <code>
- * <?php
- * ...
- * // validate that height is a number
- * $this->validatesNumericalityOf('height');
- * $this->validatesNumericalityOf('age', array('only_integer' => true));
- * ...
- * ?>
- * </code>
- *
- * @param mixed $attributes
- */
- protected function validatesNumericalityOf($attributes, $options=array())
- {
- $attributes = func_get_args();
- $last = end($attributes);
- $options = is_array($last) ? array_pop($attributes) : array();
- $this->_addValidation('numericality', $attributes, $options);
- }
- /**
- * Validate that the data isn't empty
- * Options:
- * - on: string save, create, or update. Defaults to: save
- * - message: string Defaults to: "%s can't be empty."
- *
- * <code>
- * <?php
- * ...
- * $this->validatesPresenceOf(array('name', 'description'));
- * ...
- * ?>
- * </code>
- *
- * @param mixed $attributes
- */
- protected function validatesPresenceOf($attributes, $options=array())
- {
- $attributes = func_get_args();
- $last = end($attributes);
- $options = is_array($last) ? array_pop($attributes) : array();
- $this->_addValidation('presence', $attributes, $options);
- }
- /**
- * Validate that the data is unique.
- * Options:
- * - on: string save, create, or update. Defaults to: save
- * - scope: string Limits the check to rows having the same value in the column
- * as the row being checked.
- * - message: string Defaults to: "The value for %s has already been taken."
- *
- * <code>
- * <?php
- * ...
- * $this->validatesUniquenessOf('name', array('scope' => 'parent_id'));
- * ...
- * ?>
- * </code>
- *
- * @param mixed $attributes
- */
- protected function validatesUniquenessOf($attributes, $options=array())
- {
- $attributes = func_get_args();
- $last = end($attributes);
- $options = is_array($last) ? array_pop($attributes) : array();
- $this->_addValidation('uniqueness', $attributes, $options);
- }
- /**
- * Validates an item is included in the list.
- * Options:
- * - on: string save, create, or update. Defaults to: save
- * - in: array|object array or traversable object
- * - allowNull: bool Are null values valid. Defaults to: false
- * - strict: bool If true, use === comparison. Defaults to: false (==).
- * - message: string Defaults to: "%s is not included in the list."
- *
- * <code>
- * <?php
- * ...
- * $this->validatesInclusionOf('name', array('in' => array('foo', 'bar')));
- * ...
- * ?>
- * </code>
- *
- * @param mixed $attributes
- */
- protected function validatesInclusionOf($attributes, $options = array())
- {
- $attributes = func_get_args();
- $last = end($attributes);
- $options = is_array($last) ? array_pop($attributes) : array();
- $this->_addValidation('inclusion', $attributes, $options);
- }
- /**
- * Validate that the email address is formatted correctly
- * Options:
- * - on: string
- * - message:
- *
- *
- * <code>
- * <?php
- * ...
- * $this->validatesEmailAddress('name', array('scope' => 'parent_id'));
- * ...
- * ?>
- * </code>
- */
- protected function validatesEmailAddress($attributes, $options=array())
- {
- $attributes = func_get_args();
- $last = end($attributes);
- $options = is_array($last) ? array_pop($attributes) : array();
- $with = "/^[0-9a-z_\.-]+@(([0-9]{1,3}\.){3}[0-9]{1,3}|".
- "([0-9a-z][0-9a-z-]*[0-9a-z]\.)+[a-z]{2,3})$/i";
- $msg = "must be a valid address";
- $options = array_merge(array('with' => $with, 'message' => $msg), $options);
- $this->_addValidation('format', $attributes, $options);
- }
- /*##########################################################################
- # Serialization
- ##########################################################################*/
-
- /**
- * Builds an XML document to represent the model. Some configuration is
- * available through <code>options</code>. However more complicated cases should
- * override <code>Mad_Model_Base#toXml</code>.
- *
- * By default the generated XML document will include the processing
- * instruction and all the object's attributes. For example:
- *
- * <?xml version="1.0" encoding="UTF-8"?>
- * <topic>
- * <title>The First Topic</title>
- * <author-name>David</author-name>
- * <id type="integer">1</id>
- * <approved type="boolean">false</approved>
- * <replies-count type="integer">0</replies-count>
- * <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
- * <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
- * <content>Have a nice day</content>
- * <author-email-address>david@loudthinking.com</author-email-address>
- * <parent-id></parent-id>
- * <last-read type="date">2004-04-15</last-read>
- * </topic>
- *
- * This behavior can be controlled with <code>only</code>, <code>except</code>,
- * <code>skip_instruct</code>, <code>skip_types</code> and <code>dasherize</code>.
- * The <code>only</code> and <code>except</code> options are the same as for the
- * <code>attributes</code> method. The default is to dasherize all column names, but you
- * can disable this setting <code>dasherize</code> to <code>false</code>. To not have the
- * column type included in the XML output set <code>:skip_types</code> to <code>true</code>.
- *
- * For instance:
- *
- * $topic->toXml(array('skip_instruct' => true,
- * 'except' => array('id', 'bonus_time', 'written_on', 'replies_count'));
- *
- * <topic>
- * <title>The First Topic</title>
- * <author-name>David</author-name>
- * <approved type="boolean">false</approved>
- * <content>Have a nice day</content>
- * <author-email-address>david@loudthinking.com</author-email-address>
- * <parent-id></parent-id>
- * <last-read type="date">2004-04-15</last-read>
- * </topic>
- *
- * To include first level associations use <code>include</code>:
- *
- * $firm->toXml(array('include' => array('Account', 'Clients')));
- *
- * <?xml version="1.0" encoding="UTF-8"?>
- * <firm>
- * <id type="integer">1</id>
- * <rating type="integer">1</rating>
- * <name>37signals</name>
- * <clients type="array">
- * <client>
- * <rating type="integer">1</rating>
- * <name>Summit</name>
- * </client>
- * <client>
- * <rating type="integer">1</rating>
- * <name>Microsoft</name>
- * </client>
- * </clients>
- * <account>
- * <id type="integer">1</id>
- * <credit-limit type="integer">50</credit-limit>
- * </account>
- * </firm>
- *
- * To include deeper levels of associations pass a hash like this:
- *
- * $firm->toXml(array('include' => array('Account' => array(),
- * 'Clients' => array('include' => 'Address'))));
- *
- * <?xml version="1.0" encoding="UTF-8"?>
- * <firm>
- * <id type="integer">1</id>
- * <rating type="integer">1</rating>
- * <name>37signals</name>
- * <clients type="array">
- * <client>
- * <rating type="integer">1</rating>
- * <name>Summit</name>
- * <address>
- * ...
- * </address>
- * </client>
- * <client>
- * <rating type="integer">1</rating>
- * <name>Microsoft</name>
- * <address>
- * ...
- * </address>
- * </client>
- * </clients>
- * <account>
- * <id type="integer">1</id>
- * <credit-limit type="integer">50</credit-limit>
- * </account>
- * </firm>
- *
- * To include any methods on the model being called use <code>methods</code>:
- *
- * $firm->toXml(array('methods' => array('calculated_earnings', 'real_earnings')));
- *
- * <firm>
- * # ... normal attributes as shown above ...
- * <calculated-earnings>100000000000000000</calculated-earnings>
- * <real-earnings>5</real-earnings>
- * </firm>
- *
- * As noted above, you may override <code>toXml()</code> in your <code>Mad_Model_Base</code>
- * subclasses to have complete control about what's generated. The general
- * form of doing this is:
- *
- * class IHaveMyOwnXML extends Mad_Model_Base
- * {
- * public function toXml($options = array)
- * {
- * // ...
- * }
- * }
- */
- public function toXml($options = array())
- {
- $serializer = new Mad_Model_Serializer_Xml($this, $options);
- return $serializer->serialize();
- }
- /**
- * Convert XML to an Mad_Model record
- *
- * @see Mad_Model_Base::toXml()
- * @param string $xml
- * @return Mad_Model_Base
- */
- public function fromXml($xml)
- {
- $converted = Mad_Support_ArrayObject::fromXml($xml);
- $values = array_values($converted);
- $attributes = $values[0];
- $this->setAttributes($attributes);
- return $this;
- }
- public function getXmlClassName()
- {
- return Mad_Support_Inflector::underscore($this->_className);
- }
- /**
- * Returns a JSON string representing the model. Some configuration is
- * available through <code>$options</code>.
- *
- * Without any <code>$options</code>, the returned JSON string will include all
- * the model's attributes. For example:
- *
- * $konata = User::find(1);
- * $konata->toJson();
- * # => {"id": 1, "name": "Konata Izumi", "age": 16,
- * "created_at": "2006/08/01", "awesome": true}
- *
- * The <code>only</code> and <code>except</code> options can be used to limit
- * the attributes included, and work similar to the <code>attributes</code>
- * method. For example:
- *
- * $konata->toJson(array('only' => array('id', 'name')));
- * # => {"id": 1, "name": "Konata Izumi"}
- *
- * $konata->toJson(array('except' => array('id', 'created_at', 'age')));
- * # => {"name": "Konata Izumi", "awesome": true}
- *
- * To include any methods on the model, use <code>:methods</code>.
- *
- * $konata->toJson(array('methods' => 'permalink'));
- * # => {"id": 1, "name": "Konata Izumi", "age": 16,
- * "created_at": "2006/08/01", "awesome": true,
- * "permalink": "1-konata-izumi"}
- *
- * To include associations, use <code>:include</code>.
- *
- * $konata->toJson(array('include' => 'Posts'));
- * # => {"id": 1, "name": "Konata Izumi", "age": 16,
- * "created_at": "2006/08/01", "awesome": true,
- * "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
- * {"id": 2, author_id: 1, "title": "So I was thinking"}]}
- *
- * 2nd level and higher order associations work as well:
- *
- * $konata->toJson(array('include' => array('Posts' => array(
- * 'include' => array('Comments' => array(
- * 'only' => 'body')),
- * 'only' => 'title'))));
- * # => {"id": 1, "name": "Konata Izumi", "age": 16,
- * "created_at": "2006/08/01", "awesome": true,
- * "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
- * "title": "Welcome to the weblog"},
- * {"comments": [{"body": "Don't think too hard"}],
- * "title": "So I was thinking"}]}
- *
- * @param array $options
- * @return string
- */
- public function toJson($options = array())
- {
- $serializer = new Mad_Model_Serializer_Json($this, $options);
- $serialized = $serializer->serialize();
-
- if (self::$includeRootInJson) {
- $jsonName = $this->getJsonClassName();
- return "{ $jsonName: $serialized }";
- } else {
- return $serialized;
- }
- }
-
- /**
- * Convert Json notation to an Mad_Model record
- *
- * @see Mad_Model_Base::toJson()
- * @param string $json
- * @return Mad_Model_Base
- */
- public function fromJson($json)
- {
- if (! function_exists('json_decode')) {
- throw new Mad_Model_Exception('json_decode() function required');
- }
- $attributes = (array)json_decode($json);
- $this->setAttributes($attributes);
- return $this;
- }
- public function getJsonClassName()
- {
- return '"'.Mad_Support_Inflector::underscore($this->_className).'"';
- }
- /*##########################################################################
- # Private methods
- ##########################################################################*/
- /**
- * @return string
- */
- protected function _quotedId()
- {
- return $this->_quoteValue(
- $this->id, $this->columnForAttribute($this->primaryKey())
- );
- }
-
- /**
- * Quote strings appropriately for SQL statements.
- */
- protected function _quoteValue($value, $column=null)
- {
- return $this->connection->quote($value, $column);
- }
- /**
- * Initializes the attributes array with keys matching the columns
- * from the linked table and the values matching the corresponding
- * default value of that column, so that a new instance, or one
- * populated from a passed-in Hash, still has all the attributes
- * that instances loaded from the database would.
- *
- * @todo finish
- */
- protected function _attributesFromColumnDefinition()
- {
- $attributes = array();
- foreach ($this->columns() as $col) {
- $attributes[$col->getName()] = null;
- if ($col->getName() != $this->primaryKey()) {
- $attributes[$col->getName()] = $col->getDefault();
- }
- }
- return $attributes;
- }
- /*##########################################################################
- # Find Private methods
- ##########################################################################*/
- /**
- * Check if a record exists.
- *
- * @see Mad_Model_Base::exists()
- * @param array|int $ids
- * @return boolean
- */
- protected function _exists($ids)
- {
- try {
- $this->_findFromIds($ids);
- return true;
- } catch (Mad_Model_Exception_RecordNotFound $e) {
- return false;
- }
- }
- /**
- * Where the actual work is done for find() method
- *
- * @see Mad_Model_Base::find()
- * @param mixed $type (pk or array of pks)
- * @param array $options
- * @param array $bindVars
- * @throws Mad_Model_Exception_RecordNotFound
- */
- protected function _find($type, $options, $bindVars)
- {
- $bindVars = !empty($bindVars) ? $bindVars : array();
- // find the first record that match the options
- if ($type == 'first') {
- return $this->_findInitial($options, $bindVars);
- // find all records that match the options
- } elseif ($type == 'all') {
- return $this->_findEvery($options, $bindVars);
- // type must match one of the above options
- } else {
- return $this->_findFromIds($type, $options, $bindVars);
- }
- }
- /**
- * Find by primary key values. Will either find by a single or multiple pks.
- * Single id returns a single Mad_Model_Base subclass
- * Multple ids return a Mad_Model_Collection of Mad_Model_Base subclasses
- *
- * @see Mad_Model_Base::find()
- * @param array|int $ids
- * @param array $options
- * @param array $bindVars
- *
- * @return Mad_Model_Collection|Mad_Model_Base
- * @throws Mad_Model_Exception_RecordNotFound
- */
- protected function _findFromIds($ids, $options=array(), $bindVars=array())
- {
- $expectsArray = is_array($ids);
- $ids = (array)$ids;
- foreach ($ids as &$id) {
- if (!is_int($id)) $id = trim($id);
- }
- $selectStr = $this->getColumnStr();
- if (count($ids) == 0 || !isset($ids[0])) {
- $msg = "Couldn't find ".get_class($this)." without an ID";
- throw new Mad_Model_Exception_RecordNotFound($msg);
- } elseif (count($ids) == 1) {
- $result = $this->_findOne($ids[0], $options, $bindVars);
- return $expectsArray ? new Mad_Model_Collection($this, array($result)) : $result;
- } else {
- return $this->_findSome($ids, $options, $bindVars);
- }
- }
- /**
- * Find using a single pk
- *
- * @param int $id
- * @param array $options
- * @param array $bindVars
- * @return Mad_Model_Base
- * @throws Mad_Model_Exception_RecordNotFound
- */
- protected function _findOne($id, $options, $bindVars)
- {
- $conditions = null;
- if (isset($options['conditions'])) {
- $conditions = " AND (".$options['conditions'].")";
- }
- $options['conditions'] = "$this->_tableName.$this->_primaryKey = :pkId".
- " $conditions";
- $bindVars[':pkId'] = $id;
- if ($result = $this->_findInitial($options, $bindVars)) {
- return $result;
- } else {
- $msg = "The record for id=$id was not found";
- throw new Mad_Model_Exception_RecordNotFound($msg);
- }
- }
- /**
- * Find using mutiple pks
- *
- * @param int $id
- * @param array $options
- * @return Mad_Model_Collection
- * @throws Mad_Model_Exception_RecordNotFound
- */
- protected function _findSome($ids, $options, $bindVars)
- {
- // build list of ids/binds
- $size = count($ids);
- for ($i = 0; $i < $size; $i++) $inStr[] = ":id{$i}";
- for ($i = 0; $i < $size; $i++) $bindVars[":id{$i}"] = (int) $ids[$i];
- $conditions = null;
- if (isset($options['conditions'])) {
- $conditions = " AND (".$options['conditions'].")";
- }
- $options['conditions'] = "$this->_tableName.$this->_primaryKey IN (".
- join(', ', $inStr).") $conditions";
- $result = $this->_findEvery($options, $bindVars);
- // we should always get back the same number of rows as ids
- if ($result->count() == $size) {
- return $result;
- } else {
- $msg = 'A record id IN ('.join(', ', $ids).') was not found';
- throw new Mad_Model_Exception_RecordNotFound($msg);
- }
- }
- /**
- * Find the first record matching the given options
- *
- * @see Mad_Model_Base::find()
- * @param mixed $options
- * @param array $bindVars
- * @return Mad_Model_Base
- */
- protected function _findInitial($options, $bindVars)
- {
- $result = $this->_findEvery($options, $bindVars);
- return !empty($result[0]) ? $result[0] : null;
- }
- /**
- * Find all records matching the given options
- *
- * @see Mad_Model_Base::find()
- * @param array $options
- * @param array $bindVars
- * @return array {@link Mad_Model_Base}s
- */
- protected function _findEvery($options, $bindVars)
- {
- // use eager loading associations
- if (isset($options['include'])) {
- return $this->_findWithAssociations($options, $bindVars);
- // no eager loading
- } else {
- return $this->_findEveryBySql($this->_constructFinderSql($options), $bindVars);
- }
- }
- /**
- * Count how many records match the given options
- *
- * @see Mad_Model_Base::find()
- * @param mixed $options
- * @param array $bindVars
- * @return int
- */
- protected function _count($options, $bindVars)
- {
- // if $options is a string, default it to be the conditions
- if (is_string($options)) {
- $options = array('conditions' => $options);
- }
- if (!isset($options['select'])) $options['select'] = 'COUNT(1)';
- // use eager loading associations
- if (isset($options['include'])) {
- $options['select'] = 'COUNT(DISTINCT('.$this->tableName().'.'.
- $this->primaryKey().'))';
- return $this->_countWithAssociations($options, $bindVars);
- // no eager loading
- } else {
- $sql = $this->_constructFinderSql($options);
- $sql = $this->sanitizeSql($sql, $bindVars);
- return $this->connection->selectValue($sql, "$this->_className Count");
- }
- }
- /*##########################################################################
- # FindBySql Private methods
- ##########################################################################*/
- /**
- * Where the actual work is done for findBySql() calls
- *
- * @see Mad_Model_Base::findBySql()
- * @param string $type
- * @param string $sql
- * @param array $bindVars
- */
- protected function _findBySql($type, $sql, $bindVars)
- {
- $bindVars = !empty($bindVars) ? $bindVars : array();
- // find all records that match the options
- if ($type == 'all') {
- return $this->_findEveryBySql($sql, $bindVars);
- // find the first record that match the options
- } elseif ($type == 'first') {
- return $this->_findInitialBySql($sql, $bindVars);
- }
- }
- /**
- * Find all records that are retrieved by the given sql
- *
- * @see Mad_Model_Base::findBySql()
- * @param string $sql
- * @param array $bindVars
- */
- protected function _findEveryBySql($sql, $bindVars)
- {
- $sql = $this->sanitizeSql($sql, $bindVars);
- $result = $this->connection->selectAll($sql, "$this->_className Load");
- return new Mad_Model_Collection($this, $result);
- }
- /**
- * Find the first record that is retrieved by the given sql
- *
- * @see Mad_Model_Base::findBySql()
- * @param string $sql
- * @param array $bindVars
- */
- protected function _findInitialBySql($sql, $bindVars)
- {
- $sql = $this->sanitizeSql($sql, $bindVars);
- $sql = $this->connection->addLimitOffset($sql, array('limit' => 1,
- 'offset' => 0));
- if ($row = $this->connection->selectOne($sql, "$this->_className Load")) {
- return $this->instantiate($row);
- } else {
- return null;
- }
- }
- /**
- * Count how many records are retrieved by the given sql
- *
- * @see Mad_Model_Base::findBySql()
- * @param string $sql
- * @param array $bindVars
- */
- protected function _countBySql($sql, $bindVars)
- {
- // execute query
- $sql = $this->sanitizeSql($sql, $bindVars);
- return $this->connection->selectValue($sql, "$this->_className Count");
- }
- /**
- * Paginate is a proxy to find, but determines offset/limit based on
- *
- * @see Mad_Model_Base::paginate()
- * @param array $options
- * @param array $bindVars
- * @return Mad_Model_Collection
- */
- protected function _paginate($options=null, $bindVars=null)
- {
- // determine offset/limit based on page/perPage
- $page = isset($options['page']) ? $options['page'] : 1;
- $perPage = isset($options['perPage']) ? $options['perPage'] : 15;
- unset($options['page']);
- unset($options['perPage']);
- // count records
- $countOptions = $options;
- unset($countOptions['select']);
- $total = $this->_count($countOptions, $bindVars);
- if ($total == 0) { $page = 0; }
- // find records
- if ($total) {
- $options['offset'] = $page * $perPage - $perPage;
- $options['limit'] = $perPage;
- // default to page 1 if out of range
- if ($options['offset'] > $total) {
- $page = 1;
- $options['offset'] = 0;
- }
- $results = $this->_find('all', $options, $bindVars);
- } else {
- $results = new Mad_Model_Collection($this, array());
- }
- // paginated collection
- return new Mad_Model_PaginatedCollection($results, $page, $perPage, $total);
- }
- /*##########################################################################
- # Finder SQL Construction
- ##########################################################################*/
- /**
- * Find model objects with eager loaded associations
- * @param array $options
- * @param array $bindVars
- */
- protected function _findWithAssociations($options, $bindVars)
- {
- $joinDependency = new Mad_Model_Join_Dependency($this, $options['include']);
- $sql = $this->_constructFinderSqlWithAssoc($options, $joinDependency, $bindVars);
- $sql = $this->sanitizeSql($sql, $bindVars);
- $rows = $this->connection->selectAll($sql, "$this->_className Load");
- return new Mad_Model_Collection($this, $joinDependency->instantiate($rows));
- }
-
- /**
- * Count model objects with eager loaded associations
- * @param array $options
- * @param array $bindVars
- */
- protected function _countWithAssociations($options, $bindVars)
- {
- $joinDependency = new Mad_Model_Join_Dependency($this, $options['include']);
- $sql = $this->_constructFinderSqlWithAssoc($options, $joinDependency, $bindVars);
- $sql = $this->sanitizeSql($sql, $bindVars);
- return $this->connection->selectValue($sql, "$this->_className Count");
- }
- /**
- * Construct the sql to retrieve all models w/eager associations
- * @param array $options
- * @param object $joinDependency
- * @param array $bindVars
- * @return string
- */
- protected function _constructFinderSqlWithAssoc($options, $joinDependency, $bindVars)
- {
- $valid = array('select', 'from', 'conditions', 'include',
- 'order', 'group', 'limit', 'offset');
- $options = Mad_Support_Base::assertValidKeys($options, $valid);
- // get columns from dependency
- foreach ($joinDependency->joins() as $join) {
- foreach ($join->columnNamesWithAliasForSelect() as $colAlias) {
- $cols[] = $colAlias[0].' AS '.$colAlias[1];
- }
- }
- $selectStr = isset($options['select']) ? $options['select'] : join(', ', $cols);
- $sql = "SELECT ".$selectStr;
- $sql .= " FROM ". ($options['from'] ? $options['from'] : $this->tableName());
- $sql .= $this->_constructAssociationJoinSql($joinDependency);
- $sql = $this->_addConditions($sql, $options['conditions']);
- // certain association outer joins will truncate results using 'limit'
- if (isset($options['limit']) && !$this->_usingLimitableReflections($joinDependency->reflections())) {
- $sql = $this->_addLimitedIdsCondition($sql, $options, $joinDependency, $bindVars);
- }
- if ($options['order']) $sql .= ' ORDER BY '.$options['order'];
- if ($this->_usingLimitableReflections($joinDependency->reflections())) {
- $sql = $this->connection->addLimitOffset($sql, $options);
- }
- return $sql;
- }
- /**
- * Add condition to limit our query by a specific set of ids
- * @param string $sql
- * @param array $options
- * @param object $joinDependency
- * @param array $bindVars
- * @return string
- */
- protected function _addLimitedIdsCondition($sql, $options, $joinDependency, $bindVars)
- {
- $idList = $this->_selectLimitedIdsList($options, $joinDependency, $bindVars);
- if (empty($idList)) { throw new Mad_Model_Exception('Invalid Query'); }
- $conditionWord = stristr($sql, 'where') ? ' AND ' : 'WHERE ';
- $sql .= "$conditionWord ".$this->tableName().'.'.
- $this->primaryKey()." IN ($idList)";
- return $sql;
- }
-
- /**
- * @param array $options
- * @param object $joinDependency
- * @param array $bindVars
- * @return string
- */
- protected function _selectLimitedIdsList($options, $joinDependency, $bindVars)
- {
- $result = $this->connection->selectAll(
- $this->_constructFinderSqlForAssocLimiting($options, $joinDependency, $bindVars),
- "$this->_className Load IDs For Limited Eager Loading");
- $ids = array();
- foreach ($result as $row) {
- $ids[] = $this->connection->quote($row[$this->primaryKey()]);
- }
- return join(', ', $ids);
- }
- /**
- * @param array $options
- * @param object $joinDependency
- * @param array $bindVars
- * @return string
- */
- protected function _constructFinderSqlForAssocLimiting($options, $joinDependency, $bindVars)
- {
- $isDistinct = $this->_includeEagerConditions($options) ||
- $this->_includeEagerOrder($options);
- $sql = "SELECT ";
- if ($isDistinct) {
- $sql .= $this->connection->distinct($this->tableName().'.'.$this->primaryKey());
- } else {
- $sql .= $this->primaryKey();
- }
- $sql .= ' FROM '.$this->tableName().' ';
- // add join tables/conditions/ordering
- if ($isDistinct) {
- $sql .= $this->_constructAssociationJoinSql($joinDependency);
- }
- $sql = $this->_addConditions($sql, $options['conditions']);
- if (!empty($options['order'])) {
- if ($isDistinct) {
- $sql = $this->connection->addOrderByForAssocLimiting($sql, $options);
- } else {
- $sql .= "ORDER BY ".$options['order'];
- }
- }
- $sql = $this->connection->addLimitOffset($sql, $options);
- return $this->sanitizeSql($sql, $bindVars);
- }
- /**
- * Checks if the conditions reference a table other than the
- * current model table
- *
- * @param array $options
- * @return boolean
- */
- protected function _includeEagerConditions($options)
- {
- if (!$conditions = $options['conditions']) { return false; }
- preg_match_all("/([\.\w]+)\.\w+/", $conditions, $matches);
- foreach ($matches[1] as $conditionTableName) {
- if ($conditionTableName != $this->tableName()) { return true; }
- }
- return false;
- }
- /**
- * Checks if the query order references a table other than the
- * current model's table.
- *
- * @param array $options
- * @return boolean
- */
- protected function _includeEagerOrder($options)
- {
- if (!$order = $options['order']) { return false; }
- preg_match_all("/([\.\w]+)\.\w+/", $order, $matches);
- foreach ($matches[1] as $orderTableName) {
- if ($orderTableName != $this->tableName()) { return true; }
- }
- return false;
- }
- /**
- * Cannot use LIMIT/OFFSET on certain associations
- *
- * @param array $reflections
- * @return boolean
- */
- protected function _usingLimitableReflections($reflections)
- {
- foreach ($reflections as $r) {
- $macro = $r->getMacro();
- if ($macro != 'belongsTo' || $macro != 'hasOne') { return false; }
- }
- return true;
- }
- /**
- * Construct 'OUTER JOIN' sql fragments from associations
- *
- * @param object $joinDependency
- */
- protected function _constructAssociationJoinSql($joinDependency)
- {
- // get joins from dependency
- $joins = array();
- foreach ($joinDependency->joinAssociations() as $joinAssoc) {
- $joins[] = $joinAssoc->associationJoin();
- }
- return join('', $joins);
- }
- /**
- * Construct the sql used to do a find() method
- *
- * @param array $options
- * @return string the SQL
- */
- protected function _constructFinderSql($options)
- {
- $valid = array('select', 'from', 'conditions', 'include',
- 'order', 'group', 'limit', 'offset');
- $options = Mad_Support_Base::assertValidKeys($options, $valid);
- $sql = "SELECT ".($options['select'] ? $options['select'] : $this->getColumnStr());
- $sql .= " FROM ". ($options['from'] ? $options['from'] : $this->tableName());
- $sql = $this->_addConditions($sql, $options['conditions']);
- if ($options['group']) $sql .= ' GROUP BY '.$options['group'];
- if ($options['order']) $sql .= ' ORDER BY '.$options['order'];
- return $this->connection->addLimitOffset($sql, $options);
- }
- /**
- * Add 'where' conditions to the sql
- *
- * @param string $sql
- * @param array $options
- */
- private function _addConditions($sql, $conditions)
- {
- $segments = array();
- if (!empty($conditions)) $segments[] = $conditions;
- if (!empty($segments)) $sql .= ' WHERE ('.join(') AND (', $segments).')';
- return $sql;
- }
- /*##########################################################################
- # Create/Update/Delete Private methods
- ##########################################################################*/
- /**
- * Perform save operation. Only save if model data has changed.
- * This method will perform all callback hooks for the save/update/create
- * operation.
- */
- protected function _createOrUpdate()
- {
- // before save callback
- $this->_beforeSave();
- if ($this->isNewRecord()) {
- $this->_beforeCreate();
- $this->_saveCreate();
- $this->_afterCreate();
- } else {
- $this->_beforeUpdate();
- $this->_saveUpdate();
- $this->_afterUpdate();
- }
- // after save callback
- $this->_afterSave();
- }
- /**
- * Create object during save
- *
- * @throws Mad_Model_Exception_Validation
- */
- protected function _saveCreate()
- {
- $this->_recordTimestamps();
- // build & execute SQL
- $sql = "INSERT INTO $this->_tableName (".
- " ".$this->getColumnStr().
- ") VALUES (".
- " ".$this->getInsertValuesStr().
- ")";
- $insertId = $this->connection->insert($sql, "$this->_className Insert");
- // only set the pk if it's not already set
- if ($this->primaryKey() && $this->{$this->primaryKey()} == null) {
- $this->_attributes[$this->primaryKey()] = $insertId;
- }
- return $insertId;
- }
- /**
- * Update object during save
- *
- * @throws Mad_Model_Exception_Validation
- */
- protected function _saveUpdate()
- {
- $this->_recordTimestamps();
- foreach ($this->_attributes as $column => $value) {
- if ($column != $this->primaryKey()) {
- $sets[] = $this->connection->quoteColumnName($column)." = ".
- $this->_quoteValue($value);
- } elseif ($column == $this->primaryKey()) {
- $pkVal = $this->_quoteValue($value);
- }
- }
- $sql = "UPDATE $this->_tableName ".
- " SET ".join(', ', $sets).
- " WHERE $this->_primaryKey = $pkVal";
- return $this->connection->update($sql, "$this->_className Update");
- }
- /**
- * Automatic timestamps for magic columns
- */
- protected function _recordTimestamps()
- {
- $date = date("Y-m-d");
- $time = date("Y-m-d H:i:s");
- $attr = $this->getAttributes();
- // new records
- if (array_key_exists('created_at', $attr) &&
- (empty($this->created_at) || $this->created_at == '0000-00-00 00:00:00')) {
- $this->writeAttribute('created_at', $time);
- }
- if (array_key_exists('created_on', $attr) &&
- (empty($this->created_on) || $this->created_on == '0000-00-00')) {
- $this->writeAttribute('created_on', $date);
- }
- // all saves
- if (array_key_exists('updated_at', $attr)) {
- $this->writeAttribute('updated_at', $time);
- }
- if (array_key_exists('updated_on', $attr)) {
- $this->writeAttribute('updated_on', $date);
- }
- }
- /**
- * Create a new record
- *
- * @see Mad_Model_Base::findBySql()
- * @param array $attributes
- * @return mixed single model object OR array of model objects
- */
- protected function _create($attributes)
- {
- $this->_newRecord = true;
- // MULTIPLE
- if (isset($attributes[0])) {
- $attributeList = $attributes;
- foreach ($attributeList as $attributes) {
- $obj = new $this->_className($attributes);
- $objs[] = $obj->save();
- }
- return $objs;
- // SINGLE
- } else {
- $obj = new $this->_className($attributes);
- $obj->save();
- return $obj;
- }
- }
- /**
- * Update a record
- *
- * @see Mad_Model_Base::update()
- * @param int $id
- * @param array $attributes
- * @return void
- */
- protected function _update($id, $attributes)
- {
- // MULTIPLE
- if (is_array($id)) {
- $ids = $id;
- foreach ($ids as $id) {
- $model = $this->find($id);
- $model->updateAttributes($attributes);
- $objs[] = $model;
- }
- return new Mad_Model_Collection($model, $objs);
- // SINGLE
- } else {
- $model = $this->find($id);
- return $model->updateAttributes($attributes);
- }
- }
- /**
- * Update multiple records matching the given criteria.
- *
- * @todo replacements for bindvars
- *
- * @see Mad_Model_Base::updateAll()
- * @param string $set
- * @param string $conditions
- * @param array $bindVars
- * @return void
- */
- protected function _updateAll($set, $conditions=null, $bindVars=null)
- {
- $setStr = $this->sanitizeSql($set, $bindVars);
- $conditionStr = $this->sanitizeSql($conditions, $bindVars);
- $conditionStr = !empty($conditions) ? "WHERE $conditionStr " : null;
- $sql = "UPDATE $this->_tableName ".
- " SET $setStr ".
- $conditionStr;
- return $this->connection->update($sql, "$this->_className Update");
- }
- /**
- * Perform destroy operation
- */
- protected function _destroy()
- {
- // only delete if not already deleted
- $sql = "DELETE FROM $this->_tableName ".
- " WHERE $this->_primaryKey = ".$this->_quotedId();
- return $this->connection->delete($sql, "$this->_className Delete");
- }
- /**
- * Delete a given record
- *
- * @see Mad_Model_Base::delete()
- * @param mixed $id (int or array of ints)
- * @return boolean
- */
- protected function _delete($id)
- {
- // MULTIPLE
- if (is_array($id)) {
- $ids = $id;
- foreach ($ids as $id) {
- $obj = new $this->_className();
- $obj->id = $id;
- $obj->destroy();
- }
- // SINGLE
- } else {
- $obj = new $this->_className();
- $obj->id = $id;
- $result = $obj->destroy();
- if (!$result) return false;
- }
- return true;
- }
- /**
- * Delete multiple records by the given conditions
- *
- * @todo replacements for bindvars
- *
- * @see Mad_Model_Base::deleteAll()
- * @param string $conditions
- * @param array $bindVars
- */
- protected function _deleteAll($conditions=null, $bindVars=null)
- {
- $conditionStr = $this->sanitizeSql($conditions, $bindVars);
- $conditionStr = !empty($conditions) ? "WHERE $conditionStr " : null;
- $sql = "DELETE FROM $this->_tableName $conditionStr";
- return $this->connection->delete($sql, "$this->_className Delete");
- }
- /*##########################################################################
- # Callback Methods
- ##########################################################################*/
- /**
- * Execute this callback before records are inserted
- */
- protected function _beforeValidation()
- {
- // Execute callback if it exists
- if (method_exists($this, 'beforeValidation')) {
- $this->beforeValidation();
- }
- }
- /**
- * Execute this callback after records are inserted
- */
- protected function _afterValidation()
- {
- // Execute callback if it exists
- if (method_exists($this, 'afterValidation')) {
- $this->afterValidation();
- }
- }
- /**
- * Execute this callback before records are saved
- */
- protected function _beforeSave()
- {
- $this->checkErrors();
- // Execute callback if it exists
- if (method_exists($this, 'beforeSave')) {
- $result = $this->beforeSave();
- if ($result === false) { $this->checkErrors(); }
- }
- }
- /**
- * Execute this callback before records are inserted
- */
- protected function _beforeCreate()
- {
- // Execute callback if it exists
- if (method_exists($this, 'beforeCreate')) {
- $result = $this->beforeCreate();
- if ($result === false) { $this->checkErrors(); }
- }
- }
- /**
- * Execute this callback before records are updated
- */
- protected function _beforeUpdate()
- {
- // Execute callback if it exists
- if (method_exists($this, 'beforeUpdate')) {
- $result = $this->beforeUpdate();
- if ($result === false) { $this->checkErrors(); }
- }
- }
- /**
- * Execute this callback after records are saved
- */
- protected function _afterSave()
- {
- // Execute callback if it exists
- if (method_exists($this, 'afterSave')) {
- $this->afterSave();
- }
- }
- /**
- * Execute this callback after records are inserted
- */
- protected function _afterCreate()
- {
- // Execute callback if it exists
- if (method_exists($this, 'afterCreate')) {
- $this->afterCreate();
- }
- }
- /**
- * Execute this callback after records are updated
- */
- protected function _afterUpdate()
- {
- // Execute callback if it exists
- if (method_exists($this, 'afterUpdate')) {
- $this->afterUpdate();
- }
- }
- /**
- * Execute this callback before records are destroyed
- */
- protected function _beforeDestroy()
- {
- $this->_initAssociations();
- if (isset($this->_associations)) {
- foreach ($this->_associations as $association) {
- $association->destroyDependent();
- }
- }
- // reset error stack
- $this->errors->clear();
- // Execute callback if it exists
- if (method_exists($this, 'beforeDestroy')) {
- $result = $this->beforeDestroy();
- if ($result === false) { $this->checkErrors(); }
- }
- }
- /**
- * Execute this callback after records are destroyed
- */
- protected function _afterDestroy()
- {
- // Execute callback if it exists
- if (method_exists($this, 'afterDestroy')) {
- $this->afterDestroy();
- }
- $this->_frozen = true;
- }
- /*##########################################################################
- # Validation methods
- ##########################################################################*/
- /**
- * Add a validation rule to this controller
- *
- * @param string $type
- * @param string|array $attributes
- * @param array $options
- */
- protected function _addValidation($type, $attributes, $options)
- {
- foreach ((array)$attributes as $attribute) {
- $this->_validations[] = Mad_Model_Validation_Base::factory($type, $attribute, $options);
- }
- }
- /**
- * Validate data that we are about to save
- * @return boolean true for valid, false for invalid
- */
- protected function _validateData()
- {
- // reset error stack
- $this->errors->clear();
- $this->_beforeValidation();
- // validate all
- $this->validate();
- foreach ($this->_validations as $validation) {
- $validation->validate('save', $this);
- }
- // validate create
- if ($this->isNewRecord()) {
- $this->validateOnCreate();
- foreach ($this->_validations as $validation) {
- $validation->validate('create', $this);
- }
- // validate update
- } else {
- $this->validateOnUpdate();
- foreach ($this->_validations as $validation) {
- $validation->validate('update', $this);
- }
- }
- $this->_afterValidation();
- return $this->errors->isEmpty();
- }
- /*##########################################################################
- # Association methods
- ##########################################################################*/
- /**
- * Associations are lazy initialized as needed. This function is called when needed
- * to check if we need an association method
- */
- protected function _initAssociations()
- {
- // only initialize if we haven't already
- if (!isset($this->_associationMethods) && isset($this->_associationList)) {
- // loop thru each define association
- foreach ($this->_associationList as $associationId => $info) {
- list($type, $options) = $info;
- $association = Mad_Model_Association_Base::factory($type, $associationId, $options, $this);
- $this->_associations[$associationId] = $association;
- // add list of dynamic methods this association adds
- foreach ($association->getMethods() as $methodName => $methodCall) {
- $this->_associationMethods[$methodName] = $association;
- }
- }
- }
- }
- /**
- * Force a reload of all associations.
- */
- protected function _resetAssociations()
- {
- if (isset($this->_associationMethods)) {
- $this->_associationMethods = null;
- $this->_associations = null;
- }
- }
- /**
- * Add an association to this model. This creates the appropriate Mad_Model_Association_Base
- * object and adds the object to the stack of associations for this model.
- * it also adds a list of dynamic methods that are added to this object by the
- * association.
- *
- * @param string $type
- * @param string $associationId
- * @param array $options
- */
- protected function _addAssociation($type, $associationId, $options)
- {
- $options = !empty($options) ? $options : array();
- $this->_associationList[$associationId] = array($type, $options);
- }
- /**
- * Save association model data for this model
- *
- * @param string $type (before|after)
- */
- protected function _saveAssociations($type)
- {
- if (!isset($this->_associations)) return;
- // save belongsTo before, and all others after
- foreach ($this->_associations as $association) {
- if ($association instanceof Mad_Model_Association_BelongsTo && $type == 'before') {
- $association->save();
- } elseif (!$association instanceof Mad_Model_Association_BelongsTo && $type == 'after') {
- $association->save();
- }
- }
- }
- }