cake/libs/model/model.php

1 <?php
2 /* SVN FILE: $Id$ */
3 /**
4 * Object-relational mapper.
5 *
6 * DBO-backed object data model, for mapping database tables to Cake objects.
7 *
8 * PHP versions 5
9 *
10 * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org)
11 * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
12 *
13 * Licensed under The MIT License
14 * Redistributions of files must retain the above copyright notice.
15 *
16 * @filesource
17 * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
18 * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
19 * @package cake
20 * @subpackage cake.cake.libs.model
21 * @since CakePHP(tm) v 0.10.0.0
22 * @version $Revision$
23 * @modifiedby $LastChangedBy$
24 * @lastmodified $Date$
25 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
26 */
27 /**
28 * Included libs
29 */
30 App::import('Core', array('ClassRegistry', 'Overloadable', 'Validation', 'Behavior', 'ConnectionManager', 'Set', 'String'));
31 /**
32 * Object-relational mapper.
33 *
34 * DBO-backed object data model.
35 * Automatically selects a database table name based on a pluralized lowercase object class name
36 * (i.e. class 'User' => table 'users'; class 'Man' => table 'men')
37 * The table is required to have at least 'id auto_increment' primary key.
38 *
39 * @package cake
40 * @subpackage cake.cake.libs.model
41 * @link http://book.cakephp.org/view/66/Models
42 */
43 class Model extends Overloadable {
44 /**
45 * The name of the DataSource connection that this Model uses
46 *
47 * @var string
48 * @access public
49 * @link http://book.cakephp.org/view/435/useDbConfig
50 */
51 var $useDbConfig = 'default';
52 /**
53 * Custom database table name, or null/false if no table association is desired.
54 *
55 * @var string
56 * @access public
57 * @link http://book.cakephp.org/view/436/useTable
58 */
59 var $useTable = null;
60 /**
61 * Custom display field name. Display fields are used by Scaffold, in SELECT boxes' OPTION elements.
62 *
63 * @var string
64 * @access public
65 * @link http://book.cakephp.org/view/438/displayField
66 */
67 var $displayField = null;
68 /**
69 * Value of the primary key ID of the record that this model is currently pointing to.
70 * Automatically set after database insertions.
71 *
72 * @var mixed
73 * @access public
74 */
75 var $id = false;
76 /**
77 * Container for the data that this model gets from persistent storage (usually, a database).
78 *
79 * @var array
80 * @access public
81 * @link http://book.cakephp.org/view/441/data
82 */
83 var $data = array();
84 /**
85 * Table name for this Model.
86 *
87 * @var string
88 * @access public
89 */
90 var $table = false;
91 /**
92 * The name of the primary key field for this model.
93 *
94 * @var string
95 * @access public
96 * @link http://book.cakephp.org/view/437/primaryKey
97 */
98 var $primaryKey = null;
99 /**
100 * Field-by-field table metadata.
101 *
102 * @var array
103 * @access protected
104 * @link http://book.cakephp.org/view/442/_schema
105 */
106 var $_schema = null;
107 /**
108 * List of validation rules. Append entries for validation as ('field_name' => '/^perl_compat_regexp$/')
109 * that have to match with preg_match(). Use these rules with Model::validate()
110 *
111 * @var array
112 * @access public
113 * @link http://book.cakephp.org/view/443/validate
114 * @link http://book.cakephp.org/view/125/Data-Validation
115 */
116 var $validate = array();
117 /**
118 * List of validation errors.
119 *
120 * @var array
121 * @access public
122 * @link http://book.cakephp.org/view/410/Validating-Data-from-the-Controller
123 */
124 var $validationErrors = array();
125 /**
126 * Database table prefix for tables in model.
127 *
128 * @var string
129 * @access public
130 * @link http://book.cakephp.org/view/475/tablePrefix
131 */
132 var $tablePrefix = null;
133 /**
134 * Name of the model.
135 *
136 * @var string
137 * @access public
138 * @link http://book.cakephp.org/view/444/name
139 */
140 var $name = null;
141 /**
142 * Alias name for model.
143 *
144 * @var string
145 * @access public
146 */
147 var $alias = null;
148 /**
149 * List of table names included in the model description. Used for associations.
150 *
151 * @var array
152 * @access public
153 */
154 var $tableToModel = array();
155 /**
156 * Whether or not to log transactions for this model.
157 *
158 * @var boolean
159 * @access public
160 */
161 var $logTransactions = false;
162 /**
163 * Whether or not to enable transactions for this model (i.e. BEGIN/COMMIT/ROLLBACK statements)
164 *
165 * @var boolean
166 * @access public
167 */
168 var $transactional = false;
169 /**
170 * Whether or not to cache queries for this model. This enables in-memory
171 * caching only, the results are not stored beyond the current request.
172 *
173 * @var boolean
174 * @access public
175 * @link http://book.cakephp.org/view/445/cacheQueries
176 */
177 var $cacheQueries = false;
178 /**
179 * Detailed list of belongsTo associations.
180 *
181 * @var array
182 * @access public
183 * @link http://book.cakephp.org/view/81/belongsTo
184 */
185 var $belongsTo = array();
186 /**
187 * Detailed list of hasOne associations.
188 *
189 * @var array
190 * @access public
191 * @link http://book.cakephp.org/view/80/hasOne
192 */
193 var $hasOne = array();
194 /**
195 * Detailed list of hasMany associations.
196 *
197 * @var array
198 * @access public
199 * @link http://book.cakephp.org/view/82/hasMany
200 */
201 var $hasMany = array();
202 /**
203 * Detailed list of hasAndBelongsToMany associations.
204 *
205 * @var array
206 * @access public
207 * @link http://book.cakephp.org/view/83/hasAndBelongsToMany-HABTM
208 */
209 var $hasAndBelongsToMany = array();
210 /**
211 * List of behaviors to load when the model object is initialized. Settings can be
212 * passed to behaviors by using the behavior name as index. Eg:
213 *
214 * var $actsAs = array('Translate', 'MyBehavior' => array('setting1' => 'value1'))
215 *
216 * @var array
217 * @access public
218 * @link http://book.cakephp.org/view/90/Using-Behaviors
219 */
220 var $actsAs = null;
221 /**
222 * Holds the Behavior objects currently bound to this model.
223 *
224 * @var BehaviorCollection
225 * @access public
226 */
227 var $Behaviors = null;
228 /**
229 * Whitelist of fields allowed to be saved.
230 *
231 * @var array
232 * @access public
233 */
234 var $whitelist = array();
235 /**
236 * Whether or not to cache sources for this model.
237 *
238 * @var boolean
239 * @access public
240 */
241 var $cacheSources = true;
242 /**
243 * Type of find query currently executing.
244 *
245 * @var string
246 * @access public
247 */
248 var $findQueryType = null;
249 /**
250 * Number of associations to recurse through during find calls. Fetches only
251 * the first level by default.
252 *
253 * @var integer
254 * @access public
255 * @link http://book.cakephp.org/view/439/recursive
256 */
257 var $recursive = 1;
258 /**
259 * The column name(s) and direction(s) to order find results by default.
260 *
261 * var $order = "Post.created DESC";
262 * var $order = array("Post.view_count DESC", "Post.rating DESC");
263 *
264 * @var string
265 * @access public
266 * @link http://book.cakephp.org/view/440/order
267 */
268 var $order = null;
269 /**
270 * Whether or not the model record exists, set by Model::exists().
271 *
272 * @var bool
273 * @access private
274 */
275 var $__exists = null;
276 /**
277 * Default list of association keys.
278 *
279 * @var array
280 * @access private
281 */
282 var $__associationKeys = array(
283 'belongsTo' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'counterCache'),
284 'hasOne' => array('className', 'foreignKey','conditions', 'fields','order', 'dependent'),
285 'hasMany' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'dependent', 'exclusive', 'finderQuery', 'counterQuery'),
286 'hasAndBelongsToMany' => array('className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery', 'deleteQuery', 'insertQuery')
287 );
288 /**
289 * Holds provided/generated association key names and other data for all associations.
290 *
291 * @var array
292 * @access private
293 */
294 var $__associations = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
295 /**
296 * Holds model associations temporarily to allow for dynamic (un)binding.
297 *
298 * @var array
299 * @access private
300 */
301 var $__backAssociation = array();
302 /**
303 * The ID of the model record that was last inserted.
304 *
305 * @var integer
306 * @access private
307 */
308 var $__insertID = null;
309 /**
310 * The number of records returned by the last query.
311 *
312 * @var integer
313 * @access private
314 */
315 var $__numRows = null;
316 /**
317 * The number of records affected by the last query.
318 *
319 * @var integer
320 * @access private
321 */
322 var $__affectedRows = null;
323 /**
324 * List of valid finder method options, supplied as the first parameter to find().
325 *
326 * @var array
327 * @access protected
328 */
329 var $_findMethods = array(
330 'all' => true, 'first' => true, 'count' => true,
331 'neighbors' => true, 'list' => true, 'threaded' => true
332 );
333 /**
334 * Constructor. Binds the model's database table to the object.
335 *
336 * @param integer $id Set this ID for this model on startup
337 * @param string $table Name of database table to use.
338 * @param object $ds DataSource connection object.
339 */
340 function __construct($id = false, $table = null, $ds = null) {
341 parent::__construct();
342  
343 if (is_array($id)) {
344 extract(array_merge(
345 array(
346 'id' => $this->id, 'table' => $this->useTable, 'ds' => $this->useDbConfig,
347 'name' => $this->name, 'alias' => $this->alias
348 ),
349 $id
350 ));
351 }
352  
353 if ($this->name === null) {
354 $this->name = (isset($name) ? $name : get_class($this));
355 }
356  
357 if ($this->alias === null) {
358 $this->alias = (isset($alias) ? $alias : $this->name);
359 }
360  
361 if ($this->primaryKey === null) {
362 $this->primaryKey = 'id';
363 }
364  
365 ClassRegistry::addObject($this->alias, $this);
366  
367 $this->id = $id;
368 unset($id);
369  
370 if ($table === false) {
371 $this->useTable = false;
372 } elseif ($table) {
373 $this->useTable = $table;
374 }
375  
376 if ($ds !== null) {
377 $this->useDbConfig = $ds;
378 }
379  
380 if (is_subclass_of($this, 'AppModel')) {
381 $appVars = get_class_vars('AppModel');
382 $merge = array('_findMethods');
383  
384 if ($this->actsAs !== null || $this->actsAs !== false) {
385 $merge[] = 'actsAs';
386 }
387 $parentClass = get_parent_class($this);
388 if (strtolower($parentClass) !== 'appmodel') {
389 $parentVars = get_class_vars($parentClass);
390 foreach ($merge as $var) {
391 if (isset($parentVars[$var]) && !empty($parentVars[$var])) {
392 $appVars[$var] = Set::merge($appVars[$var], $parentVars[$var]);
393 }
394 }
395 }
396  
397 foreach ($merge as $var) {
398 if (isset($appVars[$var]) && !empty($appVars[$var]) && is_array($this->{$var})) {
399 $this->{$var} = Set::merge($appVars[$var], $this->{$var});
400 }
401 }
402 }
403 $this->Behaviors = new BehaviorCollection();
404  
405 if ($this->useTable !== false) {
406 $this->setDataSource($ds);
407  
408 if ($this->useTable === null) {
409 $this->useTable = Inflector::tableize($this->name);
410 }
411 if (method_exists($this, 'setTablePrefix')) {
412 $this->setTablePrefix();
413 }
414 $this->setSource($this->useTable);
415  
416 if ($this->displayField == null) {
417 $this->displayField = $this->hasField(array('title', 'name', $this->primaryKey));
418 }
419 } elseif ($this->table === false) {
420 $this->table = Inflector::tableize($this->name);
421 }
422 $this->__createLinks();
423 $this->Behaviors->init($this->alias, $this->actsAs);
424 }
425 /**
426 * Handles custom method calls, like findBy<field> for DB models,
427 * and custom RPC calls for remote data sources.
428 *
429 * @param string $method Name of method to call.
430 * @param array $params Parameters for the method.
431 * @return mixed Whatever is returned by called method
432 * @access protected
433 */
434 function call__($method, $params) {
435 $result = $this->Behaviors->dispatchMethod($this, $method, $params);
436  
437 if ($result !== array('unhandled')) {
438 return $result;
439 }
440 $db =& ConnectionManager::getDataSource($this->useDbConfig);
441 $return = $db->query($method, $params, $this);
442  
443 if (!PHP5) {
444 $this->resetAssociations();
445 }
446 return $return;
447 }
448 /**
449 * Bind model associations on the fly.
450 *
451 * If $permanent is true, association will not be reset
452 * to the originals defined in the model.
453 *
454 * @param mixed $model A model or association name (string) or set of binding options (indexed by model name type)
455 * @param array $options If $model is a string, this is the list of association properties with which $model will
456 * be bound
457 * @param boolean $permanent Set to true to make the binding permanent
458 * @return void
459 * @access public
460 * @todo
461 */
462 function bind($model, $options = array(), $permanent = true) {
463 if (!is_array($model)) {
464 $model = array($model => $options);
465 }
466  
467 foreach ($model as $name => $options) {
468 if (isset($options['type'])) {
469 $assoc = $options['type'];
470 } elseif (isset($options[0])) {
471 $assoc = $options[0];
472 } else {
473 $assoc = 'belongsTo';
474 }
475  
476 if (!$permanent) {
477 $this->__backAssociation[$assoc] = $this->{$assoc};
478 }
479 foreach ($model as $key => $value) {
480 $assocName = $modelName = $key;
481  
482 if (isset($this->{$assoc}[$assocName])) {
483 $this->{$assoc}[$assocName] = array_merge($this->{$assoc}[$assocName], $options);
484 } else {
485 if (isset($value['className'])) {
486 $modelName = $value['className'];
487 }
488  
489 $this->__constructLinkedModel($assocName, $modelName);
490 $this->{$assoc}[$assocName] = $model[$assocName];
491 $this->__generateAssociation($assoc);
492 }
493 unset($this->{$assoc}[$assocName]['type'], $this->{$assoc}[$assocName][0]);
494 }
495 }
496 }
497 /**
498 * Bind model associations on the fly.
499 *
500 * If $reset is false, association will not be reset
501 * to the originals defined in the model
502 *
503 * Example: Add a new hasOne binding to the Profile model not
504 * defined in the model source code:
505 * <code>
506 * $this->User->bindModel( array('hasOne' => array('Profile')) );
507 * </code>
508 *
509 * @param array $params Set of bindings (indexed by binding type)
510 * @param boolean $reset Set to false to make the binding permanent
511 * @return boolean Success
512 * @access public
513 * @link http://book.cakephp.org/view/86/Creating-and-Destroying-Associations-on-the-Fly
514 */
515 function bindModel($params, $reset = true) {
516 foreach ($params as $assoc => $model) {
517 if ($reset === true) {
518 $this->__backAssociation[$assoc] = $this->{$assoc};
519 }
520  
521 foreach ($model as $key => $value) {
522 $assocName = $key;
523  
524 if (is_numeric($key)) {
525 $assocName = $value;
526 $value = array();
527 }
528 $modelName = $assocName;
529 $this->{$assoc}[$assocName] = $value;
530 }
531 }
532 $this->__createLinks();
533 return true;
534 }
535 /**
536 * Turn off associations on the fly.
537 *
538 * If $reset is false, association will not be reset
539 * to the originals defined in the model
540 *
541 * Example: Turn off the associated Model Support request,
542 * to temporarily lighten the User model:
543 * <code>
544 * $this->User->unbindModel( array('hasMany' => array('Supportrequest')) );
545 * </code>
546 *
547 * @param array $params Set of bindings to unbind (indexed by binding type)
548 * @param boolean $reset Set to false to make the unbinding permanent
549 * @return boolean Success
550 * @access public
551 * @link http://book.cakephp.org/view/86/Creating-and-Destroying-Associations-on-the-Fly
552 */
553 function unbindModel($params, $reset = true) {
554 foreach ($params as $assoc => $models) {
555 if ($reset === true) {
556 $this->__backAssociation[$assoc] = $this->{$assoc};
557 }
558  
559 foreach ($models as $model) {
560 $this->__backAssociation = array_merge($this->__backAssociation, $this->{$assoc});
561 unset ($this->__backAssociation[$model]);
562 unset ($this->{$assoc}[$model]);
563 }
564 }
565 return true;
566 }
567 /**
568 * Create a set of associations.
569 *
570 * @return void
571 * @access private
572 */
573 function __createLinks() {
574 foreach ($this->__associations as $type) {
575 if (!is_array($this->{$type})) {
576 $this->{$type} = explode(',', $this->{$type});
577  
578 foreach ($this->{$type} as $i => $className) {
579 $className = trim($className);
580 unset ($this->{$type}[$i]);
581 $this->{$type}[$className] = array();
582 }
583 }
584  
585 if (!empty($this->{$type})) {
586 foreach ($this->{$type} as $assoc => $value) {
587 $plugin = null;
588  
589 if (is_numeric($assoc)) {
590 unset ($this->{$type}[$assoc]);
591 $assoc = $value;
592 $value = array();
593 $this->{$type}[$assoc] = $value;
594  
595 if (strpos($assoc, '.') !== false) {
596 $value = $this->{$type}[$assoc];
597 unset($this->{$type}[$assoc]);
598 list($plugin, $assoc) = explode('.', $assoc);
599 $this->{$type}[$assoc] = $value;
600 $plugin = $plugin . '.';
601 }
602 }
603 $className = $assoc;
604  
605 if (isset($value['className']) && !empty($value['className'])) {
606 $className = $value['className'];
607 if (strpos($className, '.') !== false) {
608 list($plugin, $className) = explode('.', $className);
609 $plugin = $plugin . '.';
610 $this->{$type}[$assoc]['className'] = $className;
611 }
612 }
613 $this->__constructLinkedModel($assoc, $plugin . $className);
614 }
615 $this->__generateAssociation($type);
616 }
617 }
618 }
619 /**
620 * Private helper method to create associated models of a given class.
621 *
622 * @param string $assoc Association name
623 * @param string $className Class name
624 * @deprecated $this->$className use $this->$assoc instead. $assoc is the 'key' in the associations array;
625 * examples: var $hasMany = array('Assoc' => array('className' => 'ModelName'));
626 * usage: $this->Assoc->modelMethods();
627 *
628 * var $hasMany = array('ModelName');
629 * usage: $this->ModelName->modelMethods();
630 * @return void
631 * @access private
632 */
633 function __constructLinkedModel($assoc, $className = null) {
634 if (empty($className)) {
635 $className = $assoc;
636 }
637  
638 if (!isset($this->{$assoc}) || $this->{$assoc}->name !== $className) {
639 $model = array('class' => $className, 'alias' => $assoc);
640 if (PHP5) {
641 $this->{$assoc} = ClassRegistry::init($model);
642 } else {
643 $this->{$assoc} =& ClassRegistry::init($model);
644 }
645 if ($assoc) {
646 $this->tableToModel[$this->{$assoc}->table] = $assoc;
647 }
648 }
649 }
650 /**
651 * Build an array-based association from string.
652 *
653 * @param string $type 'belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'
654 * @return void
655 * @access private
656 */
657 function __generateAssociation($type) {
658 foreach ($this->{$type} as $assocKey => $assocData) {
659 $class = $assocKey;
660 $dynamicWith = false;
661  
662 foreach ($this->__associationKeys[$type] as $key) {
663  
664 if (!isset($this->{$type}[$assocKey][$key]) || $this->{$type}[$assocKey][$key] === null) {
665 $data = '';
666  
667 switch ($key) {
668 case 'fields':
669 $data = '';
670 break;
671  
672 case 'foreignKey':
673 $data = (($type == 'belongsTo') ? Inflector::underscore($assocKey) : Inflector::singularize($this->table)) . '_id';
674 break;
675  
676 case 'associationForeignKey':
677 $data = Inflector::singularize($this->{$class}->table) . '_id';
678 break;
679  
680 case 'with':
681 $data = Inflector::camelize(Inflector::singularize($this->{$type}[$assocKey]['joinTable']));
682 $dynamicWith = true;
683 break;
684  
685 case 'joinTable':
686 $tables = array($this->table, $this->{$class}->table);
687 sort ($tables);
688 $data = $tables[0] . '_' . $tables[1];
689 break;
690  
691 case 'className':
692 $data = $class;
693 break;
694  
695 case 'unique':
696 $data = true;
697 break;
698 }
699 $this->{$type}[$assocKey][$key] = $data;
700 }
701 }
702  
703 if (!empty($this->{$type}[$assocKey]['with'])) {
704 $joinClass = $this->{$type}[$assocKey]['with'];
705 if (is_array($joinClass)) {
706 $joinClass = key($joinClass);
707 }
708 $plugin = null;
709  
710 if (strpos($joinClass, '.') !== false) {
711 list($plugin, $joinClass) = explode('.', $joinClass);
712 $plugin = $plugin . '.';
713 $this->{$type}[$assocKey]['with'] = $joinClass;
714 }
715  
716 if (!ClassRegistry::isKeySet($joinClass) && $dynamicWith === true) {
717 $this->{$joinClass} = new AppModel(array(
718 'name' => $joinClass,
719 'table' => $this->{$type}[$assocKey]['joinTable'],
720 'ds' => $this->useDbConfig
721 ));
722 } else {
723 $this->__constructLinkedModel($joinClass, $plugin . $joinClass);
724 $this->{$type}[$assocKey]['joinTable'] = $this->{$joinClass}->table;
725 }
726  
727 if (count($this->{$joinClass}->schema()) <= 2 && $this->{$joinClass}->primaryKey !== false) {
728 $this->{$joinClass}->primaryKey = $this->{$type}[$assocKey]['foreignKey'];
729 }
730 }
731 }
732 }
733 /**
734 * Sets a custom table for your controller class. Used by your controller to select a database table.
735 *
736 * @param string $tableName Name of the custom table
737 * @return void
738 * @access public
739 */
740 function setSource($tableName) {
741 $this->setDataSource($this->useDbConfig);
742 $db =& ConnectionManager::getDataSource($this->useDbConfig);
743 $db->cacheSources = ($this->cacheSources && $db->cacheSources);
744  
745 if ($db->isInterfaceSupported('listSources')) {
746 $sources = $db->listSources();
747 if (is_array($sources) && !in_array(strtolower($this->tablePrefix . $tableName), array_map('strtolower', $sources))) {
748 return $this->cakeError('missingTable', array(array(
749 'className' => $this->alias,
750 'table' => $this->tablePrefix . $tableName
751 )));
752 }
753 $this->_schema = null;
754 }
755 $this->table = $this->useTable = $tableName;
756 $this->tableToModel[$this->table] = $this->alias;
757 $this->schema();
758 }
759 /**
760 * This function does two things:
761 *
762 * 1. it scans the array $one for the primary key,
763 * and if that's found, it sets the current id to the value of $one[id].
764 * For all other keys than 'id' the keys and values of $one are copied to the 'data' property of this object.
765 * 2. Returns an array with all of $one's keys and values.
766 * (Alternative indata: two strings, which are mangled to
767 * a one-item, two-dimensional array using $one for a key and $two as its value.)
768 *
769 * @param mixed $one Array or string of data
770 * @param string $two Value string for the alternative indata method
771 * @return array Data with all of $one's keys and values
772 * @access public
773 */
774 function set($one, $two = null) {
775 if (!$one) {
776 return;
777 }
778 if (is_object($one)) {
779 $one = Set::reverse($one);
780 }
781  
782 if (is_array($one)) {
783 $data = $one;
784 if (empty($one[$this->alias])) {
785 if ($this->getAssociated(key($one)) === null) {
786 $data = array($this->alias => $one);
787 }
788 }
789 } else {
790 $data = array($this->alias => array($one => $two));
791 }
792  
793 foreach ($data as $modelName => $fieldSet) {
794 if (is_array($fieldSet)) {
795  
796 foreach ($fieldSet as $fieldName => $fieldValue) {
797 if (isset($this->validationErrors[$fieldName])) {
798 unset ($this->validationErrors[$fieldName]);
799 }
800  
801 if ($modelName === $this->alias) {
802 if ($fieldName === $this->primaryKey) {
803 $this->id = $fieldValue;
804 }
805 }
806 if (is_array($fieldValue) || is_object($fieldValue)) {
807 $fieldValue = $this->deconstruct($fieldName, $fieldValue);
808 }
809 $this->data[$modelName][$fieldName] = $fieldValue;
810 }
811 }
812 }
813 return $data;
814 }
815 /**
816 * Deconstructs a complex data type (array or object) into a single field value.
817 *
818 * @param string $field The name of the field to be deconstructed
819 * @param mixed $data An array or object to be deconstructed into a field
820 * @return mixed The resulting data that should be assigned to a field
821 * @access public
822 */
823 function deconstruct($field, $data) {
824 if (!is_array($data)) {
825 return $data;
826 }
827  
828 $copy = $data;
829 $type = $this->getColumnType($field);
830  
831 if (in_array($type, array('datetime', 'timestamp', 'date', 'time'))) {
832 $useNewDate = (isset($data['year']) || isset($data['month']) ||
833 isset($data['day']) || isset($data['hour']) || isset($data['minute']));
834  
835 $dateFields = array('Y' => 'year', 'm' => 'month', 'd' => 'day', 'H' => 'hour', 'i' => 'min', 's' => 'sec');
836 $timeFields = array('H' => 'hour', 'i' => 'min', 's' => 'sec');
837  
838 $db =& ConnectionManager::getDataSource($this->useDbConfig);
839 $format = $db->columns[$type]['format'];
840 $date = array();
841  
842 if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] != 12 && 'pm' == $data['meridian']) {
843 $data['hour'] = $data['hour'] + 12;
844 }
845 if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] == 12 && 'am' == $data['meridian']) {
846 $data['hour'] = '00';
847 }
848 if ($type == 'time') {
849 foreach ($timeFields as $key => $val) {
850 if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') {
851 $data[$val] = '00';
852 } elseif ($data[$val] === '') {
853 $data[$val] = '';
854 } else {
855 $data[$val] = sprintf('%02d', $data[$val]);
856 }
857 if (!empty($data[$val])) {
858 $date[$key] = $data[$val];
859 } else {
860 return null;
861 }
862 }
863 }
864  
865 if ($type == 'datetime' || $type == 'timestamp' || $type == 'date') {
866 foreach ($dateFields as $key => $val) {
867 if ($val == 'hour' || $val == 'min' || $val == 'sec') {
868 if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') {
869 $data[$val] = '00';
870 } else {
871 $data[$val] = sprintf('%02d', $data[$val]);
872 }
873 }
874 if (!isset($data[$val]) || isset($data[$val]) && (empty($data[$val]) || $data[$val][0] === '-')) {
875 return null;
876 }
877 if (isset($data[$val]) && !empty($data[$val])) {
878 $date[$key] = $data[$val];
879 }
880 }
881 }
882 $date = str_replace(array_keys($date), array_values($date), $format);
883 if ($useNewDate && !empty($date)) {
884 return $date;
885 }
886 }
887 return $data;
888 }
889 /**
890 * Returns an array of table metadata (column names and types) from the database.
891 * $field => keys(type, null, default, key, length, extra)
892 *
893 * @param mixed $field Set to true to reload schema, or a string to return a specific field
894 * @return array Array of table metadata
895 * @access public
896 */
897 function schema($field = false) {
898 if (!is_array($this->_schema) || $field === true) {
899 $db =& ConnectionManager::getDataSource($this->useDbConfig);
900 $db->cacheSources = ($this->cacheSources && $db->cacheSources);
901 if ($db->isInterfaceSupported('describe') && $this->useTable !== false) {
902 $this->_schema = $db->describe($this, $field);
903 } elseif ($this->useTable === false) {
904 $this->_schema = array();
905 }
906 }
907 if (is_string($field)) {
908 if (isset($this->_schema[$field])) {
909 return $this->_schema[$field];
910 } else {
911 return null;
912 }
913 }
914 return $this->_schema;
915 }
916 /**
917 * Returns an associative array of field names and column types.
918 *
919 * @return array Field types indexed by field name
920 * @access public
921 */
922 function getColumnTypes() {
923 $columns = $this->schema();
924 if (empty($columns)) {
925 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);
926 }
927 $cols = array();
928 foreach ($columns as $field => $values) {
929 $cols[$field] = $values['type'];
930 }
931 return $cols;
932 }
933 /**
934 * Returns the column type of a column in the model.
935 *
936 * @param string $column The name of the model column
937 * @return string Column type
938 * @access public
939 */
940 function getColumnType($column) {
941 $db =& ConnectionManager::getDataSource($this->useDbConfig);
942 $cols = $this->schema();
943 $model = null;
944  
945 $column = str_replace(array($db->startQuote, $db->endQuote), '', $column);
946  
947 if (strpos($column, '.')) {
948 list($model, $column) = explode('.', $column);
949 }
950 if ($model != $this->alias && isset($this->{$model})) {
951 return $this->{$model}->getColumnType($column);
952 }
953 if (isset($cols[$column]) && isset($cols[$column]['type'])) {
954 return $cols[$column]['type'];
955 }
956 return null;
957 }
958 /**
959 * Returns true if the supplied field exists in the model's database table.
960 *
961 * @param mixed $name Name of field to look for, or an array of names
962 * @return mixed If $name is a string, returns a boolean indicating whether the field exists.
963 * If $name is an array of field names, returns the first field that exists,
964 * or false if none exist.
965 * @access public
966 */
967 function hasField($name) {
968 if (is_array($name)) {
969 foreach ($name as $n) {
970 if ($this->hasField($n)) {
971 return $n;
972 }
973 }
974 return false;
975 }
976  
977 if (empty($this->_schema)) {
978 $this->schema();
979 }
980  
981 if ($this->_schema != null) {
982 return isset($this->_schema[$name]);
983 }
984 return false;
985 }
986 /**
987 * Initializes the model for writing a new record, loading the default values
988 * for those fields that are not defined in $data, and clearing previous validation errors.
989 * Especially helpful for saving data in loops.
990 *
991 * @param mixed $data Optional data array to assign to the model after it is created. If null or false,
992 * schema data defaults are not merged.
993 * @param boolean $filterKey If true, overwrites any primary key input with an empty value
994 * @return array The current Model::data; after merging $data and/or defaults from database
995 * @access public
996 * @link http://book.cakephp.org/view/75/Saving-Your-Data
997 */
998 function create($data = array(), $filterKey = false) {
999 $defaults = array();
1000 $this->id = false;
1001 $this->data = array();
1002 $this->__exists = null;
1003 $this->validationErrors = array();
1004  
1005 if ($data !== null && $data !== false) {
1006 foreach ($this->schema() as $field => $properties) {
1007 if ($this->primaryKey !== $field && isset($properties['default'])) {
1008 $defaults[$field] = $properties['default'];
1009 }
1010 }
1011 $this->set(Set::filter($defaults));
1012 $this->set($data);
1013 }
1014 if ($filterKey) {
1015 $this->set($this->primaryKey, false);
1016 }
1017 return $this->data;
1018 }
1019 /**
1020 * Returns a list of fields from the database, and sets the current model
1021 * data (Model::$data) with the record found.
1022 *
1023 * @param mixed $fields String of single fieldname, or an array of fieldnames.
1024 * @param mixed $id The ID of the record to read
1025 * @return array Array of database fields, or false if not found
1026 * @access public
1027 */
1028 function read($fields = null, $id = null) {
1029 $this->validationErrors = array();
1030  
1031 if ($id != null) {
1032 $this->id = $id;
1033 }
1034  
1035 $id = $this->id;
1036  
1037 if (is_array($this->id)) {
1038 $id = $this->id[0];
1039 }
1040  
1041 if ($id !== null && $id !== false) {
1042 $this->data = $this->find('first', array(
1043 'conditions' => array($this->alias . '.' . $this->primaryKey => $id),
1044 'fields' => $fields
1045 ));
1046 return $this->data;
1047 } else {
1048 return false;
1049 }
1050 }
1051 /**
1052 * Returns the contents of a single field given the supplied conditions, in the
1053 * supplied order.
1054 *
1055 * @param string $name Name of field to get
1056 * @param array $conditions SQL conditions (defaults to NULL)
1057 * @param string $order SQL ORDER BY fragment
1058 * @return string field contents, or false if not found
1059 * @access public
1060 * @link http://book.cakephp.org/view/453/field
1061 */
1062 function field($name, $conditions = null, $order = null) {
1063 if ($conditions === null && $this->id !== false) {
1064 $conditions = array($this->alias . '.' . $this->primaryKey => $this->id);
1065 }
1066 if ($this->recursive >= 1) {
1067 $recursive = -1;
1068 } else {
1069 $recursive = $this->recursive;
1070 }
1071 if ($data = $this->find($conditions, $name, $order, $recursive)) {
1072 if (strpos($name, '.') === false) {
1073 if (isset($data[$this->alias][$name])) {
1074 return $data[$this->alias][$name];
1075 }
1076 } else {
1077 $name = explode('.', $name);
1078 if (isset($data[$name[0]][$name[1]])) {
1079 return $data[$name[0]][$name[1]];
1080 }
1081 }
1082 if (isset($data[0]) && count($data[0]) > 0) {
1083 $name = key($data[0]);
1084 return $data[0][$name];
1085 }
1086 } else {
1087 return false;
1088 }
1089 }
1090 /**
1091 * Saves the value of a single field to the database, based on the current
1092 * model ID.
1093 *
1094 * @param string $name Name of the table field
1095 * @param mixed $value Value of the field
1096 * @param array $validate See $options param in Model::save(). Does not respect 'fieldList' key if passed
1097 * @return boolean See Model::save()
1098 * @access public
1099 * @see Model::save()
1100 * @link http://book.cakephp.org/view/75/Saving-Your-Data
1101 */
1102 function saveField($name, $value, $validate = false) {
1103 $id = $this->id;
1104 $this->create(false);
1105  
1106 if (is_array($validate)) {
1107 $options = array_merge(array('validate' => false, 'fieldList' => array($name)), $validate);
1108 } else {
1109 $options = array('validate' => $validate, 'fieldList' => array($name));
1110 }
1111 return $this->save(array($this->alias => array($this->primaryKey => $id, $name => $value)), $options);
1112 }
1113 /**
1114 * Saves model data (based on white-list, if supplied) to the database. By
1115 * default, validation occurs before save.
1116 *
1117 * @param array $data Data to save.
1118 * @param mixed $validate Either a boolean, or an array.
1119 * If a boolean, indicates whether or not to validate before saving.
1120 * If an array, allows control of validate, callbacks, and fieldList
1121 * @param array $fieldList List of fields to allow to be written
1122 * @return mixed On success Model::$data if its not empty or true, false on failure
1123 * @access public
1124 * @link http://book.cakephp.org/view/75/Saving-Your-Data
1125 */
1126 function save($data = null, $validate = true, $fieldList = array()) {
1127 $defaults = array('validate' => true, 'fieldList' => array(), 'callbacks' => true);
1128 $_whitelist = $this->whitelist;
1129 $fields = array();
1130  
1131 if (!is_array($validate)) {
1132 $options = array_merge($defaults, compact('validate', 'fieldList', 'callbacks'));
1133 } else {
1134 $options = array_merge($defaults, $validate);
1135 }
1136  
1137 if (!empty($options['fieldList'])) {
1138 $this->whitelist = $options['fieldList'];
1139 } elseif ($options['fieldList'] === null) {
1140 $this->whitelist = array();
1141 }
1142 $this->set($data);
1143  
1144 if (empty($this->data) && !$this->hasField(array('created', 'updated', 'modified'))) {
1145 return false;
1146 }
1147  
1148 foreach (array('created', 'updated', 'modified') as $field) {
1149 $keyPresentAndEmpty = (
1150 isset($this->data[$this->alias]) &&
1151 array_key_exists($field, $this->data[$this->alias]) &&
1152 $this->data[$this->alias][$field] === null
1153 );
1154 if ($keyPresentAndEmpty) {
1155 unset($this->data[$this->alias][$field]);
1156 }
1157 }
1158  
1159 $this->exists();
1160 $dateFields = array('modified', 'updated');
1161  
1162 if (!$this->__exists) {
1163 $dateFields[] = 'created';
1164 }
1165 if (isset($this->data[$this->alias])) {
1166 $fields = array_keys($this->data[$this->alias]);
1167 }
1168 if ($options['validate'] && !$this->validates($options)) {
1169 $this->whitelist = $_whitelist;
1170 return false;
1171 }
1172  
1173 $db =& ConnectionManager::getDataSource($this->useDbConfig);
1174  
1175 foreach ($dateFields as $updateCol) {
1176 if ($this->hasField($updateCol) && !in_array($updateCol, $fields)) {
1177 $default = array('formatter' => 'date');
1178 $colType = array_merge($default, $db->columns[$this->getColumnType($updateCol)]);
1179 if (!array_key_exists('format', $colType)) {
1180 $time = strtotime('now');
1181 } else {
1182 $time = $colType['formatter']($colType['format']);
1183 }
1184 if (!empty($this->whitelist)) {
1185 $this->whitelist[] = $updateCol;
1186 }
1187 $this->set($updateCol, $time);
1188 }
1189 }
1190  
1191 if ($options['callbacks'] === true || $options['callbacks'] === 'before') {
1192 $result = $this->Behaviors->trigger($this, 'beforeSave', array($options), array(
1193 'break' => true, 'breakOn' => false
1194 ));
1195 if (!$result || !$this->beforeSave($options)) {
1196 $this->whitelist = $_whitelist;
1197 return false;
1198 }
1199 }
1200 $fields = $values = array();
1201  
1202 if (isset($this->data[$this->alias][$this->primaryKey]) && empty($this->data[$this->alias][$this->primaryKey])) {
1203 unset($this->data[$this->alias][$this->primaryKey]);
1204 }
1205  
1206 foreach ($this->data as $n => $v) {
1207 if (isset($this->hasAndBelongsToMany[$n])) {
1208 if (isset($v[$n])) {
1209 $v = $v[$n];
1210 }
1211 $joined[$n] = $v;
1212 } else {
1213 if ($n === $this->alias) {
1214 foreach (array('created', 'updated', 'modified') as $field) {
1215 if (array_key_exists($field, $v) && empty($v[$field])) {
1216 unset($v[$field]);
1217 }
1218 }
1219  
1220 foreach ($v as $x => $y) {
1221 if ($this->hasField($x) && (empty($this->whitelist) || in_array($x, $this->whitelist))) {
1222 list($fields[], $values[]) = array($x, $y);
1223 }
1224 }
1225 }
1226 }
1227 }
1228 $count = count($fields);
1229  
1230 if (!$this->__exists && $count > 0) {
1231 $this->id = false;
1232 }
1233 $success = true;
1234 $created = false;
1235  
1236 if ($count > 0) {
1237 $cache = $this->_prepareUpdateFields(array_combine($fields, $values));
1238  
1239 if (!empty($this->id)) {
1240 $success = (bool)$db->update($this, $fields, $values);
1241 } else {
1242 foreach ($this->_schema as $field => $properties) {
1243 if ($this->primaryKey === $field) {
1244 $fInfo = $this->_schema[$field];
1245 $isUUID = ($fInfo['length'] == 36 &&
1246 ($fInfo['type'] === 'string' || $fInfo['type'] === 'binary')
1247 );
1248 if (empty($this->data[$this->alias][$this->primaryKey]) && $isUUID) {
1249 list($fields[], $values[]) = array($this->primaryKey, String::uuid());
1250 }
1251 break;
1252 }
1253 }
1254  
1255 if (!$db->create($this, $fields, $values)) {
1256 $success = $created = false;
1257 } else {
1258 $created = true;
1259 }
1260 }
1261  
1262 if ($success && !empty($this->belongsTo)) {
1263 $this->updateCounterCache($cache, $created);
1264 }
1265 }
1266  
1267 if (!empty($joined) && $success === true) {
1268 $this->__saveMulti($joined, $this->id);
1269 }
1270  
1271 if ($success && $count > 0) {
1272 if (!empty($this->data)) {
1273 $success = $this->data;
1274 }
1275 if ($options['callbacks'] === true || $options['callbacks'] === 'after') {
1276 $this->Behaviors->trigger($this, 'afterSave', array($created, $options));
1277 $this->afterSave($created);
1278 }
1279 if (!empty($this->data)) {
1280 $success = Set::merge($success, $this->data);
1281 }
1282 $this->data = false;
1283 $this->__exists = null;
1284 $this->_clearCache();
1285 $this->validationErrors = array();
1286 }
1287 $this->whitelist = $_whitelist;
1288 return $success;
1289 }
1290 /**
1291 * Saves model hasAndBelongsToMany data to the database.
1292 *
1293 * @param array $joined Data to save
1294 * @param mixed $id ID of record in this model
1295 * @access private
1296 */
1297 function __saveMulti($joined, $id) {
1298 $db =& ConnectionManager::getDataSource($this->useDbConfig);
1299  
1300 foreach ($joined as $assoc => $data) {
1301  
1302 if (isset($this->hasAndBelongsToMany[$assoc])) {
1303 list($join) = $this->joinModel($this->hasAndBelongsToMany[$assoc]['with']);
1304  
1305 $conditions = array($join . '.' . $this->hasAndBelongsToMany[$assoc]['foreignKey'] => $id);
1306  
1307 $links = $this->{$join}->find('all', array(
1308 'conditions' => $conditions,
1309 'recursive' => -1,
1310 'fields' => $this->hasAndBelongsToMany[$assoc]['associationForeignKey']
1311 ));
1312  
1313 $isUUID = !empty($this->{$join}->primaryKey) && (
1314 $this->{$join}->_schema[$this->{$join}->primaryKey]['length'] == 36 && (
1315 $this->{$join}->_schema[$this->{$join}->primaryKey]['type'] === 'string' ||
1316 $this->{$join}->_schema[$this->{$join}->primaryKey]['type'] === 'binary'
1317 )
1318 );
1319  
1320 $newData = $newValues = array();
1321 $primaryAdded = false;
1322  
1323 $fields = array(
1324 $db->name($this->hasAndBelongsToMany[$assoc]['foreignKey']),
1325 $db->name($this->hasAndBelongsToMany[$assoc]['associationForeignKey'])
1326 );
1327  
1328 $idField = $db->name($this->{$join}->primaryKey);
1329 if ($isUUID && !in_array($idField, $fields)) {
1330 $fields[] = $idField;
1331 $primaryAdded = true;
1332 }
1333  
1334 foreach ((array)$data as $row) {
1335 if ((is_string($row) && (strlen($row) == 36 || strlen($row) == 16)) || is_numeric($row)) {
1336 $values = array(
1337 $db->value($id, $this->getColumnType($this->primaryKey)),
1338 $db->value($row)
1339 );
1340 if ($isUUID && $primaryAdded) {
1341 $values[] = $db->value(String::uuid());
1342 }
1343 $values = join(',', $values);
1344 $newValues[] = "({$values})";
1345 unset($values);
1346 } elseif (isset($row[$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
1347 $newData[] = $row;
1348 } elseif (isset($row[$join]) && isset($row[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
1349 $newData[] = $row[$join];
1350 }
1351 }
1352  
1353 if ($this->hasAndBelongsToMany[$assoc]['unique']) {
1354 $associationForeignKey = "{$join}." . $this->hasAndBelongsToMany[$assoc]['associationForeignKey'];
1355 $oldLinks = Set::extract($links, "{n}.{$associationForeignKey}");
1356 if (!empty($oldLinks)) {
1357 $conditions[$associationForeignKey] = $oldLinks;
1358 $db->delete($this->{$join}, $conditions);
1359 }
1360 }
1361  
1362 if (!empty($newData)) {
1363 foreach ($newData as $data) {
1364 $data[$this->hasAndBelongsToMany[$assoc]['foreignKey']] = $id;
1365 $this->{$join}->create($data);
1366 $this->{$join}->save();
1367 }
1368 }
1369  
1370 if (!empty($newValues)) {
1371 $fields = join(',', $fields);
1372 $db->insertMulti($this->{$join}, $fields, $newValues);
1373 }
1374 }
1375 }
1376 }
1377 /**
1378 * Updates the counter cache of belongsTo associations after a save or delete operation
1379 *
1380 * @param array $keys Optional foreign key data, defaults to the information $this->data
1381 * @param boolean $created True if a new record was created, otherwise only associations with
1382 * 'counterScope' defined get updated
1383 * @return void
1384 * @access public
1385 */
1386 function updateCounterCache($keys = array(), $created = false) {
1387 $keys = empty($keys) ? $this->data[$this->alias] : $keys;
1388 $keys['old'] = isset($keys['old']) ? $keys['old'] : array();
1389  
1390 foreach ($this->belongsTo as $parent => $assoc) {
1391 $foreignKey = $assoc['foreignKey'];
1392 $fkQuoted = $this->escapeField($assoc['foreignKey']);
1393  
1394 if (!empty($assoc['counterCache'])) {
1395 if ($assoc['counterCache'] === true) {
1396 $assoc['counterCache'] = Inflector::underscore($this->alias) . '_count';
1397 }
1398 if (!$this->{$parent}->hasField($assoc['counterCache'])) {
1399 continue;
1400 }
1401  
1402 if (!array_key_exists($foreignKey, $keys)) {
1403 $keys[$foreignKey] = $this->field($foreignKey);
1404 }
1405 $recursive = (isset($assoc['counterScope']) ? 1 : -1);
1406 $conditions = ($recursive == 1) ? (array)$assoc['counterScope'] : array();
1407  
1408 if (isset($keys['old'][$foreignKey])) {
1409 if ($keys['old'][$foreignKey] != $keys[$foreignKey]) {
1410 $conditions[$fkQuoted] = $keys['old'][$foreignKey];
1411 $count = intval($this->find('count', compact('conditions', 'recursive')));
1412  
1413 $this->{$parent}->updateAll(
1414 array($assoc['counterCache'] => $count),
1415 array($this->{$parent}->escapeField() => $keys['old'][$foreignKey])
1416 );
1417 }
1418 }
1419 $conditions[$fkQuoted] = $keys[$foreignKey];
1420  
1421 if ($recursive == 1) {
1422 $conditions = array_merge($conditions, (array)$assoc['counterScope']);
1423 }
1424 $count = intval($this->find('count', compact('conditions', 'recursive')));
1425  
1426 $this->{$parent}->updateAll(
1427 array($assoc['counterCache'] => $count),
1428 array($this->{$parent}->escapeField() => $keys[$foreignKey])
1429 );
1430 }
1431 }
1432 }
1433 /**
1434 * Helper method for Model::updateCounterCache(). Checks the fields to be updated for
1435 *
1436 * @param array $data The fields of the record that will be updated
1437 * @return array Returns updated foreign key values, along with an 'old' key containing the old
1438 * values, or empty if no foreign keys are updated.
1439 * @access protected
1440 */
1441 function _prepareUpdateFields($data) {
1442 $foreignKeys = array();
1443 foreach ($this->belongsTo as $assoc => $info) {
1444 if ($info['counterCache']) {
1445 $foreignKeys[$assoc] = $info['foreignKey'];
1446 }
1447 }
1448 $included = array_intersect($foreignKeys, array_keys($data));
1449  
1450 if (empty($included) || empty($this->id)) {
1451 return array();
1452 }
1453 $old = $this->find('first', array(
1454 'conditions' => array($this->primaryKey => $this->id),
1455 'fields' => array_values($included),
1456 'recursive' => -1
1457 ));
1458 return array_merge($data, array('old' => $old[$this->alias]));
1459 }
1460 /**
1461 * Saves multiple individual records for a single model; Also works with a single record, as well as
1462 * all its associated records.
1463 *
1464 * #### Options
1465 *
1466 * - validate: Set to false to disable validation, true to validate each record before
1467 * saving, 'first' to validate *all* records before any are saved, or 'only' to only
1468 * validate the records, but not save them.
1469 * - atomic: If true (default), will attempt to save all records in a single transaction.
1470 * Should be set to false if database/table does not support transactions.
1471 * If false, we return an array similar to the $data array passed, but values are set to true/false
1472 * depending on whether each record saved successfully.
1473 * - fieldList: Equivalent to the $fieldList parameter in Model::save()
1474 *
1475 * @param array $data Record data to save. This can be either a numerically-indexed array (for saving multiple
1476 * records of the same type), or an array indexed by association name.
1477 * @param array $options Options to use when saving record data, See $options above.
1478 * @return mixed True on success, or false on failure
1479 * @access public
1480 * @link http://book.cakephp.org/view/84/Saving-Related-Model-Data-hasOne-hasMany-belongsTo
1481 * @link http://book.cakephp.org/view/75/Saving-Your-Data
1482 */
1483 function saveAll($data = null, $options = array()) {
1484 if (empty($data)) {
1485 $data = $this->data;
1486 }
1487 $db =& ConnectionManager::getDataSource($this->useDbConfig);
1488  
1489 $options = array_merge(array('validate' => true, 'atomic' => true), $options);
1490 $this->validationErrors = $validationErrors = array();
1491 $validates = true;
1492 $return = array();
1493  
1494 if ($options['atomic'] && $options['validate'] !== 'only') {
1495 $db->begin($this);
1496 }
1497  
1498 if (Set::numeric(array_keys($data))) {
1499 while ($validates) {
1500 foreach ($data as $key => $record) {
1501 if (!$currentValidates = $this->__save($record, $options)) {
1502 $validationErrors[$key] = $this->validationErrors;
1503 }
1504  
1505 if ($options['validate'] === 'only' || $options['validate'] === 'first') {
1506 $validating = true;
1507 if ($options['atomic']) {
1508 $validates = $validates && $currentValidates;
1509 } else {
1510 $validates = $currentValidates;
1511 }
1512 } else {
1513 $validating = false;
1514 $validates = $currentValidates;
1515 }
1516  
1517 if (!$options['atomic']) {
1518 $return[] = $validates;
1519 } elseif (!$validates && !$validating) {
1520 break;
1521 }
1522 }
1523 $this->validationErrors = $validationErrors;
1524  
1525 switch (true) {
1526 case ($options['validate'] === 'only'):
1527 return ($options['atomic'] ? $validates : $return);
1528 break;
1529 case ($options['validate'] === 'first'):
1530 $options['validate'] = true;
1531 continue;
1532 break;
1533 default:
1534 if ($options['atomic']) {
1535 if ($validates && ($db->commit($this) !== false)) {
1536 return true;
1537 }
1538 $db->rollback($this);
1539 return false;
1540 }
1541 return $return;
1542 break;
1543 }
1544 }
1545 return $return;
1546 }
1547 $associations = $this->getAssociated();
1548  
1549 while ($validates) {
1550 foreach ($data as $association => $values) {
1551 if (isset($associations[$association])) {
1552 switch ($associations[$association]) {
1553 case 'belongsTo':
1554 if ($this->{$association}->__save($values, $options)) {
1555 $data[$this->alias][$this->belongsTo[$association]['foreignKey']] = $this->{$association}->id;
1556 } else {
1557 $validationErrors[$association] = $this->{$association}->validationErrors;
1558 $validates = false;
1559 }
1560 if (!$options['atomic']) {
1561 $return[$association][] = $validates;
1562 }
1563 break;
1564 }
1565 }
1566 }
1567 if (!$this->__save($data, $options)) {
1568 $validationErrors[$this->alias] = $this->validationErrors;
1569 $validates = false;
1570 }
1571 if (!$options['atomic']) {
1572 $return[$this->alias] = $validates;
1573 }
1574 $validating = ($options['validate'] === 'only' || $options['validate'] === 'first');
1575  
1576 foreach ($data as $association => $values) {
1577 if (!$validates && !$validating) {
1578 break;
1579 }
1580 if (isset($associations[$association])) {
1581 $type = $associations[$association];
1582 switch ($type) {
1583 case 'hasOne':
1584 $values[$this->{$type}[$association]['foreignKey']] = $this->id;
1585 if (!$this->{$association}->__save($values, $options)) {
1586 $validationErrors[$association] = $this->{$association}->validationErrors;
1587 $validates = false;
1588 }
1589 if (!$options['atomic']) {
1590 $return[$association][] = $validates;
1591 }
1592 break;
1593 case 'hasMany':
1594 foreach ($values as $i => $value) {
1595 $values[$i][$this->{$type}[$association]['foreignKey']] = $this->id;
1596 }
1597 $_options = array_merge($options, array('atomic' => false));
1598  
1599 if ($_options['validate'] === 'first') {
1600 $_options['validate'] = 'only';
1601 }
1602 $_return = $this->{$association}->saveAll($values, $_options);
1603  
1604 if ($_return === false || (is_array($_return) && in_array(false, $_return, true))) {
1605 $validationErrors[$association] = $this->{$association}->validationErrors;
1606 $validates = false;
1607 }
1608 if (is_array($_return)) {
1609 foreach ($_return as $val) {
1610 if (!isset($return[$association])) {
1611 $return[$association] = array();
1612 } elseif (!is_array($return[$association])) {
1613 $return[$association] = array($return[$association]);
1614 }
1615 $return[$association][] = $val;
1616 }
1617 } else {
1618 $return[$association] = $_return;
1619 }
1620 break;
1621 }
1622 }
1623 }
1624 $this->validationErrors = $validationErrors;
1625  
1626 if (isset($validationErrors[$this->alias])) {
1627 $this->validationErrors = $validationErrors[$this->alias];
1628 }
1629  
1630 switch (true) {
1631 case ($options['validate'] === 'only'):
1632 return ($options['atomic'] ? $validates : $return);
1633 break;
1634 case ($options['validate'] === 'first'):
1635 $options['validate'] = true;
1636 continue;
1637 break;
1638 default:
1639 if ($options['atomic']) {
1640 if ($validates) {
1641 return ($db->commit($this) !== false);
1642 } else {
1643 $db->rollback($this);
1644 }
1645 }
1646 return $return;
1647 break;
1648 }
1649 }
1650 }
1651 /**
1652 * Private helper method used by saveAll.
1653 *
1654 * @return boolean Success
1655 * @access private
1656 * @see Model::saveAll()
1657 */
1658 function __save($data, $options) {
1659 if ($options['validate'] === 'first' || $options['validate'] === 'only') {
1660 if (!($this->create($data) && $this->validates($options))) {
1661 return false;
1662 }
1663 } elseif (!($this->create(null) !== null && $this->save($data, $options))) {
1664 return false;
1665 }
1666 return true;
1667 }
1668 /**
1669 * Updates multiple model records based on a set of conditions.
1670 *
1671 * @param array $fields Set of fields and values, indexed by fields.
1672 * Fields are treated as SQL snippets, to insert literal values manually escape your data.
1673 * @param mixed $conditions Conditions to match, true for all records
1674 * @return boolean True on success, false on failure
1675 * @access public
1676 * @link http://book.cakephp.org/view/75/Saving-Your-Data
1677 */
1678 function updateAll($fields, $conditions = true) {
1679 $db =& ConnectionManager::getDataSource($this->useDbConfig);
1680 return $db->update($this, $fields, null, $conditions);
1681 }
1682 /**
1683 * Alias for del().
1684 *
1685 * @param mixed $id ID of record to delete
1686 * @param boolean $cascade Set to true to delete records that depend on this record
1687 * @return boolean True on success
1688 * @access public
1689 * @see Model::del()
1690 * @link http://book.cakephp.org/view/691/remove
1691 */
1692 function remove($id = null, $cascade = true) {
1693 return $this->del($id, $cascade);
1694 }
1695 /**
1696 * Removes record for given ID. If no ID is given, the current ID is used. Returns true on success.
1697 *
1698 * @param mixed $id ID of record to delete
1699 * @param boolean $cascade Set to true to delete records that depend on this record
1700 * @return boolean True on success
1701 * @access public
1702 * @link http://book.cakephp.org/view/690/del
1703 */
1704 function del($id = null, $cascade = true) {
1705 if (!empty($id)) {
1706 $this->id = $id;
1707 }
1708 $id = $this->id;
1709  
1710 if ($this->exists() && $this->beforeDelete($cascade)) {
1711 $db =& ConnectionManager::getDataSource($this->useDbConfig);
1712 if (!$this->Behaviors->trigger($this, 'beforeDelete', array($cascade), array('break' => true, 'breakOn' => false))) {
1713 return false;
1714 }
1715 $this->_deleteDependent($id, $cascade);
1716 $this->_deleteLinks($id);
1717 $this->id = $id;
1718  
1719 if (!empty($this->belongsTo)) {
1720 $keys = $this->find('first', array('fields' => $this->__collectForeignKeys()));
1721 }
1722  
1723 if ($db->delete($this)) {
1724 if (!empty($this->belongsTo)) {
1725 $this->updateCounterCache($keys[$this->alias]);
1726 }
1727 $this->Behaviors->trigger($this, 'afterDelete');
1728 $this->afterDelete();
1729 $this->_clearCache();
1730 $this->id = false;
1731 $this->__exists = null;
1732 return true;
1733 }
1734 }
1735 return false;
1736 }
1737 /**
1738 * Alias for del().
1739 *
1740 * @param mixed $id ID of record to delete
1741 * @param boolean $cascade Set to true to delete records that depend on this record
1742 * @return boolean True on success
1743 * @access public
1744 * @see Model::del()
1745 */
1746 function delete($id = null, $cascade = true) {
1747 return $this->del($id, $cascade);
1748 }
1749 /**
1750 * Cascades model deletes through associated hasMany and hasOne child records.
1751 *
1752 * @param string $id ID of record that was deleted
1753 * @param boolean $cascade Set to true to delete records that depend on this record
1754 * @return void
1755 * @access protected
1756 */
1757 function _deleteDependent($id, $cascade) {
1758 if (!empty($this->__backAssociation)) {
1759 $savedAssociatons = $this->__backAssociation;
1760 $this->__backAssociation = array();
1761 }
1762 foreach (array_merge($this->hasMany, $this->hasOne) as $assoc => $data) {
1763 if ($data['dependent'] === true && $cascade === true) {
1764  
1765 $model =& $this->{$assoc};
1766 $conditions = array($model->escapeField($data['foreignKey']) => $id);
1767 if ($data['conditions']) {
1768 $conditions = array_merge($data['conditions'], $conditions);
1769 }
1770 $model->recursive = -1;
1771  
1772 if (isset($data['exclusive']) && $data['exclusive']) {
1773 $model->deleteAll($conditions);
1774 } else {
1775 $records = $model->find('all', array('conditions' => $conditions, 'fields' => $model->primaryKey));
1776  
1777 if (!empty($records)) {
1778 foreach ($records as $record) {
1779 $model->delete($record[$model->alias][$model->primaryKey]);
1780 }
1781 }
1782 }
1783 }
1784 }
1785 if (isset($savedAssociatons)) {
1786 $this->__backAssociation = $savedAssociatons;
1787 }
1788 }
1789 /**
1790 * Cascades model deletes through HABTM join keys.
1791 *
1792 * @param string $id ID of record that was deleted
1793 * @return void
1794 * @access protected
1795 */
1796 function _deleteLinks($id) {
1797 foreach ($this->hasAndBelongsToMany as $assoc => $data) {
1798 $records = $this->{$data['with']}->find('all', array(
1799 'conditions' => array_merge(array($this->{$data['with']}->escapeField($data['foreignKey']) => $id)),
1800 'fields' => $this->{$data['with']}->primaryKey,
1801 'recursive' => -1
1802 ));
1803 if (!empty($records)) {
1804 foreach ($records as $record) {
1805 $this->{$data['with']}->delete($record[$this->{$data['with']}->alias][$this->{$data['with']}->primaryKey]);
1806 }
1807 }
1808 }
1809 }
1810 /**
1811 * Deletes multiple model records based on a set of conditions.
1812 *
1813 * @param mixed $conditions Conditions to match
1814 * @param boolean $cascade Set to true to delete records that depend on this record
1815 * @param boolean $callbacks Run callbacks (not being used)
1816 * @return boolean True on success, false on failure
1817 * @access public
1818 * @link http://book.cakephp.org/view/692/deleteAll
1819 */
1820 function deleteAll($conditions, $cascade = true, $callbacks = false) {
1821 if (empty($conditions)) {
1822 return false;
1823 }
1824 $db =& ConnectionManager::getDataSource($this->useDbConfig);
1825  
1826 if (!$cascade && !$callbacks) {
1827 return $db->delete($this, $conditions);
1828 } else {
1829 $ids = Set::extract(
1830 $this->find('all', array_merge(array('fields' => "{$this->alias}.{$this->primaryKey}", 'recursive' => 0), compact('conditions'))),
1831 "{n}.{$this->alias}.{$this->primaryKey}"
1832 );
1833  
1834 if (empty($ids)) {
1835 return true;
1836 }
1837  
1838 if ($callbacks) {
1839 $_id = $this->id;
1840 $result = true;
1841 foreach ($ids as $id) {
1842 $result = ($result && $this->delete($id, $cascade));
1843 }
1844 $this->id = $_id;
1845 return $result;
1846 } else {
1847 foreach ($ids as $id) {
1848 $this->_deleteLinks($id);
1849 if ($cascade) {
1850 $this->_deleteDependent($id, $cascade);
1851 }
1852 }
1853 return $db->delete($this, array($this->alias . '.' . $this->primaryKey => $ids));
1854 }
1855 }
1856 }
1857 /**
1858 * Collects foreign keys from associations.
1859 *
1860 * @return array
1861 * @access private
1862 */
1863 function __collectForeignKeys($type = 'belongsTo') {
1864 $result = array();
1865  
1866 foreach ($this->{$type} as $assoc => $data) {
1867 if (isset($data['foreignKey']) && is_string($data['foreignKey'])) {
1868 $result[$assoc] = $data['foreignKey'];
1869 }
1870 }
1871 return $result;
1872 }
1873 /**
1874 * Returns true if a record with the currently set ID exists.
1875 *
1876 * @param boolean $reset if true will force database query
1877 * @return boolean True if such a record exists
1878 * @access public
1879 */
1880 function exists($reset = false) {
1881 if (is_array($reset)) {
1882 extract($reset, EXTR_OVERWRITE);
1883 }
1884  
1885 if ($this->getID() === false || $this->useTable === false) {
1886 return false;
1887 }
1888 if (!empty($this->__exists) && $reset !== true) {
1889 return $this->__exists;
1890 }
1891 $conditions = array($this->alias . '.' . $this->primaryKey => $this->getID());
1892 $query = array('conditions' => $conditions, 'recursive' => -1, 'callbacks' => false);
1893  
1894 if (is_array($reset)) {
1895 $query = array_merge($query, $reset);
1896 }
1897 return $this->__exists = ($this->find('count', $query) > 0);
1898 }
1899 /**
1900 * Returns true if a record that meets given conditions exists.
1901 *
1902 * @param array $conditions SQL conditions array
1903 * @return boolean True if such a record exists
1904 * @access public
1905 */
1906 function hasAny($conditions = null) {
1907 return ($this->find('count', array('conditions' => $conditions, 'recursive' => -1)) != false);
1908 }
1909 /**
1910 * Returns a result set array.
1911 *
1912 * Also used to perform new-notation finds, where the first argument is type of find operation to perform
1913 * (all / first / count / neighbors / list / threaded ),
1914 * second parameter options for finding ( indexed array, including: 'conditions', 'limit',
1915 * 'recursive', 'page', 'fields', 'offset', 'order')
1916 *
1917 * Eg:
1918 * {{{
1919 * find('all', array(
1920 * 'conditions' => array('name' => 'Thomas Anderson'),
1921 * 'fields' => array('name', 'email'),
1922 * 'order' => 'field3 DESC',
1923 * 'recursive' => 2,
1924 * 'group' => 'type'
1925 * ));
1926 * }}}
1927 *
1928 * Specifying 'fields' for new-notation 'list':
1929 *
1930 * - If no fields are specified, then 'id' is used for key and 'model->displayField' is used for value.
1931 * - If a single field is specified, 'id' is used for key and specified field is used for value.
1932 * - If three fields are specified, they are used (in order) for key, value and group.
1933 * - Otherwise, first and second fields are used for key and value.
1934 *
1935 * @param array $conditions SQL conditions array, or type of find operation (all / first / count / neighbors / list / threaded)
1936 * @param mixed $fields Either a single string of a field name, or an array of field names, or options for matching
1937 * @param string $order SQL ORDER BY conditions (e.g. "price DESC" or "name ASC")
1938 * @param integer $recursive The number of levels deep to fetch associated records
1939 * @return array Array of records
1940 * @access public
1941 * @link http://book.cakephp.org/view/449/find
1942 */
1943 function find($conditions = null, $fields = array(), $order = null, $recursive = null) {
1944 if (!is_string($conditions) || (is_string($conditions) && !array_key_exists($conditions, $this->_findMethods))) {
1945 $type = 'first';
1946 $query = array_merge(compact('conditions', 'fields', 'order', 'recursive'), array('limit' => 1));
1947 } else {
1948 list($type, $query) = array($conditions, $fields);
1949 }
1950  
1951 $db =& ConnectionManager::getDataSource($this->useDbConfig);
1952 $this->findQueryType = $type;
1953 $this->id = $this->getID();
1954  
1955 $query = array_merge(
1956 array(
1957 'conditions' => null, 'fields' => null, 'joins' => array(), 'limit' => null,
1958 'offset' => null, 'order' => null, 'page' => null, 'group' => null, 'callbacks' => true
1959 ),
1960 (array)$query
1961 );
1962  
1963 if ($type != 'all') {
1964 if ($this->_findMethods[$type] === true) {
1965 $query = $this->{'_find' . ucfirst($type)}('before', $query);
1966 }
1967 }
1968  
1969 if (!is_numeric($query['page']) || intval($query['page']) < 1) {
1970 $query['page'] = 1;
1971 }
1972 if ($query['page'] > 1 && !empty($query['limit'])) {
1973 $query['offset'] = ($query['page'] - 1) * $query['limit'];
1974 }
1975 if ($query['order'] === null && $this->order !== null) {
1976 $query['order'] = $this->order;
1977 }
1978 $query['order'] = array($query['order']);
1979  
1980 if ($query['callbacks'] === true || $query['callbacks'] === 'before') {
1981 $return = $this->Behaviors->trigger($this, 'beforeFind', array($query), array(
1982 'break' => true, 'breakOn' => false, 'modParams' => true
1983 ));
1984 $query = (is_array($return)) ? $return : $query;
1985  
1986 if ($return === false) {
1987 return null;
1988 }
1989  
1990 $return = $this->beforeFind($query);
1991 $query = (is_array($return)) ? $return : $query;
1992  
1993 if ($return === false) {
1994 return null;
1995 }
1996 }
1997  
1998 $results = $db->read($this, $query);
1999 $this->resetAssociations();
2000 $this->findQueryType = null;
2001  
2002 if ($query['callbacks'] === true || $query['callbacks'] === 'after') {
2003 $results = $this->__filterResults($results);
2004 }
2005  
2006 if ($type === 'all') {
2007 return $results;
2008 } else {
2009 if ($this->_findMethods[$type] === true) {
2010 return $this->{'_find' . ucfirst($type)}('after', $query, $results);
2011 }
2012 }
2013 }
2014 /**
2015 * Handles the before/after filter logic for find('first') operations. Only called by Model::find().
2016 *
2017 * @param string $state Either "before" or "after"
2018 * @param array $query
2019 * @param array $data
2020 * @return array
2021 * @access protected
2022 * @see Model::find()
2023 */
2024 function _findFirst($state, $query, $results = array()) {
2025 if ($state == 'before') {
2026 $query['limit'] = 1;
2027 if (empty($query['conditions']) && !empty($this->id)) {
2028 $query['conditions'] = array($this->escapeField() => $this->id);
2029 }
2030 return $query;
2031 } elseif ($state == 'after') {
2032 if (empty($results[0])) {
2033 return false;
2034 }
2035 return $results[0];
2036 }
2037 }
2038 /**
2039 * Handles the before/after filter logic for find('count') operations. Only called by Model::find().
2040 *
2041 * @param string $state Either "before" or "after"
2042 * @param array $query
2043 * @param array $data
2044 * @return int The number of records found, or false
2045 * @access protected
2046 * @see Model::find()
2047 */
2048 function _findCount($state, $query, $results = array()) {
2049 if ($state == 'before') {
2050 $db =& ConnectionManager::getDataSource($this->useDbConfig);
2051 if (empty($query['fields'])) {
2052 $query['fields'] = $db->calculate($this, 'count');
2053 } elseif (is_string($query['fields']) && !preg_match('/count/i', $query['fields'])) {
2054 $query['fields'] = $db->calculate($this, 'count', array(
2055 $db->expression($query['fields']), 'count'
2056 ));
2057 }
2058 $query['order'] = false;
2059 return $query;
2060 } elseif ($state == 'after') {
2061 if (isset($results[0][0]['count'])) {
2062 return intval($results[0][0]['count']);
2063 } elseif (isset($results[0][$this->alias]['count'])) {
2064 return intval($results[0][$this->alias]['count']);
2065 }
2066 return false;
2067 }
2068 }
2069 /**
2070 * Handles the before/after filter logic for find('list') operations. Only called by Model::find().
2071 *
2072 * @param string $state Either "before" or "after"
2073 * @param array $query
2074 * @param array $data
2075 * @return array Key/value pairs of primary keys/display field values of all records found
2076 * @access protected
2077 * @see Model::find()
2078 */
2079 function _findList($state, $query, $results = array()) {
2080 if ($state == 'before') {
2081 if (empty($query['fields'])) {
2082 $query['fields'] = array("{$this->alias}.{$this->primaryKey}", "{$this->alias}.{$this->displayField}");
2083 $list = array("{n}.{$this->alias}.{$this->primaryKey}", "{n}.{$this->alias}.{$this->displayField}", null);
2084 } else {
2085 if (!is_array($query['fields'])) {
2086 $query['fields'] = String::tokenize($query['fields']);
2087 }
2088  
2089 if (count($query['fields']) == 1) {
2090 if (strpos($query['fields'][0], '.') === false) {
2091 $query['fields'][0] = $this->alias . '.' . $query['fields'][0];
2092 }
2093  
2094 $list = array("{n}.{$this->alias}.{$this->primaryKey}", '{n}.' . $query['fields'][0], null);
2095 $query['fields'] = array("{$this->alias}.{$this->primaryKey}", $query['fields'][0]);
2096 } elseif (count($query['fields']) == 3) {
2097 for ($i = 0; $i < 3; $i++) {
2098 if (strpos($query['fields'][$i], '.') === false) {
2099 $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i];
2100 }
2101 }
2102  
2103 $list = array('{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], '{n}.' . $query['fields'][2]);
2104 } else {
2105 for ($i = 0; $i < 2; $i++) {
2106 if (strpos($query['fields'][$i], '.') === false) {
2107 $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i];
2108 }
2109 }
2110  
2111 $list = array('{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], null);
2112 }
2113 }
2114 if (!isset($query['recursive']) || $query['recursive'] === null) {
2115 $query['recursive'] = -1;
2116 }
2117 list($query['list']['keyPath'], $query['list']['valuePath'], $query['list']['groupPath']) = $list;
2118 return $query;
2119 } elseif ($state == 'after') {
2120 if (empty($results)) {
2121 return array();
2122 }
2123 $lst = $query['list'];
2124 return Set::combine($results, $lst['keyPath'], $lst['valuePath'], $lst['groupPath']);
2125 }
2126 }
2127 /**
2128 * Detects the previous field's value, then uses logic to find the 'wrapping'
2129 * rows and return them.
2130 *
2131 * @param string $state Either "before" or "after"
2132 * @param mixed $query
2133 * @param array $results
2134 * @return array
2135 * @access protected
2136 */
2137 function _findNeighbors($state, $query, $results = array()) {
2138 if ($state == 'before') {
2139 $query = array_merge(array('recursive' => 0), $query);
2140 extract($query);
2141 $conditions = (array)$conditions;
2142 if (isset($field) && isset($value)) {
2143 if (strpos($field, '.') === false) {
2144 $field = $this->alias . '.' . $field;
2145 }
2146 } else {
2147 $field = $this->alias . '.' . $this->primaryKey;
2148 $value = $this->id;
2149 }
2150 $query['conditions'] = array_merge($conditions, array($field . ' <' => $value));
2151 $query['order'] = $field . ' DESC';
2152 $query['limit'] = 1;
2153 $query['field'] = $field;
2154 $query['value'] = $value;
2155 return $query;