1: <?php
2: /**
3: * Object-relational mapper.
4: *
5: * DBO-backed object data model, for mapping database tables to Cake objects.
6: *
7: * PHP 5
8: *
9: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
10: * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
11: *
12: * Licensed under The MIT License
13: * For full copyright and license information, please see the LICENSE.txt
14: * Redistributions of files must retain the above copyright notice.
15: *
16: * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
17: * @link http://cakephp.org CakePHP(tm) Project
18: * @package Cake.Model
19: * @since CakePHP(tm) v 0.10.0.0
20: * @license http://www.opensource.org/licenses/mit-license.php MIT License
21: */
22:
23: App::uses('ClassRegistry', 'Utility');
24: App::uses('Validation', 'Utility');
25: App::uses('String', 'Utility');
26: App::uses('Hash', 'Utility');
27: App::uses('BehaviorCollection', 'Model');
28: App::uses('ModelBehavior', 'Model');
29: App::uses('ModelValidator', 'Model');
30: App::uses('ConnectionManager', 'Model');
31: App::uses('Xml', 'Utility');
32: App::uses('CakeEvent', 'Event');
33: App::uses('CakeEventListener', 'Event');
34: App::uses('CakeEventManager', 'Event');
35:
36: /**
37: * Object-relational mapper.
38: *
39: * DBO-backed object data model.
40: * Automatically selects a database table name based on a pluralized lowercase object class name
41: * (i.e. class 'User' => table 'users'; class 'Man' => table 'men')
42: * The table is required to have at least 'id auto_increment' primary key.
43: *
44: * @package Cake.Model
45: * @link http://book.cakephp.org/2.0/en/models.html
46: */
47: class Model extends Object implements CakeEventListener {
48:
49: /**
50: * The name of the DataSource connection that this Model uses
51: *
52: * The value must be an attribute name that you defined in `app/Config/database.php`
53: * or created using `ConnectionManager::create()`.
54: *
55: * @var string
56: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#usedbconfig
57: */
58: public $useDbConfig = 'default';
59:
60: /**
61: * Custom database table name, or null/false if no table association is desired.
62: *
63: * @var string
64: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#usetable
65: */
66: public $useTable = null;
67:
68: /**
69: * Custom display field name. Display fields are used by Scaffold, in SELECT boxes' OPTION elements.
70: *
71: * This field is also used in `find('list')` when called with no extra parameters in the fields list
72: *
73: * @var string
74: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#displayfield
75: */
76: public $displayField = null;
77:
78: /**
79: * Value of the primary key ID of the record that this model is currently pointing to.
80: * Automatically set after database insertions.
81: *
82: * @var mixed
83: */
84: public $id = false;
85:
86: /**
87: * Container for the data that this model gets from persistent storage (usually, a database).
88: *
89: * @var array
90: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#data
91: */
92: public $data = array();
93:
94: /**
95: * Holds physical schema/database name for this model. Automatically set during Model creation.
96: *
97: * @var string
98: */
99: public $schemaName = null;
100:
101: /**
102: * Table name for this Model.
103: *
104: * @var string
105: */
106: public $table = false;
107:
108: /**
109: * The name of the primary key field for this model.
110: *
111: * @var string
112: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#primaryKey
113: */
114: public $primaryKey = null;
115:
116: /**
117: * Field-by-field table metadata.
118: *
119: * @var array
120: */
121: protected $_schema = null;
122:
123: /**
124: * List of validation rules. It must be an array with the field name as key and using
125: * as value one of the following possibilities
126: *
127: * ### Validating using regular expressions
128: *
129: * {{{
130: * public $validate = array(
131: * 'name' => '/^[a-z].+$/i'
132: * );
133: * }}}
134: *
135: * ### Validating using methods (no parameters)
136: *
137: * {{{
138: * public $validate = array(
139: * 'name' => 'notEmpty'
140: * );
141: * }}}
142: *
143: * ### Validating using methods (with parameters)
144: *
145: * {{{
146: * public $validate = array(
147: * 'age' => array(
148: * 'rule' => array('between', 5, 25)
149: * )
150: * );
151: * }}}
152: *
153: * ### Validating using custom method
154: *
155: * {{{
156: * public $validate = array(
157: * 'password' => array(
158: * 'rule' => array('customValidation')
159: * )
160: * );
161: * public function customValidation($data) {
162: * // $data will contain array('password' => 'value')
163: * if (isset($this->data[$this->alias]['password2'])) {
164: * return $this->data[$this->alias]['password2'] === current($data);
165: * }
166: * return true;
167: * }
168: * }}}
169: *
170: * ### Validations with messages
171: *
172: * The messages will be used in Model::$validationErrors and can be used in the FormHelper
173: *
174: * {{{
175: * public $validate = array(
176: * 'age' => array(
177: * 'rule' => array('between', 5, 25),
178: * 'message' => array('The age must be between %d and %d.')
179: * )
180: * );
181: * }}}
182: *
183: * ### Multiple validations to the same field
184: *
185: * {{{
186: * public $validate = array(
187: * 'login' => array(
188: * array(
189: * 'rule' => 'alphaNumeric',
190: * 'message' => 'Only alphabets and numbers allowed',
191: * 'last' => true
192: * ),
193: * array(
194: * 'rule' => array('minLength', 8),
195: * 'message' => array('Minimum length of %d characters')
196: * )
197: * )
198: * );
199: * }}}
200: *
201: * ### Valid keys in validations
202: *
203: * - `rule`: String with method name, regular expression (started by slash) or array with method and parameters
204: * - `message`: String with the message or array if have multiple parameters. See http://php.net/sprintf
205: * - `last`: Boolean value to indicate if continue validating the others rules if the current fail [Default: true]
206: * - `required`: Boolean value to indicate if the field must be present on save
207: * - `allowEmpty`: Boolean value to indicate if the field can be empty
208: * - `on`: Possible values: `update`, `create`. Indicate to apply this rule only on update or create
209: *
210: * @var array
211: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#validate
212: * @link http://book.cakephp.org/2.0/en/models/data-validation.html
213: */
214: public $validate = array();
215:
216: /**
217: * List of validation errors.
218: *
219: * @var array
220: */
221: public $validationErrors = array();
222:
223: /**
224: * Name of the validation string domain to use when translating validation errors.
225: *
226: * @var string
227: */
228: public $validationDomain = null;
229:
230: /**
231: * Database table prefix for tables in model.
232: *
233: * @var string
234: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#tableprefix
235: */
236: public $tablePrefix = null;
237:
238: /**
239: * Plugin model belongs to.
240: *
241: * @var string
242: */
243: public $plugin = null;
244:
245: /**
246: * Name of the model.
247: *
248: * @var string
249: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#name
250: */
251: public $name = null;
252:
253: /**
254: * Alias name for model.
255: *
256: * @var string
257: */
258: public $alias = null;
259:
260: /**
261: * List of table names included in the model description. Used for associations.
262: *
263: * @var array
264: */
265: public $tableToModel = array();
266:
267: /**
268: * Whether or not to cache queries for this model. This enables in-memory
269: * caching only, the results are not stored beyond the current request.
270: *
271: * @var boolean
272: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#cachequeries
273: */
274: public $cacheQueries = false;
275:
276: /**
277: * Detailed list of belongsTo associations.
278: *
279: * ### Basic usage
280: *
281: * `public $belongsTo = array('Group', 'Department');`
282: *
283: * ### Detailed configuration
284: *
285: * {{{
286: * public $belongsTo = array(
287: * 'Group',
288: * 'Department' => array(
289: * 'className' => 'Department',
290: * 'foreignKey' => 'department_id'
291: * )
292: * );
293: * }}}
294: *
295: * ### Possible keys in association
296: *
297: * - `className`: the classname of the model being associated to the current model.
298: * If you're defining a 'Profile belongsTo User' relationship, the className key should equal 'User.'
299: * - `foreignKey`: the name of the foreign key found in the current model. This is
300: * especially handy if you need to define multiple belongsTo relationships. The default
301: * value for this key is the underscored, singular name of the other model, suffixed with '_id'.
302: * - `conditions`: An SQL fragment used to filter related model records. It's good
303: * practice to use model names in SQL fragments: 'User.active = 1' is always
304: * better than just 'active = 1.'
305: * - `type`: the type of the join to use in the SQL query, default is LEFT which
306: * may not fit your needs in all situations, INNER may be helpful when you want
307: * everything from your main and associated models or nothing at all!(effective
308: * when used with some conditions of course). (NB: type value is in lower case - i.e. left, inner)
309: * - `fields`: A list of fields to be retrieved when the associated model data is
310: * fetched. Returns all fields by default.
311: * - `order`: An SQL fragment that defines the sorting order for the returned associated rows.
312: * - `counterCache`: If set to true the associated Model will automatically increase or
313: * decrease the "[singular_model_name]_count" field in the foreign table whenever you do
314: * a save() or delete(). If its a string then its the field name to use. The value in the
315: * counter field represents the number of related rows.
316: * - `counterScope`: Optional conditions array to use for updating counter cache field.
317: *
318: * @var array
319: * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#belongsto
320: */
321: public $belongsTo = array();
322:
323: /**
324: * Detailed list of hasOne associations.
325: *
326: * ### Basic usage
327: *
328: * `public $hasOne = array('Profile', 'Address');`
329: *
330: * ### Detailed configuration
331: *
332: * {{{
333: * public $hasOne = array(
334: * 'Profile',
335: * 'Address' => array(
336: * 'className' => 'Address',
337: * 'foreignKey' => 'user_id'
338: * )
339: * );
340: * }}}
341: *
342: * ### Possible keys in association
343: *
344: * - `className`: the classname of the model being associated to the current model.
345: * If you're defining a 'User hasOne Profile' relationship, the className key should equal 'Profile.'
346: * - `foreignKey`: the name of the foreign key found in the other model. This is
347: * especially handy if you need to define multiple hasOne relationships.
348: * The default value for this key is the underscored, singular name of the
349: * current model, suffixed with '_id'. In the example above it would default to 'user_id'.
350: * - `conditions`: An SQL fragment used to filter related model records. It's good
351: * practice to use model names in SQL fragments: "Profile.approved = 1" is
352: * always better than just "approved = 1."
353: * - `fields`: A list of fields to be retrieved when the associated model data is
354: * fetched. Returns all fields by default.
355: * - `order`: An SQL fragment that defines the sorting order for the returned associated rows.
356: * - `dependent`: When the dependent key is set to true, and the model's delete()
357: * method is called with the cascade parameter set to true, associated model
358: * records are also deleted. In this case we set it true so that deleting a
359: * User will also delete her associated Profile.
360: *
361: * @var array
362: * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasone
363: */
364: public $hasOne = array();
365:
366: /**
367: * Detailed list of hasMany associations.
368: *
369: * ### Basic usage
370: *
371: * `public $hasMany = array('Comment', 'Task');`
372: *
373: * ### Detailed configuration
374: *
375: * {{{
376: * public $hasMany = array(
377: * 'Comment',
378: * 'Task' => array(
379: * 'className' => 'Task',
380: * 'foreignKey' => 'user_id'
381: * )
382: * );
383: * }}}
384: *
385: * ### Possible keys in association
386: *
387: * - `className`: the classname of the model being associated to the current model.
388: * If you're defining a 'User hasMany Comment' relationship, the className key should equal 'Comment.'
389: * - `foreignKey`: the name of the foreign key found in the other model. This is
390: * especially handy if you need to define multiple hasMany relationships. The default
391: * value for this key is the underscored, singular name of the actual model, suffixed with '_id'.
392: * - `conditions`: An SQL fragment used to filter related model records. It's good
393: * practice to use model names in SQL fragments: "Comment.status = 1" is always
394: * better than just "status = 1."
395: * - `fields`: A list of fields to be retrieved when the associated model data is
396: * fetched. Returns all fields by default.
397: * - `order`: An SQL fragment that defines the sorting order for the returned associated rows.
398: * - `limit`: The maximum number of associated rows you want returned.
399: * - `offset`: The number of associated rows to skip over (given the current
400: * conditions and order) before fetching and associating.
401: * - `dependent`: When dependent is set to true, recursive model deletion is
402: * possible. In this example, Comment records will be deleted when their
403: * associated User record has been deleted.
404: * - `exclusive`: When exclusive is set to true, recursive model deletion does
405: * the delete with a deleteAll() call, instead of deleting each entity separately.
406: * This greatly improves performance, but may not be ideal for all circumstances.
407: * - `finderQuery`: A complete SQL query CakePHP can use to fetch associated model
408: * records. This should be used in situations that require very custom results.
409: *
410: * @var array
411: * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasmany
412: */
413: public $hasMany = array();
414:
415: /**
416: * Detailed list of hasAndBelongsToMany associations.
417: *
418: * ### Basic usage
419: *
420: * `public $hasAndBelongsToMany = array('Role', 'Address');`
421: *
422: * ### Detailed configuration
423: *
424: * {{{
425: * public $hasAndBelongsToMany = array(
426: * 'Role',
427: * 'Address' => array(
428: * 'className' => 'Address',
429: * 'foreignKey' => 'user_id',
430: * 'associationForeignKey' => 'address_id',
431: * 'joinTable' => 'addresses_users'
432: * )
433: * );
434: * }}}
435: *
436: * ### Possible keys in association
437: *
438: * - `className`: the classname of the model being associated to the current model.
439: * If you're defining a 'Recipe HABTM Tag' relationship, the className key should equal 'Tag.'
440: * - `joinTable`: The name of the join table used in this association (if the
441: * current table doesn't adhere to the naming convention for HABTM join tables).
442: * - `with`: Defines the name of the model for the join table. By default CakePHP
443: * will auto-create a model for you. Using the example above it would be called
444: * RecipesTag. By using this key you can override this default name. The join
445: * table model can be used just like any "regular" model to access the join table directly.
446: * - `foreignKey`: the name of the foreign key found in the current model.
447: * This is especially handy if you need to define multiple HABTM relationships.
448: * The default value for this key is the underscored, singular name of the
449: * current model, suffixed with '_id'.
450: * - `associationForeignKey`: the name of the foreign key found in the other model.
451: * This is especially handy if you need to define multiple HABTM relationships.
452: * The default value for this key is the underscored, singular name of the other
453: * model, suffixed with '_id'.
454: * - `unique`: If true (default value) cake will first delete existing relationship
455: * records in the foreign keys table before inserting new ones, when updating a
456: * record. So existing associations need to be passed again when updating.
457: * To prevent deletion of existing relationship records, set this key to a string 'keepExisting'.
458: * - `conditions`: An SQL fragment used to filter related model records. It's good
459: * practice to use model names in SQL fragments: "Comment.status = 1" is always
460: * better than just "status = 1."
461: * - `fields`: A list of fields to be retrieved when the associated model data is
462: * fetched. Returns all fields by default.
463: * - `order`: An SQL fragment that defines the sorting order for the returned associated rows.
464: * - `limit`: The maximum number of associated rows you want returned.
465: * - `offset`: The number of associated rows to skip over (given the current
466: * conditions and order) before fetching and associating.
467: * - `finderQuery`, A complete SQL query CakePHP
468: * can use to fetch associated model records. This should
469: * be used in situations that require very custom results.
470: *
471: * @var array
472: * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasandbelongstomany-habtm
473: */
474: public $hasAndBelongsToMany = array();
475:
476: /**
477: * List of behaviors to load when the model object is initialized. Settings can be
478: * passed to behaviors by using the behavior name as index. Eg:
479: *
480: * public $actsAs = array('Translate', 'MyBehavior' => array('setting1' => 'value1'))
481: *
482: * @var array
483: * @link http://book.cakephp.org/2.0/en/models/behaviors.html#using-behaviors
484: */
485: public $actsAs = null;
486:
487: /**
488: * Holds the Behavior objects currently bound to this model.
489: *
490: * @var BehaviorCollection
491: */
492: public $Behaviors = null;
493:
494: /**
495: * Whitelist of fields allowed to be saved.
496: *
497: * @var array
498: */
499: public $whitelist = array();
500:
501: /**
502: * Whether or not to cache sources for this model.
503: *
504: * @var boolean
505: */
506: public $cacheSources = true;
507:
508: /**
509: * Type of find query currently executing.
510: *
511: * @var string
512: */
513: public $findQueryType = null;
514:
515: /**
516: * Number of associations to recurse through during find calls. Fetches only
517: * the first level by default.
518: *
519: * @var integer
520: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#recursive
521: */
522: public $recursive = 1;
523:
524: /**
525: * The column name(s) and direction(s) to order find results by default.
526: *
527: * public $order = "Post.created DESC";
528: * public $order = array("Post.view_count DESC", "Post.rating DESC");
529: *
530: * @var string
531: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#order
532: */
533: public $order = null;
534:
535: /**
536: * Array of virtual fields this model has. Virtual fields are aliased
537: * SQL expressions. Fields added to this property will be read as other fields in a model
538: * but will not be saveable.
539: *
540: * `public $virtualFields = array('two' => '1 + 1');`
541: *
542: * Is a simplistic example of how to set virtualFields
543: *
544: * @var array
545: * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#virtualfields
546: */
547: public $virtualFields = array();
548:
549: /**
550: * Default list of association keys.
551: *
552: * @var array
553: */
554: protected $_associationKeys = array(
555: 'belongsTo' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'counterCache'),
556: 'hasOne' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'dependent'),
557: 'hasMany' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'dependent', 'exclusive', 'finderQuery', 'counterQuery'),
558: 'hasAndBelongsToMany' => array('className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery')
559: );
560:
561: /**
562: * Holds provided/generated association key names and other data for all associations.
563: *
564: * @var array
565: */
566: protected $_associations = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
567:
568: // @codingStandardsIgnoreStart
569:
570: /**
571: * Holds model associations temporarily to allow for dynamic (un)binding.
572: *
573: * @var array
574: */
575: public $__backAssociation = array();
576:
577: /**
578: * Back inner association
579: *
580: * @var array
581: */
582: public $__backInnerAssociation = array();
583:
584: /**
585: * Back original association
586: *
587: * @var array
588: */
589: public $__backOriginalAssociation = array();
590:
591: /**
592: * Back containable association
593: *
594: * @var array
595: */
596: public $__backContainableAssociation = array();
597:
598: // @codingStandardsIgnoreEnd
599:
600: /**
601: * The ID of the model record that was last inserted.
602: *
603: * @var integer
604: */
605: protected $_insertID = null;
606:
607: /**
608: * Has the datasource been configured.
609: *
610: * @var boolean
611: * @see Model::getDataSource
612: */
613: protected $_sourceConfigured = false;
614:
615: /**
616: * List of valid finder method options, supplied as the first parameter to find().
617: *
618: * @var array
619: */
620: public $findMethods = array(
621: 'all' => true, 'first' => true, 'count' => true,
622: 'neighbors' => true, 'list' => true, 'threaded' => true
623: );
624:
625: /**
626: * Instance of the CakeEventManager this model is using
627: * to dispatch inner events.
628: *
629: * @var CakeEventManager
630: */
631: protected $_eventManager = null;
632:
633: /**
634: * Instance of the ModelValidator
635: *
636: * @var ModelValidator
637: */
638: protected $_validator = null;
639:
640: /**
641: * Constructor. Binds the model's database table to the object.
642: *
643: * If `$id` is an array it can be used to pass several options into the model.
644: *
645: * - `id`: The id to start the model on.
646: * - `table`: The table to use for this model.
647: * - `ds`: The connection name this model is connected to.
648: * - `name`: The name of the model eg. Post.
649: * - `alias`: The alias of the model, this is used for registering the instance in the `ClassRegistry`.
650: * eg. `ParentThread`
651: *
652: * ### Overriding Model's __construct method.
653: *
654: * When overriding Model::__construct() be careful to include and pass in all 3 of the
655: * arguments to `parent::__construct($id, $table, $ds);`
656: *
657: * ### Dynamically creating models
658: *
659: * You can dynamically create model instances using the $id array syntax.
660: *
661: * {{{
662: * $Post = new Model(array('table' => 'posts', 'name' => 'Post', 'ds' => 'connection2'));
663: * }}}
664: *
665: * Would create a model attached to the posts table on connection2. Dynamic model creation is useful
666: * when you want a model object that contains no associations or attached behaviors.
667: *
668: * @param integer|string|array $id Set this ID for this model on startup, can also be an array of options, see above.
669: * @param string $table Name of database table to use.
670: * @param string $ds DataSource connection name.
671: */
672: public function __construct($id = false, $table = null, $ds = null) {
673: parent::__construct();
674:
675: if (is_array($id)) {
676: extract(array_merge(
677: array(
678: 'id' => $this->id, 'table' => $this->useTable, 'ds' => $this->useDbConfig,
679: 'name' => $this->name, 'alias' => $this->alias, 'plugin' => $this->plugin
680: ),
681: $id
682: ));
683: }
684:
685: if ($this->plugin === null) {
686: $this->plugin = (isset($plugin) ? $plugin : $this->plugin);
687: }
688:
689: if ($this->name === null) {
690: $this->name = (isset($name) ? $name : get_class($this));
691: }
692:
693: if ($this->alias === null) {
694: $this->alias = (isset($alias) ? $alias : $this->name);
695: }
696:
697: if ($this->primaryKey === null) {
698: $this->primaryKey = 'id';
699: }
700:
701: ClassRegistry::addObject($this->alias, $this);
702:
703: $this->id = $id;
704: unset($id);
705:
706: if ($table === false) {
707: $this->useTable = false;
708: } elseif ($table) {
709: $this->useTable = $table;
710: }
711:
712: if ($ds !== null) {
713: $this->useDbConfig = $ds;
714: }
715:
716: if (is_subclass_of($this, 'AppModel')) {
717: $merge = array('actsAs', 'findMethods');
718: $parentClass = get_parent_class($this);
719: if ($parentClass !== 'AppModel') {
720: $this->_mergeVars($merge, $parentClass);
721: }
722: $this->_mergeVars($merge, 'AppModel');
723: }
724: $this->_mergeVars(array('findMethods'), 'Model');
725:
726: $this->Behaviors = new BehaviorCollection();
727:
728: if ($this->useTable !== false) {
729:
730: if ($this->useTable === null) {
731: $this->useTable = Inflector::tableize($this->name);
732: }
733:
734: if (!$this->displayField) {
735: unset($this->displayField);
736: }
737: $this->table = $this->useTable;
738: $this->tableToModel[$this->table] = $this->alias;
739: } elseif ($this->table === false) {
740: $this->table = Inflector::tableize($this->name);
741: }
742:
743: if ($this->tablePrefix === null) {
744: unset($this->tablePrefix);
745: }
746:
747: $this->_createLinks();
748: $this->Behaviors->init($this->alias, $this->actsAs);
749: }
750:
751: /**
752: * Returns a list of all events that will fire in the model during it's lifecycle.
753: * You can override this function to add you own listener callbacks
754: *
755: * @return array
756: */
757: public function implementedEvents() {
758: return array(
759: 'Model.beforeFind' => array('callable' => 'beforeFind', 'passParams' => true),
760: 'Model.afterFind' => array('callable' => 'afterFind', 'passParams' => true),
761: 'Model.beforeValidate' => array('callable' => 'beforeValidate', 'passParams' => true),
762: 'Model.afterValidate' => array('callable' => 'afterValidate'),
763: 'Model.beforeSave' => array('callable' => 'beforeSave', 'passParams' => true),
764: 'Model.afterSave' => array('callable' => 'afterSave', 'passParams' => true),
765: 'Model.beforeDelete' => array('callable' => 'beforeDelete', 'passParams' => true),
766: 'Model.afterDelete' => array('callable' => 'afterDelete'),
767: );
768: }
769:
770: /**
771: * Returns the CakeEventManager manager instance that is handling any callbacks.
772: * You can use this instance to register any new listeners or callbacks to the
773: * model events, or create your own events and trigger them at will.
774: *
775: * @return CakeEventManager
776: */
777: public function getEventManager() {
778: if (empty($this->_eventManager)) {
779: $this->_eventManager = new CakeEventManager();
780: $this->_eventManager->attach($this->Behaviors);
781: $this->_eventManager->attach($this);
782: }
783: return $this->_eventManager;
784: }
785:
786: /**
787: * Handles custom method calls, like findBy<field> for DB models,
788: * and custom RPC calls for remote data sources.
789: *
790: * @param string $method Name of method to call.
791: * @param array $params Parameters for the method.
792: * @return mixed Whatever is returned by called method
793: */
794: public function __call($method, $params) {
795: $result = $this->Behaviors->dispatchMethod($this, $method, $params);
796: if ($result !== array('unhandled')) {
797: return $result;
798: }
799: $return = $this->getDataSource()->query($method, $params, $this);
800: return $return;
801: }
802:
803: /**
804: * Handles the lazy loading of model associations by looking in the association arrays for the requested variable
805: *
806: * @param string $name variable tested for existence in class
807: * @return boolean true if the variable exists (if is a not loaded model association it will be created), false otherwise
808: */
809: public function __isset($name) {
810: $className = false;
811:
812: foreach ($this->_associations as $type) {
813: if (isset($name, $this->{$type}[$name])) {
814: $className = empty($this->{$type}[$name]['className']) ? $name : $this->{$type}[$name]['className'];
815: break;
816: } elseif (isset($name, $this->__backAssociation[$type][$name])) {
817: $className = empty($this->__backAssociation[$type][$name]['className']) ?
818: $name : $this->__backAssociation[$type][$name]['className'];
819: break;
820: } elseif ($type === 'hasAndBelongsToMany') {
821: foreach ($this->{$type} as $k => $relation) {
822: if (empty($relation['with'])) {
823: continue;
824: }
825: if (is_array($relation['with'])) {
826: if (key($relation['with']) === $name) {
827: $className = $name;
828: }
829: } else {
830: list($plugin, $class) = pluginSplit($relation['with']);
831: if ($class === $name) {
832: $className = $relation['with'];
833: }
834: }
835: if ($className) {
836: $assocKey = $k;
837: $dynamic = !empty($relation['dynamicWith']);
838: break(2);
839: }
840: }
841: }
842: }
843:
844: if (!$className) {
845: return false;
846: }
847:
848: list($plugin, $className) = pluginSplit($className);
849:
850: if (!ClassRegistry::isKeySet($className) && !empty($dynamic)) {
851: $this->{$className} = new AppModel(array(
852: 'name' => $className,
853: 'table' => $this->hasAndBelongsToMany[$assocKey]['joinTable'],
854: 'ds' => $this->useDbConfig
855: ));
856: } else {
857: $this->_constructLinkedModel($name, $className, $plugin);
858: }
859:
860: if (!empty($assocKey)) {
861: $this->hasAndBelongsToMany[$assocKey]['joinTable'] = $this->{$name}->table;
862: if (count($this->{$name}->schema()) <= 2 && $this->{$name}->primaryKey !== false) {
863: $this->{$name}->primaryKey = $this->hasAndBelongsToMany[$assocKey]['foreignKey'];
864: }
865: }
866:
867: return true;
868: }
869:
870: /**
871: * Returns the value of the requested variable if it can be set by __isset()
872: *
873: * @param string $name variable requested for it's value or reference
874: * @return mixed value of requested variable if it is set
875: */
876: public function __get($name) {
877: if ($name === 'displayField') {
878: return $this->displayField = $this->hasField(array('title', 'name', $this->primaryKey));
879: }
880: if ($name === 'tablePrefix') {
881: $this->setDataSource();
882: if (property_exists($this, 'tablePrefix') && !empty($this->tablePrefix)) {
883: return $this->tablePrefix;
884: }
885: return $this->tablePrefix = null;
886: }
887: if (isset($this->{$name})) {
888: return $this->{$name};
889: }
890: }
891:
892: /**
893: * Bind model associations on the fly.
894: *
895: * If `$reset` is false, association will not be reset
896: * to the originals defined in the model
897: *
898: * Example: Add a new hasOne binding to the Profile model not
899: * defined in the model source code:
900: *
901: * `$this->User->bindModel( array('hasOne' => array('Profile')) );`
902: *
903: * Bindings that are not made permanent will be reset by the next Model::find() call on this
904: * model.
905: *
906: * @param array $params Set of bindings (indexed by binding type)
907: * @param boolean $reset Set to false to make the binding permanent
908: * @return boolean Success
909: * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly
910: */
911: public function bindModel($params, $reset = true) {
912: foreach ($params as $assoc => $model) {
913: if ($reset === true && !isset($this->__backAssociation[$assoc])) {
914: $this->__backAssociation[$assoc] = $this->{$assoc};
915: }
916: foreach ($model as $key => $value) {
917: $assocName = $key;
918:
919: if (is_numeric($key)) {
920: $assocName = $value;
921: $value = array();
922: }
923: $this->{$assoc}[$assocName] = $value;
924: if (property_exists($this, $assocName)) {
925: unset($this->{$assocName});
926: }
927: if ($reset === false && isset($this->__backAssociation[$assoc])) {
928: $this->__backAssociation[$assoc][$assocName] = $value;
929: }
930: }
931: }
932: $this->_createLinks();
933: return true;
934: }
935:
936: /**
937: * Turn off associations on the fly.
938: *
939: * If $reset is false, association will not be reset
940: * to the originals defined in the model
941: *
942: * Example: Turn off the associated Model Support request,
943: * to temporarily lighten the User model:
944: *
945: * `$this->User->unbindModel( array('hasMany' => array('Supportrequest')) );`
946: *
947: * unbound models that are not made permanent will reset with the next call to Model::find()
948: *
949: * @param array $params Set of bindings to unbind (indexed by binding type)
950: * @param boolean $reset Set to false to make the unbinding permanent
951: * @return boolean Success
952: * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly
953: */
954: public function unbindModel($params, $reset = true) {
955: foreach ($params as $assoc => $models) {
956: if ($reset === true && !isset($this->__backAssociation[$assoc])) {
957: $this->__backAssociation[$assoc] = $this->{$assoc};
958: }
959: foreach ($models as $model) {
960: if ($reset === false && isset($this->__backAssociation[$assoc][$model])) {
961: unset($this->__backAssociation[$assoc][$model]);
962: }
963: unset($this->{$assoc}[$model]);
964: }
965: }
966: return true;
967: }
968:
969: /**
970: * Create a set of associations.
971: *
972: * @return void
973: */
974: protected function _createLinks() {
975: foreach ($this->_associations as $type) {
976: if (!is_array($this->{$type})) {
977: $this->{$type} = explode(',', $this->{$type});
978:
979: foreach ($this->{$type} as $i => $className) {
980: $className = trim($className);
981: unset ($this->{$type}[$i]);
982: $this->{$type}[$className] = array();
983: }
984: }
985:
986: if (!empty($this->{$type})) {
987: foreach ($this->{$type} as $assoc => $value) {
988: $plugin = null;
989:
990: if (is_numeric($assoc)) {
991: unset($this->{$type}[$assoc]);
992: $assoc = $value;
993: $value = array();
994:
995: if (strpos($assoc, '.') !== false) {
996: list($plugin, $assoc) = pluginSplit($assoc, true);
997: $this->{$type}[$assoc] = array('className' => $plugin . $assoc);
998: } else {
999: $this->{$type}[$assoc] = $value;
1000: }
1001: }
1002: $this->_generateAssociation($type, $assoc);
1003: }
1004: }
1005: }
1006: }
1007:
1008: /**
1009: * Protected helper method to create associated models of a given class.
1010: *
1011: * @param string $assoc Association name
1012: * @param string $className Class name
1013: * @param string $plugin name of the plugin where $className is located
1014: * examples: public $hasMany = array('Assoc' => array('className' => 'ModelName'));
1015: * usage: $this->Assoc->modelMethods();
1016: *
1017: * public $hasMany = array('ModelName');
1018: * usage: $this->ModelName->modelMethods();
1019: * @return void
1020: */
1021: protected function _constructLinkedModel($assoc, $className = null, $plugin = null) {
1022: if (empty($className)) {
1023: $className = $assoc;
1024: }
1025:
1026: if (!isset($this->{$assoc}) || $this->{$assoc}->name !== $className) {
1027: if ($plugin) {
1028: $plugin .= '.';
1029: }
1030: $model = array('class' => $plugin . $className, 'alias' => $assoc);
1031: $this->{$assoc} = ClassRegistry::init($model);
1032: if ($plugin) {
1033: ClassRegistry::addObject($plugin . $className, $this->{$assoc});
1034: }
1035: if ($assoc) {
1036: $this->tableToModel[$this->{$assoc}->table] = $assoc;
1037: }
1038: }
1039: }
1040:
1041: /**
1042: * Build an array-based association from string.
1043: *
1044: * @param string $type 'belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'
1045: * @param string $assocKey
1046: * @return void
1047: */
1048: protected function _generateAssociation($type, $assocKey) {
1049: $class = $assocKey;
1050: $dynamicWith = false;
1051:
1052: foreach ($this->_associationKeys[$type] as $key) {
1053:
1054: if (!isset($this->{$type}[$assocKey][$key]) || $this->{$type}[$assocKey][$key] === null) {
1055: $data = '';
1056:
1057: switch ($key) {
1058: case 'fields':
1059: $data = '';
1060: break;
1061:
1062: case 'foreignKey':
1063: $data = (($type === 'belongsTo') ? Inflector::underscore($assocKey) : Inflector::singularize($this->table)) . '_id';
1064: break;
1065:
1066: case 'associationForeignKey':
1067: $data = Inflector::singularize($this->{$class}->table) . '_id';
1068: break;
1069:
1070: case 'with':
1071: $data = Inflector::camelize(Inflector::singularize($this->{$type}[$assocKey]['joinTable']));
1072: $dynamicWith = true;
1073: break;
1074:
1075: case 'joinTable':
1076: $tables = array($this->table, $this->{$class}->table);
1077: sort($tables);
1078: $data = $tables[0] . '_' . $tables[1];
1079: break;
1080:
1081: case 'className':
1082: $data = $class;
1083: break;
1084:
1085: case 'unique':
1086: $data = true;
1087: break;
1088: }
1089: $this->{$type}[$assocKey][$key] = $data;
1090: }
1091:
1092: if ($dynamicWith) {
1093: $this->{$type}[$assocKey]['dynamicWith'] = true;
1094: }
1095:
1096: }
1097: }
1098:
1099: /**
1100: * Sets a custom table for your model class. Used by your controller to select a database table.
1101: *
1102: * @param string $tableName Name of the custom table
1103: * @throws MissingTableException when database table $tableName is not found on data source
1104: * @return void
1105: */
1106: public function setSource($tableName) {
1107: $this->setDataSource($this->useDbConfig);
1108: $db = ConnectionManager::getDataSource($this->useDbConfig);
1109: $db->cacheSources = ($this->cacheSources && $db->cacheSources);
1110:
1111: if (method_exists($db, 'listSources')) {
1112: $sources = $db->listSources();
1113: if (is_array($sources) && !in_array(strtolower($this->tablePrefix . $tableName), array_map('strtolower', $sources))) {
1114: throw new MissingTableException(array(
1115: 'table' => $this->tablePrefix . $tableName,
1116: 'class' => $this->alias,
1117: 'ds' => $this->useDbConfig,
1118: ));
1119: }
1120: $this->_schema = null;
1121: }
1122: $this->table = $this->useTable = $tableName;
1123: $this->tableToModel[$this->table] = $this->alias;
1124: }
1125:
1126: /**
1127: * This function does two things:
1128: *
1129: * 1. it scans the array $one for the primary key,
1130: * and if that's found, it sets the current id to the value of $one[id].
1131: * For all other keys than 'id' the keys and values of $one are copied to the 'data' property of this object.
1132: * 2. Returns an array with all of $one's keys and values.
1133: * (Alternative indata: two strings, which are mangled to
1134: * a one-item, two-dimensional array using $one for a key and $two as its value.)
1135: *
1136: * @param string|array|SimpleXmlElement|DomNode $one Array or string of data
1137: * @param string $two Value string for the alternative indata method
1138: * @return array Data with all of $one's keys and values
1139: * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html
1140: */
1141: public function set($one, $two = null) {
1142: if (!$one) {
1143: return;
1144: }
1145: if (is_object($one)) {
1146: if ($one instanceof SimpleXMLElement || $one instanceof DOMNode) {
1147: $one = $this->_normalizeXmlData(Xml::toArray($one));
1148: } else {
1149: $one = Set::reverse($one);
1150: }
1151: }
1152:
1153: if (is_array($one)) {
1154: $data = $one;
1155: if (empty($one[$this->alias])) {
1156: $data = $this->_setAliasData($one);
1157: }
1158: } else {
1159: $data = array($this->alias => array($one => $two));
1160: }
1161:
1162: foreach ($data as $modelName => $fieldSet) {
1163: if (is_array($fieldSet)) {
1164:
1165: foreach ($fieldSet as $fieldName => $fieldValue) {
1166: if (isset($this->validationErrors[$fieldName])) {
1167: unset($this->validationErrors[$fieldName]);
1168: }
1169:
1170: if ($modelName === $this->alias) {
1171: if ($fieldName === $this->primaryKey) {
1172: $this->id = $fieldValue;
1173: }
1174: }
1175: if (is_array($fieldValue) || is_object($fieldValue)) {
1176: $fieldValue = $this->deconstruct($fieldName, $fieldValue);
1177: }
1178: $this->data[$modelName][$fieldName] = $fieldValue;
1179: }
1180: }
1181: }
1182: return $data;
1183: }
1184:
1185: /**
1186: * Move values to alias
1187: *
1188: * @param array $data
1189: * @return array
1190: */
1191: protected function _setAliasData($data) {
1192: $models = array_keys($this->getAssociated());
1193: $schema = array_keys((array)$this->schema());
1194: foreach ($data as $field => $value) {
1195: if (in_array($field, $schema) || !in_array($field, $models)) {
1196: $data[$this->alias][$field] = $value;
1197: unset($data[$field]);
1198: }
1199: }
1200: return $data;
1201: }
1202:
1203: /**
1204: * Normalize `Xml::toArray()` to use in `Model::save()`
1205: *
1206: * @param array $xml XML as array
1207: * @return array
1208: */
1209: protected function _normalizeXmlData(array $xml) {
1210: $return = array();
1211: foreach ($xml as $key => $value) {
1212: if (is_array($value)) {
1213: $return[Inflector::camelize($key)] = $this->_normalizeXmlData($value);
1214: } elseif ($key[0] === '@') {
1215: $return[substr($key, 1)] = $value;
1216: } else {
1217: $return[$key] = $value;
1218: }
1219: }
1220: return $return;
1221: }
1222:
1223: /**
1224: * Deconstructs a complex data type (array or object) into a single field value.
1225: *
1226: * @param string $field The name of the field to be deconstructed
1227: * @param array|object $data An array or object to be deconstructed into a field
1228: * @return mixed The resulting data that should be assigned to a field
1229: */
1230: public function deconstruct($field, $data) {
1231: if (!is_array($data)) {
1232: return $data;
1233: }
1234:
1235: $type = $this->getColumnType($field);
1236:
1237: if (!in_array($type, array('datetime', 'timestamp', 'date', 'time'))) {
1238: return $data;
1239: }
1240:
1241: $useNewDate = (isset($data['year']) || isset($data['month']) ||
1242: isset($data['day']) || isset($data['hour']) || isset($data['minute']));
1243:
1244: $dateFields = array('Y' => 'year', 'm' => 'month', 'd' => 'day', 'H' => 'hour', 'i' => 'min', 's' => 'sec');
1245: $timeFields = array('H' => 'hour', 'i' => 'min', 's' => 'sec');
1246: $date = array();
1247:
1248: if (isset($data['meridian']) && empty($data['meridian'])) {
1249: return null;
1250: }
1251:
1252: if (
1253: isset($data['hour']) &&
1254: isset($data['meridian']) &&
1255: !empty($data['hour']) &&
1256: $data['hour'] != 12 &&
1257: $data['meridian'] === 'pm'
1258: ) {
1259: $data['hour'] = $data['hour'] + 12;
1260: }
1261: if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] == 12 && $data['meridian'] === 'am') {
1262: $data['hour'] = '00';
1263: }
1264: if ($type === 'time') {
1265: foreach ($timeFields as $key => $val) {
1266: if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') {
1267: $data[$val] = '00';
1268: } elseif ($data[$val] !== '') {
1269: $data[$val] = sprintf('%02d', $data[$val]);
1270: }
1271: if (!empty($data[$val])) {
1272: $date[$key] = $data[$val];
1273: } else {
1274: return null;
1275: }
1276: }
1277: }
1278:
1279: if ($type === 'datetime' || $type === 'timestamp' || $type === 'date') {
1280: foreach ($dateFields as $key => $val) {
1281: if ($val === 'hour' || $val === 'min' || $val === 'sec') {
1282: if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') {
1283: $data[$val] = '00';
1284: } else {
1285: $data[$val] = sprintf('%02d', $data[$val]);
1286: }
1287: }
1288: if (!isset($data[$val]) || isset($data[$val]) && (empty($data[$val]) || $data[$val][0] === '-')) {
1289: return null;
1290: }
1291: if (isset($data[$val]) && !empty($data[$val])) {
1292: $date[$key] = $data[$val];
1293: }
1294: }
1295: }
1296:
1297: if ($useNewDate && !empty($date)) {
1298: $format = $this->getDataSource()->columns[$type]['format'];
1299: foreach (array('m', 'd', 'H', 'i', 's') as $index) {
1300: if (isset($date[$index])) {
1301: $date[$index] = sprintf('%02d', $date[$index]);
1302: }
1303: }
1304: return str_replace(array_keys($date), array_values($date), $format);
1305: }
1306: return $data;
1307: }
1308:
1309: /**
1310: * Returns an array of table metadata (column names and types) from the database.
1311: * $field => keys(type, null, default, key, length, extra)
1312: *
1313: * @param boolean|string $field Set to true to reload schema, or a string to return a specific field
1314: * @return array Array of table metadata
1315: */
1316: public function schema($field = false) {
1317: if ($this->useTable !== false && (!is_array($this->_schema) || $field === true)) {
1318: $db = $this->getDataSource();
1319: $db->cacheSources = ($this->cacheSources && $db->cacheSources);
1320: if (method_exists($db, 'describe')) {
1321: $this->_schema = $db->describe($this);
1322: }
1323: }
1324: if (!is_string($field)) {
1325: return $this->_schema;
1326: }
1327: if (isset($this->_schema[$field])) {
1328: return $this->_schema[$field];
1329: }
1330: return null;
1331: }
1332:
1333: /**
1334: * Returns an associative array of field names and column types.
1335: *
1336: * @return array Field types indexed by field name
1337: */
1338: public function getColumnTypes() {
1339: $columns = $this->schema();
1340: if (empty($columns)) {
1341: trigger_error(__d('cake_dev', '(Model::getColumnTypes) Unable to build model field data. If you are using a model without a database table, try implementing schema()'), E_USER_WARNING);
1342: }
1343: $cols = array();
1344: foreach ($columns as $field => $values) {
1345: $cols[$field] = $values['type'];
1346: }
1347: return $cols;
1348: }
1349:
1350: /**
1351: * Returns the column type of a column in the model.
1352: *
1353: * @param string $column The name of the model column
1354: * @return string Column type
1355: */
1356: public function getColumnType($column) {
1357: $db = $this->getDataSource();
1358: $cols = $this->schema();
1359: $model = null;
1360:
1361: $startQuote = isset($db->startQuote) ? $db->startQuote : null;
1362: $endQuote = isset($db->endQuote) ? $db->endQuote : null;
1363: $column = str_replace(array($startQuote, $endQuote), '', $column);
1364:
1365: if (strpos($column, '.')) {
1366: list($model, $column) = explode('.', $column);
1367: }
1368: if ($model != $this->alias && isset($this->{$model})) {
1369: return $this->{$model}->getColumnType($column);
1370: }
1371: if (isset($cols[$column]) && isset($cols[$column]['type'])) {
1372: return $cols[$column]['type'];
1373: }
1374: return null;
1375: }
1376:
1377: /**
1378: * Returns true if the supplied field exists in the model's database table.
1379: *
1380: * @param string|array $name Name of field to look for, or an array of names
1381: * @param boolean $checkVirtual checks if the field is declared as virtual
1382: * @return mixed If $name is a string, returns a boolean indicating whether the field exists.
1383: * If $name is an array of field names, returns the first field that exists,
1384: * or false if none exist.
1385: */
1386: public function hasField($name, $checkVirtual = false) {
1387: if (is_array($name)) {
1388: foreach ($name as $n) {
1389: if ($this->hasField($n, $checkVirtual)) {
1390: return $n;
1391: }
1392: }
1393: return false;
1394: }
1395:
1396: if ($checkVirtual && !empty($this->virtualFields) && $this->isVirtualField($name)) {
1397: return true;
1398: }
1399:
1400: if (empty($this->_schema)) {
1401: $this->schema();
1402: }
1403:
1404: if ($this->_schema) {
1405: return isset($this->_schema[$name]);
1406: }
1407: return false;
1408: }
1409:
1410: /**
1411: * Check that a method is callable on a model. This will check both the model's own methods, its
1412: * inherited methods and methods that could be callable through behaviors.
1413: *
1414: * @param string $method The method to be called.
1415: * @return boolean True on method being callable.
1416: */
1417: public function hasMethod($method) {
1418: if (method_exists($this, $method)) {
1419: return true;
1420: }
1421: return $this->Behaviors->hasMethod($method);
1422: }
1423:
1424: /**
1425: * Returns true if the supplied field is a model Virtual Field
1426: *
1427: * @param string $field Name of field to look for
1428: * @return boolean indicating whether the field exists as a model virtual field.
1429: */
1430: public function isVirtualField($field) {
1431: if (empty($this->virtualFields) || !is_string($field)) {
1432: return false;
1433: }
1434: if (isset($this->virtualFields[$field])) {
1435: return true;
1436: }
1437: if (strpos($field, '.') !== false) {
1438: list($model, $field) = explode('.', $field);
1439: if ($model == $this->alias && isset($this->virtualFields[$field])) {
1440: return true;
1441: }
1442: }
1443: return false;
1444: }
1445:
1446: /**
1447: * Returns the expression for a model virtual field
1448: *
1449: * @param string $field Name of field to look for
1450: * @return mixed If $field is string expression bound to virtual field $field
1451: * If $field is null, returns an array of all model virtual fields
1452: * or false if none $field exist.
1453: */
1454: public function getVirtualField($field = null) {
1455: if (!$field) {
1456: return empty($this->virtualFields) ? false : $this->virtualFields;
1457: }
1458: if ($this->isVirtualField($field)) {
1459: if (strpos($field, '.') !== false) {
1460: list(, $field) = pluginSplit($field);
1461: }
1462: return $this->virtualFields[$field];
1463: }
1464: return false;
1465: }
1466:
1467: /**
1468: * Initializes the model for writing a new record, loading the default values
1469: * for those fields that are not defined in $data, and clearing previous validation errors.
1470: * Especially helpful for saving data in loops.
1471: *
1472: * @param boolean|array $data Optional data array to assign to the model after it is created. If null or false,
1473: * schema data defaults are not merged.
1474: * @param boolean $filterKey If true, overwrites any primary key input with an empty value
1475: * @return array The current Model::data; after merging $data and/or defaults from database
1476: * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-create-array-data-array
1477: */
1478: public function create($data = array(), $filterKey = false) {
1479: $defaults = array();
1480: $this->id = false;
1481: $this->data = array();
1482: $this->validationErrors = array();
1483:
1484: if ($data !== null && $data !== false) {
1485: $schema = (array)$this->schema();
1486: foreach ($schema as $field => $properties) {
1487: if ($this->primaryKey !== $field && isset($properties['default']) && $properties['default'] !== '') {
1488: $defaults[$field] = $properties['default'];
1489: }
1490: }
1491: $this->set($defaults);
1492: $this->set($data);
1493: }
1494: if ($filterKey) {
1495: $this->set($this->primaryKey, false);
1496: }
1497: return $this->data;
1498: }
1499:
1500: /**
1501: * Returns a list of fields from the database, and sets the current model
1502: * data (Model::$data) with the record found.
1503: *
1504: * @param string|array $fields String of single field name, or an array of field names.
1505: * @param integer|string $id The ID of the record to read
1506: * @return array Array of database fields, or false if not found
1507: * @link http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-read
1508: */
1509: public function read($fields = null, $id = null) {
1510: $this->validationErrors = array();
1511:
1512: if ($id) {
1513: $this->id = $id;
1514: }
1515:
1516: $id = $this->id;
1517:
1518: if (is_array($this->id)) {
1519: $id = $this->id[0];
1520: }
1521:
1522: if ($id !== null && $id !== false) {
1523: $this->data = $this->find('first', array(
1524: 'conditions' => array($this->alias . '.' . $this->primaryKey => $id),
1525: 'fields' => $fields
1526: ));
1527: return $this->data;
1528: }
1529: return false;
1530: }
1531:
1532: /**
1533: * Returns the contents of a single field given the supplied conditions, in the
1534: * supplied order.
1535: *
1536: * @param string $name Name of field to get
1537: * @param array $conditions SQL conditions (defaults to NULL)
1538: * @param string $order SQL ORDER BY fragment
1539: * @return string field contents, or false if not found
1540: * @link http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-field
1541: */
1542: public function field($name, $conditions = null, $order = null) {
1543: if ($conditions === null && $this->id !== false) {
1544: $conditions = array($this->alias . '.' . $this->primaryKey => $this->id);
1545: }
1546: $recursive = $this->recursive;
1547: if ($this->recursive >= 1) {
1548: $recursive = -1;
1549: }
1550: $fields = $name;
1551: $data = $this->find('first', compact('conditions', 'fields', 'order', 'recursive'));
1552: if (!$data) {
1553: return false;
1554: }
1555: if (strpos($name, '.') === false) {
1556: if (isset($data[$this->alias][$name])) {
1557: return $data[$this->alias][$name];
1558: }
1559: } else {
1560: $name = explode('.', $name);
1561: if (isset($data[$name[0]][$name[1]])) {
1562: return $data[$name[0]][$name[1]];
1563: }
1564: }
1565: if (isset($data[0]) && count($data[0]) > 0) {
1566: return array_shift($data[0]);
1567: }
1568: }
1569:
1570: /**
1571: * Saves the value of a single field to the database, based on the current
1572: * model ID.
1573: *
1574: * @param string $name Name of the table field
1575: * @param mixed $value Value of the field
1576: * @param boolean|array $validate Either a boolean, or an array.
1577: * If a boolean, indicates whether or not to validate before saving.
1578: * If an array, allows control of 'validate' and 'callbacks' options.
1579: * @return boolean See Model::save()
1580: * @see Model::save()
1581: * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-savefield-string-fieldname-string-fieldvalue-validate-false
1582: */
1583: public function saveField($name, $value, $validate = false) {
1584: $id = $this->id;
1585: $this->create(false);
1586:
1587: $options = array('validate' => $validate, 'fieldList' => array($name));
1588: if (is_array($validate)) {
1589: $options = array_merge(array('validate' => false, 'fieldList' => array($name)), $validate);
1590: }
1591: return $this->save(array($this->alias => array($this->primaryKey => $id, $name => $value)), $options);
1592: }
1593:
1594: /**
1595: * Saves model data (based on white-list, if supplied) to the database. By
1596: * default, validation occurs before save.
1597: *
1598: * @param array $data Data to save.
1599: * @param boolean|array $validate Either a boolean, or an array.
1600: * If a boolean, indicates whether or not to validate before saving.
1601: * If an array, can have following keys:
1602: *
1603: * - validate: Set to true/false to enable or disable validation.
1604: * - fieldList: An array of fields you want to allow for saving.
1605: * - callbacks: Set to false to disable callbacks. Using 'before' or 'after'
1606: * will enable only those callbacks.
1607: *
1608: * @param array $fieldList List of fields to allow to be saved
1609: * @return mixed On success Model::$data if its not empty or true, false on failure
1610: * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html
1611: */
1612: public function save($data = null, $validate = true, $fieldList = array()) {
1613: $defaults = array('validate' => true, 'fieldList' => array(), 'callbacks' => true);
1614: $_whitelist = $this->whitelist;
1615: $fields = array();
1616:
1617: if (!is_array($validate)) {
1618: $options = array_merge($defaults, compact('validate', 'fieldList'));
1619: } else {
1620: $options = array_merge($defaults, $validate);
1621: }
1622:
1623: if (!empty($options['fieldList'])) {
1624: if (!empty($options['fieldList'][$this->alias]) && is_array($options['fieldList'][$this->alias])) {
1625: $this->whitelist = $options['fieldList'][$this->alias];
1626: } elseif (Hash::dimensions($options['fieldList']) < 2) {
1627: $this->whitelist = $options['fieldList'];
1628: }
1629: } elseif ($options['fieldList'] === null) {
1630: $this->whitelist = array();
1631: }
1632: $this->set($data);
1633:
1634: if (empty($this->data) && !$this->hasField(array('created', 'updated', 'modified'))) {
1635: $this->whitelist = $_whitelist;
1636: return false;
1637: }
1638:
1639: foreach (array('created', 'updated', 'modified') as $field) {
1640: $keyPresentAndEmpty = (
1641: isset($this->data[$this->alias]) &&
1642: array_key_exists($field, $this->data[$this->alias]) &&
1643: $this->data[$this->alias][$field] === null
1644: );
1645: if ($keyPresentAndEmpty) {
1646: unset($this->data[$this->alias][$field]);
1647: }
1648: }
1649:
1650: $exists = $this->exists();
1651: $dateFields = array('modified', 'updated');
1652:
1653: if (!$exists) {
1654: $dateFields[] = 'created';
1655: }
1656: if (isset($this->data[$this->alias])) {
1657: $fields = array_keys($this->data[$this->alias]);
1658: }
1659: if ($options['validate'] && !$this->validates($options)) {
1660: $this->whitelist = $_whitelist;
1661: return false;
1662: }
1663:
1664: $db = $this->getDataSource();
1665: $now = time();
1666:
1667: foreach ($dateFields as $updateCol) {
1668: if ($this->hasField($updateCol) && !in_array($updateCol, $fields)) {
1669: $default = array('formatter' => 'date');
1670: $colType = array_merge($default, $db->columns[$this->getColumnType($updateCol)]);
1671: if (!array_key_exists('format', $colType)) {
1672: $time = $now;
1673: } else {
1674: $time = call_user_func($colType['formatter'], $colType['format']);
1675: }
1676: if (!empty($this->whitelist)) {
1677: $this->whitelist[] = $updateCol;
1678: }
1679: $this->set($updateCol, $time);
1680: }
1681: }
1682:
1683: if ($options['callbacks'] === true || $options['callbacks'] === 'before') {
1684: $event = new CakeEvent('Model.beforeSave', $this, array($options));
1685: list($event->break, $event->breakOn) = array(true, array(false, null));
1686: $this->getEventManager()->dispatch($event);
1687: if (!$event->result) {
1688: $this->whitelist = $_whitelist;
1689: return false;
1690: }
1691: }
1692:
1693: $db = $this->getDataSource();
1694:
1695: if (empty($this->data[$this->alias][$this->primaryKey])) {
1696: unset($this->data[$this->alias][$this->primaryKey]);
1697: }
1698: $fields = $values = array();
1699:
1700: foreach ($this->data as $n => $v) {
1701: if (isset($this->hasAndBelongsToMany[$n])) {
1702: if (isset($v[$n])) {
1703: $v = $v[$n];
1704: }
1705: $joined[$n] = $v;
1706: } else {
1707: if ($n === $this->alias) {
1708: foreach (array('created', 'updated', 'modified') as $field) {
1709: if (array_key_exists($field, $v) && empty($v[$field])) {
1710: unset($v[$field]);
1711: }
1712: }
1713:
1714: foreach ($v as $x => $y) {
1715: if ($this->hasField($x) && (empty($this->whitelist) || in_array($x, $this->whitelist))) {
1716: list($fields[], $values[]) = array($x, $y);
1717: }
1718: }
1719: }
1720: }
1721: }
1722: $count = count($fields);
1723:
1724: if (!$exists && $count > 0) {
1725: $this->id = false;
1726: }
1727: $success = true;
1728: $created = false;
1729:
1730: if ($count > 0) {
1731: $cache = $this->_prepareUpdateFields(array_combine($fields, $values));
1732:
1733: if (!empty($this->id)) {
1734: $success = (bool)$db->update($this, $fields, $values);
1735: } else {
1736: if (empty($this->data[$this->alias][$this->primaryKey]) && $this->_isUUIDField($this->primaryKey)) {
1737: if (array_key_exists($this->primaryKey, $this->data[$this->alias])) {
1738: $j = array_search($this->primaryKey, $fields);
1739: $values[$j] = String::uuid();
1740: } else {
1741: list($fields[], $values[]) = array($this->primaryKey, String::uuid());
1742: }
1743: }
1744:
1745: if (!$db->create($this, $fields, $values)) {
1746: $success = false;
1747: } else {
1748: $created = true;
1749: }
1750: }
1751:
1752: if ($success && !empty($this->belongsTo)) {
1753: $this->updateCounterCache($cache, $created);
1754: }
1755: }
1756:
1757: if (!empty($joined) && $success === true) {
1758: $this->_saveMulti($joined, $this->id, $db);
1759: }
1760:
1761: if ($success && $count === 0) {
1762: $success = false;
1763: }
1764:
1765: if ($success && $count > 0) {
1766: if (!empty($this->data)) {
1767: if ($created) {
1768: $this->data[$this->alias][$this->primaryKey] = $this->id;
1769: }
1770: }
1771: if ($options['callbacks'] === true || $options['callbacks'] === 'after') {
1772: $event = new CakeEvent('Model.afterSave', $this, array($created, $options));
1773: $this->getEventManager()->dispatch($event);
1774: }
1775: if (!empty($this->data)) {
1776: $success = $this->data;
1777: }
1778: $this->data = false;
1779: $this->_clearCache();
1780: $this->validationErrors = array();
1781: }
1782: $this->whitelist = $_whitelist;
1783: return $success;
1784: }
1785:
1786: /**
1787: * Check if the passed in field is a UUID field
1788: *
1789: * @param string $field the field to check
1790: * @return boolean
1791: */
1792: protected function _isUUIDField($field) {
1793: $field = $this->schema($field);
1794: return $field['length'] == 36 && in_array($field['type'], array('string', 'binary'));
1795: }
1796:
1797: /**
1798: * Saves model hasAndBelongsToMany data to the database.
1799: *
1800: * @param array $joined Data to save
1801: * @param integer|string $id ID of record in this model
1802: * @param DataSource $db
1803: * @return void
1804: */
1805: protected function _saveMulti($joined, $id, $db) {
1806: foreach ($joined as $assoc => $data) {
1807:
1808: if (isset($this->hasAndBelongsToMany[$assoc])) {
1809: list($join) = $this->joinModel($this->hasAndBelongsToMany[$assoc]['with']);
1810:
1811: if ($with = $this->hasAndBelongsToMany[$assoc]['with']) {
1812: $withModel = is_array($with) ? key($with) : $with;
1813: list(, $withModel) = pluginSplit($withModel);
1814: $dbMulti = $this->{$withModel}->getDataSource();
1815: } else {
1816: $dbMulti = $db;
1817: }
1818:
1819: $isUUID = !empty($this->{$join}->primaryKey) && $this->{$join}->_isUUIDField($this->{$join}->primaryKey);
1820:
1821: $newData = $newValues = $newJoins = array();
1822: $primaryAdded = false;
1823:
1824: $fields = array(
1825: $dbMulti->name($this->hasAndBelongsToMany[$assoc]['foreignKey']),
1826: $dbMulti->name($this->hasAndBelongsToMany[$assoc]['associationForeignKey'])
1827: );
1828:
1829: $idField = $db->name($this->{$join}->primaryKey);
1830: if ($isUUID && !in_array($idField, $fields)) {
1831: $fields[] = $idField;
1832: $primaryAdded = true;
1833: }
1834:
1835: foreach ((array)$data as $row) {
1836: if ((is_string($row) && (strlen($row) == 36 || strlen($row) == 16)) || is_numeric($row)) {
1837: $newJoins[] = $row;
1838: $values = array($id, $row);
1839: if ($isUUID && $primaryAdded) {
1840: $values[] = String::uuid();
1841: }
1842: $newValues[$row] = $values;
1843: unset($values);
1844: } elseif (isset($row[$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
1845: if (!empty($row[$this->{$join}->primaryKey])) {
1846: $newJoins[] = $row[$this->hasAndBelongsToMany[$assoc]['associationForeignKey']];
1847: }
1848: $newData[] = $row;
1849: } elseif (isset($row[$join]) && isset($row[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
1850: if (!empty($row[$join][$this->{$join}->primaryKey])) {
1851: $newJoins[] = $row[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']];
1852: }
1853: $newData[] = $row[$join];
1854: }
1855: }
1856:
1857: $keepExisting = $this->hasAndBelongsToMany[$assoc]['unique'] === 'keepExisting';
1858: if ($this->hasAndBelongsToMany[$assoc]['unique']) {
1859: $conditions = array(
1860: $join . '.' . $this->hasAndBelongsToMany[$assoc]['foreignKey'] => $id
1861: );
1862: if (!empty($this->hasAndBelongsToMany[$assoc]['conditions'])) {
1863: $conditions = array_merge($conditions, (array)$this->hasAndBelongsToMany[$assoc]['conditions']);
1864: }
1865: $associationForeignKey = $this->{$join}->alias . '.' . $this->hasAndBelongsToMany[$assoc]['associationForeignKey'];
1866: $links = $this->{$join}->find('all', array(
1867: 'conditions' => $conditions,
1868: 'recursive' => empty($this->hasAndBelongsToMany[$assoc]['conditions']) ? -1 : 0,
1869: 'fields' => $associationForeignKey,
1870: ));
1871:
1872: $oldLinks = Hash::extract($links, "{n}.{$associationForeignKey}");
1873: if (!empty($oldLinks)) {
1874: if ($keepExisting && !empty($newJoins)) {
1875: $conditions[$associationForeignKey] = array_diff($oldLinks, $newJoins);
1876: } else {
1877: $conditions[$associationForeignKey] = $oldLinks;
1878: }
1879: $dbMulti->delete($this->{$join}, $conditions);
1880: }
1881: }
1882:
1883: if (!empty($newData)) {
1884: foreach ($newData as $data) {
1885: $data[$this->hasAndBelongsToMany[$assoc]['foreignKey']] = $id;
1886: if (empty($data[$this->{$join}->primaryKey])) {
1887: $this->{$join}->create();
1888: }
1889: $this->{$join}->save($data);
1890: }
1891: }
1892:
1893: if (!empty($newValues)) {
1894: if ($keepExisting && !empty($links)) {
1895: foreach ($links as $link) {
1896: $oldJoin = $link[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']];
1897: if (!in_array($oldJoin, $newJoins)) {
1898: $conditions[$associationForeignKey] = $oldJoin;
1899: $db->delete($this->{$join}, $conditions);
1900: } else {
1901: unset($newValues[$oldJoin]);
1902: }
1903: }
1904: $newValues = array_values($newValues);
1905: }
1906: if (!empty($newValues)) {
1907: $dbMulti->insertMulti($this->{$join}, $fields, $newValues);
1908: }
1909: }
1910: }
1911: }
1912: }
1913:
1914: /**
1915: * Updates the counter cache of belongsTo associations after a save or delete operation
1916: *
1917: * @param array $keys Optional foreign key data, defaults to the information $this->data
1918: * @param boolean $created True if a new record was created, otherwise only associations with
1919: * 'counterScope' defined get updated
1920: * @return void
1921: */
1922: public function updateCounterCache($keys = array(), $created = false) {
1923: $keys = empty($keys) ? $this->data[$this->alias] : $keys;
1924: $keys['old'] = isset($keys['old']) ? $keys['old'] : array();
1925:
1926: foreach ($this->belongsTo as $parent => $assoc) {
1927: if (!empty($assoc['counterCache'])) {
1928: if (!is_array($assoc['counterCache'])) {
1929: if (isset($assoc['counterScope'])) {
1930: $assoc['counterCache'] = array($assoc['counterCache'] => $assoc['counterScope']);
1931: } else {
1932: $assoc['counterCache'] = array($assoc['counterCache'] => array());
1933: }
1934: }
1935:
1936: $foreignKey = $assoc['foreignKey'];
1937: $fkQuoted = $this->escapeField($assoc['foreignKey']);
1938:
1939: foreach ($assoc['counterCache'] as $field => $conditions) {
1940: if (!is_string($field)) {
1941: $field = Inflector::underscore($this->alias) . '_count';
1942: }
1943: if (!$this->{$parent}->hasField($field)) {
1944: continue;
1945: }
1946: if ($conditions === true) {
1947: $conditions = array();
1948: } else {
1949: $conditions = (array)$conditions;
1950: }
1951:
1952: if (!array_key_exists($foreignKey, $keys)) {
1953: $keys[$foreignKey] = $this->field($foreignKey);
1954: }
1955: $recursive = (empty($conditions) ? -1 : 0);
1956:
1957: if (isset($keys['old'][$foreignKey])) {
1958: if ($keys['old'][$foreignKey] != $keys[$foreignKey]) {
1959: $conditions[$fkQuoted] = $keys['old'][$foreignKey];
1960: $count = intval($this->find('count', compact('conditions', 'recursive')));
1961:
1962: $this->{$parent}->updateAll(
1963: array($field => $count),
1964: array($this->{$parent}->escapeField() => $keys['old'][$foreignKey])
1965: );
1966: }
1967: }
1968: $conditions[$fkQuoted] = $keys[$foreignKey];
1969:
1970: if ($recursive === 0) {
1971: $conditions = array_merge($conditions, (array)$conditions);
1972: }
1973: $count = intval($this->find('count', compact('conditions', 'recursive')));
1974:
1975: $this->{$parent}->updateAll(
1976: array($field => $count),
1977: array($this->{$parent}->escapeField() => $keys[$foreignKey])
1978: );
1979: }
1980: }
1981: }
1982: }
1983:
1984: /**
1985: * Helper method for `Model::updateCounterCache()`. Checks the fields to be updated for
1986: *
1987: * @param array $data The fields of the record that will be updated
1988: * @return array Returns updated foreign key values, along with an 'old' key containing the old
1989: * values, or empty if no foreign keys are updated.
1990: */
1991: protected function _prepareUpdateFields($data) {
1992: $foreignKeys = array();
1993: foreach ($this->belongsTo as $assoc => $info) {
1994: if ($info['counterCache']) {
1995: $foreignKeys[$assoc] = $info['foreignKey'];
1996: }
1997: }
1998: $included = array_intersect($foreignKeys, array_keys($data));
1999:
2000: if (empty($included) || empty($this->id)) {
2001: return array();
2002: }
2003: $old = $this->find('first', array(
2004: 'conditions' => array($this->alias . '.' . $this->primaryKey => $this->id),
2005: 'fields' => array_values($included),
2006: 'recursive' => -1
2007: ));
2008: return array_merge($data, array('old' => $old[$this->alias]));
2009: }
2010:
2011: /**
2012: * Backwards compatible passthrough method for:
2013: * saveMany(), validateMany(), saveAssociated() and validateAssociated()
2014: *
2015: * Saves multiple individual records for a single model; Also works with a single record, as well as
2016: * all its associated records.
2017: *
2018: * #### Options
2019: *
2020: * - `validate`: Set to false to disable validation, true to validate each record before saving,
2021: * 'first' to validate *all* records before any are saved (default),
2022: * or 'only' to only validate the records, but not save them.
2023: * - `atomic`: If true (default), will attempt to save all records in a single transaction.
2024: * Should be set to false if database/table does not support transactions.
2025: * - `fieldList`: Equivalent to the $fieldList parameter in Model::save().
2026: * It should be an associate array with model name as key and array of fields as value. Eg.
2027: * {{{
2028: * array(
2029: * 'SomeModel' => array('field'),
2030: * 'AssociatedModel' => array('field', 'otherfield')
2031: * )
2032: * }}}
2033: * - `deep`: see saveMany/saveAssociated
2034: *
2035: * @param array $data Record data to save. This can be either a numerically-indexed array (for saving multiple
2036: * records of the same type), or an array indexed by association name.
2037: * @param array $options Options to use when saving record data, See $options above.
2038: * @return mixed If atomic: True on success, or false on failure.
2039: * Otherwise: array similar to the $data array passed, but values are set to true/false
2040: * depending on whether each record saved successfully.
2041: * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array
2042: * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveall-array-data-null-array-options-array
2043: */
2044: public function saveAll($data = array(), $options = array()) {
2045: $options = array_merge(array('validate' => 'first'), $options);
2046: if (Hash::numeric(array_keys($data))) {
2047: if ($options['validate'] === 'only') {
2048: return $this->validateMany($data, $options);
2049: }
2050: return $this->saveMany($data, $options);
2051: }
2052: if ($options['validate'] === 'only') {
2053: return $this->validateAssociated($data, $options);
2054: }
2055: return $this->saveAssociated($data, $options);
2056: }
2057:
2058: /**
2059: * Saves multiple individual records for a single model
2060: *
2061: * #### Options
2062: *
2063: * - `validate`: Set to false to disable validation, true to validate each record before saving,
2064: * 'first' to validate *all* records before any are saved (default),
2065: * - `atomic`: If true (default), will attempt to save all records in a single transaction.
2066: * Should be set to false if database/table does not support transactions.
2067: * - `fieldList`: Equivalent to the $fieldList parameter in Model::save()
2068: * - `deep`: If set to true, all associated data will be saved as well.
2069: *
2070: * @param array $data Record data to save. This should be a numerically-indexed array
2071: * @param array $options Options to use when saving record data, See $options above.
2072: * @return mixed If atomic: True on success, or false on failure.
2073: * Otherwise: array similar to the $data array passed, but values are set to true/false
2074: * depending on whether each record saved successfully.
2075: * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-savemany-array-data-null-array-options-array
2076: */
2077: public function saveMany($data = null, $options = array()) {
2078: if (empty($data)) {
2079: $data = $this->data;
2080: }
2081:
2082: $options = array_merge(array('validate' => 'first', 'atomic' => true, 'deep' => false), $options);
2083: $this->validationErrors = $validationErrors = array();
2084:
2085: if (empty($data) && $options['validate'] !== false) {
2086: $result = $this->save($data, $options);
2087: if (!$options['atomic']) {
2088: return array(!empty($result));
2089: }
2090: return !empty($result);
2091: }
2092:
2093: if ($options['validate'] === 'first') {
2094: $validates = $this->validateMany($data, $options);
2095: if ((!$validates && $options['atomic']) || (!$options['atomic'] && in_array(false, $validates, true))) {
2096: return $validates;
2097: }
2098: $options['validate'] = false;
2099: }
2100:
2101: if ($options['atomic']) {
2102: $db = $this->getDataSource();
2103: $transactionBegun = $db->begin();
2104: }
2105: $return = array();
2106: foreach ($data as $key => $record) {
2107: $validates = $this->create(null) !== null;
2108: $saved = false;
2109: if ($validates) {
2110: if ($options['deep']) {
2111: $saved = $this->saveAssociated($record, array_merge($options, array('atomic' => false)));
2112: } else {
2113: $saved = $this->save($record, $options);
2114: }
2115: }
2116: $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true))));
2117: if (!$validates) {
2118: $validationErrors[$key] = $this->validationErrors;
2119: }
2120: if (!$options['atomic']) {
2121: $return[$key] = $validates;
2122: } elseif (!$validates) {
2123: break;
2124: }
2125: }
2126: $this->validationErrors = $validationErrors;
2127:
2128: if (!$options['atomic']) {
2129: return $return;
2130: }
2131: if ($validates) {
2132: if ($transactionBegun) {
2133: return $db->commit() !== false;
2134: }
2135: return true;
2136: }
2137: $db->rollback();
2138: return false;
2139: }
2140:
2141: /**
2142: * Validates multiple individual records for a single model
2143: *
2144: * #### Options
2145: *
2146: * - `atomic`: If true (default), returns boolean. If false returns array.
2147: * - `fieldList`: Equivalent to the $fieldList parameter in Model::save()
2148: * - `deep`: If set to true, all associated data will be validated as well.
2149: *
2150: * Warning: This method could potentially change the passed argument `$data`,
2151: * If you do not want this to happen, make a copy of `$data` before passing it
2152: * to this method
2153: *
2154: * @param array $data Record data to validate. This should be a numerically-indexed array
2155: * @param array $options Options to use when validating record data (see above), See also $options of validates().
2156: * @return boolean|array If atomic: True on success, or false on failure.
2157: * Otherwise: array similar to the $data array passed, but values are set to true/false
2158: * depending on whether each record validated successfully.
2159: */
2160: public function validateMany(&$data, $options = array()) {
2161: return $this->validator()->validateMany($data, $options);
2162: }
2163:
2164: /**
2165: * Saves a single record, as well as all its directly associated records.
2166: *
2167: * #### Options
2168: *
2169: * - `validate`: Set to `false` to disable validation, `true` to validate each record before saving,
2170: * 'first' to validate *all* records before any are saved(default),
2171: * - `atomic`: If true (default), will attempt to save all records in a single transaction.
2172: * Should be set to false if database/table does not support transactions.
2173: * - `fieldList`: Equivalent to the $fieldList parameter in Model::save().
2174: * It should be an associate array with model name as key and array of fields as value. Eg.
2175: * {{{
2176: * array(
2177: * 'SomeModel' => array('field'),
2178: * 'AssociatedModel' => array('field', 'otherfield')
2179: * )
2180: * }}}
2181: * - `deep`: If set to true, not only directly associated data is saved, but deeper nested associated data as well.
2182: *
2183: * @param array $data Record data to save. This should be an array indexed by association name.
2184: * @param array $options Options to use when saving record data, See $options above.
2185: * @return mixed If atomic: True on success, or false on failure.
2186: * Otherwise: array similar to the $data array passed, but values are set to true/false
2187: * depending on whether each record saved successfully.
2188: * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array
2189: */
2190: public function saveAssociated($data = null, $options = array()) {
2191: if (empty($data)) {
2192: $data = $this->data;
2193: }
2194:
2195: $options = array_merge(array('validate' => 'first', 'atomic' => true, 'deep' => false), $options);
2196: $this->validationErrors = $validationErrors = array();
2197:
2198: if (empty($data) && $options['validate'] !== false) {
2199: $result = $this->save($data, $options);
2200: if (!$options['atomic']) {
2201: return array(!empty($result));
2202: }
2203: return !empty($result);
2204: }
2205:
2206: if ($options['validate'] === 'first') {
2207: $validates = $this->validateAssociated($data, $options);
2208: if ((!$validates && $options['atomic']) || (!$options['atomic'] && in_array(false, Hash::flatten($validates), true))) {
2209: return $validates;
2210: }
2211: $options['validate'] = false;
2212: }
2213: if ($options['atomic']) {
2214: $db = $this->getDataSource();
2215: $transactionBegun = $db->begin();
2216: }
2217:
2218: $associations = $this->getAssociated();
2219: $return = array();
2220: $validates = true;
2221: foreach ($data as $association => $values) {
2222: $notEmpty = !empty($values[$association]) || (!isset($values[$association]) && !empty($values));
2223: if (isset($associations[$association]) && $associations[$association] === 'belongsTo' && $notEmpty) {
2224: $validates = $this->{$association}->create(null) !== null;
2225: $saved = false;
2226: if ($validates) {
2227: if ($options['deep']) {
2228: $saved = $this->{$association}->saveAssociated($values, array_merge($options, array('atomic' => false)));
2229: } else {
2230: $saved = $this->{$association}->save($values, array_merge($options, array('atomic' => false)));
2231: }
2232: $validates = ($saved === true || (is_array($saved) && !in_array(false, $saved, true)));
2233: }
2234: if ($validates) {
2235: $key = $this->belongsTo[$association]['foreignKey'];
2236: if (isset($data[$this->alias])) {
2237: $data[$this->alias][$key] = $this->{$association}->id;
2238: } else {
2239: $data = array_merge(array($key => $this->{$association}->id), $data, array($key => $this->{$association}->id));
2240: }
2241: $options = $this->_addToWhiteList($key, $options);
2242: } else {
2243: $validationErrors[$association] = $this->{$association}->validationErrors;
2244: }
2245: $return[$association] = $validates;
2246: }
2247: }
2248: if ($validates && !($this->create(null) !== null && $this->save($data, $options))) {
2249: $validationErrors[$this->alias] = $this->validationErrors;
2250: $validates = false;
2251: }
2252: $return[$this->alias] = $validates;
2253:
2254: foreach ($data as $association => $values) {
2255: if (!$validates) {
2256: break;
2257: }
2258: $notEmpty = !empty($values[$association]) || (!isset($values[$association]) && !empty($values));
2259: if (isset($associations[$association]) && $notEmpty) {
2260: $type = $associations[$association];
2261: $key = $this->{$type}[$association]['foreignKey'];
2262: switch ($type) {
2263: case 'hasOne':
2264: if (isset($values[$association])) {
2265: $values[$association][$key] = $this->id;
2266: } else {
2267: $values = array_merge(array($key => $this->id), $values, array($key => $this->id));
2268: }
2269: $validates = $this->{$association}->create(null) !== null;
2270: $saved = false;
2271: if ($validates) {
2272: $options = $this->{$association}->_addToWhiteList($key, $options);
2273: if ($options['deep']) {
2274: $saved = $this->{$association}->saveAssociated($values, array_merge($options, array('atomic' => false)));
2275: } else {
2276: $saved = $this->{$association}->save($values, $options);
2277: }
2278: }
2279: $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true))));
2280: if (!$validates) {
2281: $validationErrors[$association] = $this->{$association}->validationErrors;
2282: }
2283: $return[$association] = $validates;
2284: break;
2285: case 'hasMany':
2286: foreach ($values as $i => $value) {
2287: if (isset($values[$i][$association])) {
2288: $values[$i][$association][$key] = $this->id;
2289: } else {
2290: $values[$i] = array_merge(array($key => $this->id), $value, array($key => $this->id));
2291: }
2292: }
2293: $options = $this->{$association}->_addToWhiteList($key, $options);
2294: $_return = $this->{$association}->saveMany($values, array_merge($options, array('atomic' => false)));
2295: if (in_array(false, $_return, true)) {
2296: $validationErrors[$association] = $this->{$association}->validationErrors;
2297: $validates = false;
2298: }
2299: $return[$association] = $_return;
2300: break;
2301: }
2302: }
2303: }
2304: $this->validationErrors = $validationErrors;
2305:
2306: if (isset($validationErrors[$this->alias])) {
2307: $this->validationErrors = $validationErrors[$this->alias];
2308: unset($validationErrors[$this->alias]);
2309: $this->validationErrors = array_merge($this->validationErrors, $validationErrors);
2310: }
2311:
2312: if (!$options['atomic']) {
2313: return $return;
2314: }
2315: if ($validates) {
2316: if ($transactionBegun) {
2317: return $db->commit() !== false;
2318: }
2319: return true;
2320: }
2321: $db->rollback();
2322: return false;
2323: }
2324:
2325: /**
2326: * Helper method for saveAll() and friends, to add foreign key to fieldlist
2327: *
2328: * @param string $key fieldname to be added to list
2329: * @param array $options
2330: * @return array $options
2331: */
2332: protected function _addToWhiteList($key, $options) {
2333: if (empty($options['fieldList']) && $this->whitelist && !in_array($key, $this->whitelist)) {
2334: $options['fieldList'][$this->alias] = $this->whitelist;
2335: $options['fieldList'][$this->alias][] = $key;
2336: return $options;
2337: }
2338: if (!empty($options['fieldList'][$this->alias]) && is_array($options['fieldList'][$this->alias])) {
2339: $options['fieldList'][$this->alias][] = $key;
2340: return $options;
2341: }
2342: if (!empty($options['fieldList']) &&
2343: is_array($options['fieldList']) &&
2344: Hash::dimensions($options['fieldList']) < 2
2345: ) {
2346: $options['fieldList'][] = $key;
2347: }
2348: return $options;
2349: }
2350:
2351: /**
2352: * Validates a single record, as well as all its directly associated records.
2353: *
2354: * #### Options
2355: *
2356: * - `atomic`: If true (default), returns boolean. If false returns array.
2357: * - `fieldList`: Equivalent to the $fieldList parameter in Model::save()
2358: * - `deep`: If set to true, not only directly associated data , but deeper nested associated data is validated as well.
2359: *
2360: * Warning: This method could potentially change the passed argument `$data`,
2361: * If you do not want this to happen, make a copy of `$data` before passing it
2362: * to this method
2363: *
2364: * @param array $data Record data to validate. This should be an array indexed by association name.
2365: * @param array $options Options to use when validating record data (see above), See also $options of validates().
2366: * @return array|boolean If atomic: True on success, or false on failure.
2367: * Otherwise: array similar to the $data array passed, but values are set to true/false
2368: * depending on whether each record validated successfully.
2369: */
2370: public function validateAssociated(&$data, $options = array()) {
2371: return $this->validator()->validateAssociated($data, $options);
2372: }
2373:
2374: /**
2375: * Updates multiple model records based on a set of conditions.
2376: *
2377: * @param array $fields Set of fields and values, indexed by fields.
2378: * Fields are treated as SQL snippets, to insert literal values manually escape your data.
2379: * @param mixed $conditions Conditions to match, true for all records
2380: * @return boolean True on success, false on failure
2381: * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-updateall-array-fields-array-conditions
2382: */
2383: public function updateAll($fields, $conditions = true) {
2384: return $this->getDataSource()->update($this, $fields, null, $conditions);
2385: }
2386:
2387: /**
2388: * Removes record for given ID. If no ID is given, the current ID is used. Returns true on success.
2389: *
2390: * @param integer|string $id ID of record to delete
2391: * @param boolean $cascade Set to true to delete records that depend on this record
2392: * @return boolean True on success
2393: * @link http://book.cakephp.org/2.0/en/models/deleting-data.html
2394: */
2395: public function delete($id = null, $cascade = true) {
2396: if (!empty($id)) {
2397: $this->id = $id;
2398: }
2399: $id = $this->id;
2400:
2401: $event = new CakeEvent('Model.beforeDelete', $this, array($cascade));
2402: list($event->break, $event->breakOn) = array(true, array(false, null));
2403: $this->getEventManager()->dispatch($event);
2404: if ($event->isStopped()) {
2405: return false;
2406: }
2407: if (!$this->exists()) {
2408: return false;
2409: }
2410:
2411: $this->_deleteDependent($id, $cascade);
2412: $this->_deleteLinks($id);
2413: $this->id = $id;
2414:
2415: if (!empty($this->belongsTo)) {
2416: foreach ($this->belongsTo as $assoc) {
2417: if (empty($assoc['counterCache'])) {
2418: continue;
2419: }
2420:
2421: $keys = $this->find('first', array(
2422: 'fields' => $this->_collectForeignKeys(),
2423: 'conditions' => array($this->alias . '.' . $this->primaryKey => $id),
2424: 'recursive' => -1,
2425: 'callbacks' => false
2426: ));
2427: break;
2428: }
2429: }
2430:
2431: if (!$this->getDataSource()->delete($this, array($this->alias . '.' . $this->primaryKey => $id))) {
2432: return false;
2433: }
2434:
2435: if (!empty($keys[$this->alias])) {
2436: $this->updateCounterCache($keys[$this->alias]);
2437: }
2438: $this->getEventManager()->dispatch(new CakeEvent('Model.afterDelete', $this));
2439: $this->_clearCache();
2440: $this->id = false;
2441: return true;
2442: }
2443:
2444: /**
2445: * Cascades model deletes through associated hasMany and hasOne child records.
2446: *
2447: * @param string $id ID of record that was deleted
2448: * @param boolean $cascade Set to true to delete records that depend on this record
2449: * @return void
2450: */
2451: protected function _deleteDependent($id, $cascade) {
2452: if ($cascade !== true) {
2453: return;
2454: }
2455: if (!empty($this->__backAssociation)) {
2456: $savedAssociations = $this->__backAssociation;
2457: $this->__backAssociation = array();
2458: }
2459:
2460: foreach (array_merge($this->hasMany, $this->hasOne) as $assoc => $data) {
2461: if ($data['dependent'] !== true) {
2462: continue;
2463: }
2464:
2465: $model = $this->{$assoc};
2466:
2467: if ($data['foreignKey'] === false && $data['conditions'] && in_array($this->name, $model->getAssociated('belongsTo'))) {
2468: $model->recursive = 0;
2469: $conditions = array($this->escapeField(null, $this->name) => $id);
2470: } else {
2471: $model->recursive = -1;
2472: $conditions = array($model->escapeField($data['foreignKey']) => $id);
2473: if ($data['conditions']) {
2474: $conditions = array_merge((array)$data['conditions'], $conditions);
2475: }
2476: }
2477:
2478: if (isset($data['exclusive']) && $data['exclusive']) {
2479: $model->deleteAll($conditions);
2480: } else {
2481: $records = $model->find('all', array(
2482: 'conditions' => $conditions, 'fields' => $model->primaryKey
2483: ));
2484:
2485: if (!empty($records)) {
2486: foreach ($records as $record) {
2487: $model->delete($record[$model->alias][$model->primaryKey]);
2488: }
2489: }
2490: }
2491: }
2492: if (isset($savedAssociations)) {
2493: $this->__backAssociation = $savedAssociations;
2494: }
2495: }
2496:
2497: /**
2498: * Cascades model deletes through HABTM join keys.
2499: *
2500: * @param string $id ID of record that was deleted
2501: * @return void
2502: */
2503: protected function _deleteLinks($id) {
2504: foreach ($this->hasAndBelongsToMany as $data) {
2505: list(, $joinModel) = pluginSplit($data['with']);
2506: $records = $this->{$joinModel}->find('all', array(
2507: 'conditions' => array($this->{$joinModel}->escapeField($data['foreignKey']) => $id),
2508: 'fields' => $this->{$joinModel}->primaryKey,
2509: 'recursive' => -1,
2510: 'callbacks' => false
2511: ));
2512: if (!empty($records)) {
2513: foreach ($records as $record) {
2514: $this->{$joinModel}->delete($record[$this->{$joinModel}->alias][$this->{$joinModel}->primaryKey]);
2515: }
2516: }
2517: }
2518: }
2519:
2520: /**
2521: * Deletes multiple model records based on a set of conditions.
2522: *
2523: * @param mixed $conditions Conditions to match
2524: * @param boolean $cascade Set to true to delete records that depend on this record
2525: * @param boolean $callbacks Run callbacks
2526: * @return boolean True on success, false on failure
2527: * @link http://book.cakephp.org/2.0/en/models/deleting-data.html#deleteall
2528: */
2529: public function deleteAll($conditions, $cascade = true, $callbacks = false) {
2530: if (empty($conditions)) {
2531: return false;
2532: }
2533: $db = $this->getDataSource();
2534:
2535: if (!$cascade && !$callbacks) {
2536: return $db->delete($this, $conditions);
2537: }
2538: $ids = $this->find('all', array_merge(array(
2539: 'fields' => "{$this->alias}.{$this->primaryKey}",
2540: 'recursive' => 0), compact('conditions'))
2541: );
2542: if ($ids === false || $ids === null) {
2543: return false;
2544: }
2545:
2546: $ids = Hash::extract($ids, "{n}.{$this->alias}.{$this->primaryKey}");
2547: if (empty($ids)) {
2548: return true;
2549: }
2550:
2551: if ($callbacks) {
2552: $_id = $this->id;
2553: $result = true;
2554: foreach ($ids as $id) {
2555: $result = $result && $this->delete($id, $cascade);
2556: }
2557: $this->id = $_id;
2558: return $result;
2559: }
2560:
2561: foreach ($ids as $id) {
2562: $this->_deleteLinks($id);
2563: if ($cascade) {
2564: $this->_deleteDependent($id, $cascade);
2565: }
2566: }
2567: return $db->delete($this, array($this->alias . '.' . $this->primaryKey => $ids));
2568: }
2569:
2570: /**
2571: * Collects foreign keys from associations.
2572: *
2573: * @param string $type
2574: * @return array
2575: */
2576: protected function _collectForeignKeys($type = 'belongsTo') {
2577: $result = array();
2578:
2579: foreach ($this->{$type} as $assoc => $data) {
2580: if (isset($data['foreignKey']) && is_string($data['foreignKey'])) {
2581: $result[$assoc] = $data['foreignKey'];
2582: }
2583: }
2584: return $result;
2585: }
2586:
2587: /**
2588: * Returns true if a record with particular ID exists.
2589: *
2590: * If $id is not passed it calls `Model::getID()` to obtain the current record ID,
2591: * and then performs a `Model::find('count')` on the currently configured datasource
2592: * to ascertain the existence of the record in persistent storage.
2593: *
2594: * @param integer|string $id ID of record to check for existence
2595: * @return boolean True if such a record exists
2596: */
2597: public function exists($id = null) {
2598: if ($id === null) {
2599: $id = $this->getID();
2600: }
2601: if ($id === false) {
2602: return false;
2603: }
2604: return (bool)$this->find('count', array(
2605: 'conditions' => array(
2606: $this->alias . '.' . $this->primaryKey => $id
2607: ),
2608: 'recursive' => -1,
2609: 'callbacks' => false
2610: ));
2611: }
2612:
2613: /**
2614: * Returns true if a record that meets given conditions exists.
2615: *
2616: * @param array $conditions SQL conditions array
2617: * @return boolean True if such a record exists
2618: */
2619: public function hasAny($conditions = null) {
2620: return (bool)$this->find('count', array('conditions' => $conditions, 'recursive' => -1));
2621: }
2622:
2623: /**
2624: * Queries the datasource and returns a result set array.
2625: *
2626: * Used to perform find operations, where the first argument is type of find operation to perform
2627: * (all / first / count / neighbors / list / threaded),
2628: * second parameter options for finding ( indexed array, including: 'conditions', 'limit',
2629: * 'recursive', 'page', 'fields', 'offset', 'order', 'callbacks')
2630: *
2631: * Eg:
2632: * {{{
2633: * $model->find('all', array(
2634: * 'conditions' => array('name' => 'Thomas Anderson'),
2635: * 'fields' => array('name', 'email'),
2636: * 'order' => 'field3 DESC',
2637: * 'recursive' => 2,
2638: * 'group' => 'type',
2639: * 'callbacks' => false,
2640: * ));
2641: * }}}
2642: *
2643: * In addition to the standard query keys above, you can provide Datasource, and behavior specific
2644: * keys. For example, when using a SQL based datasource you can use the joins key to specify additional
2645: * joins that should be part of the query.
2646: *
2647: * {{{
2648: * $model->find('all', array(
2649: * 'conditions' => array('name' => 'Thomas Anderson'),
2650: * 'joins' => array(
2651: * array(
2652: * 'alias' => 'Thought',
2653: * 'table' => 'thoughts',
2654: * 'type' => 'LEFT',
2655: * 'conditions' => '`Thought`.`person_id` = `Person`.`id`'
2656: * )
2657: * )
2658: * ));
2659: * }}}
2660: *
2661: * ### Disabling callbacks
2662: *
2663: * The `callbacks` key allows you to disable or specify the callbacks that should be run. To
2664: * disable beforeFind & afterFind callbacks set `'callbacks' => false` in your options. You can
2665: * also set the callbacks option to 'before' or 'after' to enable only the specified callback.
2666: *
2667: * ### Adding new find types
2668: *
2669: * Behaviors and find types can also define custom finder keys which are passed into find().
2670: * See the documentation for custom find types
2671: * (http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#creating-custom-find-types)
2672: * for how to implement custom find types.
2673: *
2674: * Specifying 'fields' for notation 'list':
2675: *
2676: * - If no fields are specified, then 'id' is used for key and 'model->displayField' is used for value.
2677: * - If a single field is specified, 'id' is used for key and specified field is used for value.
2678: * - If three fields are specified, they are used (in order) for key, value and group.
2679: * - Otherwise, first and second fields are used for key and value.
2680: *
2681: * Note: find(list) + database views have issues with MySQL 5.0. Try upgrading to MySQL 5.1 if you
2682: * have issues with database views.
2683: *
2684: * Note: find(count) has its own return values.
2685: *
2686: * @param string $type Type of find operation (all / first / count / neighbors / list / threaded)
2687: * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks)
2688: * @return array Array of records, or Null on failure.
2689: * @link http://book.cakephp.org/2.0/en/models/retrieving-your-data.html
2690: */
2691: public function find($type = 'first', $query = array()) {
2692: $this->findQueryType = $type;
2693: $this->id = $this->getID();
2694:
2695: $query = $this->buildQuery($type, $query);
2696: if ($query === null) {
2697: return null;
2698: }
2699:
2700: $results = $this->getDataSource()->read($this, $query);
2701: $this->resetAssociations();
2702:
2703: if ($query['callbacks'] === true || $query['callbacks'] === 'after') {
2704: $results = $this->_filterResults($results);
2705: }
2706:
2707: $this->findQueryType = null;
2708:
2709: if ($type === 'all') {
2710: return $results;
2711: }
2712:
2713: if ($this->findMethods[$type] === true) {
2714: return $this->{'_find' . ucfirst($type)}('after', $query, $results);
2715: }
2716: }
2717:
2718: /**
2719: * Builds the query array that is used by the data source to generate the query to fetch the data.
2720: *
2721: * @param string $type Type of find operation (all / first / count / neighbors / list / threaded)
2722: * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks)
2723: * @return array Query array or null if it could not be build for some reasons
2724: * @see Model::find()
2725: */
2726: public function buildQuery($type = 'first', $query = array()) {
2727: $query = array_merge(
2728: array(
2729: 'conditions' => null, 'fields' => null, 'joins' => array(), 'limit' => null,
2730: 'offset' => null, 'order' => null, 'page' => 1, 'group' => null, 'callbacks' => true,
2731: ),
2732: (array)$query
2733: );
2734:
2735: if ($type !== 'all' && $this->findMethods[$type] === true) {
2736: $query = $this->{'_find' . ucfirst($type)}('before', $query);
2737: }
2738:
2739: if (!is_numeric($query['page']) || intval($query['page']) < 1) {
2740: $query['page'] = 1;
2741: }
2742: if ($query['page'] > 1 && !empty($query['limit'])) {
2743: $query['offset'] = ($query['page'] - 1) * $query['limit'];
2744: }
2745: if ($query['order'] === null && $this->order !== null) {
2746: $query['order'] = $this->order;
2747: }
2748: $query['order'] = array($query['order']);
2749:
2750: if ($query['callbacks'] === true || $query['callbacks'] === 'before') {
2751: $event = new CakeEvent('Model.beforeFind', $this, array($query));
2752: list($event->break, $event->breakOn, $event->modParams) = array(true, array(false, null), 0);
2753: $this->getEventManager()->dispatch($event);
2754: if ($event->isStopped()) {
2755: return null;
2756: }
2757: $query = $event->result === true ? $event->data[0] : $event->result;
2758: }
2759:
2760: return $query;
2761: }
2762:
2763: /**
2764: * Handles the before/after filter logic for find('first') operations. Only called by Model::find().
2765: *
2766: * @param string $state Either "before" or "after"
2767: * @param array $query
2768: * @param array $results
2769: * @return array
2770: * @see Model::find()
2771: */
2772: protected function _findFirst($state, $query, $results = array()) {
2773: if ($state === 'before') {
2774: $query['limit'] = 1;
2775: return $query;
2776: } elseif ($state === 'after') {
2777: if (empty($results[0])) {
2778: return array();
2779: }
2780: return $results[0];
2781: }
2782: }
2783:
2784: /**
2785: * Handles the before/after filter logic for find('count') operations. Only called by Model::find().
2786: *
2787: * @param string $state Either "before" or "after"
2788: * @param array $query
2789: * @param array $results
2790: * @return integer The number of records found, or false
2791: * @see Model::find()
2792: */
2793: protected function _findCount($state, $query, $results = array()) {
2794: if ($state === 'before') {
2795: if (!empty($query['type']) && isset($this->findMethods[$query['type']]) && $query['type'] !== 'count') {
2796: $query['operation'] = 'count';
2797: $query = $this->{'_find' . ucfirst($query['type'])}('before', $query);
2798: }
2799: $db = $this->getDataSource();
2800: $query['order'] = false;
2801: if (!method_exists($db, 'calculate')) {
2802: return $query;
2803: }
2804: if (!empty($query['fields']) && is_array($query['fields'])) {
2805: if (!preg_match('/^count/i', current($query['fields']))) {
2806: unset($query['fields']);
2807: }
2808: }
2809: if (empty($query['fields'])) {
2810: $query['fields'] = $db->calculate($this, 'count');
2811: } elseif (method_exists($db, 'expression') && is_string($query['fields']) && !preg_match('/count/i', $query['fields'])) {
2812: $query['fields'] = $db->calculate($this, 'count', array(
2813: $db->expression($query['fields']), 'count'
2814: ));
2815: }
2816: return $query;
2817: } elseif ($state === 'after') {
2818: foreach (array(0, $this->alias) as $key) {
2819: if (isset($results[0][$key]['count'])) {
2820: if ($query['group']) {
2821: return count($results);
2822: }
2823: return intval($results[0][$key]['count']);
2824: }
2825: }
2826: return false;
2827: }
2828: }
2829:
2830: /**
2831: * Handles the before/after filter logic for find('list') operations. Only called by Model::find().
2832: *
2833: * @param string $state Either "before" or "after"
2834: * @param array $query
2835: * @param array $results
2836: * @return array Key/value pairs of primary keys/display field values of all records found
2837: * @see Model::find()
2838: */
2839: protected function _findList($state, $query, $results = array()) {
2840: if ($state === 'before') {
2841: if (empty($query['fields'])) {
2842: $query['fields'] = array("{$this->alias}.{$this->primaryKey}", "{$this->alias}.{$this->displayField}");
2843: $list = array("{n}.{$this->alias}.{$this->primaryKey}", "{n}.{$this->alias}.{$this->displayField}", null);
2844: } else {
2845: if (!is_array($query['fields'])) {
2846: $query['fields'] = String::tokenize($query['fields']);
2847: }
2848:
2849: if (count($query['fields']) === 1) {
2850: if (strpos($query['fields'][0], '.') === false) {
2851: $query['fields'][0] = $this->alias . '.' . $query['fields'][0];
2852: }
2853:
2854: $list = array("{n}.{$this->alias}.{$this->primaryKey}", '{n}.' . $query['fields'][0], null);
2855: $query['fields'] = array("{$this->alias}.{$this->primaryKey}", $query['fields'][0]);
2856: } elseif (count($query['fields']) === 3) {
2857: for ($i = 0; $i < 3; $i++) {
2858: if (strpos($query['fields'][$i], '.') === false) {
2859: $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i];
2860: }
2861: }
2862:
2863: $list = array('{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], '{n}.' . $query['fields'][2]);
2864: } else {
2865: for ($i = 0; $i < 2; $i++) {
2866: if (strpos($query['fields'][$i], '.') === false) {
2867: $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i];
2868: }
2869: }
2870:
2871: $list = array('{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], null);
2872: }
2873: }
2874: if (!isset($query['recursive']) || $query['recursive'] === null) {
2875: $query['recursive'] = -1;
2876: }
2877: list($query['list']['keyPath'], $query['list']['valuePath'], $query['list']['groupPath']) = $list;
2878: return $query;
2879: } elseif ($state === 'after') {
2880: if (empty($results)) {
2881: return array();
2882: }
2883: return Hash::combine($results, $query['list']['keyPath'], $query['list']['valuePath'], $query['list']['groupPath']);
2884: }
2885: }
2886:
2887: /**
2888: * Detects the previous field's value, then uses logic to find the 'wrapping'
2889: * rows and return them.
2890: *
2891: * @param string $state Either "before" or "after"
2892: * @param array $query
2893: * @param array $results
2894: * @return array
2895: */
2896: protected function _findNeighbors($state, $query, $results = array()) {
2897: if ($state === 'before') {
2898: extract($query);
2899: $conditions = (array)$conditions;
2900: if (isset($field) && isset($value)) {
2901: if (strpos($field, '.') === false) {
2902: $field = $this->alias . '.' . $field;
2903: }
2904: } else {
2905: $field = $this->alias . '.' . $this->primaryKey;
2906: $value = $this->id;
2907: }
2908: $query['conditions'] = array_merge($conditions, array($field . ' <' => $value));
2909: $query['order'] = $field . ' DESC';
2910: $query['limit'] = 1;
2911: $query['field'] = $field;
2912: $query['value'] = $value;
2913: return $query;
2914: } elseif ($state === 'after') {
2915: extract($query);
2916: unset($query['conditions'][$field . ' <']);
2917: $return = array();
2918: if (isset($results[0])) {
2919: $prevVal = Hash::get($results[0], $field);
2920: $query['conditions'][$field . ' >='] = $prevVal;
2921: $query['conditions'][$field . ' !='] = $value;
2922: $query['limit'] = 2;
2923: } else {
2924: $return['prev'] = null;
2925: $query['conditions'][$field . ' >'] = $value;
2926: $query['limit'] = 1;
2927: }
2928: $query['order'] = $field . ' ASC';
2929: $neighbors = $this->find('all', $query);
2930: if (!array_key_exists('prev', $return)) {
2931: $return['prev'] = isset($neighbors[0]) ? $neighbors[0] : null;
2932: }
2933: if (count($neighbors) === 2) {
2934: $return['next'] = $neighbors[1];
2935: } elseif (count($neighbors) === 1 && !$return['prev']) {
2936: $return['next'] = $neighbors[0];
2937: } else {
2938: $return['next'] = null;
2939: }
2940: return $return;
2941: }
2942: }
2943:
2944: /**
2945: * In the event of ambiguous results returned (multiple top level results, with different parent_ids)
2946: * top level results with different parent_ids to the first result will be dropped
2947: *
2948: * @param string $state
2949: * @param mixed $query
2950: * @param array $results
2951: * @return array Threaded results
2952: */
2953: protected function _findThreaded($state, $query, $results = array()) {
2954: if ($state === 'before') {
2955: return $query;
2956: } elseif ($state === 'after') {
2957: $parent = 'parent_id';
2958: if (isset($query['parent'])) {
2959: $parent = $query['parent'];
2960: }
2961: return Hash::nest($results, array(
2962: 'idPath' => '{n}.' . $this->alias . '.' . $this->primaryKey,
2963: 'parentPath' => '{n}.' . $this->alias . '.' . $parent
2964: ));
2965: }
2966: }
2967:
2968: /**
2969: * Passes query results through model and behavior afterFilter() methods.
2970: *
2971: * @param array $results Results to filter
2972: * @param boolean $primary If this is the primary model results (results from model where the find operation was performed)
2973: * @return array Set of filtered results
2974: */
2975: protected function _filterResults($results, $primary = true) {
2976: $event = new CakeEvent('Model.afterFind', $this, array($results, $primary));
2977: $event->modParams = 0;
2978: $this->getEventManager()->dispatch($event);
2979: return $event->result;
2980: }
2981:
2982: /**
2983: * This resets the association arrays for the model back
2984: * to those originally defined in the model. Normally called at the end
2985: * of each call to Model::find()
2986: *
2987: * @return boolean Success
2988: */
2989: public function resetAssociations() {
2990: if (!empty($this->__backAssociation)) {
2991: foreach ($this->_associations as $type) {
2992: if (isset($this->__backAssociation[$type])) {
2993: $this->{$type} = $this->__backAssociation[$type];
2994: }
2995: }
2996: $this->__backAssociation = array();
2997: }
2998:
2999: foreach ($this->_associations as $type) {
3000: foreach ($this->{$type} as $key => $name) {
3001: if (property_exists($this, $key) && !empty($this->{$key}->__backAssociation)) {
3002: $this->{$key}->resetAssociations();
3003: }
3004: }
3005: }
3006: $this->__backAssociation = array();
3007: return true;
3008: }
3009:
3010: /**
3011: * Returns false if any fields passed match any (by default, all if $or = false) of their matching values.
3012: *
3013: * @param array $fields Field/value pairs to search (if no values specified, they are pulled from $this->data)
3014: * @param boolean $or If false, all fields specified must match in order for a false return value
3015: * @return boolean False if any records matching any fields are found
3016: */
3017: public function isUnique($fields, $or = true) {
3018: if (!is_array($fields)) {
3019: $fields = func_get_args();
3020: if (is_bool($fields[count($fields) - 1])) {
3021: $or = $fields[count($fields) - 1];
3022: unset($fields[count($fields) - 1]);
3023: }
3024: }
3025:
3026: foreach ($fields as $field => $value) {
3027: if (is_numeric($field)) {
3028: unset($fields[$field]);
3029:
3030: $field = $value;
3031: $value = null;
3032: if (isset($this->data[$this->alias][$field])) {
3033: $value = $this->data[$this->alias][$field];
3034: }
3035: }
3036:
3037: if (strpos($field, '.') === false) {
3038: unset($fields[$field]);
3039: $fields[$this->alias . '.' . $field] = $value;
3040: }
3041: }
3042: if ($or) {
3043: $fields = array('or' => $fields);
3044: }
3045: if (!empty($this->id)) {
3046: $fields[$this->alias . '.' . $this->primaryKey . ' !='] = $this->id;
3047: }
3048: return !$this->find('count', array('conditions' => $fields, 'recursive' => -1));
3049: }
3050:
3051: /**
3052: * Returns a resultset for a given SQL statement. Custom SQL queries should be performed with this method.
3053: *
3054: * @param string $sql SQL statement
3055: * @param boolean|array $params Either a boolean to control query caching or an array of parameters
3056: * for use with prepared statement placeholders.
3057: * @param boolean $cache If $params is provided, a boolean flag for enabling/disabled
3058: * query caching.
3059: * @return mixed Resultset array or boolean indicating success / failure depending on the query executed
3060: * @link http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-query
3061: */
3062: public function query($sql) {
3063: $params = func_get_args();
3064: $db = $this->getDataSource();
3065: return call_user_func_array(array(&$db, 'query'), $params);
3066: }
3067:
3068: /**
3069: * Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations
3070: * that use the 'with' key as well. Since _saveMulti is incapable of exiting a save operation.
3071: *
3072: * Will validate the currently set data. Use Model::set() or Model::create() to set the active data.
3073: *
3074: * @param array $options An optional array of custom options to be made available in the beforeValidate callback
3075: * @return boolean True if there are no errors
3076: */
3077: public function validates($options = array()) {
3078: return $this->validator()->validates($options);
3079: }
3080:
3081: /**
3082: * Returns an array of fields that have failed validation. On the current model.
3083: *
3084: * @param string $options An optional array of custom options to be made available in the beforeValidate callback
3085: * @return array Array of invalid fields
3086: * @see Model::validates()
3087: */
3088: public function invalidFields($options = array()) {
3089: return $this->validator()->errors($options);
3090: }
3091:
3092: /**
3093: * Marks a field as invalid, optionally setting the name of validation
3094: * rule (in case of multiple validation for field) that was broken.
3095: *
3096: * @param string $field The name of the field to invalidate
3097: * @param mixed $value Name of validation rule that was not failed, or validation message to
3098: * be returned. If no validation key is provided, defaults to true.
3099: * @return void
3100: */
3101: public function invalidate($field, $value = true) {
3102: $this->validator()->invalidate($field, $value);
3103: }
3104:
3105: /**
3106: * Returns true if given field name is a foreign key in this model.
3107: *
3108: * @param string $field Returns true if the input string ends in "_id"
3109: * @return boolean True if the field is a foreign key listed in the belongsTo array.
3110: */
3111: public function isForeignKey($field) {
3112: $foreignKeys = array();
3113: if (!empty($this->belongsTo)) {
3114: foreach ($this->belongsTo as $data) {
3115: $foreignKeys[] = $data['foreignKey'];
3116: }
3117: }
3118: return in_array($field, $foreignKeys);
3119: }
3120:
3121: /**
3122: * Escapes the field name and prepends the model name. Escaping is done according to the
3123: * current database driver's rules.
3124: *
3125: * @param string $field Field to escape (e.g: id)
3126: * @param string $alias Alias for the model (e.g: Post)
3127: * @return string The name of the escaped field for this Model (i.e. id becomes `Post`.`id`).
3128: */
3129: public function escapeField($field = null, $alias = null) {
3130: if (empty($alias)) {
3131: $alias = $this->alias;
3132: }
3133: if (empty($field)) {
3134: $field = $this->primaryKey;
3135: }
3136: $db = $this->getDataSource();
3137: if (strpos($field, $db->name($alias) . '.') === 0) {
3138: return $field;
3139: }
3140: return $db->name($alias . '.' . $field);
3141: }
3142:
3143: /**
3144: * Returns the current record's ID
3145: *
3146: * @param integer $list Index on which the composed ID is located
3147: * @return mixed The ID of the current record, false if no ID
3148: */
3149: public function getID($list = 0) {
3150: if (empty($this->id) || (is_array($this->id) && isset($this->id[0]) && empty($this->id[0]))) {
3151: return false;
3152: }
3153: if (!is_array($this->id)) {
3154: return $this->id;
3155: }
3156: if (isset($this->id[$list]) && !empty($this->id[$list])) {
3157: return $this->id[$list];
3158: }
3159: if (isset($this->id[$list])) {
3160: return false;
3161: }
3162:
3163: return current($this->id);
3164: }
3165:
3166: /**
3167: * Returns the ID of the last record this model inserted.
3168: *
3169: * @return mixed Last inserted ID
3170: */
3171: public function getLastInsertID() {
3172: return $this->getInsertID();
3173: }
3174:
3175: /**
3176: * Returns the ID of the last record this model inserted.
3177: *
3178: * @return mixed Last inserted ID
3179: */
3180: public function getInsertID() {
3181: return $this->_insertID;
3182: }
3183:
3184: /**
3185: * Sets the ID of the last record this model inserted
3186: *
3187: * @param integer|string $id Last inserted ID
3188: * @return void
3189: */
3190: public function setInsertID($id) {
3191: $this->_insertID = $id;
3192: }
3193:
3194: /**
3195: * Returns the number of rows returned from the last query.
3196: *
3197: * @return integer Number of rows
3198: */
3199: public function getNumRows() {
3200: return $this->getDataSource()->lastNumRows();
3201: }
3202:
3203: /**
3204: * Returns the number of rows affected by the last query.
3205: *
3206: * @return integer Number of rows
3207: */
3208: public function getAffectedRows() {
3209: return $this->getDataSource()->lastAffected();
3210: }
3211:
3212: /**
3213: * Sets the DataSource to which this model is bound.
3214: *
3215: * @param string $dataSource The name of the DataSource, as defined in app/Config/database.php
3216: * @return void
3217: * @throws MissingConnectionException
3218: */
3219: public function setDataSource($dataSource = null) {
3220: $oldConfig = $this->useDbConfig;
3221:
3222: if ($dataSource) {
3223: $this->useDbConfig = $dataSource;
3224: }
3225: $db = ConnectionManager::getDataSource($this->useDbConfig);
3226: if (!empty($oldConfig) && isset($db->config['prefix'])) {
3227: $oldDb = ConnectionManager::getDataSource($oldConfig);
3228:
3229: if (!isset($this->tablePrefix) || (!isset($oldDb->config['prefix']) || $this->tablePrefix == $oldDb->config['prefix'])) {
3230: $this->tablePrefix = $db->config['prefix'];
3231: }
3232: } elseif (isset($db->config['prefix'])) {
3233: $this->tablePrefix = $db->config['prefix'];
3234: }
3235:
3236: $this->schemaName = $db->getSchemaName();
3237: }
3238:
3239: /**
3240: * Gets the DataSource to which this model is bound.
3241: *
3242: * @return DataSource A DataSource object
3243: */
3244: public function getDataSource() {
3245: if (!$this->_sourceConfigured && $this->useTable !== false) {
3246: $this->_sourceConfigured = true;
3247: $this->setSource($this->useTable);
3248: }
3249: return ConnectionManager::getDataSource($this->useDbConfig);
3250: }
3251:
3252: /**
3253: * Get associations
3254: *
3255: * @return array
3256: */
3257: public function associations() {
3258: return $this->_associations;
3259: }
3260:
3261: /**
3262: * Gets all the models with which this model is associated.
3263: *
3264: * @param string $type Only result associations of this type
3265: * @return array Associations
3266: */
3267: public function getAssociated($type = null) {
3268: if (!$type) {
3269: $associated = array();
3270: foreach ($this->_associations as $assoc) {
3271: if (!empty($this->{$assoc})) {
3272: $models = array_keys($this->{$assoc});
3273: foreach ($models as $m) {
3274: $associated[$m] = $assoc;
3275: }
3276: }
3277: }
3278: return $associated;
3279: }
3280: if (in_array($type, $this->_associations)) {
3281: if (empty($this->{$type})) {
3282: return array();
3283: }
3284: return array_keys($this->{$type});
3285: }
3286:
3287: $assoc = array_merge(
3288: $this->hasOne,
3289: $this->hasMany,
3290: $this->belongsTo,
3291: $this->hasAndBelongsToMany
3292: );
3293: if (array_key_exists($type, $assoc)) {
3294: foreach ($this->_associations as $a) {
3295: if (isset($this->{$a}[$type])) {
3296: $assoc[$type]['association'] = $a;
3297: break;
3298: }
3299: }
3300: return $assoc[$type];
3301: }
3302: return null;
3303: }
3304:
3305: /**
3306: * Gets the name and fields to be used by a join model. This allows specifying join fields
3307: * in the association definition.
3308: *
3309: * @param string|array $assoc The model to be joined
3310: * @param array $keys Any join keys which must be merged with the keys queried
3311: * @return array
3312: */
3313: public function joinModel($assoc, $keys = array()) {
3314: if (is_string($assoc)) {
3315: list(, $assoc) = pluginSplit($assoc);
3316: return array($assoc, array_keys($this->{$assoc}->schema()));
3317: }
3318: if (is_array($assoc)) {
3319: $with = key($assoc);
3320: return array($with, array_unique(array_merge($assoc[$with], $keys)));
3321: }
3322: trigger_error(
3323: __d('cake_dev', 'Invalid join model settings in %s. The association parameter has the wrong type, expecting a string or array, but was passed type: %s', $this->alias, gettype($assoc)),
3324: E_USER_WARNING
3325: );
3326: }
3327:
3328: /**
3329: * Called before each find operation. Return false if you want to halt the find
3330: * call, otherwise return the (modified) query data.
3331: *
3332: * @param array $queryData Data used to execute this query, i.e. conditions, order, etc.
3333: * @return mixed true if the operation should continue, false if it should abort; or, modified
3334: * $queryData to continue with new $queryData
3335: * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforefind
3336: */
3337: public function beforeFind($queryData) {
3338: return true;
3339: }
3340:
3341: /**
3342: * Called after each find operation. Can be used to modify any results returned by find().
3343: * Return value should be the (modified) results.
3344: *
3345: * @param mixed $results The results of the find operation
3346: * @param boolean $primary Whether this model is being queried directly (vs. being queried as an association)
3347: * @return mixed Result of the find operation
3348: * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#afterfind
3349: */
3350: public function afterFind($results, $primary = false) {
3351: return $results;
3352: }
3353:
3354: /**
3355: * Called before each save operation, after validation. Return a non-true result
3356: * to halt the save.
3357: *
3358: * @param array $options
3359: * @return boolean True if the operation should continue, false if it should abort
3360: * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforesave
3361: */
3362: public function beforeSave($options = array()) {
3363: return true;
3364: }
3365:
3366: /**
3367: * Called after each successful save operation.
3368: *
3369: * @param boolean $created True if this save created a new record
3370: * @return void
3371: * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#aftersave
3372: */
3373: public function afterSave($created) {
3374: }
3375:
3376: /**
3377: * Called before every deletion operation.
3378: *
3379: * @param boolean $cascade If true records that depend on this record will also be deleted
3380: * @return boolean True if the operation should continue, false if it should abort
3381: * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforedelete
3382: */
3383: public function beforeDelete($cascade = true) {
3384: return true;
3385: }
3386:
3387: /**
3388: * Called after every deletion operation.
3389: *
3390: * @return void
3391: * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#afterdelete
3392: */
3393: public function afterDelete() {
3394: }
3395:
3396: /**
3397: * Called during validation operations, before validation. Please note that custom
3398: * validation rules can be defined in $validate.
3399: *
3400: * @param array $options Options passed from model::save(), see $options of model::save().
3401: * @return boolean True if validate operation should continue, false to abort
3402: * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforevalidate
3403: */
3404: public function beforeValidate($options = array()) {
3405: return true;
3406: }
3407:
3408: /**
3409: * Called after data has been checked for errors
3410: *
3411: * @return void
3412: */
3413: public function afterValidate() {
3414: }
3415:
3416: /**
3417: * Called when a DataSource-level error occurs.
3418: *
3419: * @return void
3420: * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#onerror
3421: */
3422: public function onError() {
3423: }
3424:
3425: /**
3426: * Clears cache for this model.
3427: *
3428: * @param string $type If null this deletes cached views if Cache.check is true
3429: * Will be used to allow deleting query cache also
3430: * @return boolean true on delete
3431: */
3432: protected function _clearCache($type = null) {
3433: if ($type === null) {
3434: if (Configure::read('Cache.check') === true) {
3435: $assoc[] = strtolower(Inflector::pluralize($this->alias));
3436: $assoc[] = strtolower(Inflector::underscore(Inflector::pluralize($this->alias)));
3437: foreach ($this->_associations as $key => $association) {
3438: foreach ($this->$association as $key => $className) {
3439: $check = strtolower(Inflector::pluralize($className['className']));
3440: if (!in_array($check, $assoc)) {
3441: $assoc[] = strtolower(Inflector::pluralize($className['className']));
3442: $assoc[] = strtolower(Inflector::underscore(Inflector::pluralize($className['className'])));
3443: }
3444: }
3445: }
3446: clearCache($assoc);
3447: return true;
3448: }
3449: } else {
3450: //Will use for query cache deleting
3451: }
3452: }
3453:
3454: /**
3455: * Returns an instance of a model validator for this class
3456: *
3457: * @param ModelValidator Model validator instance.
3458: * If null a new ModelValidator instance will be made using current model object
3459: * @return ModelValidator
3460: */
3461: public function validator(ModelValidator $instance = null) {
3462: if ($instance) {
3463: $this->_validator = $instance;
3464: } elseif (!$this->_validator) {
3465: $this->_validator = new ModelValidator($this);
3466: }
3467: return $this->_validator;
3468: }
3469:
3470: }
3471: