model.php

Go to the documentation of this file.
00001 <?php
00002 /* SVN FILE: $Id: libs_2model_2model_8php-source.html 580 2008-07-01 14:45:49Z gwoo $ */
00003 /**
00004  * Object-relational mapper.
00005  *
00006  * DBO-backed object data model, for mapping database tables to Cake objects.
00007  *
00008  * PHP versions 5
00009  *
00010  * CakePHP(tm) :  Rapid Development Framework <http://www.cakephp.org/>
00011  * Copyright 2005-2008, Cake Software Foundation, Inc.
00012  *                              1785 E. Sahara Avenue, Suite 490-204
00013  *                              Las Vegas, Nevada 89104
00014  *
00015  * Licensed under The MIT License
00016  * Redistributions of files must retain the above copyright notice.
00017  *
00018  * @filesource
00019  * @copyright       Copyright 2005-2008, Cake Software Foundation, Inc.
00020  * @link                http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
00021  * @package         cake
00022  * @subpackage      cake.cake.libs.model
00023  * @since           CakePHP(tm) v 0.10.0.0
00024  * @version         $Revision: 580 $
00025  * @modifiedby      $LastChangedBy: gwoo $
00026  * @lastmodified    $Date: 2008-07-01 09:45:49 -0500 (Tue, 01 Jul 2008) $
00027  * @license         http://www.opensource.org/licenses/mit-license.php The MIT License
00028  */
00029 /**
00030  * Included libs
00031  */
00032 App::import('Core', array('ClassRegistry', 'Overloadable', 'Validation', 'Behavior', 'ConnectionManager', 'Set'));
00033 /**
00034  * Object-relational mapper.
00035  *
00036  * DBO-backed object data model.
00037  * Automatically selects a database table name based on a pluralized lowercase object class name
00038  * (i.e. class 'User' => table 'users'; class 'Man' => table 'men')
00039  * The table is required to have at least 'id auto_increment', 'created datetime',
00040  * and 'modified datetime' fields.
00041  *
00042  * @package     cake
00043  * @subpackage  cake.cake.libs.model
00044  */
00045 class Model extends Overloadable {
00046 /**
00047  * The name of the DataSource connection that this Model uses
00048  *
00049  * @var string
00050  * @access public
00051  */
00052     var $useDbConfig = 'default';
00053 /**
00054  * Custom database table name.
00055  *
00056  * @var string
00057  * @access public
00058  */
00059     var $useTable = null;
00060 /**
00061  * Custom display field name. Display fields are used by Scaffold, in SELECT boxes' OPTION elements.
00062  *
00063  * @var string
00064  * @access public
00065  */
00066     var $displayField = null;
00067 /**
00068  * Value of the primary key ID of the record that this model is currently pointing to
00069  *
00070  * @var mixed
00071  * @access public
00072  */
00073     var $id = false;
00074 /**
00075  * Container for the data that this model gets from persistent storage (the database).
00076  *
00077  * @var array
00078  * @access public
00079  */
00080     var $data = array();
00081 /**
00082  * Table name for this Model.
00083  *
00084  * @var string
00085  * @access public
00086  */
00087     var $table = false;
00088 /**
00089  * The name of the ID field for this Model.
00090  *
00091  * @var string
00092  * @access public
00093  */
00094     var $primaryKey = null;
00095 /**
00096  * Table metadata
00097  *
00098  * @var array
00099  * @access protected
00100  */
00101     var $_schema = null;
00102 /**
00103  * List of validation rules. Append entries for validation as ('field_name' => '/^perl_compat_regexp$/')
00104  * that have to match with preg_match(). Use these rules with Model::validate()
00105  *
00106  * @var array
00107  * @access public
00108  */
00109     var $validate = array();
00110 /**
00111  * Errors in validation
00112  *
00113  * @var array
00114  * @access public
00115  */
00116     var $validationErrors = array();
00117 /**
00118  * Database table prefix for tables in model.
00119  *
00120  * @var string
00121  * @access public
00122  */
00123     var $tablePrefix = null;
00124 /**
00125  * Name of the model.
00126  *
00127  * @var string
00128  * @access public
00129  */
00130     var $name = null;
00131 /**
00132  * Alias name for model.
00133  *
00134  * @var string
00135  * @access public
00136  */
00137     var $alias = null;
00138 /**
00139  * List of table names included in the Model description. Used for associations.
00140  *
00141  * @var array
00142  * @access public
00143  */
00144     var $tableToModel = array();
00145 /**
00146  * Whether or not transactions for this model should be logged
00147  *
00148  * @var boolean
00149  * @access public
00150  */
00151     var $logTransactions = false;
00152 /**
00153  * Whether or not to enable transactions for this model (i.e. BEGIN/COMMIT/ROLLBACK)
00154  *
00155  * @var boolean
00156  * @access public
00157  */
00158     var $transactional = false;
00159 /**
00160  * Whether or not to cache queries for this model.  This enables in-memory
00161  * caching only, the results are not stored beyond this execution.
00162  *
00163  * @var boolean
00164  * @access public
00165  */
00166     var $cacheQueries = false;
00167 /**
00168  * belongsTo association
00169  *
00170  * @var array
00171  * @access public
00172  */
00173     var $belongsTo = array();
00174 /**
00175  * hasOne association
00176  *
00177  * @var array
00178  * @access public
00179  */
00180     var $hasOne = array();
00181 /**
00182  * hasMany association
00183  *
00184  * @var array
00185  * @access public
00186  */
00187     var $hasMany = array();
00188 /**
00189  * hasAndBelongsToMany association
00190  *
00191  * @var array
00192  * @access public
00193  */
00194     var $hasAndBelongsToMany = array();
00195 /**
00196  * List of behaviors to load when the model object is initialized. Settings can be
00197  * passed to behaviors by using the behavior name as index. Eg:
00198  *
00199  * array('Translate', 'MyBehavior' => array('setting1' => 'value1'))
00200  *
00201  * @var array
00202  * @access public
00203  */
00204     var $actsAs = null;
00205 /**
00206  * Holds the Behavior objects currently bound to this model
00207  *
00208  * @var object
00209  * @access public
00210  */
00211     var $Behaviors = null;
00212 /**
00213  * Whitelist of fields allowed to be saved
00214  *
00215  * @var array
00216  * @access public
00217  */
00218     var $whitelist = array();
00219 /**
00220  * Should sources for this model be cached.
00221  *
00222  * @var boolean
00223  * @access public
00224  */
00225     var $cacheSources = true;
00226 /**
00227  * Type of find query currently executing
00228  *
00229  * @var string
00230  * @access public
00231  */
00232     var $findQueryType = null;
00233 /**
00234  * Depth of recursive association
00235  *
00236  * @var integer
00237  * @access public
00238  */
00239     var $recursive = 1;
00240 /**
00241  * Default ordering of model records
00242  *
00243  * @var string
00244  * @access public
00245  */
00246     var $order = null;
00247 /**
00248  * Whether or not the model record exists, set by Model::exists()
00249  *
00250  * @var bool
00251  * @access private
00252  */
00253     var $__exists = null;
00254 /**
00255  * Default association keys
00256  *
00257  * @var array
00258  * @access private
00259  */
00260     var $__associationKeys = array(
00261             'belongsTo' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'counterCache'),
00262             'hasOne' => array('className', 'foreignKey','conditions', 'fields','order', 'dependent'),
00263             'hasMany' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'dependent', 'exclusive', 'finderQuery', 'counterQuery'),
00264             'hasAndBelongsToMany' => array('className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery', 'deleteQuery', 'insertQuery'));
00265 /**
00266  * Holds provided/generated association key names and other data for all associations
00267  *
00268  * @var array
00269  * @access private
00270  */
00271     var $__associations = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
00272 /**
00273  * Holds model associations temporarily to allow for dynamic (un)binding
00274  *
00275  * @var array
00276  * @access private
00277  */
00278     var $__backAssociation = array();
00279 /**
00280  * The last inserted ID of the data that this model created
00281  *
00282  * @var integer
00283  * @access private
00284  */
00285     var $__insertID = null;
00286 /**
00287  * The number of records returned by the last query
00288  *
00289  * @var integer
00290  * @access private
00291  */
00292     var $__numRows = null;
00293 /**
00294  * The number of records affected by the last query
00295  *
00296  * @var integer
00297  * @access private
00298  */
00299     var $__affectedRows = null;
00300 /**
00301  * List of valid finder method options
00302  *
00303  * @var array
00304  * @access private
00305  */
00306     var $__findMethods = array('all' => true, 'first' => true, 'count' => true, 'neighbors' => true, 'list' => true, 'threaded' => true);
00307 /**
00308  * Constructor. Binds the Model's database table to the object.
00309  *
00310  * @param integer $id Set this ID for this model on startup
00311  * @param string $table Name of database table to use.
00312  * @param object $ds DataSource connection object.
00313  */
00314     function __construct($id = false, $table = null, $ds = null) {
00315         parent::__construct();
00316 
00317         if (is_array($id)) {
00318             extract(array_merge(array('id' => false, 'table' => null, 'ds' => null, 'name' => null, 'alias' => null), $id));
00319             $this->name = $name;
00320             $this->alias = $alias;
00321         }
00322 
00323         if ($this->name === null) {
00324             $this->name = get_class($this);
00325         }
00326 
00327         if ($this->alias === null) {
00328             $this->alias = $this->name;
00329         }
00330 
00331         if ($this->primaryKey === null) {
00332             $this->primaryKey = 'id';
00333         }
00334         ClassRegistry::addObject($this->alias, $this);
00335 
00336         $this->id = $id;
00337         unset($id);
00338 
00339         if ($table === false) {
00340             $this->useTable = false;
00341         } elseif ($table) {
00342             $this->useTable = $table;
00343         }
00344 
00345         if ($this->useTable !== false) {
00346             $this->setDataSource($ds);
00347 
00348             if ($this->useTable === null) {
00349                 $this->useTable = Inflector::tableize($this->name);
00350             }
00351 
00352             if (in_array('settableprefix', get_class_methods($this))) {
00353                 $this->setTablePrefix();
00354             }
00355 
00356             $this->setSource($this->useTable);
00357             $this->__createLinks();
00358 
00359             if ($this->displayField == null) {
00360                 $this->displayField = $this->hasField(array('title', 'name', $this->primaryKey));
00361             }
00362         }
00363 
00364         if (is_subclass_of($this, 'AppModel')) {
00365             $appVars = get_class_vars('AppModel');
00366             $merge = array();
00367 
00368             if ($this->actsAs !== null || $this->actsAs !== false) {
00369                 $merge[] = 'actsAs';
00370             }
00371 
00372             foreach ($merge as $var) {
00373                 if (isset($appVars[$var]) && !empty($appVars[$var]) && is_array($this->{$var})) {
00374                     $this->{$var} = Set::merge($appVars[$var], $this->{$var});
00375                 }
00376             }
00377         }
00378         $this->Behaviors = new BehaviorCollection();
00379         $this->Behaviors->init($this->alias, $this->actsAs);
00380     }
00381 /**
00382  * Handles custom method calls, like findBy<field> for DB models,
00383  * and custom RPC calls for remote data sources.
00384  *
00385  * @param string $method Name of method to call.
00386  * @param array $params Parameters for the method.
00387  * @return mixed Whatever is returned by called method
00388  * @access protected
00389  */
00390     function call__($method, $params) {
00391         $result = $this->Behaviors->dispatchMethod($this, $method, $params);
00392 
00393         if ($result !== array('unhandled')) {
00394             return $result;
00395         }
00396         $db =& ConnectionManager::getDataSource($this->useDbConfig);
00397         $return = $db->query($method, $params, $this);
00398 
00399         if (!PHP5) {
00400             $this->resetAssociations();
00401         }
00402         return $return;
00403     }
00404 /**
00405  * Bind model associations on the fly.
00406  *
00407  * If $permanent is true, association will not be reset
00408  * to the originals defined in the model.
00409  *
00410  * @param mixed $model A model or association name (string) or set of binding options (indexed by model name type)
00411  * @param array $options If $model is a string, this is the list of association properties with which $model will
00412  *                       be bound
00413  * @param boolean $permanent Set to true to make the binding permanent
00414  * @access public
00415  * @todo
00416  */
00417     function bind($model, $options = array(), $permanent = true) {
00418         if (!is_array($model)) {
00419             $model = array($model => $options);
00420         }
00421 
00422         foreach ($model as $name => $options) {
00423             if (isset($options['type'])) {
00424                 $assoc = $options['type'];
00425             } elseif (isset($options[0])) {
00426                 $assoc = $options[0];
00427             } else {
00428                 $assoc = 'belongsTo';
00429             }
00430 
00431             if (!$permanent) {
00432                 $this->__backAssociation[$assoc] = $this->{$assoc};
00433             }
00434             foreach ($model as $key => $value) {
00435                 $assocName = $modelName = $key;
00436 
00437                 if (isset($this->{$assoc}[$assocName])) {
00438                     $this->{$assoc}[$assocName] = array_merge($this->{$assoc}[$assocName], $options);
00439                 } else {
00440                     if (isset($value['className'])) {
00441                         $modelName = $value['className'];
00442                     }
00443 
00444                     $this->__constructLinkedModel($assocName, $modelName);
00445                     $this->{$assoc}[$assocName] = $model[$assocName];
00446                     $this->__generateAssociation($assoc);
00447                 }
00448                 unset($this->{$assoc}[$assocName]['type'], $this->{$assoc}[$assocName][0]);
00449             }
00450         }
00451     }
00452 /**
00453  * Bind model associations on the fly.
00454  *
00455  * If $reset is false, association will not be reset
00456  * to the originals defined in the model
00457  *
00458  * Example: Add a new hasOne binding to the Profile model not
00459  * defined in the model source code:
00460  * <code>
00461  * $this->User->bindModel( array('hasOne' => array('Profile')) );
00462  * </code>
00463  *
00464  * @param array $params Set of bindings (indexed by binding type)
00465  * @param boolean $reset Set to false to make the binding permanent
00466  * @return boolean Success
00467  * @access public
00468  */
00469     function bindModel($params, $reset = true) {
00470         foreach ($params as $assoc => $model) {
00471             if ($reset === true) {
00472                 $this->__backAssociation[$assoc] = $this->{$assoc};
00473             }
00474 
00475             foreach ($model as $key => $value) {
00476                 $assocName = $key;
00477 
00478                 if (is_numeric($key)) {
00479                     $assocName = $value;
00480                     $value = array();
00481                 }
00482                 $modelName = $assocName;
00483                 $this->{$assoc}[$assocName] = $value;
00484             }
00485         }
00486         $this->__createLinks();
00487         return true;
00488     }
00489 /**
00490  * Turn off associations on the fly.
00491  *
00492  * If $reset is false, association will not be reset
00493  * to the originals defined in the model
00494  *
00495  * Example: Turn off the associated Model Support request,
00496  * to temporarily lighten the User model:
00497  * <code>
00498  * $this->User->unbindModel( array('hasMany' => array('Supportrequest')) );
00499  * </code>
00500  *
00501  * @param array $params Set of bindings to unbind (indexed by binding type)
00502  * @param boolean $reset  Set to false to make the unbinding permanent
00503  * @return boolean Success
00504  * @access public
00505  */
00506     function unbindModel($params, $reset = true) {
00507         foreach ($params as $assoc => $models) {
00508             if ($reset === true) {
00509                 $this->__backAssociation[$assoc] = $this->{$assoc};
00510             }
00511 
00512             foreach ($models as $model) {
00513                 $this->__backAssociation = array_merge($this->__backAssociation, $this->{$assoc});
00514                 unset ($this->__backAssociation[$model]);
00515                 unset ($this->{$assoc}[$model]);
00516             }
00517         }
00518         return true;
00519     }
00520 /**
00521  * Create a set of associations
00522  *
00523  * @access private
00524  */
00525     function __createLinks() {
00526         foreach ($this->__associations as $type) {
00527             if (!is_array($this->{$type})) {
00528                 $this->{$type} = explode(',', $this->{$type});
00529 
00530                 foreach ($this->{$type} as $i => $className) {
00531                     $className = trim($className);
00532                     unset ($this->{$type}[$i]);
00533                     $this->{$type}[$className] = array();
00534                 }
00535             }
00536 
00537             if (!empty($this->{$type})) {
00538                 foreach ($this->{$type} as $assoc => $value) {
00539                     $plugin = null;
00540 
00541                     if (is_numeric($assoc)) {
00542                         unset ($this->{$type}[$assoc]);
00543                         $assoc = $value;
00544                         $value = array();
00545                         $this->{$type}[$assoc] = $value;
00546 
00547                         if (strpos($assoc, '.') !== false) {
00548                             $value = $this->{$type}[$assoc];
00549                             unset($this->{$type}[$assoc]);
00550                             list($plugin, $assoc) = explode('.', $assoc);
00551                             $this->{$type}[$assoc] = $value;
00552                             $plugin = $plugin . '.';
00553                         }
00554                     }
00555                     $className =  $assoc;
00556 
00557                     if (isset($value['className']) && !empty($value['className'])) {
00558                         $className = $value['className'];
00559                         if (strpos($className, '.') !== false) {
00560                             list($plugin, $className) = explode('.', $className);
00561                             $plugin = $plugin . '.';
00562                             $this->{$type}[$assoc]['className'] = $className;
00563                         }
00564                     }
00565                     $this->__constructLinkedModel($assoc, $plugin . $className);
00566                 }
00567                 $this->__generateAssociation($type);
00568             }
00569         }
00570     }
00571 /**
00572  * Private helper method to create associated models of given class.
00573  *
00574  * @param string $assoc Association name
00575  * @param string $className Class name
00576  * @deprecated $this->$className use $this->$assoc instead. $assoc is the 'key' in the associations array;
00577  *  examples: var $hasMany = array('Assoc' => array('className' => 'ModelName'));
00578  *                  usage: $this->Assoc->modelMethods();
00579  *
00580  *              var $hasMany = array('ModelName');
00581  *                  usage: $this->ModelName->modelMethods();
00582  * @access private
00583  */
00584     function __constructLinkedModel($assoc, $className = null) {
00585         if(empty($className)) {
00586             $className = $assoc;
00587         }
00588 
00589         if (!isset($this->{$assoc})) {
00590             $model = array('class' => $className, 'alias' => $assoc);
00591             if (PHP5) {
00592                 $this->{$assoc} = ClassRegistry::init($model);
00593             } else {
00594                 $this->{$assoc} =& ClassRegistry::init($model);
00595             }
00596             if ($assoc) {
00597                 $this->tableToModel[$this->{$assoc}->table] = $assoc;
00598             }
00599         }
00600     }
00601 /**
00602  * Build array-based association from string.
00603  *
00604  * @param string $type 'belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'
00605  * @access private
00606  */
00607     function __generateAssociation($type) {
00608         foreach ($this->{$type} as $assocKey => $assocData) {
00609             $class = $assocKey;
00610 
00611             foreach ($this->__associationKeys[$type] as $key) {
00612                 if (!isset($this->{$type}[$assocKey][$key]) || $this->{$type}[$assocKey][$key] === null) {
00613                     $data = '';
00614 
00615                     switch($key) {
00616                         case 'fields':
00617                             $data = '';
00618                         break;
00619 
00620                         case 'foreignKey':
00621                             $data = ife($type == 'belongsTo', Inflector::underscore($assocKey) . '_id', Inflector::singularize($this->table) . '_id');
00622                         break;
00623 
00624                         case 'associationForeignKey':
00625                             $data = Inflector::singularize($this->{$class}->table) . '_id';
00626                         break;
00627 
00628                         case 'with':
00629                             $data = Inflector::camelize(Inflector::singularize($this->{$type}[$assocKey]['joinTable']));
00630                         break;
00631 
00632                         case 'joinTable':
00633                             $tables = array($this->table, $this->{$class}->table);
00634                             sort ($tables);
00635                             $data = $tables[0] . '_' . $tables[1];
00636                         break;
00637 
00638                         case 'className':
00639                             $data = $class;
00640                         break;
00641 
00642                         case 'unique':
00643                             $data = true;
00644                         break;
00645                     }
00646                     $this->{$type}[$assocKey][$key] = $data;
00647                 }
00648             }
00649 
00650             if (isset($this->{$type}[$assocKey]['with']) && !empty($this->{$type}[$assocKey]['with'])) {
00651                 $joinClass = $this->{$type}[$assocKey]['with'];
00652                 if (is_array($joinClass)) {
00653                     $joinClass = key($joinClass);
00654                 }
00655                 $plugin = null;
00656 
00657                 if (strpos($joinClass, '.') !== false) {
00658                     list($plugin, $joinClass) = explode('.', $joinClass);
00659                     $plugin = $plugin . '.';
00660                     $this->{$type}[$assocKey]['with'] = $joinClass;
00661                 }
00662 
00663                 if (!App::import('Model', $plugin . $joinClass)) {
00664                     $this->{$joinClass} = new AppModel(array(
00665                         'name' => $joinClass,
00666                         'table' => $this->{$type}[$assocKey]['joinTable'],
00667                         'ds' => $this->useDbConfig
00668                     ));
00669                     $this->{$joinClass}->primaryKey = $this->{$type}[$assocKey]['foreignKey'];
00670 
00671                 } else {
00672                     $this->__constructLinkedModel($joinClass, $plugin . $joinClass);
00673                     $this->{$joinClass}->primaryKey = $this->{$type}[$assocKey]['foreignKey'];
00674                     $this->{$type}[$assocKey]['joinTable'] = $this->{$joinClass}->table;
00675                 }
00676 
00677                 if (count($this->{$joinClass}->_schema) > 2) {
00678                     if (isset($this->{$joinClass}->_schema['id'])) {
00679                         $this->{$joinClass}->primaryKey = 'id';
00680                     }
00681                 }
00682             }
00683         }
00684     }
00685 /**
00686  * Sets a custom table for your controller class. Used by your controller to select a database table.
00687  *
00688  * @param string $tableName Name of the custom table
00689  * @access public
00690  */
00691     function setSource($tableName) {
00692         $this->setDataSource($this->useDbConfig);
00693         $db =& ConnectionManager::getDataSource($this->useDbConfig);
00694         $db->cacheSources = $this->cacheSources;
00695 
00696         if ($db->isInterfaceSupported('listSources')) {
00697             $sources = $db->listSources();
00698             if (is_array($sources) && !in_array(strtolower($this->tablePrefix . $tableName), array_map('strtolower', $sources))) {
00699                 return $this->cakeError('missingTable', array(array(
00700                     'className' => $this->alias,
00701                     'table' => $this->tablePrefix . $tableName
00702                 )));
00703             }
00704             $this->_schema = null;
00705         }
00706         $this->table = $this->useTable = $tableName;
00707         $this->tableToModel[$this->table] = $this->alias;
00708         $this->schema();
00709     }
00710 /**
00711  * This function does two things: 1) it scans the array $one for the primary key,
00712  * and if that's found, it sets the current id to the value of $one[id].
00713  * For all other keys than 'id' the keys and values of $one are copied to the 'data' property of this object.
00714  * 2) Returns an array with all of $one's keys and values.
00715  * (Alternative indata: two strings, which are mangled to
00716  * a one-item, two-dimensional array using $one for a key and $two as its value.)
00717  *
00718  * @param mixed $one Array or string of data
00719  * @param string $two Value string for the alternative indata method
00720  * @return array Data with all of $one's keys and values
00721  * @access public
00722  */
00723     function set($one, $two = null) {
00724         if (!$one) {
00725             return;
00726         }
00727         if (is_object($one)) {
00728             $one = Set::reverse($one);
00729         }
00730 
00731         if (is_array($one)) {
00732             $data = $one;
00733             if (empty($one[$this->alias])) {
00734                 $keys = array_keys($one);
00735                 if (in_array($keys[0], array_keys($this->_schema))) {
00736                     $data = array($this->alias => $one);
00737                 }
00738             }
00739         } else {
00740             $data = array($this->alias => array($one => $two));
00741         }
00742 
00743         foreach ($data as $n => $v) {
00744             if (is_array($v)) {
00745 
00746                 foreach ($v as $x => $y) {
00747                     if (isset($this->validationErrors[$x])) {
00748                         unset ($this->validationErrors[$x]);
00749                     }
00750 
00751                     if ($n === $this->alias) {
00752                         if ($x === $this->primaryKey) {
00753                             $this->id = $y;
00754                         }
00755                     }
00756                     if (is_array($y) || is_object($y)) {
00757                         $y = $this->deconstruct($x, $y);
00758                     }
00759                     $this->data[$n][$x] = $y;
00760                 }
00761             }
00762         }
00763         return $data;
00764     }
00765 /**
00766  * Deconstructs a complex data type (array or object) into a single field value
00767  *
00768  * @param string $field The name of the field to be deconstructed
00769  * @param mixed $data An array or object to be deconstructed into a field
00770  * @return mixed The resulting data that should be assigned to a field
00771  * @access public
00772  */
00773     function deconstruct($field, $data) {
00774         $copy = $data;
00775         $type = $this->getColumnType($field);
00776         $db =& ConnectionManager::getDataSource($this->useDbConfig);
00777 
00778         if (in_array($type, array('datetime', 'timestamp', 'date', 'time'))) {
00779             $useNewDate = (isset($data['year']) || isset($data['month']) || isset($data['day']) || isset($data['hour']) || isset($data['minute']));
00780             $dateFields = array('Y' => 'year', 'm' => 'month', 'd' => 'day', 'H' => 'hour', 'i' => 'min', 's' => 'sec');
00781             $format = $db->columns[$type]['format'];
00782             $date = array();
00783 
00784             if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] != 12 && 'pm' == $data['meridian']) {
00785                 $data['hour'] = $data['hour'] + 12;
00786             }
00787             if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] == 12 && 'am' == $data['meridian']) {
00788                 $data['hour'] = '00';
00789             }
00790 
00791             foreach ($dateFields as $key => $val) {
00792                 if (in_array($val, array('hour', 'min', 'sec'))) {
00793                     if (!isset($data[$val]) || $data[$val] === '0' || empty($data[$val])) {
00794                         $data[$val] = '00';
00795                     } else {
00796                         $data[$val] = sprintf('%02d', $data[$val]);
00797                     }
00798                 }
00799                 if (in_array($type, array('datetime', 'timestamp', 'date')) && !isset($data[$val]) || isset($data[$val]) && (empty($data[$val]) || $data[$val][0] === '-')) {
00800                     return null;
00801                 } elseif (isset($data[$val]) && !empty($data[$val])) {
00802                     $date[$key] = $data[$val];
00803                 }
00804             }
00805             $date = str_replace(array_keys($date), array_values($date), $format);
00806 
00807             if ($useNewDate && (!empty($date))) {
00808                 return $date;
00809             }
00810         }
00811         return $data;
00812     }
00813 /**
00814  * Returns an array of table metadata (column names and types) from the database.
00815  * $field => keys(type, null, default, key, length, extra)
00816  *
00817  * @param mixed $field Set to true to reload schema, or a string to return a specific field
00818  * @return array Array of table metadata
00819  * @access public
00820  */
00821     function schema($field = false) {
00822         if (!is_array($this->_schema) || $field === true) {
00823             $db =& ConnectionManager::getDataSource($this->useDbConfig);
00824             $db->cacheSources = $this->cacheSources;
00825             if ($db->isInterfaceSupported('describe') && $this->useTable !== false) {
00826                 $this->_schema = $db->describe($this, $field);
00827             } elseif ($this->useTable === false) {
00828                 $this->_schema = array();
00829             }
00830         }
00831         if (is_string($field)) {
00832             if (isset($this->_schema[$field])) {
00833                 return $this->_schema[$field];
00834             } else {
00835                 return null;
00836             }
00837         }
00838         return $this->_schema;
00839     }
00840 /**
00841  * Returns an associative array of field names and column types.
00842  *
00843  * @return array Field types indexed by field name
00844  * @access public
00845  */
00846     function getColumnTypes() {
00847         $columns = $this->schema();
00848         if (empty($columns)) {
00849             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);
00850         }
00851         $cols = array();
00852         foreach ($columns as $field => $values) {
00853             $cols[$field] = $values['type'];
00854         }
00855         return $cols;
00856     }
00857 /**
00858  * Returns the column type of a column in the model
00859  *
00860  * @param string $column The name of the model column
00861  * @return string Column type
00862  * @access public
00863  */
00864     function getColumnType($column) {
00865         $db =& ConnectionManager::getDataSource($this->useDbConfig);
00866         $cols = $this->schema();
00867         $model = null;
00868 
00869         $column = str_replace(array($db->startQuote, $db->endQuote), '', $column);
00870 
00871         if (strpos($column, '.')) {
00872             list($model, $column) = explode('.', $column);
00873         }
00874         if ($model != $this->alias && isset($this->{$model})) {
00875             return $this->{$model}->getColumnType($column);
00876         }
00877         if (isset($cols[$column]) && isset($cols[$column]['type'])) {
00878             return $cols[$column]['type'];
00879         }
00880         return null;
00881     }
00882 /**
00883  * Returns true if this Model has given field in its database table.
00884  *
00885  * @param mixed $name Name of field to look for, or an array of names
00886  * @return mixed If $name is a string, returns a boolean indicating whether the field exists.
00887  *               If $name is an array of field names, returns the first field that exists,
00888  *               or false if none exist.
00889  * @access public
00890  */
00891     function hasField($name) {
00892         if (is_array($name)) {
00893             foreach ($name as $n) {
00894                 if ($this->hasField($n)) {
00895                     return $n;
00896                 }
00897             }
00898             return false;
00899         }
00900 
00901         if (empty($this->_schema)) {
00902             $this->schema();
00903         }
00904 
00905         if ($this->_schema != null) {
00906             return isset($this->_schema[$name]);
00907         }
00908         return false;
00909     }
00910 /**
00911  * Initializes the model for writing a new record, loading the default values
00912  * for those fields that are not defined in $data.
00913  *
00914  * @param mixed $data Optional data array to assign to the model after it is created.  If null or false,
00915  *                    schema data defaults are not merged.
00916  * @param boolean $filterKey If true, overwrites any primary key input with an empty value
00917  * @return array The current Model::data; after merging $data and/or defaults from database
00918  * @access public
00919  */
00920     function create($data = array(), $filterKey = false) {
00921         $defaults = array();
00922         $this->id = false;
00923         $this->data = array();
00924         $this->validationErrors = array();
00925 
00926         if ($data !== null && $data !== false) {
00927             foreach ($this->schema() as $field => $properties) {
00928                 if ($this->primaryKey !== $field && isset($properties['default'])) {
00929                     $defaults[$field] = $properties['default'];
00930                 }
00931             }
00932             $this->set(Set::filter($defaults));
00933             $this->set($data);
00934         }
00935         if ($filterKey) {
00936             $this->set($this->primaryKey, false);
00937         }
00938         return $this->data;
00939     }
00940 /**
00941  * Returns a list of fields from the database, and sets the current model
00942  * data (Model::$data) with the record found.
00943  *
00944  * @param mixed $fields String of single fieldname, or an array of fieldnames.
00945  * @param mixed $id The ID of the record to read
00946  * @return array Array of database fields, or false if not found
00947  * @access public
00948  */
00949     function read($fields = null, $id = null) {
00950         $this->validationErrors = array();
00951 
00952         if ($id != null) {
00953             $this->id = $id;
00954         }
00955 
00956         $id = $this->id;
00957 
00958         if (is_array($this->id)) {
00959             $id = $this->id[0];
00960         }
00961 
00962         if ($id !== null && $id !== false) {
00963             $this->data = $this->find(array($this->alias . '.' . $this->primaryKey => $id), $fields);
00964             return $this->data;
00965         } else {
00966             return false;
00967         }
00968     }
00969 /**
00970  * Returns contents of a field in a query matching given conditions.
00971  *
00972  * @param string $name Name of field to get
00973  * @param array $conditions SQL conditions (defaults to NULL)
00974  * @param string $order SQL ORDER BY fragment
00975  * @return string field contents, or false if not found
00976  * @access public
00977  */
00978     function field($name, $conditions = null, $order = null) {
00979         if ($conditions === null && $this->id !== false) {
00980             $conditions = array($this->alias . '.' . $this->primaryKey => $this->id);
00981         }
00982         if ($this->recursive >= 1) {
00983             $recursive = -1;
00984         } else {
00985             $recursive = $this->recursive;
00986         }
00987         if ($data = $this->find($conditions, $name, $order, $recursive)) {
00988             if (strpos($name, '.') === false) {
00989                 if (isset($data[$this->alias][$name])) {
00990                     return $data[$this->alias][$name];
00991                 }
00992             } else {
00993                 $name = explode('.', $name);
00994                 if (isset($data[$name[0]][$name[1]])) {
00995                     return $data[$name[0]][$name[1]];
00996                 }
00997             }
00998             if (isset($data[0]) && count($data[0]) > 0) {
00999                 $name = key($data[0]);
01000                 return $data[0][$name];
01001             }
01002         } else {
01003             return false;
01004         }
01005     }
01006 /**
01007  * Saves a single field to the database.
01008  *
01009  * @param string $name Name of the table field
01010  * @param mixed $value Value of the field
01011  * @param array $options See $options param in Model::save(). Does not respect 'fieldList' key if passed
01012  * @return boolean See Model::save()
01013  * @access public
01014  * @see Model::save()
01015  */
01016     function saveField($name, $value, $validate = false) {
01017         $id = $this->id;
01018         $this->create(false);
01019 
01020         if (is_array($validate)) {
01021             $options = array_merge(array('validate' => false, 'fieldList' => array($name)), $options);
01022         } else {
01023             $options = array('validate' => $validate, 'fieldList' => array($name));
01024         }
01025 
01026         return $this->save(
01027             array($this->alias => array($this->primaryKey => $id, $name => $value)), $options
01028         );
01029     }
01030 /**
01031  * Saves model data to the database. By default, validation occurs before save.
01032  *
01033  * @param array $data Data to save.
01034  * @param boolean $validate If set, validation will be done before the save
01035  * @param array $fieldList List of fields to allow to be written
01036  * @return mixed On success Model::$data if its not empty or true, false on failure
01037  * @access public
01038  */
01039     function save($data = null, $validate = true, $fieldList = array()) {
01040         $db =& ConnectionManager::getDataSource($this->useDbConfig);
01041         $_whitelist = $this->whitelist;
01042         $defaults = array('validate' => true, 'fieldList' => array(), 'callbacks' => true);
01043         $fields = array();
01044 
01045         if (!is_array($validate)) {
01046             $options = array_merge($defaults, compact('validate', 'fieldList', 'callbacks'));
01047         } else {
01048             $options = array_merge($defaults, $validate);
01049         }
01050 
01051         if (!empty($options['fieldList'])) {
01052             $this->whitelist = $options['fieldList'];
01053         } elseif ($options['fieldList'] === null) {
01054             $this->whitelist = array();
01055         }
01056         $this->set($data);
01057 
01058         if (empty($this->data) && !$this->hasField(array('created', 'updated', 'modified'))) {
01059             return false;
01060         }
01061 
01062         foreach (array('created', 'updated', 'modified') as $field) {
01063             if (isset($this->data[$this->alias]) && array_key_exists($field, $this->data[$this->alias]) && $this->data[$this->alias][$field] === null) {
01064                 unset($this->data[$this->alias][$field]);
01065             }
01066         }
01067 
01068         $this->exists();
01069         $dateFields = array('modified', 'updated');
01070 
01071         if (!$this->__exists) {
01072             $dateFields[] = 'created';
01073         }
01074         if (isset($this->data[$this->alias])) {
01075             $fields = array_keys($this->data[$this->alias]);
01076         }
01077         if ($options['validate'] && !$this->validates($options)) {
01078             $this->whitelist = $_whitelist;
01079             return false;
01080         }
01081 
01082         foreach ($dateFields as $updateCol) {
01083             if ($this->hasField($updateCol) && !in_array($updateCol, $fields)) {
01084                 $colType = array_merge(array('formatter' => 'date'), $db->columns[$this->getColumnType($updateCol)]);
01085                 if (!array_key_exists('formatter', $colType) || !array_key_exists('format', $colType)) {
01086                     $time = strtotime('now');
01087                 } else {
01088                     $time = $colType['formatter']($colType['format']);
01089                 }
01090                 if (!empty($this->whitelist)) {
01091                     $this->whitelist[] = $updateCol;
01092                 }
01093                 $this->set($updateCol, $time);
01094             }
01095         }
01096 
01097         if ($options['callbacks'] === true || $options['callbacks'] === 'before') {
01098             if (!$this->Behaviors->trigger($this, 'beforeSave', array($options), array('break' => true, 'breakOn' => false)) || !$this->beforeSave($options)) {
01099                 $this->whitelist = $_whitelist;
01100                 return false;
01101             }
01102         }
01103         $fields = $values = array();
01104 
01105         if (isset($this->data[$this->alias][$this->primaryKey]) && empty($this->data[$this->alias][$this->primaryKey])) {
01106             unset($this->data[$this->alias][$this->primaryKey]);
01107         }
01108 
01109         foreach ($this->data as $n => $v) {
01110             if (isset($this->hasAndBelongsToMany[$n])) {
01111                 if (isset($v[$n])) {
01112                     $v = $v[$n];
01113                 }
01114                 $joined[$n] = $v;
01115             } else {
01116                 if ($n === $this->alias) {
01117                     foreach (array('created', 'updated', 'modified') as $field) {
01118                         if (array_key_exists($field, $v) && empty($v[$field])) {
01119                             unset($v[$field]);
01120                         }
01121                     }
01122 
01123                     foreach ($v as $x => $y) {
01124                         if ($this->hasField($x) && (empty($this->whitelist) || in_array($x, $this->whitelist))) {
01125                             list($fields[], $values[]) = array($x, $y);
01126                         }
01127                     }
01128                 }
01129             }
01130         }
01131         $count = count($fields);
01132 
01133         if (!$this->__exists && $count > 0) {
01134             $this->id = false;
01135         }
01136         $success = true;
01137         $created = false;
01138 
01139         if ($count > 0) {
01140             if (!empty($this->id)) {
01141                 if (!$db->update($this, $fields, $values)) {
01142                     $success = false;
01143                 }
01144             } else {
01145                 foreach ($this->_schema as $field => $properties) {
01146                     if ($this->primaryKey === $field) {
01147                         $isUUID = ($this->_schema[$field]['type'] === 'string' && $this->_schema[$field]['length'] === 36)
01148                                 || ($this->_schema[$field]['type'] === 'binary' && $this->_schema[$field]['length'] === 16);
01149                         if (empty($this->data[$this->alias][$this->primaryKey]) && $isUUID) {
01150                             list($fields[], $values[]) = array($this->primaryKey, String::uuid());
01151                         }
01152                         break;
01153                     }
01154                 }
01155 
01156                 if (!$db->create($this, $fields, $values)) {
01157                     $success = $created = false;
01158                 } else {
01159                     $created = true;
01160                 }
01161             }
01162         }
01163 
01164         if (!empty($this->belongsTo)) {
01165             $this->updateCounterCache(array(), $created);
01166         }
01167 
01168         if (!empty($joined) && $success === true) {
01169             $this->__saveMulti($joined, $this->id);
01170         }
01171 
01172         if ($success && $count > 0) {
01173             if (!empty($this->data)) {
01174                 $success = $this->data;
01175             }
01176             if ($options['callbacks'] === true || $options['callbacks'] === 'after') {
01177                 $this->Behaviors->trigger($this, 'afterSave', array($created, $options));
01178                 $this->afterSave($created);
01179             }
01180             if (!empty($this->data)) {
01181                 $success = Set::merge($success, $this->data);
01182             }
01183             $this->data = false;
01184             $this->__exists = null;
01185             $this->_clearCache();
01186             $this->validationErrors = array();
01187         }
01188         $this->whitelist = $_whitelist;
01189         return $success;
01190     }
01191 /**
01192  * Saves model hasAndBelongsToMany data to the database.
01193  *
01194  * @param array $joined Data to save
01195  * @param mixed $id ID of record in this model
01196  * @access private
01197  */
01198     function __saveMulti($joined, $id) {
01199         $db =& ConnectionManager::getDataSource($this->useDbConfig);
01200 
01201         foreach ($joined as $assoc => $value) {
01202             $newValues = array();
01203             if (empty($value)) {
01204                 $value = array();
01205             }
01206             if (isset($this->hasAndBelongsToMany[$assoc])) {
01207                 list($join) = $this->joinModel($this->hasAndBelongsToMany[$assoc]['with']);
01208                 $conditions = array($join . '.' . $this->hasAndBelongsToMany[$assoc]['foreignKey'] => $id);
01209                 $links = array();
01210 
01211                 if ($this->hasAndBelongsToMany[$assoc]['unique']) {
01212                     $this->{$join}->deleteAll($conditions);
01213                 } else {
01214                     list($recursive, $fields) = array(-1, $this->hasAndBelongsToMany[$assoc]['associationForeignKey']);
01215                     $links = Set::extract(
01216                         $this->{$join}->find('all', compact('conditions', 'recursive', 'fields')),
01217                         "{n}.{$join}." . $this->hasAndBelongsToMany[$assoc]['associationForeignKey']
01218                     );
01219                 }
01220 
01221                 foreach ($value as $update) {
01222                     if (!empty($update)) {
01223                         if (is_array($update)) {
01224                             $update[$this->hasAndBelongsToMany[$assoc]['foreignKey']] = $id;
01225                             $this->{$join}->create($update);
01226                             $this->{$join}->save();
01227                         } elseif (!in_array($update, $links)) {
01228                             $values  = join(',', array(
01229                                 $db->value($id, $this->getColumnType($this->primaryKey)),
01230                                 $db->value($update)
01231                             ));
01232                             $newValues[] = "({$values})";
01233                             unset($values);
01234                         }
01235                     }
01236                 }
01237 
01238                 if (!empty($newValues)) {
01239                     $fields = join(',', array(
01240                         $db->name($this->hasAndBelongsToMany[$assoc]['foreignKey']),
01241                         $db->name($this->hasAndBelongsToMany[$assoc]['associationForeignKey'])
01242                     ));
01243                     $db->insertMulti($this->{$join}, $fields, $newValues);
01244                 }
01245             }
01246         }
01247     }
01248 /**
01249  * Updates the counter cache of belongsTo associations after a save or delete operation
01250  *
01251  * @param array $keys Optional foreign key data, defaults to the information $this->data
01252  * @param boolean $created True if a new record was created, otherwise only associations with
01253  *                'counterScope' defined get updated
01254  * @return void
01255  * @access public
01256  */
01257     function updateCounterCache($keys = array(), $created = false) {
01258         if (empty($keys)) {
0