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