Mercurial > hg > Members > shoshi > webvirt
diff cake/libs/model/model.php @ 0:261e66bd5a0c
hg init
author | Shoshi TAMAKI <shoshi@cr.ie.u-ryukyu.ac.jp> |
---|---|
date | Sun, 24 Jul 2011 21:08:31 +0900 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cake/libs/model/model.php Sun Jul 24 21:08:31 2011 +0900 @@ -0,0 +1,3074 @@ +<?php +/** + * Object-relational mapper. + * + * DBO-backed object data model, for mapping database tables to Cake objects. + * + * PHP versions 5 + * + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @package cake + * @subpackage cake.cake.libs.model + * @since CakePHP(tm) v 0.10.0.0 + * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + */ + +/** + * Included libs + */ +App::import('Core', array('ClassRegistry', 'Validation', 'Set', 'String')); +App::import('Model', 'ModelBehavior', false); +App::import('Model', 'ConnectionManager', false); + +if (!class_exists('Overloadable')) { + require LIBS . 'overloadable.php'; +} + +/** + * Object-relational mapper. + * + * DBO-backed object data model. + * Automatically selects a database table name based on a pluralized lowercase object class name + * (i.e. class 'User' => table 'users'; class 'Man' => table 'men') + * The table is required to have at least 'id auto_increment' primary key. + * + * @package cake + * @subpackage cake.cake.libs.model + * @link http://book.cakephp.org/view/1000/Models + */ +class Model extends Overloadable { + +/** + * The name of the DataSource connection that this Model uses + * + * @var string + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#useDbConfig-1058 + */ + var $useDbConfig = 'default'; + +/** + * Custom database table name, or null/false if no table association is desired. + * + * @var string + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#useTable-1059 + */ + var $useTable = null; + +/** + * Custom display field name. Display fields are used by Scaffold, in SELECT boxes' OPTION elements. + * + * @var string + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#displayField-1062 + */ + var $displayField = null; + +/** + * Value of the primary key ID of the record that this model is currently pointing to. + * Automatically set after database insertions. + * + * @var mixed + * @access public + */ + var $id = false; + +/** + * Container for the data that this model gets from persistent storage (usually, a database). + * + * @var array + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#data-1065 + */ + var $data = array(); + +/** + * Table name for this Model. + * + * @var string + * @access public + */ + var $table = false; + +/** + * The name of the primary key field for this model. + * + * @var string + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#primaryKey-1061 + */ + var $primaryKey = null; + +/** + * Field-by-field table metadata. + * + * @var array + * @access protected + * @link http://book.cakephp.org/view/1057/Model-Attributes#_schema-1066 + */ + var $_schema = null; + +/** + * List of validation rules. Append entries for validation as ('field_name' => '/^perl_compat_regexp$/') + * that have to match with preg_match(). Use these rules with Model::validate() + * + * @var array + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#validate-1067 + * @link http://book.cakephp.org/view/1143/Data-Validation + */ + var $validate = array(); + +/** + * List of validation errors. + * + * @var array + * @access public + * @link http://book.cakephp.org/view/1182/Validating-Data-from-the-Controller + */ + var $validationErrors = array(); + +/** + * Database table prefix for tables in model. + * + * @var string + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#tablePrefix-1060 + */ + var $tablePrefix = null; + +/** + * Name of the model. + * + * @var string + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#name-1068 + */ + var $name = null; + +/** + * Alias name for model. + * + * @var string + * @access public + */ + var $alias = null; + +/** + * List of table names included in the model description. Used for associations. + * + * @var array + * @access public + */ + var $tableToModel = array(); + +/** + * Whether or not to log transactions for this model. + * + * @var boolean + * @access public + */ + var $logTransactions = false; + +/** + * Whether or not to cache queries for this model. This enables in-memory + * caching only, the results are not stored beyond the current request. + * + * @var boolean + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#cacheQueries-1069 + */ + var $cacheQueries = false; + +/** + * Detailed list of belongsTo associations. + * + * @var array + * @access public + * @link http://book.cakephp.org/view/1042/belongsTo + */ + var $belongsTo = array(); + +/** + * Detailed list of hasOne associations. + * + * @var array + * @access public + * @link http://book.cakephp.org/view/1041/hasOne + */ + var $hasOne = array(); + +/** + * Detailed list of hasMany associations. + * + * @var array + * @access public + * @link http://book.cakephp.org/view/1043/hasMany + */ + var $hasMany = array(); + +/** + * Detailed list of hasAndBelongsToMany associations. + * + * @var array + * @access public + * @link http://book.cakephp.org/view/1044/hasAndBelongsToMany-HABTM + */ + var $hasAndBelongsToMany = array(); + +/** + * List of behaviors to load when the model object is initialized. Settings can be + * passed to behaviors by using the behavior name as index. Eg: + * + * var $actsAs = array('Translate', 'MyBehavior' => array('setting1' => 'value1')) + * + * @var array + * @access public + * @link http://book.cakephp.org/view/1072/Using-Behaviors + */ + var $actsAs = null; + +/** + * Holds the Behavior objects currently bound to this model. + * + * @var BehaviorCollection + * @access public + */ + var $Behaviors = null; + +/** + * Whitelist of fields allowed to be saved. + * + * @var array + * @access public + */ + var $whitelist = array(); + +/** + * Whether or not to cache sources for this model. + * + * @var boolean + * @access public + */ + var $cacheSources = true; + +/** + * Type of find query currently executing. + * + * @var string + * @access public + */ + var $findQueryType = null; + +/** + * Number of associations to recurse through during find calls. Fetches only + * the first level by default. + * + * @var integer + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#recursive-1063 + */ + var $recursive = 1; + +/** + * The column name(s) and direction(s) to order find results by default. + * + * var $order = "Post.created DESC"; + * var $order = array("Post.view_count DESC", "Post.rating DESC"); + * + * @var string + * @access public + * @link http://book.cakephp.org/view/1057/Model-Attributes#order-1064 + */ + var $order = null; + +/** + * Array of virtual fields this model has. Virtual fields are aliased + * SQL expressions. Fields added to this property will be read as other fields in a model + * but will not be saveable. + * + * `var $virtualFields = array('two' => '1 + 1');` + * + * Is a simplistic example of how to set virtualFields + * + * @var array + * @access public + */ + var $virtualFields = array(); + +/** + * Default list of association keys. + * + * @var array + * @access private + */ + var $__associationKeys = array( + 'belongsTo' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'counterCache'), + 'hasOne' => array('className', 'foreignKey','conditions', 'fields','order', 'dependent'), + 'hasMany' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'dependent', 'exclusive', 'finderQuery', 'counterQuery'), + 'hasAndBelongsToMany' => array('className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery', 'deleteQuery', 'insertQuery') + ); + +/** + * Holds provided/generated association key names and other data for all associations. + * + * @var array + * @access private + */ + var $__associations = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); + +/** + * Holds model associations temporarily to allow for dynamic (un)binding. + * + * @var array + * @access private + */ + var $__backAssociation = array(); + +/** + * The ID of the model record that was last inserted. + * + * @var integer + * @access private + */ + var $__insertID = null; + +/** + * The number of records returned by the last query. + * + * @var integer + * @access private + */ + var $__numRows = null; + +/** + * The number of records affected by the last query. + * + * @var integer + * @access private + */ + var $__affectedRows = null; + +/** + * List of valid finder method options, supplied as the first parameter to find(). + * + * @var array + * @access protected + */ + var $_findMethods = array( + 'all' => true, 'first' => true, 'count' => true, + 'neighbors' => true, 'list' => true, 'threaded' => true + ); + +/** + * Constructor. Binds the model's database table to the object. + * + * If `$id` is an array it can be used to pass several options into the model. + * + * - id - The id to start the model on. + * - table - The table to use for this model. + * - ds - The connection name this model is connected to. + * - name - The name of the model eg. Post. + * - alias - The alias of the model, this is used for registering the instance in the `ClassRegistry`. + * eg. `ParentThread` + * + * ### Overriding Model's __construct method. + * + * When overriding Model::__construct() be careful to include and pass in all 3 of the + * arguments to `parent::__construct($id, $table, $ds);` + * + * ### Dynamically creating models + * + * You can dynamically create model instances using the $id array syntax. + * + * {{{ + * $Post = new Model(array('table' => 'posts', 'name' => 'Post', 'ds' => 'connection2')); + * }}} + * + * Would create a model attached to the posts table on connection2. Dynamic model creation is useful + * when you want a model object that contains no associations or attached behaviors. + * + * @param mixed $id Set this ID for this model on startup, can also be an array of options, see above. + * @param string $table Name of database table to use. + * @param string $ds DataSource connection name. + */ + function __construct($id = false, $table = null, $ds = null) { + parent::__construct(); + + if (is_array($id)) { + extract(array_merge( + array( + 'id' => $this->id, 'table' => $this->useTable, 'ds' => $this->useDbConfig, + 'name' => $this->name, 'alias' => $this->alias + ), + $id + )); + } + + if ($this->name === null) { + $this->name = (isset($name) ? $name : get_class($this)); + } + + if ($this->alias === null) { + $this->alias = (isset($alias) ? $alias : $this->name); + } + + if ($this->primaryKey === null) { + $this->primaryKey = 'id'; + } + + ClassRegistry::addObject($this->alias, $this); + + $this->id = $id; + unset($id); + + if ($table === false) { + $this->useTable = false; + } elseif ($table) { + $this->useTable = $table; + } + + if ($ds !== null) { + $this->useDbConfig = $ds; + } + + if (is_subclass_of($this, 'AppModel')) { + $appVars = get_class_vars('AppModel'); + $merge = array('_findMethods'); + + if ($this->actsAs !== null || $this->actsAs !== false) { + $merge[] = 'actsAs'; + } + $parentClass = get_parent_class($this); + if (strtolower($parentClass) !== 'appmodel') { + $parentVars = get_class_vars($parentClass); + foreach ($merge as $var) { + if (isset($parentVars[$var]) && !empty($parentVars[$var])) { + $appVars[$var] = Set::merge($appVars[$var], $parentVars[$var]); + } + } + } + + foreach ($merge as $var) { + if (isset($appVars[$var]) && !empty($appVars[$var]) && is_array($this->{$var})) { + $this->{$var} = Set::merge($appVars[$var], $this->{$var}); + } + } + } + $this->Behaviors = new BehaviorCollection(); + + if ($this->useTable !== false) { + $this->setDataSource($ds); + + if ($this->useTable === null) { + $this->useTable = Inflector::tableize($this->name); + } + $this->setSource($this->useTable); + + if ($this->displayField == null) { + $this->displayField = $this->hasField(array('title', 'name', $this->primaryKey)); + } + } elseif ($this->table === false) { + $this->table = Inflector::tableize($this->name); + } + $this->__createLinks(); + $this->Behaviors->init($this->alias, $this->actsAs); + } + +/** + * Handles custom method calls, like findBy<field> for DB models, + * and custom RPC calls for remote data sources. + * + * @param string $method Name of method to call. + * @param array $params Parameters for the method. + * @return mixed Whatever is returned by called method + * @access protected + */ + function call__($method, $params) { + $result = $this->Behaviors->dispatchMethod($this, $method, $params); + + if ($result !== array('unhandled')) { + return $result; + } + $db =& ConnectionManager::getDataSource($this->useDbConfig); + $return = $db->query($method, $params, $this); + + if (!PHP5) { + $this->resetAssociations(); + } + return $return; + } + +/** + * Bind model associations on the fly. + * + * If `$reset` is false, association will not be reset + * to the originals defined in the model + * + * Example: Add a new hasOne binding to the Profile model not + * defined in the model source code: + * + * `$this->User->bindModel( array('hasOne' => array('Profile')) );` + * + * Bindings that are not made permanent will be reset by the next Model::find() call on this + * model. + * + * @param array $params Set of bindings (indexed by binding type) + * @param boolean $reset Set to false to make the binding permanent + * @return boolean Success + * @access public + * @link http://book.cakephp.org/view/1045/Creating-and-Destroying-Associations-on-the-Fly + */ + function bindModel($params, $reset = true) { + foreach ($params as $assoc => $model) { + if ($reset === true && !isset($this->__backAssociation[$assoc])) { + $this->__backAssociation[$assoc] = $this->{$assoc}; + } + foreach ($model as $key => $value) { + $assocName = $key; + + if (is_numeric($key)) { + $assocName = $value; + $value = array(); + } + $modelName = $assocName; + $this->{$assoc}[$assocName] = $value; + + if ($reset === false && isset($this->__backAssociation[$assoc])) { + $this->__backAssociation[$assoc][$assocName] = $value; + } + } + } + $this->__createLinks(); + return true; + } + +/** + * Turn off associations on the fly. + * + * If $reset is false, association will not be reset + * to the originals defined in the model + * + * Example: Turn off the associated Model Support request, + * to temporarily lighten the User model: + * + * `$this->User->unbindModel( array('hasMany' => array('Supportrequest')) );` + * + * unbound models that are not made permanent will reset with the next call to Model::find() + * + * @param array $params Set of bindings to unbind (indexed by binding type) + * @param boolean $reset Set to false to make the unbinding permanent + * @return boolean Success + * @access public + * @link http://book.cakephp.org/view/1045/Creating-and-Destroying-Associations-on-the-Fly + */ + function unbindModel($params, $reset = true) { + foreach ($params as $assoc => $models) { + if ($reset === true && !isset($this->__backAssociation[$assoc])) { + $this->__backAssociation[$assoc] = $this->{$assoc}; + } + foreach ($models as $model) { + if ($reset === false && isset($this->__backAssociation[$assoc][$model])) { + unset($this->__backAssociation[$assoc][$model]); + } + unset($this->{$assoc}[$model]); + } + } + return true; + } + +/** + * Create a set of associations. + * + * @return void + * @access private + */ + function __createLinks() { + foreach ($this->__associations as $type) { + if (!is_array($this->{$type})) { + $this->{$type} = explode(',', $this->{$type}); + + foreach ($this->{$type} as $i => $className) { + $className = trim($className); + unset ($this->{$type}[$i]); + $this->{$type}[$className] = array(); + } + } + + if (!empty($this->{$type})) { + foreach ($this->{$type} as $assoc => $value) { + $plugin = null; + + if (is_numeric($assoc)) { + unset ($this->{$type}[$assoc]); + $assoc = $value; + $value = array(); + $this->{$type}[$assoc] = $value; + + if (strpos($assoc, '.') !== false) { + $value = $this->{$type}[$assoc]; + unset($this->{$type}[$assoc]); + list($plugin, $assoc) = pluginSplit($assoc, true); + $this->{$type}[$assoc] = $value; + } + } + $className = $assoc; + + if (!empty($value['className'])) { + list($plugin, $className) = pluginSplit($value['className'], true); + $this->{$type}[$assoc]['className'] = $className; + } + $this->__constructLinkedModel($assoc, $plugin . $className); + } + $this->__generateAssociation($type); + } + } + } + +/** + * Private helper method to create associated models of a given class. + * + * @param string $assoc Association name + * @param string $className Class name + * @deprecated $this->$className use $this->$assoc instead. $assoc is the 'key' in the associations array; + * examples: var $hasMany = array('Assoc' => array('className' => 'ModelName')); + * usage: $this->Assoc->modelMethods(); + * + * var $hasMany = array('ModelName'); + * usage: $this->ModelName->modelMethods(); + * @return void + * @access private + */ + function __constructLinkedModel($assoc, $className = null) { + if (empty($className)) { + $className = $assoc; + } + + if (!isset($this->{$assoc}) || $this->{$assoc}->name !== $className) { + $model = array('class' => $className, 'alias' => $assoc); + if (PHP5) { + $this->{$assoc} = ClassRegistry::init($model); + } else { + $this->{$assoc} =& ClassRegistry::init($model); + } + if (strpos($className, '.') !== false) { + ClassRegistry::addObject($className, $this->{$assoc}); + } + if ($assoc) { + $this->tableToModel[$this->{$assoc}->table] = $assoc; + } + } + } + +/** + * Build an array-based association from string. + * + * @param string $type 'belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany' + * @return void + * @access private + */ + function __generateAssociation($type) { + foreach ($this->{$type} as $assocKey => $assocData) { + $class = $assocKey; + $dynamicWith = false; + + foreach ($this->__associationKeys[$type] as $key) { + + if (!isset($this->{$type}[$assocKey][$key]) || $this->{$type}[$assocKey][$key] === null) { + $data = ''; + + switch ($key) { + case 'fields': + $data = ''; + break; + + case 'foreignKey': + $data = (($type == 'belongsTo') ? Inflector::underscore($assocKey) : Inflector::singularize($this->table)) . '_id'; + break; + + case 'associationForeignKey': + $data = Inflector::singularize($this->{$class}->table) . '_id'; + break; + + case 'with': + $data = Inflector::camelize(Inflector::singularize($this->{$type}[$assocKey]['joinTable'])); + $dynamicWith = true; + break; + + case 'joinTable': + $tables = array($this->table, $this->{$class}->table); + sort ($tables); + $data = $tables[0] . '_' . $tables[1]; + break; + + case 'className': + $data = $class; + break; + + case 'unique': + $data = true; + break; + } + $this->{$type}[$assocKey][$key] = $data; + } + } + + if (!empty($this->{$type}[$assocKey]['with'])) { + $joinClass = $this->{$type}[$assocKey]['with']; + if (is_array($joinClass)) { + $joinClass = key($joinClass); + } + + $plugin = null; + if (strpos($joinClass, '.') !== false) { + list($plugin, $joinClass) = explode('.', $joinClass); + $plugin .= '.'; + $this->{$type}[$assocKey]['with'] = $joinClass; + } + + if (!ClassRegistry::isKeySet($joinClass) && $dynamicWith === true) { + $this->{$joinClass} = new AppModel(array( + 'name' => $joinClass, + 'table' => $this->{$type}[$assocKey]['joinTable'], + 'ds' => $this->useDbConfig + )); + } else { + $this->__constructLinkedModel($joinClass, $plugin . $joinClass); + $this->{$type}[$assocKey]['joinTable'] = $this->{$joinClass}->table; + } + + if (count($this->{$joinClass}->schema()) <= 2 && $this->{$joinClass}->primaryKey !== false) { + $this->{$joinClass}->primaryKey = $this->{$type}[$assocKey]['foreignKey']; + } + } + } + } + +/** + * Sets a custom table for your controller class. Used by your controller to select a database table. + * + * @param string $tableName Name of the custom table + * @return void + * @access public + */ + function setSource($tableName) { + $this->setDataSource($this->useDbConfig); + $db =& ConnectionManager::getDataSource($this->useDbConfig); + $db->cacheSources = ($this->cacheSources && $db->cacheSources); + + if ($db->isInterfaceSupported('listSources')) { + $sources = $db->listSources(); + if (is_array($sources) && !in_array(strtolower($this->tablePrefix . $tableName), array_map('strtolower', $sources))) { + return $this->cakeError('missingTable', array(array( + 'className' => $this->alias, + 'table' => $this->tablePrefix . $tableName, + 'code' => 500 + ))); + } + $this->_schema = null; + } + $this->table = $this->useTable = $tableName; + $this->tableToModel[$this->table] = $this->alias; + $this->schema(); + } + +/** + * This function does two things: + * + * 1. it scans the array $one for the primary key, + * and if that's found, it sets the current id to the value of $one[id]. + * For all other keys than 'id' the keys and values of $one are copied to the 'data' property of this object. + * 2. Returns an array with all of $one's keys and values. + * (Alternative indata: two strings, which are mangled to + * a one-item, two-dimensional array using $one for a key and $two as its value.) + * + * @param mixed $one Array or string of data + * @param string $two Value string for the alternative indata method + * @return array Data with all of $one's keys and values + * @access public + * @link http://book.cakephp.org/view/1031/Saving-Your-Data + */ + function set($one, $two = null) { + if (!$one) { + return; + } + if (is_object($one)) { + $one = Set::reverse($one); + } + + if (is_array($one)) { + $data = $one; + if (empty($one[$this->alias])) { + if ($this->getAssociated(key($one)) === null) { + $data = array($this->alias => $one); + } + } + } else { + $data = array($this->alias => array($one => $two)); + } + + foreach ($data as $modelName => $fieldSet) { + if (is_array($fieldSet)) { + + foreach ($fieldSet as $fieldName => $fieldValue) { + if (isset($this->validationErrors[$fieldName])) { + unset ($this->validationErrors[$fieldName]); + } + + if ($modelName === $this->alias) { + if ($fieldName === $this->primaryKey) { + $this->id = $fieldValue; + } + } + if (is_array($fieldValue) || is_object($fieldValue)) { + $fieldValue = $this->deconstruct($fieldName, $fieldValue); + } + $this->data[$modelName][$fieldName] = $fieldValue; + } + } + } + return $data; + } + +/** + * Deconstructs a complex data type (array or object) into a single field value. + * + * @param string $field The name of the field to be deconstructed + * @param mixed $data An array or object to be deconstructed into a field + * @return mixed The resulting data that should be assigned to a field + * @access public + */ + function deconstruct($field, $data) { + if (!is_array($data)) { + return $data; + } + + $copy = $data; + $type = $this->getColumnType($field); + + if (in_array($type, array('datetime', 'timestamp', 'date', 'time'))) { + $useNewDate = (isset($data['year']) || isset($data['month']) || + isset($data['day']) || isset($data['hour']) || isset($data['minute'])); + + $dateFields = array('Y' => 'year', 'm' => 'month', 'd' => 'day', 'H' => 'hour', 'i' => 'min', 's' => 'sec'); + $timeFields = array('H' => 'hour', 'i' => 'min', 's' => 'sec'); + + $db =& ConnectionManager::getDataSource($this->useDbConfig); + $format = $db->columns[$type]['format']; + $date = array(); + + if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] != 12 && 'pm' == $data['meridian']) { + $data['hour'] = $data['hour'] + 12; + } + if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] == 12 && 'am' == $data['meridian']) { + $data['hour'] = '00'; + } + if ($type == 'time') { + foreach ($timeFields as $key => $val) { + if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') { + $data[$val] = '00'; + } elseif ($data[$val] === '') { + $data[$val] = ''; + } else { + $data[$val] = sprintf('%02d', $data[$val]); + } + if (!empty($data[$val])) { + $date[$key] = $data[$val]; + } else { + return null; + } + } + } + + if ($type == 'datetime' || $type == 'timestamp' || $type == 'date') { + foreach ($dateFields as $key => $val) { + if ($val == 'hour' || $val == 'min' || $val == 'sec') { + if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') { + $data[$val] = '00'; + } else { + $data[$val] = sprintf('%02d', $data[$val]); + } + } + if (!isset($data[$val]) || isset($data[$val]) && (empty($data[$val]) || $data[$val][0] === '-')) { + return null; + } + if (isset($data[$val]) && !empty($data[$val])) { + $date[$key] = $data[$val]; + } + } + } + $date = str_replace(array_keys($date), array_values($date), $format); + if ($useNewDate && !empty($date)) { + return $date; + } + } + return $data; + } + +/** + * Returns an array of table metadata (column names and types) from the database. + * $field => keys(type, null, default, key, length, extra) + * + * @param mixed $field Set to true to reload schema, or a string to return a specific field + * @return array Array of table metadata + * @access public + */ + function schema($field = false) { + if (!is_array($this->_schema) || $field === true) { + $db =& ConnectionManager::getDataSource($this->useDbConfig); + $db->cacheSources = ($this->cacheSources && $db->cacheSources); + if ($db->isInterfaceSupported('describe') && $this->useTable !== false) { + $this->_schema = $db->describe($this, $field); + } elseif ($this->useTable === false) { + $this->_schema = array(); + } + } + if (is_string($field)) { + if (isset($this->_schema[$field])) { + return $this->_schema[$field]; + } else { + return null; + } + } + return $this->_schema; + } + +/** + * Returns an associative array of field names and column types. + * + * @return array Field types indexed by field name + * @access public + */ + function getColumnTypes() { + $columns = $this->schema(); + if (empty($columns)) { + trigger_error(__('(Model::getColumnTypes) Unable to build model field data. If you are using a model without a database table, try implementing schema()', true), E_USER_WARNING); + } + $cols = array(); + foreach ($columns as $field => $values) { + $cols[$field] = $values['type']; + } + return $cols; + } + +/** + * Returns the column type of a column in the model. + * + * @param string $column The name of the model column + * @return string Column type + * @access public + */ + function getColumnType($column) { + $db =& ConnectionManager::getDataSource($this->useDbConfig); + $cols = $this->schema(); + $model = null; + + $column = str_replace(array($db->startQuote, $db->endQuote), '', $column); + + if (strpos($column, '.')) { + list($model, $column) = explode('.', $column); + } + if ($model != $this->alias && isset($this->{$model})) { + return $this->{$model}->getColumnType($column); + } + if (isset($cols[$column]) && isset($cols[$column]['type'])) { + return $cols[$column]['type']; + } + return null; + } + +/** + * Returns true if the supplied field exists in the model's database table. + * + * @param mixed $name Name of field to look for, or an array of names + * @param boolean $checkVirtual checks if the field is declared as virtual + * @return mixed If $name is a string, returns a boolean indicating whether the field exists. + * If $name is an array of field names, returns the first field that exists, + * or false if none exist. + * @access public + */ + function hasField($name, $checkVirtual = false) { + if (is_array($name)) { + foreach ($name as $n) { + if ($this->hasField($n, $checkVirtual)) { + return $n; + } + } + return false; + } + + if ($checkVirtual && !empty($this->virtualFields)) { + if ($this->isVirtualField($name)) { + return true; + } + } + + if (empty($this->_schema)) { + $this->schema(); + } + + if ($this->_schema != null) { + return isset($this->_schema[$name]); + } + return false; + } + +/** + * Returns true if the supplied field is a model Virtual Field + * + * @param mixed $name Name of field to look for + * @return boolean indicating whether the field exists as a model virtual field. + * @access public + */ + function isVirtualField($field) { + if (empty($this->virtualFields) || !is_string($field)) { + return false; + } + if (isset($this->virtualFields[$field])) { + return true; + } + if (strpos($field, '.') !== false) { + list($model, $field) = explode('.', $field); + if (isset($this->virtualFields[$field])) { + return true; + } + } + return false; + } + +/** + * Returns the expression for a model virtual field + * + * @param mixed $name Name of field to look for + * @return mixed If $field is string expression bound to virtual field $field + * If $field is null, returns an array of all model virtual fields + * or false if none $field exist. + * @access public + */ + function getVirtualField($field = null) { + if ($field == null) { + return empty($this->virtualFields) ? false : $this->virtualFields; + } + if ($this->isVirtualField($field)) { + if (strpos($field, '.') !== false) { + list($model, $field) = explode('.', $field); + } + return $this->virtualFields[$field]; + } + return false; + } + +/** + * Initializes the model for writing a new record, loading the default values + * for those fields that are not defined in $data, and clearing previous validation errors. + * Especially helpful for saving data in loops. + * + * @param mixed $data Optional data array to assign to the model after it is created. If null or false, + * schema data defaults are not merged. + * @param boolean $filterKey If true, overwrites any primary key input with an empty value + * @return array The current Model::data; after merging $data and/or defaults from database + * @access public + * @link http://book.cakephp.org/view/1031/Saving-Your-Data + */ + function create($data = array(), $filterKey = false) { + $defaults = array(); + $this->id = false; + $this->data = array(); + $this->validationErrors = array(); + + if ($data !== null && $data !== false) { + foreach ($this->schema() as $field => $properties) { + if ($this->primaryKey !== $field && isset($properties['default']) && $properties['default'] !== '') { + $defaults[$field] = $properties['default']; + } + } + $this->set($defaults); + $this->set($data); + } + if ($filterKey) { + $this->set($this->primaryKey, false); + } + return $this->data; + } + +/** + * Returns a list of fields from the database, and sets the current model + * data (Model::$data) with the record found. + * + * @param mixed $fields String of single fieldname, or an array of fieldnames. + * @param mixed $id The ID of the record to read + * @return array Array of database fields, or false if not found + * @access public + * @link http://book.cakephp.org/view/1017/Retrieving-Your-Data#read-1029 + */ + function read($fields = null, $id = null) { + $this->validationErrors = array(); + + if ($id != null) { + $this->id = $id; + } + + $id = $this->id; + + if (is_array($this->id)) { + $id = $this->id[0]; + } + + if ($id !== null && $id !== false) { + $this->data = $this->find('first', array( + 'conditions' => array($this->alias . '.' . $this->primaryKey => $id), + 'fields' => $fields + )); + return $this->data; + } else { + return false; + } + } + +/** + * Returns the contents of a single field given the supplied conditions, in the + * supplied order. + * + * @param string $name Name of field to get + * @param array $conditions SQL conditions (defaults to NULL) + * @param string $order SQL ORDER BY fragment + * @return string field contents, or false if not found + * @access public + * @link http://book.cakephp.org/view/1017/Retrieving-Your-Data#field-1028 + */ + function field($name, $conditions = null, $order = null) { + if ($conditions === null && $this->id !== false) { + $conditions = array($this->alias . '.' . $this->primaryKey => $this->id); + } + if ($this->recursive >= 1) { + $recursive = -1; + } else { + $recursive = $this->recursive; + } + $fields = $name; + if ($data = $this->find('first', compact('conditions', 'fields', 'order', 'recursive'))) { + if (strpos($name, '.') === false) { + if (isset($data[$this->alias][$name])) { + return $data[$this->alias][$name]; + } + } else { + $name = explode('.', $name); + if (isset($data[$name[0]][$name[1]])) { + return $data[$name[0]][$name[1]]; + } + } + if (isset($data[0]) && count($data[0]) > 0) { + return array_shift($data[0]); + } + } else { + return false; + } + } + +/** + * Saves the value of a single field to the database, based on the current + * model ID. + * + * @param string $name Name of the table field + * @param mixed $value Value of the field + * @param array $validate See $options param in Model::save(). Does not respect 'fieldList' key if passed + * @return boolean See Model::save() + * @access public + * @see Model::save() + * @link http://book.cakephp.org/view/1031/Saving-Your-Data + */ + function saveField($name, $value, $validate = false) { + $id = $this->id; + $this->create(false); + + if (is_array($validate)) { + $options = array_merge(array('validate' => false, 'fieldList' => array($name)), $validate); + } else { + $options = array('validate' => $validate, 'fieldList' => array($name)); + } + return $this->save(array($this->alias => array($this->primaryKey => $id, $name => $value)), $options); + } + +/** + * Saves model data (based on white-list, if supplied) to the database. By + * default, validation occurs before save. + * + * @param array $data Data to save. + * @param mixed $validate Either a boolean, or an array. + * If a boolean, indicates whether or not to validate before saving. + * If an array, allows control of validate, callbacks, and fieldList + * @param array $fieldList List of fields to allow to be written + * @return mixed On success Model::$data if its not empty or true, false on failure + * @access public + * @link http://book.cakephp.org/view/1031/Saving-Your-Data + */ + function save($data = null, $validate = true, $fieldList = array()) { + $defaults = array('validate' => true, 'fieldList' => array(), 'callbacks' => true); + $_whitelist = $this->whitelist; + $fields = array(); + + if (!is_array($validate)) { + $options = array_merge($defaults, compact('validate', 'fieldList', 'callbacks')); + } else { + $options = array_merge($defaults, $validate); + } + + if (!empty($options['fieldList'])) { + $this->whitelist = $options['fieldList']; + } elseif ($options['fieldList'] === null) { + $this->whitelist = array(); + } + $this->set($data); + + if (empty($this->data) && !$this->hasField(array('created', 'updated', 'modified'))) { + return false; + } + + foreach (array('created', 'updated', 'modified') as $field) { + $keyPresentAndEmpty = ( + isset($this->data[$this->alias]) && + array_key_exists($field, $this->data[$this->alias]) && + $this->data[$this->alias][$field] === null + ); + if ($keyPresentAndEmpty) { + unset($this->data[$this->alias][$field]); + } + } + + $exists = $this->exists(); + $dateFields = array('modified', 'updated'); + + if (!$exists) { + $dateFields[] = 'created'; + } + if (isset($this->data[$this->alias])) { + $fields = array_keys($this->data[$this->alias]); + } + if ($options['validate'] && !$this->validates($options)) { + $this->whitelist = $_whitelist; + return false; + } + + $db =& ConnectionManager::getDataSource($this->useDbConfig); + + foreach ($dateFields as $updateCol) { + if ($this->hasField($updateCol) && !in_array($updateCol, $fields)) { + $default = array('formatter' => 'date'); + $colType = array_merge($default, $db->columns[$this->getColumnType($updateCol)]); + if (!array_key_exists('format', $colType)) { + $time = strtotime('now'); + } else { + $time = $colType['formatter']($colType['format']); + } + if (!empty($this->whitelist)) { + $this->whitelist[] = $updateCol; + } + $this->set($updateCol, $time); + } + } + + if ($options['callbacks'] === true || $options['callbacks'] === 'before') { + $result = $this->Behaviors->trigger($this, 'beforeSave', array($options), array( + 'break' => true, 'breakOn' => false + )); + if (!$result || !$this->beforeSave($options)) { + $this->whitelist = $_whitelist; + return false; + } + } + + if (empty($this->data[$this->alias][$this->primaryKey])) { + unset($this->data[$this->alias][$this->primaryKey]); + } + $fields = $values = array(); + + foreach ($this->data as $n => $v) { + if (isset($this->hasAndBelongsToMany[$n])) { + if (isset($v[$n])) { + $v = $v[$n]; + } + $joined[$n] = $v; + } else { + if ($n === $this->alias) { + foreach (array('created', 'updated', 'modified') as $field) { + if (array_key_exists($field, $v) && empty($v[$field])) { + unset($v[$field]); + } + } + + foreach ($v as $x => $y) { + if ($this->hasField($x) && (empty($this->whitelist) || in_array($x, $this->whitelist))) { + list($fields[], $values[]) = array($x, $y); + } + } + } + } + } + $count = count($fields); + + if (!$exists && $count > 0) { + $this->id = false; + } + $success = true; + $created = false; + + if ($count > 0) { + $cache = $this->_prepareUpdateFields(array_combine($fields, $values)); + + if (!empty($this->id)) { + $success = (bool)$db->update($this, $fields, $values); + } else { + $fInfo = $this->_schema[$this->primaryKey]; + $isUUID = ($fInfo['length'] == 36 && + ($fInfo['type'] === 'string' || $fInfo['type'] === 'binary') + ); + if (empty($this->data[$this->alias][$this->primaryKey]) && $isUUID) { + if (array_key_exists($this->primaryKey, $this->data[$this->alias])) { + $j = array_search($this->primaryKey, $fields); + $values[$j] = String::uuid(); + } else { + list($fields[], $values[]) = array($this->primaryKey, String::uuid()); + } + } + + if (!$db->create($this, $fields, $values)) { + $success = $created = false; + } else { + $created = true; + } + } + + if ($success && !empty($this->belongsTo)) { + $this->updateCounterCache($cache, $created); + } + } + + if (!empty($joined) && $success === true) { + $this->__saveMulti($joined, $this->id, $db); + } + + if ($success && $count > 0) { + if (!empty($this->data)) { + $success = $this->data; + } + if ($options['callbacks'] === true || $options['callbacks'] === 'after') { + $this->Behaviors->trigger($this, 'afterSave', array($created, $options)); + $this->afterSave($created); + } + if (!empty($this->data)) { + $success = Set::merge($success, $this->data); + } + $this->data = false; + $this->_clearCache(); + $this->validationErrors = array(); + } + $this->whitelist = $_whitelist; + return $success; + } + +/** + * Saves model hasAndBelongsToMany data to the database. + * + * @param array $joined Data to save + * @param mixed $id ID of record in this model + * @access private + */ + function __saveMulti($joined, $id, &$db) { + foreach ($joined as $assoc => $data) { + + if (isset($this->hasAndBelongsToMany[$assoc])) { + list($join) = $this->joinModel($this->hasAndBelongsToMany[$assoc]['with']); + + $isUUID = !empty($this->{$join}->primaryKey) && ( + $this->{$join}->_schema[$this->{$join}->primaryKey]['length'] == 36 && ( + $this->{$join}->_schema[$this->{$join}->primaryKey]['type'] === 'string' || + $this->{$join}->_schema[$this->{$join}->primaryKey]['type'] === 'binary' + ) + ); + + $newData = $newValues = array(); + $primaryAdded = false; + + $fields = array( + $db->name($this->hasAndBelongsToMany[$assoc]['foreignKey']), + $db->name($this->hasAndBelongsToMany[$assoc]['associationForeignKey']) + ); + + $idField = $db->name($this->{$join}->primaryKey); + if ($isUUID && !in_array($idField, $fields)) { + $fields[] = $idField; + $primaryAdded = true; + } + + foreach ((array)$data as $row) { + if ((is_string($row) && (strlen($row) == 36 || strlen($row) == 16)) || is_numeric($row)) { + $values = array( + $db->value($id, $this->getColumnType($this->primaryKey)), + $db->value($row) + ); + if ($isUUID && $primaryAdded) { + $values[] = $db->value(String::uuid()); + } + $values = implode(',', $values); + $newValues[] = "({$values})"; + unset($values); + } elseif (isset($row[$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { + $newData[] = $row; + } elseif (isset($row[$join]) && isset($row[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { + $newData[] = $row[$join]; + } + } + + if ($this->hasAndBelongsToMany[$assoc]['unique']) { + $conditions = array( + $join . '.' . $this->hasAndBelongsToMany[$assoc]['foreignKey'] => $id + ); + if (!empty($this->hasAndBelongsToMany[$assoc]['conditions'])) { + $conditions = array_merge($conditions, (array)$this->hasAndBelongsToMany[$assoc]['conditions']); + } + $links = $this->{$join}->find('all', array( + 'conditions' => $conditions, + 'recursive' => empty($this->hasAndBelongsToMany[$assoc]['conditions']) ? -1 : 0, + 'fields' => $this->hasAndBelongsToMany[$assoc]['associationForeignKey'] + )); + + $associationForeignKey = "{$join}." . $this->hasAndBelongsToMany[$assoc]['associationForeignKey']; + $oldLinks = Set::extract($links, "{n}.{$associationForeignKey}"); + if (!empty($oldLinks)) { + $conditions[$associationForeignKey] = $oldLinks; + $db->delete($this->{$join}, $conditions); + } + } + + if (!empty($newData)) { + foreach ($newData as $data) { + $data[$this->hasAndBelongsToMany[$assoc]['foreignKey']] = $id; + $this->{$join}->create($data); + $this->{$join}->save(); + } + } + + if (!empty($newValues)) { + $fields = implode(',', $fields); + $db->insertMulti($this->{$join}, $fields, $newValues); + } + } + } + } + +/** + * Updates the counter cache of belongsTo associations after a save or delete operation + * + * @param array $keys Optional foreign key data, defaults to the information $this->data + * @param boolean $created True if a new record was created, otherwise only associations with + * 'counterScope' defined get updated + * @return void + * @access public + */ + function updateCounterCache($keys = array(), $created = false) { + $keys = empty($keys) ? $this->data[$this->alias] : $keys; + $keys['old'] = isset($keys['old']) ? $keys['old'] : array(); + + foreach ($this->belongsTo as $parent => $assoc) { + $foreignKey = $assoc['foreignKey']; + $fkQuoted = $this->escapeField($assoc['foreignKey']); + + if (!empty($assoc['counterCache'])) { + if ($assoc['counterCache'] === true) { + $assoc['counterCache'] = Inflector::underscore($this->alias) . '_count'; + } + if (!$this->{$parent}->hasField($assoc['counterCache'])) { + continue; + } + + if (!array_key_exists($foreignKey, $keys)) { + $keys[$foreignKey] = $this->field($foreignKey); + } + $recursive = (isset($assoc['counterScope']) ? 1 : -1); + $conditions = ($recursive == 1) ? (array)$assoc['counterScope'] : array(); + + if (isset($keys['old'][$foreignKey])) { + if ($keys['old'][$foreignKey] != $keys[$foreignKey]) { + $conditions[$fkQuoted] = $keys['old'][$foreignKey]; + $count = intval($this->find('count', compact('conditions', 'recursive'))); + + $this->{$parent}->updateAll( + array($assoc['counterCache'] => $count), + array($this->{$parent}->escapeField() => $keys['old'][$foreignKey]) + ); + } + } + $conditions[$fkQuoted] = $keys[$foreignKey]; + + if ($recursive == 1) { + $conditions = array_merge($conditions, (array)$assoc['counterScope']); + } + $count = intval($this->find('count', compact('conditions', 'recursive'))); + + $this->{$parent}->updateAll( + array($assoc['counterCache'] => $count), + array($this->{$parent}->escapeField() => $keys[$foreignKey]) + ); + } + } + } + +/** + * Helper method for Model::updateCounterCache(). Checks the fields to be updated for + * + * @param array $data The fields of the record that will be updated + * @return array Returns updated foreign key values, along with an 'old' key containing the old + * values, or empty if no foreign keys are updated. + * @access protected + */ + function _prepareUpdateFields($data) { + $foreignKeys = array(); + foreach ($this->belongsTo as $assoc => $info) { + if ($info['counterCache']) { + $foreignKeys[$assoc] = $info['foreignKey']; + } + } + $included = array_intersect($foreignKeys, array_keys($data)); + + if (empty($included) || empty($this->id)) { + return array(); + } + $old = $this->find('first', array( + 'conditions' => array($this->primaryKey => $this->id), + 'fields' => array_values($included), + 'recursive' => -1 + )); + return array_merge($data, array('old' => $old[$this->alias])); + } + +/** + * Saves multiple individual records for a single model; Also works with a single record, as well as + * all its associated records. + * + * #### Options + * + * - validate: Set to false to disable validation, true to validate each record before saving, + * 'first' to validate *all* records before any are saved (default), + * or 'only' to only validate the records, but not save them. + * - atomic: If true (default), will attempt to save all records in a single transaction. + * Should be set to false if database/table does not support transactions. + * - fieldList: Equivalent to the $fieldList parameter in Model::save() + * + * @param array $data Record data to save. This can be either a numerically-indexed array (for saving multiple + * records of the same type), or an array indexed by association name. + * @param array $options Options to use when saving record data, See $options above. + * @return mixed If atomic: True on success, or false on failure. + * Otherwise: array similar to the $data array passed, but values are set to true/false + * depending on whether each record saved successfully. + * @access public + * @link http://book.cakephp.org/view/1032/Saving-Related-Model-Data-hasOne-hasMany-belongsTo + * @link http://book.cakephp.org/view/1031/Saving-Your-Data + */ + function saveAll($data = null, $options = array()) { + if (empty($data)) { + $data = $this->data; + } + $db =& ConnectionManager::getDataSource($this->useDbConfig); + + $options = array_merge(array('validate' => 'first', 'atomic' => true), $options); + $this->validationErrors = $validationErrors = array(); + $validates = true; + $return = array(); + + if (empty($data) && $options['validate'] !== false) { + $result = $this->save($data, $options); + return !empty($result); + } + + if ($options['atomic'] && $options['validate'] !== 'only') { + $transactionBegun = $db->begin($this); + } + + if (Set::numeric(array_keys($data))) { + while ($validates) { + $return = array(); + foreach ($data as $key => $record) { + if (!$currentValidates = $this->__save($record, $options)) { + $validationErrors[$key] = $this->validationErrors; + } + + if ($options['validate'] === 'only' || $options['validate'] === 'first') { + $validating = true; + if ($options['atomic']) { + $validates = $validates && $currentValidates; + } else { + $validates = $currentValidates; + } + } else { + $validating = false; + $validates = $currentValidates; + } + + if (!$options['atomic']) { + $return[] = $validates; + } elseif (!$validates && !$validating) { + break; + } + } + $this->validationErrors = $validationErrors; + + switch (true) { + case ($options['validate'] === 'only'): + return ($options['atomic'] ? $validates : $return); + break; + case ($options['validate'] === 'first'): + $options['validate'] = true; + break; + default: + if ($options['atomic']) { + if ($validates) { + if ($transactionBegun) { + return $db->commit($this) !== false; + } else { + return true; + } + } + $db->rollback($this); + return false; + } + return $return; + break; + } + } + if ($options['atomic'] && !$validates) { + $db->rollback($this); + return false; + } + return $return; + } + $associations = $this->getAssociated(); + + while ($validates) { + foreach ($data as $association => $values) { + if (isset($associations[$association])) { + switch ($associations[$association]) { + case 'belongsTo': + if ($this->{$association}->__save($values, $options)) { + $data[$this->alias][$this->belongsTo[$association]['foreignKey']] = $this->{$association}->id; + } else { + $validationErrors[$association] = $this->{$association}->validationErrors; + $validates = false; + } + if (!$options['atomic']) { + $return[$association][] = $validates; + } + break; + } + } + } + + if (!$this->__save($data, $options)) { + $validationErrors[$this->alias] = $this->validationErrors; + $validates = false; + } + if (!$options['atomic']) { + $return[$this->alias] = $validates; + } + $validating = ($options['validate'] === 'only' || $options['validate'] === 'first'); + + foreach ($data as $association => $values) { + if (!$validates && !$validating) { + break; + } + if (isset($associations[$association])) { + $type = $associations[$association]; + switch ($type) { + case 'hasOne': + $values[$this->{$type}[$association]['foreignKey']] = $this->id; + if (!$this->{$association}->__save($values, $options)) { + $validationErrors[$association] = $this->{$association}->validationErrors; + $validates = false; + } + if (!$options['atomic']) { + $return[$association][] = $validates; + } + break; + case 'hasMany': + foreach ($values as $i => $value) { + $values[$i][$this->{$type}[$association]['foreignKey']] = $this->id; + } + $_options = array_merge($options, array('atomic' => false)); + + if ($_options['validate'] === 'first') { + $_options['validate'] = 'only'; + } + $_return = $this->{$association}->saveAll($values, $_options); + + if ($_return === false || (is_array($_return) && in_array(false, $_return, true))) { + $validationErrors[$association] = $this->{$association}->validationErrors; + $validates = false; + } + if (is_array($_return)) { + foreach ($_return as $val) { + if (!isset($return[$association])) { + $return[$association] = array(); + } elseif (!is_array($return[$association])) { + $return[$association] = array($return[$association]); + } + $return[$association][] = $val; + } + } else { + $return[$association] = $_return; + } + break; + } + } + } + $this->validationErrors = $validationErrors; + + if (isset($validationErrors[$this->alias])) { + $this->validationErrors = $validationErrors[$this->alias]; + } + + switch (true) { + case ($options['validate'] === 'only'): + return ($options['atomic'] ? $validates : $return); + break; + case ($options['validate'] === 'first'): + $options['validate'] = true; + $return = array(); + break; + default: + if ($options['atomic']) { + if ($validates) { + if ($transactionBegun) { + return $db->commit($this) !== false; + } else { + return true; + } + } else { + $db->rollback($this); + } + } + return $return; + break; + } + if ($options['atomic'] && !$validates) { + $db->rollback($this); + return false; + } + } + } + +/** + * Private helper method used by saveAll. + * + * @return boolean Success + * @access private + * @see Model::saveAll() + */ + function __save($data, $options) { + if ($options['validate'] === 'first' || $options['validate'] === 'only') { + if (!($this->create($data) && $this->validates($options))) { + return false; + } + } elseif (!($this->create(null) !== null && $this->save($data, $options))) { + return false; + } + return true; + } + +/** + * Updates multiple model records based on a set of conditions. + * + * @param array $fields Set of fields and values, indexed by fields. + * Fields are treated as SQL snippets, to insert literal values manually escape your data. + * @param mixed $conditions Conditions to match, true for all records + * @return boolean True on success, false on failure + * @access public + * @link http://book.cakephp.org/view/1031/Saving-Your-Data + */ + function updateAll($fields, $conditions = true) { + $db =& ConnectionManager::getDataSource($this->useDbConfig); + return $db->update($this, $fields, null, $conditions); + } + +/** + * Removes record for given ID. If no ID is given, the current ID is used. Returns true on success. + * + * @param mixed $id ID of record to delete + * @param boolean $cascade Set to true to delete records that depend on this record + * @return boolean True on success + * @access public + * @link http://book.cakephp.org/view/1036/delete + */ + function delete($id = null, $cascade = true) { + if (!empty($id)) { + $this->id = $id; + } + $id = $this->id; + + if ($this->beforeDelete($cascade)) { + $filters = $this->Behaviors->trigger($this, 'beforeDelete', array($cascade), array( + 'break' => true, 'breakOn' => false + )); + if (!$filters || !$this->exists()) { + return false; + } + $db =& ConnectionManager::getDataSource($this->useDbConfig); + + $this->_deleteDependent($id, $cascade); + $this->_deleteLinks($id); + $this->id = $id; + + if (!empty($this->belongsTo)) { + $keys = $this->find('first', array( + 'fields' => $this->__collectForeignKeys(), + 'conditions' => array($this->alias . '.' . $this->primaryKey => $id) + )); + } + + if ($db->delete($this, array($this->alias . '.' . $this->primaryKey => $id))) { + if (!empty($this->belongsTo)) { + $this->updateCounterCache($keys[$this->alias]); + } + $this->Behaviors->trigger($this, 'afterDelete'); + $this->afterDelete(); + $this->_clearCache(); + $this->id = false; + return true; + } + } + return false; + } + +/** + * Cascades model deletes through associated hasMany and hasOne child records. + * + * @param string $id ID of record that was deleted + * @param boolean $cascade Set to true to delete records that depend on this record + * @return void + * @access protected + */ + function _deleteDependent($id, $cascade) { + if (!empty($this->__backAssociation)) { + $savedAssociatons = $this->__backAssociation; + $this->__backAssociation = array(); + } + foreach (array_merge($this->hasMany, $this->hasOne) as $assoc => $data) { + if ($data['dependent'] === true && $cascade === true) { + + $model =& $this->{$assoc}; + $conditions = array($model->escapeField($data['foreignKey']) => $id); + if ($data['conditions']) { + $conditions = array_merge((array)$data['conditions'], $conditions); + } + $model->recursive = -1; + + if (isset($data['exclusive']) && $data['exclusive']) { + $model->deleteAll($conditions); + } else { + $records = $model->find('all', array( + 'conditions' => $conditions, 'fields' => $model->primaryKey + )); + + if (!empty($records)) { + foreach ($records as $record) { + $model->delete($record[$model->alias][$model->primaryKey]); + } + } + } + } + } + if (isset($savedAssociatons)) { + $this->__backAssociation = $savedAssociatons; + } + } + +/** + * Cascades model deletes through HABTM join keys. + * + * @param string $id ID of record that was deleted + * @return void + * @access protected + */ + function _deleteLinks($id) { + foreach ($this->hasAndBelongsToMany as $assoc => $data) { + $joinModel = $data['with']; + $records = $this->{$joinModel}->find('all', array( + 'conditions' => array_merge(array($this->{$joinModel}->escapeField($data['foreignKey']) => $id)), + 'fields' => $this->{$joinModel}->primaryKey, + 'recursive' => -1 + )); + if (!empty($records)) { + foreach ($records as $record) { + $this->{$joinModel}->delete($record[$this->{$joinModel}->alias][$this->{$joinModel}->primaryKey]); + } + } + } + } + +/** + * Deletes multiple model records based on a set of conditions. + * + * @param mixed $conditions Conditions to match + * @param boolean $cascade Set to true to delete records that depend on this record + * @param boolean $callbacks Run callbacks + * @return boolean True on success, false on failure + * @access public + * @link http://book.cakephp.org/view/1038/deleteAll + */ + function deleteAll($conditions, $cascade = true, $callbacks = false) { + if (empty($conditions)) { + return false; + } + $db =& ConnectionManager::getDataSource($this->useDbConfig); + + if (!$cascade && !$callbacks) { + return $db->delete($this, $conditions); + } else { + $ids = $this->find('all', array_merge(array( + 'fields' => "{$this->alias}.{$this->primaryKey}", + 'recursive' => 0), compact('conditions')) + ); + if ($ids === false) { + return false; + } + + $ids = Set::extract($ids, "{n}.{$this->alias}.{$this->primaryKey}"); + if (empty($ids)) { + return true; + } + + if ($callbacks) { + $_id = $this->id; + $result = true; + foreach ($ids as $id) { + $result = ($result && $this->delete($id, $cascade)); + } + $this->id = $_id; + return $result; + } else { + foreach ($ids as $id) { + $this->_deleteLinks($id); + if ($cascade) { + $this->_deleteDependent($id, $cascade); + } + } + return $db->delete($this, array($this->alias . '.' . $this->primaryKey => $ids)); + } + } + } + +/** + * Collects foreign keys from associations. + * + * @return array + * @access private + */ + function __collectForeignKeys($type = 'belongsTo') { + $result = array(); + + foreach ($this->{$type} as $assoc => $data) { + if (isset($data['foreignKey']) && is_string($data['foreignKey'])) { + $result[$assoc] = $data['foreignKey']; + } + } + return $result; + } + +/** + * Returns true if a record with the currently set ID exists. + * + * Internally calls Model::getID() to obtain the current record ID to verify, + * and then performs a Model::find('count') on the currently configured datasource + * to ascertain the existence of the record in persistent storage. + * + * @return boolean True if such a record exists + * @access public + */ + function exists() { + if ($this->getID() === false) { + return false; + } + $conditions = array($this->alias . '.' . $this->primaryKey => $this->getID()); + $query = array('conditions' => $conditions, 'recursive' => -1, 'callbacks' => false); + return ($this->find('count', $query) > 0); + } + +/** + * Returns true if a record that meets given conditions exists. + * + * @param array $conditions SQL conditions array + * @return boolean True if such a record exists + * @access public + */ + function hasAny($conditions = null) { + return ($this->find('count', array('conditions' => $conditions, 'recursive' => -1)) != false); + } + +/** + * Queries the datasource and returns a result set array. + * + * Also used to perform new-notation finds, where the first argument is type of find operation to perform + * (all / first / count / neighbors / list / threaded ), + * second parameter options for finding ( indexed array, including: 'conditions', 'limit', + * 'recursive', 'page', 'fields', 'offset', 'order') + * + * Eg: + * {{{ + * find('all', array( + * 'conditions' => array('name' => 'Thomas Anderson'), + * 'fields' => array('name', 'email'), + * 'order' => 'field3 DESC', + * 'recursive' => 2, + * 'group' => 'type' + * )); + * }}} + * + * In addition to the standard query keys above, you can provide Datasource, and behavior specific + * keys. For example, when using a SQL based datasource you can use the joins key to specify additional + * joins that should be part of the query. + * + * {{{ + * find('all', array( + * 'conditions' => array('name' => 'Thomas Anderson'), + * 'joins' => array( + * array( + * 'alias' => 'Thought', + * 'table' => 'thoughts', + * 'type' => 'LEFT', + * 'conditions' => '`Thought`.`person_id` = `Person`.`id`' + * ) + * ) + * )); + * }}} + * + * Behaviors and find types can also define custom finder keys which are passed into find(). + * + * Specifying 'fields' for new-notation 'list': + * + * - If no fields are specified, then 'id' is used for key and 'model->displayField' is used for value. + * - If a single field is specified, 'id' is used for key and specified field is used for value. + * - If three fields are specified, they are used (in order) for key, value and group. + * - Otherwise, first and second fields are used for key and value. + * + * @param array $conditions SQL conditions array, or type of find operation (all / first / count / + * neighbors / list / threaded) + * @param mixed $fields Either a single string of a field name, or an array of field names, or + * options for matching + * @param string $order SQL ORDER BY conditions (e.g. "price DESC" or "name ASC") + * @param integer $recursive The number of levels deep to fetch associated records + * @return array Array of records + * @access public + * @link http://book.cakephp.org/view/1018/find + */ + function find($conditions = null, $fields = array(), $order = null, $recursive = null) { + if (!is_string($conditions) || (is_string($conditions) && !array_key_exists($conditions, $this->_findMethods))) { + $type = 'first'; + $query = array_merge(compact('conditions', 'fields', 'order', 'recursive'), array('limit' => 1)); + } else { + list($type, $query) = array($conditions, $fields); + } + + $this->findQueryType = $type; + $this->id = $this->getID(); + + $query = array_merge( + array( + 'conditions' => null, 'fields' => null, 'joins' => array(), 'limit' => null, + 'offset' => null, 'order' => null, 'page' => null, 'group' => null, 'callbacks' => true + ), + (array)$query + ); + + if ($type != 'all') { + if ($this->_findMethods[$type] === true) { + $query = $this->{'_find' . ucfirst($type)}('before', $query); + } + } + + if (!is_numeric($query['page']) || intval($query['page']) < 1) { + $query['page'] = 1; + } + if ($query['page'] > 1 && !empty($query['limit'])) { + $query['offset'] = ($query['page'] - 1) * $query['limit']; + } + if ($query['order'] === null && $this->order !== null) { + $query['order'] = $this->order; + } + $query['order'] = array($query['order']); + + if ($query['callbacks'] === true || $query['callbacks'] === 'before') { + $return = $this->Behaviors->trigger($this, 'beforeFind', array($query), array( + 'break' => true, 'breakOn' => false, 'modParams' => true + )); + $query = (is_array($return)) ? $return : $query; + + if ($return === false) { + return null; + } + + $return = $this->beforeFind($query); + $query = (is_array($return)) ? $return : $query; + + if ($return === false) { + return null; + } + } + + if (!$db =& ConnectionManager::getDataSource($this->useDbConfig)) { + return false; + } + + $results = $db->read($this, $query); + $this->resetAssociations(); + + if ($query['callbacks'] === true || $query['callbacks'] === 'after') { + $results = $this->__filterResults($results); + } + + $this->findQueryType = null; + + if ($type === 'all') { + return $results; + } else { + if ($this->_findMethods[$type] === true) { + return $this->{'_find' . ucfirst($type)}('after', $query, $results); + } + } + } + +/** + * Handles the before/after filter logic for find('first') operations. Only called by Model::find(). + * + * @param string $state Either "before" or "after" + * @param array $query + * @param array $data + * @return array + * @access protected + * @see Model::find() + */ + function _findFirst($state, $query, $results = array()) { + if ($state == 'before') { + $query['limit'] = 1; + return $query; + } elseif ($state == 'after') { + if (empty($results[0])) { + return false; + } + return $results[0]; + } + } + +/** + * Handles the before/after filter logic for find('count') operations. Only called by Model::find(). + * + * @param string $state Either "before" or "after" + * @param array $query + * @param array $data + * @return int The number of records found, or false + * @access protected + * @see Model::find() + */ + function _findCount($state, $query, $results = array()) { + if ($state == 'before') { + $db =& ConnectionManager::getDataSource($this->useDbConfig); + if (empty($query['fields'])) { + $query['fields'] = $db->calculate($this, 'count'); + } elseif (is_string($query['fields']) && !preg_match('/count/i', $query['fields'])) { + $query['fields'] = $db->calculate($this, 'count', array( + $db->expression($query['fields']), 'count' + )); + } + $query['order'] = false; + return $query; + } elseif ($state == 'after') { + if (isset($results[0][0]['count'])) { + return intval($results[0][0]['count']); + } elseif (isset($results[0][$this->alias]['count'])) { + return intval($results[0][$this->alias]['count']); + } + return false; + } + } + +/** + * Handles the before/after filter logic for find('list') operations. Only called by Model::find(). + * + * @param string $state Either "before" or "after" + * @param array $query + * @param array $data + * @return array Key/value pairs of primary keys/display field values of all records found + * @access protected + * @see Model::find() + */ + function _findList($state, $query, $results = array()) { + if ($state == 'before') { + if (empty($query['fields'])) { + $query['fields'] = array("{$this->alias}.{$this->primaryKey}", "{$this->alias}.{$this->displayField}"); + $list = array("{n}.{$this->alias}.{$this->primaryKey}", "{n}.{$this->alias}.{$this->displayField}", null); + } else { + if (!is_array($query['fields'])) { + $query['fields'] = String::tokenize($query['fields']); + } + + if (count($query['fields']) == 1) { + if (strpos($query['fields'][0], '.') === false) { + $query['fields'][0] = $this->alias . '.' . $query['fields'][0]; + } + + $list = array("{n}.{$this->alias}.{$this->primaryKey}", '{n}.' . $query['fields'][0], null); + $query['fields'] = array("{$this->alias}.{$this->primaryKey}", $query['fields'][0]); + } elseif (count($query['fields']) == 3) { + for ($i = 0; $i < 3; $i++) { + if (strpos($query['fields'][$i], '.') === false) { + $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i]; + } + } + + $list = array('{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], '{n}.' . $query['fields'][2]); + } else { + for ($i = 0; $i < 2; $i++) { + if (strpos($query['fields'][$i], '.') === false) { + $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i]; + } + } + + $list = array('{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], null); + } + } + if (!isset($query['recursive']) || $query['recursive'] === null) { + $query['recursive'] = -1; + } + list($query['list']['keyPath'], $query['list']['valuePath'], $query['list']['groupPath']) = $list; + return $query; + } elseif ($state == 'after') { + if (empty($results)) { + return array(); + } + $lst = $query['list']; + return Set::combine($results, $lst['keyPath'], $lst['valuePath'], $lst['groupPath']); + } + } + +/** + * Detects the previous field's value, then uses logic to find the 'wrapping' + * rows and return them. + * + * @param string $state Either "before" or "after" + * @param mixed $query + * @param array $results + * @return array + * @access protected + */ + function _findNeighbors($state, $query, $results = array()) { + if ($state == 'before') { + $query = array_merge(array('recursive' => 0), $query); + extract($query); + $conditions = (array)$conditions; + if (isset($field) && isset($value)) { + if (strpos($field, '.') === false) { + $field = $this->alias . '.' . $field; + } + } else { + $field = $this->alias . '.' . $this->primaryKey; + $value = $this->id; + } + $query['conditions'] = array_merge($conditions, array($field . ' <' => $value)); + $query['order'] = $field . ' DESC'; + $query['limit'] = 1; + $query['field'] = $field; + $query['value'] = $value; + return $query; + } elseif ($state == 'after') { + extract($query); + unset($query['conditions'][$field . ' <']); + $return = array(); + if (isset($results[0])) { + $prevVal = Set::extract('/' . str_replace('.', '/', $field), $results[0]); + $query['conditions'][$field . ' >='] = $prevVal[0]; + $query['conditions'][$field . ' !='] = $value; + $query['limit'] = 2; + } else { + $return['prev'] = null; + $query['conditions'][$field . ' >'] = $value; + $query['limit'] = 1; + } + $query['order'] = $field . ' ASC'; + $return2 = $this->find('all', $query); + if (!array_key_exists('prev', $return)) { + $return['prev'] = $return2[0]; + } + if (count($return2) == 2) { + $return['next'] = $return2[1]; + } elseif (count($return2) == 1 && !$return['prev']) { + $return['next'] = $return2[0]; + } else { + $return['next'] = null; + } + return $return; + } + } + +/** + * In the event of ambiguous results returned (multiple top level results, with different parent_ids) + * top level results with different parent_ids to the first result will be dropped + * + * @param mixed $state + * @param mixed $query + * @param array $results + * @return array Threaded results + * @access protected + */ + function _findThreaded($state, $query, $results = array()) { + if ($state == 'before') { + return $query; + } elseif ($state == 'after') { + $return = $idMap = array(); + $ids = Set::extract($results, '{n}.' . $this->alias . '.' . $this->primaryKey); + + foreach ($results as $result) { + $result['children'] = array(); + $id = $result[$this->alias][$this->primaryKey]; + $parentId = $result[$this->alias]['parent_id']; + if (isset($idMap[$id]['children'])) { + $idMap[$id] = array_merge($result, (array)$idMap[$id]); + } else { + $idMap[$id] = array_merge($result, array('children' => array())); + } + if (!$parentId || !in_array($parentId, $ids)) { + $return[] =& $idMap[$id]; + } else { + $idMap[$parentId]['children'][] =& $idMap[$id]; + } + } + if (count($return) > 1) { + $ids = array_unique(Set::extract('/' . $this->alias . '/parent_id', $return)); + if (count($ids) > 1) { + $root = $return[0][$this->alias]['parent_id']; + foreach ($return as $key => $value) { + if ($value[$this->alias]['parent_id'] != $root) { + unset($return[$key]); + } + } + } + } + return $return; + } + } + +/** + * Passes query results through model and behavior afterFilter() methods. + * + * @param array Results to filter + * @param boolean $primary If this is the primary model results (results from model where the find operation was performed) + * @return array Set of filtered results + * @access private + */ + function __filterResults($results, $primary = true) { + $return = $this->Behaviors->trigger($this, 'afterFind', array($results, $primary), array('modParams' => true)); + if ($return !== true) { + $results = $return; + } + return $this->afterFind($results, $primary); + } + +/** + * This resets the association arrays for the model back + * to those originally defined in the model. Normally called at the end + * of each call to Model::find() + * + * @return boolean Success + * @access public + */ + function resetAssociations() { + if (!empty($this->__backAssociation)) { + foreach ($this->__associations as $type) { + if (isset($this->__backAssociation[$type])) { + $this->{$type} = $this->__backAssociation[$type]; + } + } + $this->__backAssociation = array(); + } + + foreach ($this->__associations as $type) { + foreach ($this->{$type} as $key => $name) { + if (!empty($this->{$key}->__backAssociation)) { + $this->{$key}->resetAssociations(); + } + } + } + $this->__backAssociation = array(); + return true; + } + +/** + * Returns false if any fields passed match any (by default, all if $or = false) of their matching values. + * + * @param array $fields Field/value pairs to search (if no values specified, they are pulled from $this->data) + * @param boolean $or If false, all fields specified must match in order for a false return value + * @return boolean False if any records matching any fields are found + * @access public + */ + function isUnique($fields, $or = true) { + if (!is_array($fields)) { + $fields = func_get_args(); + if (is_bool($fields[count($fields) - 1])) { + $or = $fields[count($fields) - 1]; + unset($fields[count($fields) - 1]); + } + } + + foreach ($fields as $field => $value) { + if (is_numeric($field)) { + unset($fields[$field]); + + $field = $value; + if (isset($this->data[$this->alias][$field])) { + $value = $this->data[$this->alias][$field]; + } else { + $value = null; + } + } + + if (strpos($field, '.') === false) { + unset($fields[$field]); + $fields[$this->alias . '.' . $field] = $value; + } + } + if ($or) { + $fields = array('or' => $fields); + } + if (!empty($this->id)) { + $fields[$this->alias . '.' . $this->primaryKey . ' !='] = $this->id; + } + return ($this->find('count', array('conditions' => $fields, 'recursive' => -1)) == 0); + } + +/** + * Returns a resultset for a given SQL statement. Custom SQL queries should be performed with this method. + * + * @param string $sql SQL statement + * @return array Resultset + * @access public + * @link http://book.cakephp.org/view/1027/query + */ + function query() { + $params = func_get_args(); + $db =& ConnectionManager::getDataSource($this->useDbConfig); + return call_user_func_array(array(&$db, 'query'), $params); + } + +/** + * Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations + * that use the 'with' key as well. Since __saveMulti is incapable of exiting a save operation. + * + * Will validate the currently set data. Use Model::set() or Model::create() to set the active data. + * + * @param string $options An optional array of custom options to be made available in the beforeValidate callback + * @return boolean True if there are no errors + * @access public + * @link http://book.cakephp.org/view/1182/Validating-Data-from-the-Controller + */ + function validates($options = array()) { + $errors = $this->invalidFields($options); + if (empty($errors) && $errors !== false) { + $errors = $this->__validateWithModels($options); + } + if (is_array($errors)) { + return count($errors) === 0; + } + return $errors; + } + +/** + * Returns an array of fields that have failed validation. On the current model. + * + * @param string $options An optional array of custom options to be made available in the beforeValidate callback + * @return array Array of invalid fields + * @see Model::validates() + * @access public + * @link http://book.cakephp.org/view/1182/Validating-Data-from-the-Controller + */ + function invalidFields($options = array()) { + if ( + !$this->Behaviors->trigger( + $this, + 'beforeValidate', + array($options), + array('break' => true, 'breakOn' => false) + ) || + $this->beforeValidate($options) === false + ) { + return false; + } + + if (!isset($this->validate) || empty($this->validate)) { + return $this->validationErrors; + } + + $data = $this->data; + $methods = array_map('strtolower', get_class_methods($this)); + $behaviorMethods = array_keys($this->Behaviors->methods()); + + if (isset($data[$this->alias])) { + $data = $data[$this->alias]; + } elseif (!is_array($data)) { + $data = array(); + } + + $Validation =& Validation::getInstance(); + $exists = $this->exists(); + + $_validate = $this->validate; + $whitelist = $this->whitelist; + + if (!empty($options['fieldList'])) { + $whitelist = $options['fieldList']; + } + + if (!empty($whitelist)) { + $validate = array(); + foreach ((array)$whitelist as $f) { + if (!empty($this->validate[$f])) { + $validate[$f] = $this->validate[$f]; + } + } + $this->validate = $validate; + } + + foreach ($this->validate as $fieldName => $ruleSet) { + if (!is_array($ruleSet) || (is_array($ruleSet) && isset($ruleSet['rule']))) { + $ruleSet = array($ruleSet); + } + $default = array( + 'allowEmpty' => null, + 'required' => null, + 'rule' => 'blank', + 'last' => false, + 'on' => null + ); + + foreach ($ruleSet as $index => $validator) { + if (!is_array($validator)) { + $validator = array('rule' => $validator); + } + $validator = array_merge($default, $validator); + + if (isset($validator['message'])) { + $message = $validator['message']; + } else { + $message = __('This field cannot be left blank', true); + } + + if ( + empty($validator['on']) || ($validator['on'] == 'create' && + !$exists) || ($validator['on'] == 'update' && $exists + )) { + $required = ( + (!isset($data[$fieldName]) && $validator['required'] === true) || + ( + isset($data[$fieldName]) && (empty($data[$fieldName]) && + !is_numeric($data[$fieldName])) && $validator['allowEmpty'] === false + ) + ); + + if ($required) { + $this->invalidate($fieldName, $message); + if ($validator['last']) { + break; + } + } elseif (array_key_exists($fieldName, $data)) { + if (empty($data[$fieldName]) && $data[$fieldName] != '0' && $validator['allowEmpty'] === true) { + break; + } + if (is_array($validator['rule'])) { + $rule = $validator['rule'][0]; + unset($validator['rule'][0]); + $ruleParams = array_merge(array($data[$fieldName]), array_values($validator['rule'])); + } else { + $rule = $validator['rule']; + $ruleParams = array($data[$fieldName]); + } + + $valid = true; + + if (in_array(strtolower($rule), $methods)) { + $ruleParams[] = $validator; + $ruleParams[0] = array($fieldName => $ruleParams[0]); + $valid = $this->dispatchMethod($rule, $ruleParams); + } elseif (in_array($rule, $behaviorMethods) || in_array(strtolower($rule), $behaviorMethods)) { + $ruleParams[] = $validator; + $ruleParams[0] = array($fieldName => $ruleParams[0]); + $valid = $this->Behaviors->dispatchMethod($this, $rule, $ruleParams); + } elseif (method_exists($Validation, $rule)) { + $valid = $Validation->dispatchMethod($rule, $ruleParams); + } elseif (!is_array($validator['rule'])) { + $valid = preg_match($rule, $data[$fieldName]); + } elseif (Configure::read('debug') > 0) { + trigger_error(sprintf(__('Could not find validation handler %s for %s', true), $rule, $fieldName), E_USER_WARNING); + } + + if (!$valid || (is_string($valid) && strlen($valid) > 0)) { + if (is_string($valid) && strlen($valid) > 0) { + $validator['message'] = $valid; + } elseif (!isset($validator['message'])) { + if (is_string($index)) { + $validator['message'] = $index; + } elseif (is_numeric($index) && count($ruleSet) > 1) { + $validator['message'] = $index + 1; + } else { + $validator['message'] = $message; + } + } + $this->invalidate($fieldName, $validator['message']); + + if ($validator['last']) { + break; + } + } + } + } + } + } + $this->validate = $_validate; + return $this->validationErrors; + } + +/** + * Runs validation for hasAndBelongsToMany associations that have 'with' keys + * set. And data in the set() data set. + * + * @param array $options Array of options to use on Valdation of with models + * @return boolean Failure of validation on with models. + * @access private + * @see Model::validates() + */ + function __validateWithModels($options) { + $valid = true; + foreach ($this->hasAndBelongsToMany as $assoc => $association) { + if (empty($association['with']) || !isset($this->data[$assoc])) { + continue; + } + list($join) = $this->joinModel($this->hasAndBelongsToMany[$assoc]['with']); + $data = $this->data[$assoc]; + + $newData = array(); + foreach ((array)$data as $row) { + if (isset($row[$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { + $newData[] = $row; + } elseif (isset($row[$join]) && isset($row[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { + $newData[] = $row[$join]; + } + } + if (empty($newData)) { + continue; + } + foreach ($newData as $data) { + $data[$this->hasAndBelongsToMany[$assoc]['foreignKey']] = $this->id; + $this->{$join}->create($data); + $valid = ($valid && $this->{$join}->validates($options)); + } + } + return $valid; + } +/** + * Marks a field as invalid, optionally setting the name of validation + * rule (in case of multiple validation for field) that was broken. + * + * @param string $field The name of the field to invalidate + * @param mixed $value Name of validation rule that was not failed, or validation message to + * be returned. If no validation key is provided, defaults to true. + * @access public + */ + function invalidate($field, $value = true) { + if (!is_array($this->validationErrors)) { + $this->validationErrors = array(); + } + $this->validationErrors[$field] = $value; + } + +/** + * Returns true if given field name is a foreign key in this model. + * + * @param string $field Returns true if the input string ends in "_id" + * @return boolean True if the field is a foreign key listed in the belongsTo array. + * @access public + */ + function isForeignKey($field) { + $foreignKeys = array(); + if (!empty($this->belongsTo)) { + foreach ($this->belongsTo as $assoc => $data) { + $foreignKeys[] = $data['foreignKey']; + } + } + return in_array($field, $foreignKeys); + } + +/** + * Escapes the field name and prepends the model name. Escaping is done according to the + * current database driver's rules. + * + * @param string $field Field to escape (e.g: id) + * @param string $alias Alias for the model (e.g: Post) + * @return string The name of the escaped field for this Model (i.e. id becomes `Post`.`id`). + * @access public + */ + function escapeField($field = null, $alias = null) { + if (empty($alias)) { + $alias = $this->alias; + } + if (empty($field)) { + $field = $this->primaryKey; + } + $db =& ConnectionManager::getDataSource($this->useDbConfig); + if (strpos($field, $db->name($alias) . '.') === 0) { + return $field; + } + return $db->name($alias . '.' . $field); + } + +/** + * Returns the current record's ID + * + * @param integer $list Index on which the composed ID is located + * @return mixed The ID of the current record, false if no ID + * @access public + */ + function getID($list = 0) { + if (empty($this->id) || (is_array($this->id) && isset($this->id[0]) && empty($this->id[0]))) { + return false; + } + + if (!is_array($this->id)) { + return $this->id; + } + + if (empty($this->id)) { + return false; + } + + if (isset($this->id[$list]) && !empty($this->id[$list])) { + return $this->id[$list]; + } elseif (isset($this->id[$list])) { + return false; + } + + foreach ($this->id as $id) { + return $id; + } + + return false; + } + +/** + * Returns the ID of the last record this model inserted. + * + * @return mixed Last inserted ID + * @access public + */ + function getLastInsertID() { + return $this->getInsertID(); + } + +/** + * Returns the ID of the last record this model inserted. + * + * @return mixed Last inserted ID + * @access public + */ + function getInsertID() { + return $this->__insertID; + } + +/** + * Sets the ID of the last record this model inserted + * + * @param mixed Last inserted ID + * @access public + */ + function setInsertID($id) { + $this->__insertID = $id; + } + +/** + * Returns the number of rows returned from the last query. + * + * @return int Number of rows + * @access public + */ + function getNumRows() { + $db =& ConnectionManager::getDataSource($this->useDbConfig); + return $db->lastNumRows(); + } + +/** + * Returns the number of rows affected by the last query. + * + * @return int Number of rows + * @access public + */ + function getAffectedRows() { + $db =& ConnectionManager::getDataSource($this->useDbConfig); + return $db->lastAffected(); + } + +/** + * Sets the DataSource to which this model is bound. + * + * @param string $dataSource The name of the DataSource, as defined in app/config/database.php + * @return boolean True on success + * @access public + */ + function setDataSource($dataSource = null) { + $oldConfig = $this->useDbConfig; + + if ($dataSource != null) { + $this->useDbConfig = $dataSource; + } + $db =& ConnectionManager::getDataSource($this->useDbConfig); + if (!empty($oldConfig) && isset($db->config['prefix'])) { + $oldDb =& ConnectionManager::getDataSource($oldConfig); + + if (!isset($this->tablePrefix) || (!isset($oldDb->config['prefix']) || $this->tablePrefix == $oldDb->config['prefix'])) { + $this->tablePrefix = $db->config['prefix']; + } + } elseif (isset($db->config['prefix'])) { + $this->tablePrefix = $db->config['prefix']; + } + + if (empty($db) || !is_object($db)) { + return $this->cakeError('missingConnection', array(array('code' => 500, 'className' => $this->alias))); + } + } + +/** + * Gets the DataSource to which this model is bound. + * Not safe for use with some versions of PHP4, because this class is overloaded. + * + * @return object A DataSource object + * @access public + */ + function &getDataSource() { + $db =& ConnectionManager::getDataSource($this->useDbConfig); + return $db; + } + +/** + * Gets all the models with which this model is associated. + * + * @param string $type Only result associations of this type + * @return array Associations + * @access public + */ + function getAssociated($type = null) { + if ($type == null) { + $associated = array(); + foreach ($this->__associations as $assoc) { + if (!empty($this->{$assoc})) { + $models = array_keys($this->{$assoc}); + foreach ($models as $m) { + $associated[$m] = $assoc; + } + } + } + return $associated; + } elseif (in_array($type, $this->__associations)) { + if (empty($this->{$type})) { + return array(); + } + return array_keys($this->{$type}); + } else { + $assoc = array_merge( + $this->hasOne, + $this->hasMany, + $this->belongsTo, + $this->hasAndBelongsToMany + ); + if (array_key_exists($type, $assoc)) { + foreach ($this->__associations as $a) { + if (isset($this->{$a}[$type])) { + $assoc[$type]['association'] = $a; + break; + } + } + return $assoc[$type]; + } + return null; + } + } + +/** + * Gets the name and fields to be used by a join model. This allows specifying join fields + * in the association definition. + * + * @param object $model The model to be joined + * @param mixed $with The 'with' key of the model association + * @param array $keys Any join keys which must be merged with the keys queried + * @return array + * @access public + */ + function joinModel($assoc, $keys = array()) { + if (is_string($assoc)) { + return array($assoc, array_keys($this->{$assoc}->schema())); + } elseif (is_array($assoc)) { + $with = key($assoc); + return array($with, array_unique(array_merge($assoc[$with], $keys))); + } + trigger_error( + sprintf(__('Invalid join model settings in %s', true), $model->alias), + E_USER_WARNING + ); + } + +/** + * Called before each find operation. Return false if you want to halt the find + * call, otherwise return the (modified) query data. + * + * @param array $queryData Data used to execute this query, i.e. conditions, order, etc. + * @return mixed true if the operation should continue, false if it should abort; or, modified + * $queryData to continue with new $queryData + * @access public + * @link http://book.cakephp.org/view/1048/Callback-Methods#beforeFind-1049 + */ + function beforeFind($queryData) { + return true; + } + +/** + * Called after each find operation. Can be used to modify any results returned by find(). + * Return value should be the (modified) results. + * + * @param mixed $results The results of the find operation + * @param boolean $primary Whether this model is being queried directly (vs. being queried as an association) + * @return mixed Result of the find operation + * @access public + * @link http://book.cakephp.org/view/1048/Callback-Methods#afterFind-1050 + */ + function afterFind($results, $primary = false) { + return $results; + } + +/** + * Called before each save operation, after validation. Return a non-true result + * to halt the save. + * + * @return boolean True if the operation should continue, false if it should abort + * @access public + * @link http://book.cakephp.org/view/1048/Callback-Methods#beforeSave-1052 + */ + function beforeSave($options = array()) { + return true; + } + +/** + * Called after each successful save operation. + * + * @param boolean $created True if this save created a new record + * @access public + * @link http://book.cakephp.org/view/1048/Callback-Methods#afterSave-1053 + */ + function afterSave($created) { + } + +/** + * Called before every deletion operation. + * + * @param boolean $cascade If true records that depend on this record will also be deleted + * @return boolean True if the operation should continue, false if it should abort + * @access public + * @link http://book.cakephp.org/view/1048/Callback-Methods#beforeDelete-1054 + */ + function beforeDelete($cascade = true) { + return true; + } + +/** + * Called after every deletion operation. + * + * @access public + * @link http://book.cakephp.org/view/1048/Callback-Methods#afterDelete-1055 + */ + function afterDelete() { + } + +/** + * Called during validation operations, before validation. Please note that custom + * validation rules can be defined in $validate. + * + * @return boolean True if validate operation should continue, false to abort + * @param $options array Options passed from model::save(), see $options of model::save(). + * @access public + * @link http://book.cakephp.org/view/1048/Callback-Methods#beforeValidate-1051 + */ + function beforeValidate($options = array()) { + return true; + } + +/** + * Called when a DataSource-level error occurs. + * + * @access public + * @link http://book.cakephp.org/view/1048/Callback-Methods#onError-1056 + */ + function onError() { + } + +/** + * Private method. Clears cache for this model. + * + * @param string $type If null this deletes cached views if Cache.check is true + * Will be used to allow deleting query cache also + * @return boolean true on delete + * @access protected + * @todo + */ + function _clearCache($type = null) { + if ($type === null) { + if (Configure::read('Cache.check') === true) { + $assoc[] = strtolower(Inflector::pluralize($this->alias)); + $assoc[] = strtolower(Inflector::underscore(Inflector::pluralize($this->alias))); + foreach ($this->__associations as $key => $association) { + foreach ($this->$association as $key => $className) { + $check = strtolower(Inflector::pluralize($className['className'])); + if (!in_array($check, $assoc)) { + $assoc[] = strtolower(Inflector::pluralize($className['className'])); + $assoc[] = strtolower(Inflector::underscore(Inflector::pluralize($className['className']))); + } + } + } + clearCache($assoc); + return true; + } + } else { + //Will use for query cache deleting + } + } + +/** + * Called when serializing a model. + * + * @return array Set of object variable names this model has + * @access private + */ + function __sleep() { + $return = array_keys(get_object_vars($this)); + return $return; + } + +/** + * Called when de-serializing a model. + * + * @access private + * @todo + */ + function __wakeup() { + } +} +if (!defined('CAKEPHP_UNIT_TEST_EXECUTION')) { + Overloadable::overload('Model'); +}