Model::buildQuery()   C
last analyzed

Complexity

Conditions 12
Paths 64

Size

Total Lines 41
Code Lines 23

Duplication

Lines 3
Ratio 7.32 %

Importance

Changes 0
Metric Value
cc 12
eloc 23
c 0
b 0
f 0
nc 64
nop 2
dl 3
loc 41
rs 5.1612

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
0 ignored issues
show
Documentation Bug introduced by
The property $useTable was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
707
		} elseif ($table) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $table of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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 View Code Duplication
	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);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class DataSource as the method query() does only exist in the following sub-classes of DataSource: DboDummy, DboMock, DboPostgresTestDb, DboSecondTestSource, DboSource, DboSqliteTestDb, DboTestSource, Mysql, Postgres, Sqlite, Sqlserver, SqlserverTestDb. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
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'],
0 ignored issues
show
Bug introduced by
The variable $assocKey does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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 View Code Duplication
			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 View Code Duplication
				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 View Code Duplication
			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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $plugin of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1044
				$plugin .= '.';
1045
			}
1046
1047
			$model = array('class' => $plugin . $className, 'alias' => $assoc);
1048
			$this->{$assoc} = ClassRegistry::init($model);
1049
1050
			if ($plugin) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $plugin of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sources of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1144
				$this->_schema = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $_schema.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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 View Code Duplication
				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 View Code Duplication
					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 View Code Duplication
				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'];
0 ignored issues
show
Bug introduced by
The property columns does not seem to exist in DataSource.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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;
0 ignored issues
show
Bug introduced by
The property startQuote does not seem to exist in DataSource.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1404
		$endQuote = isset($db->endQuote) ? $db->endQuote : null;
0 ignored issues
show
Bug introduced by
The property endQuote does not seem to exist in DataSource.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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 View Code Duplication
		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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_schema of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $field of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 1532 can also be of type boolean; however, Model::set() does only seem to accept string|array|object<Simp...lement>|object<DOMNode>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->find('first', arr..., 'fields' => $fields)) can be null. However, the property $data is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Model::field of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 1689 can also be of type null; however, Model::set() does only seem to accept string|array|object<Simp...lement>|object<DOMNode>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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)]);
0 ignored issues
show
Bug introduced by
The property columns does not seem to exist in DataSource.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$joined was never initialized. Although not strictly required by PHP, it is generally a good practice to add $joined = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type array of property $data.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class DataSource as the method name() does only exist in the following sub-classes of DataSource: DboDummy, DboMock, DboPostgresTestDb, DboSecondTestSource, DboSource, DboSqliteTestDb, DboTestSource, Mysql, Postgres, Sqlite, Sqlserver, SqlserverTestDb. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
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 View Code Duplication
				} 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;
0 ignored issues
show
Bug introduced by
The variable $conditions does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $associationForeignKey does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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) {
0 ignored issues
show
Unused Code introduced by
The parameter $created is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2029 View Code Duplication
		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 ($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 View Code Duplication
		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 View Code Duplication
		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) {
0 ignored issues
show
Bug introduced by
The variable $validates does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2263
			if ($transactionBegun) {
0 ignored issues
show
Bug introduced by
The variable $transactionBegun does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2264
				return $db->commit() !== false;
0 ignored issues
show
Bug introduced by
The variable $db does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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 View Code Duplication
		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 View Code Duplication
		if ($options['validate'] === 'first') {
2342
			$validates = $this->validateAssociated($data, $options);
2343
			if ((!$validates && $options['atomic']) || (!$options['atomic'] && in_array(false, Hash::flatten($validates), true))) {
0 ignored issues
show
Bug introduced by
It seems like $validates defined by $this->validateAssociated($data, $options) on line 2342 can also be of type boolean; however, Hash::flatten() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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 View Code Duplication
				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 View Code Duplication
						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) {
0 ignored issues
show
Bug introduced by
The variable $transactionBegun does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2472
				return $db->commit() !== false;
0 ignored issues
show
Bug introduced by
The variable $db does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->whitelist of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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 View Code Duplication
				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 View Code Duplication
			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 View Code Duplication
		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 View Code Duplication
					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 View Code Duplication
					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;
0 ignored issues
show
Bug introduced by
The property modParams does not seem to exist in CakeEvent.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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
0 ignored issues
show
Bug introduced by
There is no parameter named $params. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
3298
 *    for use with prepared statement placeholders.
3299
 * @param boolean $cache If $params is provided, a boolean flag for enabling/disabled
0 ignored issues
show
Bug introduced by
There is no parameter named $cache. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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) {
0 ignored issues
show
Unused Code introduced by
The parameter $sql is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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 View Code Duplication
		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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $id can also be of type string. However, the property $_insertID is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataSource of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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) {
0 ignored issues
show
Unused Code introduced by
The parameter $primary is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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()) {
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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()) {
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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) {
0 ignored issues
show
Unused Code introduced by
The parameter $cascade is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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()) {
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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