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