<?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>hide@address.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>hide@address.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);
return $obj->save();
}
}
/**
* 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();
}
}
}
}