Table::saveOrFail()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 9
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 9
loc 9
rs 9.9666
c 0
b 0
f 0
1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11
 * @link          https://cakephp.org CakePHP(tm) Project
12
 * @since         3.0.0
13
 * @license       https://opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\ORM;
16
17
use ArrayObject;
18
use BadMethodCallException;
19
use Cake\Core\App;
20
use Cake\Database\Schema\TableSchema;
21
use Cake\Database\Type;
22
use Cake\Datasource\ConnectionInterface;
23
use Cake\Datasource\EntityInterface;
24
use Cake\Datasource\Exception\InvalidPrimaryKeyException;
25
use Cake\Datasource\RepositoryInterface;
26
use Cake\Datasource\RulesAwareTrait;
27
use Cake\Event\EventDispatcherInterface;
28
use Cake\Event\EventDispatcherTrait;
29
use Cake\Event\EventListenerInterface;
30
use Cake\Event\EventManager;
31
use Cake\ORM\Association\BelongsTo;
32
use Cake\ORM\Association\BelongsToMany;
33
use Cake\ORM\Association\HasMany;
34
use Cake\ORM\Association\HasOne;
35
use Cake\ORM\Exception\MissingEntityException;
36
use Cake\ORM\Exception\PersistenceFailedException;
37
use Cake\ORM\Exception\RolledbackTransactionException;
38
use Cake\ORM\Rule\IsUnique;
39
use Cake\Utility\Inflector;
40
use Cake\Validation\ValidatorAwareInterface;
41
use Cake\Validation\ValidatorAwareTrait;
42
use InvalidArgumentException;
43
use RuntimeException;
44
45
/**
46
 * Represents a single database table.
47
 *
48
 * Exposes methods for retrieving data out of it, and manages the associations
49
 * this table has to other tables. Multiple instances of this class can be created
50
 * for the same database table with different aliases, this allows you to address
51
 * your database structure in a richer and more expressive way.
52
 *
53
 * ### Retrieving data
54
 *
55
 * The primary way to retrieve data is using Table::find(). See that method
56
 * for more information.
57
 *
58
 * ### Dynamic finders
59
 *
60
 * In addition to the standard find($type) finder methods, CakePHP provides dynamic
61
 * finder methods. These methods allow you to easily set basic conditions up. For example
62
 * to filter users by username you would call
63
 *
64
 * ```
65
 * $query = $users->findByUsername('mark');
66
 * ```
67
 *
68
 * You can also combine conditions on multiple fields using either `Or` or `And`:
69
 *
70
 * ```
71
 * $query = $users->findByUsernameOrEmail('mark', '[email protected]');
72
 * ```
73
 *
74
 * ### Bulk updates/deletes
75
 *
76
 * You can use Table::updateAll() and Table::deleteAll() to do bulk updates/deletes.
77
 * You should be aware that events will *not* be fired for bulk updates/deletes.
78
 *
79
 * ### Callbacks/events
80
 *
81
 * Table objects provide a few callbacks/events you can hook into to augment/replace
82
 * find operations. Each event uses the standard event subsystem in CakePHP
83
 *
84
 * - `beforeFind(Event $event, Query $query, ArrayObject $options, boolean $primary)`
85
 *   Fired before each find operation. By stopping the event and supplying a
86
 *   return value you can bypass the find operation entirely. Any changes done
87
 *   to the $query instance will be retained for the rest of the find. The
88
 *   $primary parameter indicates whether or not this is the root query,
89
 *   or an associated query.
90
 *
91
 * - `buildValidator(Event $event, Validator $validator, string $name)`
92
 *   Allows listeners to modify validation rules for the provided named validator.
93
 *
94
 * - `buildRules(Event $event, RulesChecker $rules)`
95
 *   Allows listeners to modify the rules checker by adding more rules.
96
 *
97
 * - `beforeRules(Event $event, EntityInterface $entity, ArrayObject $options, string $operation)`
98
 *   Fired before an entity is validated using the rules checker. By stopping this event,
99
 *   you can return the final value of the rules checking operation.
100
 *
101
 * - `afterRules(Event $event, EntityInterface $entity, ArrayObject $options, bool $result, string $operation)`
102
 *   Fired after the rules have been checked on the entity. By stopping this event,
103
 *   you can return the final value of the rules checking operation.
104
 *
105
 * - `beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)`
106
 *   Fired before each entity is saved. Stopping this event will abort the save
107
 *   operation. When the event is stopped the result of the event will be returned.
108
 *
109
 * - `afterSave(Event $event, EntityInterface $entity, ArrayObject $options)`
110
 *   Fired after an entity is saved.
111
 *
112
 * - `afterSaveCommit(Event $event, EntityInterface $entity, ArrayObject $options)`
113
 *   Fired after the transaction in which the save operation is wrapped has been committed.
114
 *   It’s also triggered for non atomic saves where database operations are implicitly committed.
115
 *   The event is triggered only for the primary table on which save() is directly called.
116
 *   The event is not triggered if a transaction is started before calling save.
117
 *
118
 * - `beforeDelete(Event $event, EntityInterface $entity, ArrayObject $options)`
119
 *   Fired before an entity is deleted. By stopping this event you will abort
120
 *   the delete operation.
121
 *
122
 * - `afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)`
123
 *   Fired after an entity has been deleted.
124
 *
125
 * @see \Cake\Event\EventManager for reference on the events system.
126
 */
127
class Table implements RepositoryInterface, EventListenerInterface, EventDispatcherInterface, ValidatorAwareInterface
128
{
129
    use EventDispatcherTrait;
130
    use RulesAwareTrait;
131
    use ValidatorAwareTrait;
132
133
    /**
134
     * The alias this object is assigned to validators as.
135
     *
136
     * @var string
137
     */
138
    const VALIDATOR_PROVIDER_NAME = 'table';
139
140
    /**
141
     * The name of the event dispatched when a validator has been built.
142
     *
143
     * @var string
144
     */
145
    const BUILD_VALIDATOR_EVENT = 'Model.buildValidator';
146
147
    /**
148
     * The rules class name that is used.
149
     *
150
     * @var string
151
     */
152
    const RULES_CLASS = RulesChecker::class;
153
154
    /**
155
     * The IsUnique class name that is used.
156
     *
157
     * @var string
158
     */
159
    const IS_UNIQUE_CLASS = IsUnique::class;
160
161
    /**
162
     * Name of the table as it can be found in the database
163
     *
164
     * @var string
165
     */
166
    protected $_table;
167
168
    /**
169
     * Human name giving to this particular instance. Multiple objects representing
170
     * the same database table can exist by using different aliases.
171
     *
172
     * @var string
173
     */
174
    protected $_alias;
175
176
    /**
177
     * Connection instance
178
     *
179
     * @var \Cake\Database\Connection
180
     */
181
    protected $_connection;
182
183
    /**
184
     * The schema object containing a description of this table fields
185
     *
186
     * @var \Cake\Database\Schema\TableSchema
187
     */
188
    protected $_schema;
189
190
    /**
191
     * The name of the field that represents the primary key in the table
192
     *
193
     * @var string|string[]
194
     */
195
    protected $_primaryKey;
196
197
    /**
198
     * The name of the field that represents a human readable representation of a row
199
     *
200
     * @var string
201
     */
202
    protected $_displayField;
203
204
    /**
205
     * The associations container for this Table.
206
     *
207
     * @var \Cake\ORM\AssociationCollection
208
     */
209
    protected $_associations;
210
211
    /**
212
     * BehaviorRegistry for this table
213
     *
214
     * @var \Cake\ORM\BehaviorRegistry
215
     */
216
    protected $_behaviors;
217
218
    /**
219
     * The name of the class that represent a single row for this table
220
     *
221
     * @var string
222
     */
223
    protected $_entityClass;
224
225
    /**
226
     * Registry key used to create this table object
227
     *
228
     * @var string
229
     */
230
    protected $_registryAlias;
231
232
    /**
233
     * Initializes a new instance
234
     *
235
     * The $config array understands the following keys:
236
     *
237
     * - table: Name of the database table to represent
238
     * - alias: Alias to be assigned to this table (default to table name)
239
     * - connection: The connection instance to use
240
     * - entityClass: The fully namespaced class name of the entity class that will
241
     *   represent rows in this table.
242
     * - schema: A \Cake\Database\Schema\TableSchema object or an array that can be
243
     *   passed to it.
244
     * - eventManager: An instance of an event manager to use for internal events
245
     * - behaviors: A BehaviorRegistry. Generally not used outside of tests.
246
     * - associations: An AssociationCollection instance.
247
     * - validator: A Validator instance which is assigned as the "default"
248
     *   validation set, or an associative array, where key is the name of the
249
     *   validation set and value the Validator instance.
250
     *
251
     * @param array $config List of options for this table
252
     */
253
    public function __construct(array $config = [])
254
    {
255
        if (!empty($config['registryAlias'])) {
256
            $this->setRegistryAlias($config['registryAlias']);
257
        }
258
        if (!empty($config['table'])) {
259
            $this->setTable($config['table']);
260
        }
261
        if (!empty($config['alias'])) {
262
            $this->setAlias($config['alias']);
263
        }
264
        if (!empty($config['connection'])) {
265
            $this->setConnection($config['connection']);
266
        }
267
        if (!empty($config['schema'])) {
268
            $this->setSchema($config['schema']);
269
        }
270
        if (!empty($config['entityClass'])) {
271
            $this->setEntityClass($config['entityClass']);
272
        }
273
        $eventManager = $behaviors = $associations = null;
274
        if (!empty($config['eventManager'])) {
275
            $eventManager = $config['eventManager'];
276
        }
277
        if (!empty($config['behaviors'])) {
278
            $behaviors = $config['behaviors'];
279
        }
280
        if (!empty($config['associations'])) {
281
            $associations = $config['associations'];
282
        }
283
        if (!empty($config['validator'])) {
284
            if (!is_array($config['validator'])) {
285
                $this->setValidator(static::DEFAULT_VALIDATOR, $config['validator']);
286
            } else {
287
                foreach ($config['validator'] as $name => $validator) {
288
                    $this->setValidator($name, $validator);
289
                }
290
            }
291
        }
292
        $this->_eventManager = $eventManager ?: new EventManager();
293
        $this->_behaviors = $behaviors ?: new BehaviorRegistry();
294
        $this->_behaviors->setTable($this);
295
        $this->_associations = $associations ?: new AssociationCollection();
296
297
        $this->initialize($config);
298
        $this->_eventManager->on($this);
299
        $this->dispatchEvent('Model.initialize');
300
    }
301
302
    /**
303
     * Get the default connection name.
304
     *
305
     * This method is used to get the fallback connection name if an
306
     * instance is created through the TableLocator without a connection.
307
     *
308
     * @return string
309
     * @see \Cake\ORM\Locator\TableLocator::get()
310
     */
311
    public static function defaultConnectionName()
312
    {
313
        return 'default';
314
    }
315
316
    /**
317
     * Initialize a table instance. Called after the constructor.
318
     *
319
     * You can use this method to define associations, attach behaviors
320
     * define validation and do any other initialization logic you need.
321
     *
322
     * ```
323
     *  public function initialize(array $config)
324
     *  {
325
     *      $this->belongsTo('Users');
326
     *      $this->belongsToMany('Tagging.Tags');
327
     *      $this->setPrimaryKey('something_else');
328
     *  }
329
     * ```
330
     *
331
     * @param array $config Configuration options passed to the constructor
332
     * @return void
333
     */
334
    public function initialize(array $config)
335
    {
336
    }
337
338
    /**
339
     * Sets the database table name.
340
     *
341
     * This can include the database schema name in the form 'schema.table'.
342
     * If the name must be quoted, enable automatic identifier quoting.
343
     *
344
     * @param string $table Table name.
345
     * @return $this
346
     */
347
    public function setTable($table)
348
    {
349
        $this->_table = $table;
350
351
        return $this;
352
    }
353
354
    /**
355
     * Returns the database table name.
356
     *
357
     * This can include the database schema name if set using `setTable()`.
358
     *
359
     * @return string
360
     */
361
    public function getTable()
362
    {
363
        if ($this->_table === null) {
364
            $table = namespaceSplit(get_class($this));
365
            $table = substr(end($table), 0, -5);
366
            if (!$table) {
367
                $table = $this->getAlias();
368
            }
369
            $this->_table = Inflector::underscore($table);
370
        }
371
372
        return $this->_table;
373
    }
374
375
    /**
376
     * Returns the database table name or sets a new one.
377
     *
378
     * @deprecated 3.4.0 Use setTable()/getTable() instead.
379
     * @param string|null $table the new table name
380
     * @return string
381
     */
382
    public function table($table = null)
383
    {
384
        deprecationWarning(
385
            get_called_class() . '::table() is deprecated. ' .
386
            'Use setTable()/getTable() instead.'
387
        );
388
        if ($table !== null) {
389
            $this->setTable($table);
390
        }
391
392
        return $this->getTable();
393
    }
394
395
    /**
396
     * Sets the table alias.
397
     *
398
     * @param string $alias Table alias
399
     * @return $this
400
     */
401
    public function setAlias($alias)
402
    {
403
        $this->_alias = $alias;
404
405
        return $this;
406
    }
407
408
    /**
409
     * Returns the table alias.
410
     *
411
     * @return string
412
     */
413
    public function getAlias()
414
    {
415
        if ($this->_alias === null) {
416
            $alias = namespaceSplit(get_class($this));
417
            $alias = substr(end($alias), 0, -5) ?: $this->_table;
418
            $this->_alias = $alias;
419
        }
420
421
        return $this->_alias;
422
    }
423
424
    /**
425
     * {@inheritDoc}
426
     * @deprecated 3.4.0 Use setAlias()/getAlias() instead.
427
     */
428 View Code Duplication
    public function alias($alias = null)
429
    {
430
        deprecationWarning(
431
            get_called_class() . '::alias() is deprecated. ' .
432
            'Use setAlias()/getAlias() instead.'
433
        );
434
        if ($alias !== null) {
435
            $this->setAlias($alias);
436
        }
437
438
        return $this->getAlias();
439
    }
440
441
    /**
442
     * Alias a field with the table's current alias.
443
     *
444
     * If field is already aliased it will result in no-op.
445
     *
446
     * @param string $field The field to alias.
447
     * @return string The field prefixed with the table alias.
448
     */
449
    public function aliasField($field)
450
    {
451
        if (strpos($field, '.') !== false) {
452
            return $field;
453
        }
454
455
        return $this->getAlias() . '.' . $field;
456
    }
457
458
    /**
459
     * Sets the table registry key used to create this table instance.
460
     *
461
     * @param string $registryAlias The key used to access this object.
462
     * @return $this
463
     */
464
    public function setRegistryAlias($registryAlias)
465
    {
466
        $this->_registryAlias = $registryAlias;
467
468
        return $this;
469
    }
470
471
    /**
472
     * Returns the table registry key used to create this table instance.
473
     *
474
     * @return string
475
     */
476
    public function getRegistryAlias()
477
    {
478
        if ($this->_registryAlias === null) {
479
            $this->_registryAlias = $this->getAlias();
480
        }
481
482
        return $this->_registryAlias;
483
    }
484
485
    /**
486
     * Returns the table registry key used to create this table instance or sets one.
487
     *
488
     * @deprecated 3.4.0 Use setRegistryAlias()/getRegistryAlias() instead.
489
     * @param string|null $registryAlias the key used to access this object
490
     * @return string
491
     */
492
    public function registryAlias($registryAlias = null)
493
    {
494
        deprecationWarning(
495
            get_called_class() . '::registryAlias() is deprecated. ' .
496
            'Use setRegistryAlias()/getRegistryAlias() instead.'
497
        );
498
        if ($registryAlias !== null) {
499
            $this->setRegistryAlias($registryAlias);
500
        }
501
502
        return $this->getRegistryAlias();
503
    }
504
505
    /**
506
     * Sets the connection instance.
507
     *
508
     * @param \Cake\Database\Connection $connection The connection instance
509
     * @return $this
510
     */
511
    public function setConnection(ConnectionInterface $connection)
512
    {
513
        $this->_connection = $connection;
0 ignored issues
show
Documentation Bug introduced by
$connection is of type object<Cake\Datasource\ConnectionInterface>, but the property $_connection was declared to be of type object<Cake\Database\Connection>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof 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 given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
514
515
        return $this;
516
    }
517
518
    /**
519
     * Returns the connection instance.
520
     *
521
     * @return \Cake\Database\Connection
522
     */
523
    public function getConnection()
524
    {
525
        return $this->_connection;
526
    }
527
528
    /**
529
     * Returns the connection instance or sets a new one
530
     *
531
     * @deprecated 3.4.0 Use setConnection()/getConnection() instead.
532
     * @param \Cake\Datasource\ConnectionInterface|null $connection The new connection instance
533
     * @return \Cake\Datasource\ConnectionInterface
534
     */
535
    public function connection(ConnectionInterface $connection = null)
536
    {
537
        deprecationWarning(
538
            get_called_class() . '::connection() is deprecated. ' .
539
            'Use setConnection()/getConnection() instead.'
540
        );
541
        if ($connection !== null) {
542
            $this->setConnection($connection);
543
        }
544
545
        return $this->getConnection();
546
    }
547
548
    /**
549
     * Returns the schema table object describing this table's properties.
550
     *
551
     * @return \Cake\Database\Schema\TableSchema
552
     */
553
    public function getSchema()
554
    {
555
        if ($this->_schema === null) {
556
            $this->_schema = $this->_initializeSchema(
557
                $this->getConnection()
558
                    ->getSchemaCollection()
559
                    ->describe($this->getTable())
560
            );
561
        }
562
563
        return $this->_schema;
564
    }
565
566
    /**
567
     * Sets the schema table object describing this table's properties.
568
     *
569
     * If an array is passed, a new TableSchema will be constructed
570
     * out of it and used as the schema for this table.
571
     *
572
     * @param array|\Cake\Database\Schema\TableSchema $schema Schema to be used for this table
573
     * @return $this
574
     */
575
    public function setSchema($schema)
576
    {
577
        if (is_array($schema)) {
578
            $constraints = [];
579
580
            if (isset($schema['_constraints'])) {
581
                $constraints = $schema['_constraints'];
582
                unset($schema['_constraints']);
583
            }
584
585
            $schema = new TableSchema($this->getTable(), $schema);
586
587
            foreach ($constraints as $name => $value) {
588
                $schema->addConstraint($name, $value);
589
            }
590
        }
591
592
        $this->_schema = $schema;
593
594
        return $this;
595
    }
596
597
    /**
598
     * Returns the schema table object describing this table's properties.
599
     *
600
     * If a TableSchema is passed, it will be used for this table
601
     * instead of the default one.
602
     *
603
     * If an array is passed, a new TableSchema will be constructed
604
     * out of it and used as the schema for this table.
605
     *
606
     * @deprecated 3.4.0 Use setSchema()/getSchema() instead.
607
     * @param array|\Cake\Database\Schema\TableSchema|null $schema New schema to be used for this table
608
     * @return \Cake\Database\Schema\TableSchema
609
     */
610
    public function schema($schema = null)
611
    {
612
        deprecationWarning(
613
            get_called_class() . '::schema() is deprecated. ' .
614
            'Use setSchema()/getSchema() instead.'
615
        );
616
        if ($schema !== null) {
617
            $this->setSchema($schema);
618
        }
619
620
        return $this->getSchema();
621
    }
622
623
    /**
624
     * Override this function in order to alter the schema used by this table.
625
     * This function is only called after fetching the schema out of the database.
626
     * If you wish to provide your own schema to this table without touching the
627
     * database, you can override schema() or inject the definitions though that
628
     * method.
629
     *
630
     * ### Example:
631
     *
632
     * ```
633
     * protected function _initializeSchema(\Cake\Database\Schema\TableSchema $schema) {
634
     *  $schema->setColumnType('preferences', 'json');
635
     *  return $schema;
636
     * }
637
     * ```
638
     *
639
     * @param \Cake\Database\Schema\TableSchema $schema The table definition fetched from database.
640
     * @return \Cake\Database\Schema\TableSchema the altered schema
641
     */
642
    protected function _initializeSchema(TableSchema $schema)
643
    {
644
        return $schema;
645
    }
646
647
    /**
648
     * Test to see if a Table has a specific field/column.
649
     *
650
     * Delegates to the schema object and checks for column presence
651
     * using the Schema\Table instance.
652
     *
653
     * @param string $field The field to check for.
654
     * @return bool True if the field exists, false if it does not.
655
     */
656
    public function hasField($field)
657
    {
658
        $schema = $this->getSchema();
659
660
        return $schema->getColumn($field) !== null;
661
    }
662
663
    /**
664
     * Sets the primary key field name.
665
     *
666
     * @param string|string[] $key Sets a new name to be used as primary key
667
     * @return $this
668
     */
669
    public function setPrimaryKey($key)
670
    {
671
        $this->_primaryKey = $key;
672
673
        return $this;
674
    }
675
676
    /**
677
     * Returns the primary key field name.
678
     *
679
     * @return string|string[]
680
     */
681
    public function getPrimaryKey()
682
    {
683
        if ($this->_primaryKey === null) {
684
            $key = (array)$this->getSchema()->primaryKey();
685
            if (count($key) === 1) {
686
                $key = $key[0];
687
            }
688
            $this->_primaryKey = $key;
689
        }
690
691
        return $this->_primaryKey;
692
    }
693
694
    /**
695
     * Returns the primary key field name or sets a new one
696
     *
697
     * @deprecated 3.4.0 Use setPrimaryKey()/getPrimaryKey() instead.
698
     * @param string|string[]|null $key Sets a new name to be used as primary key
699
     * @return string|string[]
700
     */
701
    public function primaryKey($key = null)
702
    {
703
        deprecationWarning(
704
            get_called_class() . '::primaryKey() is deprecated. ' .
705
            'Use setPrimaryKey()/getPrimaryKey() instead.'
706
        );
707
        if ($key !== null) {
708
            $this->setPrimaryKey($key);
709
        }
710
711
        return $this->getPrimaryKey();
712
    }
713
714
    /**
715
     * Sets the display field.
716
     *
717
     * @param string $key Name to be used as display field.
718
     * @return $this
719
     */
720
    public function setDisplayField($key)
721
    {
722
        $this->_displayField = $key;
723
724
        return $this;
725
    }
726
727
    /**
728
     * Returns the display field.
729
     *
730
     * @return string
731
     */
732
    public function getDisplayField()
733
    {
734
        if ($this->_displayField === null) {
735
            $schema = $this->getSchema();
736
            $primary = (array)$this->getPrimaryKey();
737
            $this->_displayField = array_shift($primary);
738
            if ($schema->getColumn('title')) {
739
                $this->_displayField = 'title';
740
            }
741
            if ($schema->getColumn('name')) {
742
                $this->_displayField = 'name';
743
            }
744
        }
745
746
        return $this->_displayField;
747
    }
748
749
    /**
750
     * Returns the display field or sets a new one
751
     *
752
     * @deprecated 3.4.0 Use setDisplayField()/getDisplayField() instead.
753
     * @param string|null $key sets a new name to be used as display field
754
     * @return string
755
     */
756
    public function displayField($key = null)
757
    {
758
        deprecationWarning(
759
            get_called_class() . '::displayField() is deprecated. ' .
760
            'Use setDisplayField()/getDisplayField() instead.'
761
        );
762
        if ($key !== null) {
763
            $this->setDisplayField($key);
764
765
            return $key;
766
        }
767
768
        return $this->getDisplayField();
769
    }
770
771
    /**
772
     * Returns the class used to hydrate rows for this table.
773
     *
774
     * @return string
775
     */
776
    public function getEntityClass()
777
    {
778
        if (!$this->_entityClass) {
779
            $default = Entity::class;
780
            $self = get_called_class();
781
            $parts = explode('\\', $self);
782
783
            if ($self === __CLASS__ || count($parts) < 3) {
784
                return $this->_entityClass = $default;
785
            }
786
787
            $alias = Inflector::classify(Inflector::underscore(substr(array_pop($parts), 0, -5)));
788
            $name = implode('\\', array_slice($parts, 0, -1)) . '\\Entity\\' . $alias;
789
            if (!class_exists($name)) {
790
                return $this->_entityClass = $default;
791
            }
792
793
            $class = App::className($name, 'Model/Entity');
794
            if (!$class) {
795
                throw new MissingEntityException([$name]);
796
            }
797
798
            $this->_entityClass = $class;
799
        }
800
801
        return $this->_entityClass;
802
    }
803
804
    /**
805
     * Sets the class used to hydrate rows for this table.
806
     *
807
     * @param string $name The name of the class to use
808
     * @throws \Cake\ORM\Exception\MissingEntityException when the entity class cannot be found
809
     * @return $this
810
     */
811
    public function setEntityClass($name)
812
    {
813
        $class = App::className($name, 'Model/Entity');
814
        if (!$class) {
815
            throw new MissingEntityException([$name]);
816
        }
817
818
        $this->_entityClass = $class;
819
820
        return $this;
821
    }
822
823
    /**
824
     * Returns the class used to hydrate rows for this table or sets
825
     * a new one
826
     *
827
     * @deprecated 3.4.0 Use setEntityClass()/getEntityClass() instead.
828
     * @param string|null $name The name of the class to use
829
     * @throws \Cake\ORM\Exception\MissingEntityException when the entity class cannot be found
830
     * @return string
831
     */
832 View Code Duplication
    public function entityClass($name = null)
833
    {
834
        deprecationWarning(
835
            get_called_class() . '::entityClass() is deprecated. ' .
836
            'Use setEntityClass()/getEntityClass() instead.'
837
        );
838
        if ($name !== null) {
839
            $this->setEntityClass($name);
840
        }
841
842
        return $this->getEntityClass();
843
    }
844
845
    /**
846
     * Add a behavior.
847
     *
848
     * Adds a behavior to this table's behavior collection. Behaviors
849
     * provide an easy way to create horizontally re-usable features
850
     * that can provide trait like functionality, and allow for events
851
     * to be listened to.
852
     *
853
     * Example:
854
     *
855
     * Load a behavior, with some settings.
856
     *
857
     * ```
858
     * $this->addBehavior('Tree', ['parent' => 'parentId']);
859
     * ```
860
     *
861
     * Behaviors are generally loaded during Table::initialize().
862
     *
863
     * @param string $name The name of the behavior. Can be a short class reference.
864
     * @param array $options The options for the behavior to use.
865
     * @return $this
866
     * @throws \RuntimeException If a behavior is being reloaded.
867
     * @see \Cake\ORM\Behavior
868
     */
869
    public function addBehavior($name, array $options = [])
870
    {
871
        $this->_behaviors->load($name, $options);
872
873
        return $this;
874
    }
875
876
    /**
877
     * Adds an array of behaviors to the table's behavior collection.
878
     *
879
     * Example:
880
     *
881
     * ```
882
     * $this->addBehaviors([
883
     *      'Timestamp',
884
     *      'Tree' => ['level' => 'level'],
885
     * ]);
886
     * ```
887
     *
888
     * @param array $behaviors All of the behaviors to load.
889
     * @return $this
890
     * @throws \RuntimeException If a behavior is being reloaded.
891
     */
892
    public function addBehaviors(array $behaviors)
893
    {
894
        foreach ($behaviors as $name => $options) {
895
            if (is_int($name)) {
896
                $name = $options;
897
                $options = [];
898
            }
899
900
            $this->addBehavior($name, $options);
901
        }
902
903
        return $this;
904
    }
905
906
    /**
907
     * Removes a behavior from this table's behavior registry.
908
     *
909
     * Example:
910
     *
911
     * Remove a behavior from this table.
912
     *
913
     * ```
914
     * $this->removeBehavior('Tree');
915
     * ```
916
     *
917
     * @param string $name The alias that the behavior was added with.
918
     * @return $this
919
     * @see \Cake\ORM\Behavior
920
     */
921
    public function removeBehavior($name)
922
    {
923
        $this->_behaviors->unload($name);
924
925
        return $this;
926
    }
927
928
    /**
929
     * Returns the behavior registry for this table.
930
     *
931
     * @return \Cake\ORM\BehaviorRegistry The BehaviorRegistry instance.
932
     */
933
    public function behaviors()
934
    {
935
        return $this->_behaviors;
936
    }
937
938
    /**
939
     * Get a behavior from the registry.
940
     *
941
     * @param string $name The behavior alias to get from the registry.
942
     * @return \Cake\ORM\Behavior
943
     * @throws \InvalidArgumentException If the behavior does not exist.
944
     */
945
    public function getBehavior($name)
946
    {
947
        /** @var \Cake\ORM\Behavior $behavior */
948
        $behavior = $this->_behaviors->get($name);
949
        if ($behavior === null) {
950
            throw new InvalidArgumentException(sprintf(
951
                'The %s behavior is not defined on %s.',
952
                $name,
953
                get_class($this)
954
            ));
955
        }
956
957
        return $behavior;
958
    }
959
960
    /**
961
     * Check if a behavior with the given alias has been loaded.
962
     *
963
     * @param string $name The behavior alias to check.
964
     * @return bool Whether or not the behavior exists.
965
     */
966
    public function hasBehavior($name)
967
    {
968
        return $this->_behaviors->has($name);
969
    }
970
971
    /**
972
     * Returns an association object configured for the specified alias if any.
973
     *
974
     * @deprecated 3.6.0 Use getAssociation() and Table::hasAssociation() instead.
975
     * @param string $name the alias used for the association.
976
     * @return \Cake\ORM\Association|null Either the association or null.
977
     */
978
    public function association($name)
979
    {
980
        deprecationWarning('Use Table::getAssociation() and Table::hasAssociation() instead.');
981
982
        return $this->findAssociation($name);
983
    }
984
985
    /**
986
     * Returns an association object configured for the specified alias.
987
     *
988
     * The name argument also supports dot syntax to access deeper associations.
989
     *
990
     * ```
991
     * $users = $this->getAssociation('Articles.Comments.Users');
992
     * ```
993
     *
994
     * Note that this method requires the association to be present or otherwise
995
     * throws an exception.
996
     * If you are not sure, use hasAssociation() before calling this method.
997
     *
998
     * @param string $name The alias used for the association.
999
     * @return \Cake\ORM\Association The association.
1000
     * @throws \InvalidArgumentException
1001
     */
1002
    public function getAssociation($name)
1003
    {
1004
        $association = $this->findAssociation($name);
1005
        if (!$association) {
1006
            throw new InvalidArgumentException("The {$name} association is not defined on {$this->getAlias()}.");
1007
        }
1008
1009
        return $association;
1010
    }
1011
1012
    /**
1013
     * Checks whether a specific association exists on this Table instance.
1014
     *
1015
     * The name argument also supports dot syntax to access deeper associations.
1016
     *
1017
     * ```
1018
     * $hasUsers = $this->hasAssociation('Articles.Comments.Users');
1019
     * ```
1020
     *
1021
     * @param string $name The alias used for the association.
1022
     * @return bool
1023
     */
1024
    public function hasAssociation($name)
1025
    {
1026
        return $this->findAssociation($name) !== null;
1027
    }
1028
1029
    /**
1030
     * Returns an association object configured for the specified alias if any.
1031
     *
1032
     * The name argument also supports dot syntax to access deeper associations.
1033
     *
1034
     * ```
1035
     * $users = $this->getAssociation('Articles.Comments.Users');
1036
     * ```
1037
     *
1038
     * @param string $name The alias used for the association.
1039
     * @return \Cake\ORM\Association|null Either the association or null.
1040
     */
1041
    protected function findAssociation($name)
1042
    {
1043
        if (strpos($name, '.') === false) {
1044
            return $this->_associations->get($name);
1045
        }
1046
1047
        list($name, $next) = array_pad(explode('.', $name, 2), 2, null);
1048
        $result = $this->_associations->get($name);
1049
1050
        if ($result !== null && $next !== null) {
1051
            $result = $result->getTarget()->getAssociation($next);
1052
        }
1053
1054
        return $result;
1055
    }
1056
1057
    /**
1058
     * Get the associations collection for this table.
1059
     *
1060
     * @return \Cake\ORM\AssociationCollection|\Cake\ORM\Association[] The collection of association objects.
1061
     */
1062
    public function associations()
1063
    {
1064
        return $this->_associations;
1065
    }
1066
1067
    /**
1068
     * Setup multiple associations.
1069
     *
1070
     * It takes an array containing set of table names indexed by association type
1071
     * as argument:
1072
     *
1073
     * ```
1074
     * $this->Posts->addAssociations([
1075
     *   'belongsTo' => [
1076
     *     'Users' => ['className' => 'App\Model\Table\UsersTable']
1077
     *   ],
1078
     *   'hasMany' => ['Comments'],
1079
     *   'belongsToMany' => ['Tags']
1080
     * ]);
1081
     * ```
1082
     *
1083
     * Each association type accepts multiple associations where the keys
1084
     * are the aliases, and the values are association config data. If numeric
1085
     * keys are used the values will be treated as association aliases.
1086
     *
1087
     * @param array $params Set of associations to bind (indexed by association type)
1088
     * @return $this
1089
     * @see \Cake\ORM\Table::belongsTo()
1090
     * @see \Cake\ORM\Table::hasOne()
1091
     * @see \Cake\ORM\Table::hasMany()
1092
     * @see \Cake\ORM\Table::belongsToMany()
1093
     */
1094
    public function addAssociations(array $params)
1095
    {
1096
        foreach ($params as $assocType => $tables) {
1097
            foreach ($tables as $associated => $options) {
1098
                if (is_numeric($associated)) {
1099
                    $associated = $options;
1100
                    $options = [];
1101
                }
1102
                $this->{$assocType}($associated, $options);
1103
            }
1104
        }
1105
1106
        return $this;
1107
    }
1108
1109
    /**
1110
     * Creates a new BelongsTo association between this table and a target
1111
     * table. A "belongs to" association is a N-1 relationship where this table
1112
     * is the N side, and where there is a single associated record in the target
1113
     * table for each one in this table.
1114
     *
1115
     * Target table can be inferred by its name, which is provided in the
1116
     * first argument, or you can either pass the to be instantiated or
1117
     * an instance of it directly.
1118
     *
1119
     * The options array accept the following keys:
1120
     *
1121
     * - className: The class name of the target table object
1122
     * - targetTable: An instance of a table object to be used as the target table
1123
     * - foreignKey: The name of the field to use as foreign key, if false none
1124
     *   will be used
1125
     * - conditions: array with a list of conditions to filter the join with
1126
     * - joinType: The type of join to be used (e.g. INNER)
1127
     * - strategy: The loading strategy to use. 'join' and 'select' are supported.
1128
     * - finder: The finder method to use when loading records from this association.
1129
     *   Defaults to 'all'. When the strategy is 'join', only the fields, containments,
1130
     *   and where conditions will be used from the finder.
1131
     *
1132
     * This method will return the association object that was built.
1133
     *
1134
     * @param string $associated the alias for the target table. This is used to
1135
     * uniquely identify the association
1136
     * @param array $options list of options to configure the association definition
1137
     * @return \Cake\ORM\Association\BelongsTo
1138
     */
1139 View Code Duplication
    public function belongsTo($associated, array $options = [])
1140
    {
1141
        $options += ['sourceTable' => $this];
1142
1143
        /** @var \Cake\ORM\Association\BelongsTo $association */
1144
        $association = $this->_associations->load(BelongsTo::class, $associated, $options);
1145
1146
        return $association;
1147
    }
1148
1149
    /**
1150
     * Creates a new HasOne association between this table and a target
1151
     * table. A "has one" association is a 1-1 relationship.
1152
     *
1153
     * Target table can be inferred by its name, which is provided in the
1154
     * first argument, or you can either pass the class name to be instantiated or
1155
     * an instance of it directly.
1156
     *
1157
     * The options array accept the following keys:
1158
     *
1159
     * - className: The class name of the target table object
1160
     * - targetTable: An instance of a table object to be used as the target table
1161
     * - foreignKey: The name of the field to use as foreign key, if false none
1162
     *   will be used
1163
     * - dependent: Set to true if you want CakePHP to cascade deletes to the
1164
     *   associated table when an entity is removed on this table. The delete operation
1165
     *   on the associated table will not cascade further. To get recursive cascades enable
1166
     *   `cascadeCallbacks` as well. Set to false if you don't want CakePHP to remove
1167
     *   associated data, or when you are using database constraints.
1168
     * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
1169
     *   cascaded deletes. If false the ORM will use deleteAll() to remove data.
1170
     *   When true records will be loaded and then deleted.
1171
     * - conditions: array with a list of conditions to filter the join with
1172
     * - joinType: The type of join to be used (e.g. LEFT)
1173
     * - strategy: The loading strategy to use. 'join' and 'select' are supported.
1174
     * - finder: The finder method to use when loading records from this association.
1175
     *   Defaults to 'all'. When the strategy is 'join', only the fields, containments,
1176
     *   and where conditions will be used from the finder.
1177
     *
1178
     * This method will return the association object that was built.
1179
     *
1180
     * @param string $associated the alias for the target table. This is used to
1181
     * uniquely identify the association
1182
     * @param array $options list of options to configure the association definition
1183
     * @return \Cake\ORM\Association\HasOne
1184
     */
1185 View Code Duplication
    public function hasOne($associated, array $options = [])
1186
    {
1187
        $options += ['sourceTable' => $this];
1188
1189
        /** @var \Cake\ORM\Association\HasOne $association */
1190
        $association = $this->_associations->load(HasOne::class, $associated, $options);
1191
1192
        return $association;
1193
    }
1194
1195
    /**
1196
     * Creates a new HasMany association between this table and a target
1197
     * table. A "has many" association is a 1-N relationship.
1198
     *
1199
     * Target table can be inferred by its name, which is provided in the
1200
     * first argument, or you can either pass the class name to be instantiated or
1201
     * an instance of it directly.
1202
     *
1203
     * The options array accept the following keys:
1204
     *
1205
     * - className: The class name of the target table object
1206
     * - targetTable: An instance of a table object to be used as the target table
1207
     * - foreignKey: The name of the field to use as foreign key, if false none
1208
     *   will be used
1209
     * - dependent: Set to true if you want CakePHP to cascade deletes to the
1210
     *   associated table when an entity is removed on this table. The delete operation
1211
     *   on the associated table will not cascade further. To get recursive cascades enable
1212
     *   `cascadeCallbacks` as well. Set to false if you don't want CakePHP to remove
1213
     *   associated data, or when you are using database constraints.
1214
     * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
1215
     *   cascaded deletes. If false the ORM will use deleteAll() to remove data.
1216
     *   When true records will be loaded and then deleted.
1217
     * - conditions: array with a list of conditions to filter the join with
1218
     * - sort: The order in which results for this association should be returned
1219
     * - saveStrategy: Either 'append' or 'replace'. When 'append' the current records
1220
     *   are appended to any records in the database. When 'replace' associated records
1221
     *   not in the current set will be removed. If the foreign key is a null able column
1222
     *   or if `dependent` is true records will be orphaned.
1223
     * - strategy: The strategy to be used for selecting results Either 'select'
1224
     *   or 'subquery'. If subquery is selected the query used to return results
1225
     *   in the source table will be used as conditions for getting rows in the
1226
     *   target table.
1227
     * - finder: The finder method to use when loading records from this association.
1228
     *   Defaults to 'all'.
1229
     *
1230
     * This method will return the association object that was built.
1231
     *
1232
     * @param string $associated the alias for the target table. This is used to
1233
     * uniquely identify the association
1234
     * @param array $options list of options to configure the association definition
1235
     * @return \Cake\ORM\Association\HasMany
1236
     */
1237 View Code Duplication
    public function hasMany($associated, array $options = [])
1238
    {
1239
        $options += ['sourceTable' => $this];
1240
1241
        /** @var \Cake\ORM\Association\HasMany $association */
1242
        $association = $this->_associations->load(HasMany::class, $associated, $options);
1243
1244
        return $association;
1245
    }
1246
1247
    /**
1248
     * Creates a new BelongsToMany association between this table and a target
1249
     * table. A "belongs to many" association is a M-N relationship.
1250
     *
1251
     * Target table can be inferred by its name, which is provided in the
1252
     * first argument, or you can either pass the class name to be instantiated or
1253
     * an instance of it directly.
1254
     *
1255
     * The options array accept the following keys:
1256
     *
1257
     * - className: The class name of the target table object.
1258
     * - targetTable: An instance of a table object to be used as the target table.
1259
     * - foreignKey: The name of the field to use as foreign key.
1260
     * - targetForeignKey: The name of the field to use as the target foreign key.
1261
     * - joinTable: The name of the table representing the link between the two
1262
     * - through: If you choose to use an already instantiated link table, set this
1263
     *   key to a configured Table instance containing associations to both the source
1264
     *   and target tables in this association.
1265
     * - dependent: Set to false, if you do not want junction table records removed
1266
     *   when an owning record is removed.
1267
     * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
1268
     *   cascaded deletes. If false the ORM will use deleteAll() to remove data.
1269
     *   When true join/junction table records will be loaded and then deleted.
1270
     * - conditions: array with a list of conditions to filter the join with.
1271
     * - sort: The order in which results for this association should be returned.
1272
     * - strategy: The strategy to be used for selecting results Either 'select'
1273
     *   or 'subquery'. If subquery is selected the query used to return results
1274
     *   in the source table will be used as conditions for getting rows in the
1275
     *   target table.
1276
     * - saveStrategy: Either 'append' or 'replace'. Indicates the mode to be used
1277
     *   for saving associated entities. The former will only create new links
1278
     *   between both side of the relation and the latter will do a wipe and
1279
     *   replace to create the links between the passed entities when saving.
1280
     * - strategy: The loading strategy to use. 'select' and 'subquery' are supported.
1281
     * - finder: The finder method to use when loading records from this association.
1282
     *   Defaults to 'all'.
1283
     *
1284
     * This method will return the association object that was built.
1285
     *
1286
     * @param string $associated the alias for the target table. This is used to
1287
     * uniquely identify the association
1288
     * @param array $options list of options to configure the association definition
1289
     * @return \Cake\ORM\Association\BelongsToMany
1290
     */
1291 View Code Duplication
    public function belongsToMany($associated, array $options = [])
1292
    {
1293
        $options += ['sourceTable' => $this];
1294
1295
        /** @var \Cake\ORM\Association\BelongsToMany $association */
1296
        $association = $this->_associations->load(BelongsToMany::class, $associated, $options);
1297
1298
        return $association;
1299
    }
1300
1301
    /**
1302
     * Creates a new Query for this repository and applies some defaults based on the
1303
     * type of search that was selected.
1304
     *
1305
     * ### Model.beforeFind event
1306
     *
1307
     * Each find() will trigger a `Model.beforeFind` event for all attached
1308
     * listeners. Any listener can set a valid result set using $query
1309
     *
1310
     * By default, `$options` will recognize the following keys:
1311
     *
1312
     * - fields
1313
     * - conditions
1314
     * - order
1315
     * - limit
1316
     * - offset
1317
     * - page
1318
     * - group
1319
     * - having
1320
     * - contain
1321
     * - join
1322
     *
1323
     * ### Usage
1324
     *
1325
     * Using the options array:
1326
     *
1327
     * ```
1328
     * $query = $articles->find('all', [
1329
     *   'conditions' => ['published' => 1],
1330
     *   'limit' => 10,
1331
     *   'contain' => ['Users', 'Comments']
1332
     * ]);
1333
     * ```
1334
     *
1335
     * Using the builder interface:
1336
     *
1337
     * ```
1338
     * $query = $articles->find()
1339
     *   ->where(['published' => 1])
1340
     *   ->limit(10)
1341
     *   ->contain(['Users', 'Comments']);
1342
     * ```
1343
     *
1344
     * ### Calling finders
1345
     *
1346
     * The find() method is the entry point for custom finder methods.
1347
     * You can invoke a finder by specifying the type:
1348
     *
1349
     * ```
1350
     * $query = $articles->find('published');
1351
     * ```
1352
     *
1353
     * Would invoke the `findPublished` method.
1354
     *
1355
     * @param string $type the type of query to perform
1356
     * @param array|\ArrayAccess $options An array that will be passed to Query::applyOptions()
1357
     * @return \Cake\ORM\Query The query builder
1358
     */
1359
    public function find($type = 'all', $options = [])
1360
    {
1361
        $query = $this->query();
1362
        $query->select();
1363
1364
        return $this->callFinder($type, $query, $options);
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 1359 can also be of type object<ArrayAccess>; however, Cake\ORM\Table::callFinder() does only seem to accept array, 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...
1365
    }
1366
1367
    /**
1368
     * Returns the query as passed.
1369
     *
1370
     * By default findAll() applies no conditions, you
1371
     * can override this method in subclasses to modify how `find('all')` works.
1372
     *
1373
     * @param \Cake\ORM\Query $query The query to find with
1374
     * @param array $options The options to use for the find
1375
     * @return \Cake\ORM\Query The query builder
1376
     */
1377
    public function findAll(Query $query, array $options)
1378
    {
1379
        return $query;
1380
    }
1381
1382
    /**
1383
     * Sets up a query object so results appear as an indexed array, useful for any
1384
     * place where you would want a list such as for populating input select boxes.
1385
     *
1386
     * When calling this finder, the fields passed are used to determine what should
1387
     * be used as the array key, value and optionally what to group the results by.
1388
     * By default the primary key for the model is used for the key, and the display
1389
     * field as value.
1390
     *
1391
     * The results of this finder will be in the following form:
1392
     *
1393
     * ```
1394
     * [
1395
     *  1 => 'value for id 1',
1396
     *  2 => 'value for id 2',
1397
     *  4 => 'value for id 4'
1398
     * ]
1399
     * ```
1400
     *
1401
     * You can specify which property will be used as the key and which as value
1402
     * by using the `$options` array, when not specified, it will use the results
1403
     * of calling `primaryKey` and `displayField` respectively in this table:
1404
     *
1405
     * ```
1406
     * $table->find('list', [
1407
     *  'keyField' => 'name',
1408
     *  'valueField' => 'age'
1409
     * ]);
1410
     * ```
1411
     *
1412
     * Results can be put together in bigger groups when they share a property, you
1413
     * can customize the property to use for grouping by setting `groupField`:
1414
     *
1415
     * ```
1416
     * $table->find('list', [
1417
     *  'groupField' => 'category_id',
1418
     * ]);
1419
     * ```
1420
     *
1421
     * When using a `groupField` results will be returned in this format:
1422
     *
1423
     * ```
1424
     * [
1425
     *  'group_1' => [
1426
     *      1 => 'value for id 1',
1427
     *      2 => 'value for id 2',
1428
     *  ]
1429
     *  'group_2' => [
1430
     *      4 => 'value for id 4'
1431
     *  ]
1432
     * ]
1433
     * ```
1434
     *
1435
     * @param \Cake\ORM\Query $query The query to find with
1436
     * @param array $options The options for the find
1437
     * @return \Cake\ORM\Query The query builder
1438
     */
1439
    public function findList(Query $query, array $options)
1440
    {
1441
        $options += [
1442
            'keyField' => $this->getPrimaryKey(),
1443
            'valueField' => $this->getDisplayField(),
1444
            'groupField' => null,
1445
        ];
1446
1447 View Code Duplication
        if (isset($options['idField'])) {
1448
            $options['keyField'] = $options['idField'];
1449
            unset($options['idField']);
1450
            deprecationWarning('Option "idField" is deprecated, use "keyField" instead.');
1451
        }
1452
1453
        if (
1454
            !$query->clause('select') &&
1455
            !is_object($options['keyField']) &&
1456
            !is_object($options['valueField']) &&
1457
            !is_object($options['groupField'])
1458
        ) {
1459
            $fields = array_merge(
1460
                (array)$options['keyField'],
1461
                (array)$options['valueField'],
1462
                (array)$options['groupField']
1463
            );
1464
            $columns = $this->getSchema()->columns();
1465
            if (count($fields) === count(array_intersect($fields, $columns))) {
1466
                $query->select($fields);
1467
            }
1468
        }
1469
1470
        $options = $this->_setFieldMatchers(
1471
            $options,
1472
            ['keyField', 'valueField', 'groupField']
1473
        );
1474
1475
        return $query->formatResults(function ($results) use ($options) {
1476
            /** @var \Cake\Collection\CollectionInterface $results */
1477
            return $results->combine(
1478
                $options['keyField'],
1479
                $options['valueField'],
1480
                $options['groupField']
1481
            );
1482
        });
1483
    }
1484
1485
    /**
1486
     * Results for this finder will be a nested array, and is appropriate if you want
1487
     * to use the parent_id field of your model data to build nested results.
1488
     *
1489
     * Values belonging to a parent row based on their parent_id value will be
1490
     * recursively nested inside the parent row values using the `children` property
1491
     *
1492
     * You can customize what fields are used for nesting results, by default the
1493
     * primary key and the `parent_id` fields are used. If you wish to change
1494
     * these defaults you need to provide the keys `keyField`, `parentField` or `nestingKey` in
1495
     * `$options`:
1496
     *
1497
     * ```
1498
     * $table->find('threaded', [
1499
     *  'keyField' => 'id',
1500
     *  'parentField' => 'ancestor_id'
1501
     *  'nestingKey' => 'children'
1502
     * ]);
1503
     * ```
1504
     *
1505
     * @param \Cake\ORM\Query $query The query to find with
1506
     * @param array $options The options to find with
1507
     * @return \Cake\ORM\Query The query builder
1508
     */
1509
    public function findThreaded(Query $query, array $options)
1510
    {
1511
        $options += [
1512
            'keyField' => $this->getPrimaryKey(),
1513
            'parentField' => 'parent_id',
1514
            'nestingKey' => 'children',
1515
        ];
1516
1517 View Code Duplication
        if (isset($options['idField'])) {
1518
            $options['keyField'] = $options['idField'];
1519
            unset($options['idField']);
1520
            deprecationWarning('Option "idField" is deprecated, use "keyField" instead.');
1521
        }
1522
1523
        $options = $this->_setFieldMatchers($options, ['keyField', 'parentField']);
1524
1525
        return $query->formatResults(function ($results) use ($options) {
1526
            /** @var \Cake\Collection\CollectionInterface $results */
1527
            return $results->nest($options['keyField'], $options['parentField'], $options['nestingKey']);
1528
        });
1529
    }
1530
1531
    /**
1532
     * Out of an options array, check if the keys described in `$keys` are arrays
1533
     * and change the values for closures that will concatenate the each of the
1534
     * properties in the value array when passed a row.
1535
     *
1536
     * This is an auxiliary function used for result formatters that can accept
1537
     * composite keys when comparing values.
1538
     *
1539
     * @param array $options the original options passed to a finder
1540
     * @param array $keys the keys to check in $options to build matchers from
1541
     * the associated value
1542
     * @return array
1543
     */
1544
    protected function _setFieldMatchers($options, $keys)
1545
    {
1546
        foreach ($keys as $field) {
1547
            if (!is_array($options[$field])) {
1548
                continue;
1549
            }
1550
1551
            if (count($options[$field]) === 1) {
1552
                $options[$field] = current($options[$field]);
1553
                continue;
1554
            }
1555
1556
            $fields = $options[$field];
1557
            $options[$field] = function ($row) use ($fields) {
1558
                $matches = [];
1559
                foreach ($fields as $field) {
1560
                    $matches[] = $row[$field];
1561
                }
1562
1563
                return implode(';', $matches);
1564
            };
1565
        }
1566
1567
        return $options;
1568
    }
1569
1570
    /**
1571
     * {@inheritDoc}
1572
     *
1573
     * ### Usage
1574
     *
1575
     * Get an article and some relationships:
1576
     *
1577
     * ```
1578
     * $article = $articles->get(1, ['contain' => ['Users', 'Comments']]);
1579
     * ```
1580
     *
1581
     * @throws \Cake\Datasource\Exception\InvalidPrimaryKeyException When $primaryKey has an
1582
     *      incorrect number of elements.
1583
     */
1584
    public function get($primaryKey, $options = [])
1585
    {
1586
        $key = (array)$this->getPrimaryKey();
1587
        $alias = $this->getAlias();
1588
        foreach ($key as $index => $keyname) {
1589
            $key[$index] = $alias . '.' . $keyname;
1590
        }
1591
        $primaryKey = (array)$primaryKey;
1592
        if (count($key) !== count($primaryKey)) {
1593
            $primaryKey = $primaryKey ?: [null];
1594
            $primaryKey = array_map(function ($key) {
1595
                return var_export($key, true);
1596
            }, $primaryKey);
1597
1598
            throw new InvalidPrimaryKeyException(sprintf(
1599
                'Record not found in table "%s" with primary key [%s]',
1600
                $this->getTable(),
1601
                implode(', ', $primaryKey)
1602
            ));
1603
        }
1604
        $conditions = array_combine($key, $primaryKey);
1605
1606
        $cacheConfig = isset($options['cache']) ? $options['cache'] : false;
1607
        $cacheKey = isset($options['key']) ? $options['key'] : false;
1608
        $finder = isset($options['finder']) ? $options['finder'] : 'all';
1609
        unset($options['key'], $options['cache'], $options['finder']);
1610
1611
        $query = $this->find($finder, $options)->where($conditions);
1612
1613
        if ($cacheConfig) {
1614
            if (!$cacheKey) {
1615
                $cacheKey = sprintf(
1616
                    'get:%s.%s%s',
1617
                    $this->getConnection()->configName(),
1618
                    $this->getTable(),
1619
                    json_encode($primaryKey)
1620
                );
1621
            }
1622
            $query->cache($cacheKey, $cacheConfig);
1623
        }
1624
1625
        return $query->firstOrFail();
0 ignored issues
show
Bug Compatibility introduced by
The expression $query->firstOrFail(); of type Cake\Datasource\EntityInterface|array adds the type array to the return on line 1625 which is incompatible with the return type declared by the interface Cake\Datasource\RepositoryInterface::get of type Cake\Datasource\EntityInterface.
Loading history...
1626
    }
1627
1628
    /**
1629
     * Handles the logic executing of a worker inside a transaction.
1630
     *
1631
     * @param callable $worker The worker that will run inside the transaction.
1632
     * @param bool $atomic Whether to execute the worker inside a database transaction.
1633
     * @return mixed
1634
     */
1635
    protected function _executeTransaction(callable $worker, $atomic = true)
1636
    {
1637
        if ($atomic) {
1638
            return $this->getConnection()->transactional(function () use ($worker) {
1639
                return $worker();
1640
            });
1641
        }
1642
1643
        return $worker();
1644
    }
1645
1646
    /**
1647
     * Checks if the caller would have executed a commit on a transaction.
1648
     *
1649
     * @param bool $atomic True if an atomic transaction was used.
1650
     * @param bool $primary True if a primary was used.
1651
     * @return bool Returns true if a transaction was committed.
1652
     */
1653
    protected function _transactionCommitted($atomic, $primary)
1654
    {
1655
        return !$this->getConnection()->inTransaction() && ($atomic || (!$atomic && $primary));
1656
    }
1657
1658
    /**
1659
     * Finds an existing record or creates a new one.
1660
     *
1661
     * A find() will be done to locate an existing record using the attributes
1662
     * defined in $search. If records matches the conditions, the first record
1663
     * will be returned.
1664
     *
1665
     * If no record can be found, a new entity will be created
1666
     * with the $search properties. If a callback is provided, it will be
1667
     * called allowing you to define additional default values. The new
1668
     * entity will be saved and returned.
1669
     *
1670
     * If your find conditions require custom order, associations or conditions, then the $search
1671
     * parameter can be a callable that takes the Query as the argument, or a \Cake\ORM\Query object passed
1672
     * as the $search parameter. Allowing you to customize the find results.
1673
     *
1674
     * ### Options
1675
     *
1676
     * The options array is passed to the save method with exception to the following keys:
1677
     *
1678
     * - atomic: Whether to execute the methods for find, save and callbacks inside a database
1679
     *   transaction (default: true)
1680
     * - defaults: Whether to use the search criteria as default values for the new entity (default: true)
1681
     *
1682
     * @param array|callable|\Cake\ORM\Query $search The criteria to find existing
1683
     *   records by. Note that when you pass a query object you'll have to use
1684
     *   the 2nd arg of the method to modify the entity data before saving.
1685
     * @param callable|null $callback A callback that will be invoked for newly
1686
     *   created entities. This callback will be called *before* the entity
1687
     *   is persisted.
1688
     * @param array $options The options to use when saving.
1689
     * @return \Cake\Datasource\EntityInterface An entity.
1690
     * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved
1691
     */
1692
    public function findOrCreate($search, callable $callback = null, $options = [])
1693
    {
1694
        $options = new ArrayObject($options + [
1695
            'atomic' => true,
1696
            'defaults' => true,
1697
        ]);
1698
1699
        $entity = $this->_executeTransaction(function () use ($search, $callback, $options) {
1700
            return $this->_processFindOrCreate($search, $callback, $options->getArrayCopy());
1701
        }, $options['atomic']);
1702
1703 View Code Duplication
        if ($entity && $this->_transactionCommitted($options['atomic'], true)) {
1704
            $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options'));
1705
        }
1706
1707
        return $entity;
1708
    }
1709
1710
    /**
1711
     * Performs the actual find and/or create of an entity based on the passed options.
1712
     *
1713
     * @param array|callable|\Cake\ORM\Query $search The criteria to find an existing record by, or a callable tha will
1714
     *   customize the find query.
1715
     * @param callable|null $callback A callback that will be invoked for newly
1716
     *   created entities. This callback will be called *before* the entity
1717
     *   is persisted.
1718
     * @param array $options The options to use when saving.
1719
     * @return \Cake\Datasource\EntityInterface An entity.
1720
     * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved
1721
     */
1722
    protected function _processFindOrCreate($search, callable $callback = null, $options = [])
1723
    {
1724
        $query = $this->_getFindOrCreateQuery($search);
1725
        $row = $query->first();
0 ignored issues
show
Bug Compatibility introduced by
The expression $query->first(); of type Cake\Datasource\EntityInterface|array|null adds the type array to the return on line 1727 which is incompatible with the return type documented by Cake\ORM\Table::_processFindOrCreate of type Cake\Datasource\EntityInterface.
Loading history...
1726
        if ($row !== null) {
1727
            return $row;
1728
        }
1729
1730
        $entity = $this->newEntity();
1731
        if ($options['defaults'] && is_array($search)) {
1732
            $accessibleFields = array_combine(array_keys($search), array_fill(0, count($search), true));
1733
            $entity = $this->patchEntity($entity, $search, ['accessibleFields' => $accessibleFields]);
1734
        }
1735
        if ($callback !== null) {
1736
            $entity = $callback($entity) ?: $entity;
1737
        }
1738
        unset($options['defaults']);
1739
1740
        $result = $this->save($entity, $options);
1741
1742
        if ($result === false) {
1743
            throw new PersistenceFailedException($entity, ['findOrCreate']);
1744
        }
1745
1746
        return $entity;
1747
    }
1748
1749
    /**
1750
     * Gets the query object for findOrCreate().
1751
     *
1752
     * @param array|callable|\Cake\ORM\Query $search The criteria to find existing records by.
1753
     * @return \Cake\ORM\Query
1754
     */
1755
    protected function _getFindOrCreateQuery($search)
1756
    {
1757
        if (is_callable($search)) {
1758
            $query = $this->find();
1759
            $search($query);
1760
        } elseif (is_array($search)) {
1761
            $query = $this->find()->where($search);
1762
        } elseif ($search instanceof Query) {
1763
            $query = $search;
1764
        } else {
1765
            throw new InvalidArgumentException('Search criteria must be an array, callable or Query');
1766
        }
1767
1768
        return $query;
1769
    }
1770
1771
    /**
1772
     * Creates a new Query instance for a table.
1773
     *
1774
     * @return \Cake\ORM\Query
1775
     */
1776
    public function query()
1777
    {
1778
        return new Query($this->getConnection(), $this);
1779
    }
1780
1781
    /**
1782
     * {@inheritDoc}
1783
     */
1784 View Code Duplication
    public function updateAll($fields, $conditions)
1785
    {
1786
        $query = $this->query();
1787
        $query->update()
1788
            ->set($fields)
1789
            ->where($conditions);
1790
        $statement = $query->execute();
1791
        $statement->closeCursor();
1792
1793
        return $statement->rowCount();
1794
    }
1795
1796
    /**
1797
     * {@inheritDoc}
1798
     */
1799 View Code Duplication
    public function deleteAll($conditions)
1800
    {
1801
        $query = $this->query()
1802
            ->delete()
1803
            ->where($conditions);
1804
        $statement = $query->execute();
1805
        $statement->closeCursor();
1806
1807
        return $statement->rowCount();
1808
    }
1809
1810
    /**
1811
     * {@inheritDoc}
1812
     */
1813
    public function exists($conditions)
1814
    {
1815
        return (bool)count(
1816
            $this->find('all')
1817
            ->select(['existing' => 1])
1818
            ->where($conditions)
0 ignored issues
show
Bug introduced by
It seems like $conditions defined by parameter $conditions on line 1813 can also be of type object<ArrayAccess>; however, Cake\Database\Query::where() does only seem to accept object<Cake\Database\Exp...nterface>|callable|null, 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...
1819
            ->limit(1)
1820
            ->disableHydration()
1821
            ->toArray()
1822
        );
1823
    }
1824
1825
    /**
1826
     * {@inheritDoc}
1827
     *
1828
     * ### Options
1829
     *
1830
     * The options array accepts the following keys:
1831
     *
1832
     * - atomic: Whether to execute the save and callbacks inside a database
1833
     *   transaction (default: true)
1834
     * - checkRules: Whether or not to check the rules on entity before saving, if the checking
1835
     *   fails, it will abort the save operation. (default:true)
1836
     * - associated: If `true` it will save 1st level associated entities as they are found
1837
     *   in the passed `$entity` whenever the property defined for the association
1838
     *   is marked as dirty. If an array, it will be interpreted as the list of associations
1839
     *   to be saved. It is possible to provide different options for saving on associated
1840
     *   table objects using this key by making the custom options the array value.
1841
     *   If `false` no associated records will be saved. (default: `true`)
1842
     * - checkExisting: Whether or not to check if the entity already exists, assuming that the
1843
     *   entity is marked as not new, and the primary key has been set.
1844
     *
1845
     * ### Events
1846
     *
1847
     * When saving, this method will trigger four events:
1848
     *
1849
     * - Model.beforeRules: Will be triggered right before any rule checking is done
1850
     *   for the passed entity if the `checkRules` key in $options is not set to false.
1851
     *   Listeners will receive as arguments the entity, options array and the operation type.
1852
     *   If the event is stopped the rules check result will be set to the result of the event itself.
1853
     * - Model.afterRules: Will be triggered right after the `checkRules()` method is
1854
     *   called for the entity. Listeners will receive as arguments the entity,
1855
     *   options array, the result of checking the rules and the operation type.
1856
     *   If the event is stopped the checking result will be set to the result of
1857
     *   the event itself.
1858
     * - Model.beforeSave: Will be triggered just before the list of fields to be
1859
     *   persisted is calculated. It receives both the entity and the options as
1860
     *   arguments. The options array is passed as an ArrayObject, so any changes in
1861
     *   it will be reflected in every listener and remembered at the end of the event
1862
     *   so it can be used for the rest of the save operation. Returning false in any
1863
     *   of the listeners will abort the saving process. If the event is stopped
1864
     *   using the event API, the event object's `result` property will be returned.
1865
     *   This can be useful when having your own saving strategy implemented inside a
1866
     *   listener.
1867
     * - Model.afterSave: Will be triggered after a successful insert or save,
1868
     *   listeners will receive the entity and the options array as arguments. The type
1869
     *   of operation performed (insert or update) can be determined by checking the
1870
     *   entity's method `isNew`, true meaning an insert and false an update.
1871
     * - Model.afterSaveCommit: Will be triggered after the transaction is committed
1872
     *   for atomic save, listeners will receive the entity and the options array
1873
     *   as arguments.
1874
     *
1875
     * This method will determine whether the passed entity needs to be
1876
     * inserted or updated in the database. It does that by checking the `isNew`
1877
     * method on the entity. If the entity to be saved returns a non-empty value from
1878
     * its `errors()` method, it will not be saved.
1879
     *
1880
     * ### Saving on associated tables
1881
     *
1882
     * This method will by default persist entities belonging to associated tables,
1883
     * whenever a dirty property matching the name of the property name set for an
1884
     * association in this table. It is possible to control what associations will
1885
     * be saved and to pass additional option for saving them.
1886
     *
1887
     * ```
1888
     * // Only save the comments association
1889
     * $articles->save($entity, ['associated' => ['Comments']]);
1890
     *
1891
     * // Save the company, the employees and related addresses for each of them.
1892
     * // For employees do not check the entity rules
1893
     * $companies->save($entity, [
1894
     *   'associated' => [
1895
     *     'Employees' => [
1896
     *       'associated' => ['Addresses'],
1897
     *       'checkRules' => false
1898
     *     ]
1899
     *   ]
1900
     * ]);
1901
     *
1902
     * // Save no associations
1903
     * $articles->save($entity, ['associated' => false]);
1904
     * ```
1905
     *
1906
     * @param \Cake\Datasource\EntityInterface $entity
1907
     * @param array $options
1908
     * @return \Cake\Datasource\EntityInterface|false
1909
     * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction is aborted in the afterSave event.
1910
     */
1911
    public function save(EntityInterface $entity, $options = [])
1912
    {
1913
        if ($options instanceof SaveOptionsBuilder) {
1914
            $options = $options->toArray();
1915
        }
1916
1917
        $options = new ArrayObject((array)$options + [
1918
            'atomic' => true,
1919
            'associated' => true,
1920
            'checkRules' => true,
1921
            'checkExisting' => true,
1922
            '_primary' => true,
1923
        ]);
1924
1925
        if ($entity->hasErrors($options['associated'])) {
0 ignored issues
show
Bug introduced by
The method hasErrors() does not exist on Cake\Datasource\EntityInterface. Did you maybe mean errors()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1926
            return false;
1927
        }
1928
1929
        if ($entity->isNew() === false && !$entity->isDirty()) {
1930
            return $entity;
1931
        }
1932
1933
        $success = $this->_executeTransaction(function () use ($entity, $options) {
1934
            return $this->_processSave($entity, $options);
1935
        }, $options['atomic']);
1936
1937
        if ($success) {
1938 View Code Duplication
            if ($this->_transactionCommitted($options['atomic'], $options['_primary'])) {
1939
                $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options'));
1940
            }
1941 View Code Duplication
            if ($options['atomic'] || $options['_primary']) {
1942
                $entity->clean();
1943
                $entity->isNew(false);
1944
                $entity->setSource($this->getRegistryAlias());
1945
            }
1946
        }
1947
1948
        return $success;
1949
    }
1950
1951
    /**
1952
     * Try to save an entity or throw a PersistenceFailedException if the application rules checks failed,
1953
     * the entity contains errors or the save was aborted by a callback.
1954
     *
1955
     * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
1956
     * @param array|\ArrayAccess $options The options to use when saving.
1957
     * @return \Cake\Datasource\EntityInterface
1958
     * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved
1959
     * @see \Cake\ORM\Table::save()
1960
     */
1961 View Code Duplication
    public function saveOrFail(EntityInterface $entity, $options = [])
1962
    {
1963
        $saved = $this->save($entity, $options);
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 1961 can also be of type object<ArrayAccess>; however, Cake\ORM\Table::save() does only seem to accept array, 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...
1964
        if ($saved === false) {
1965
            throw new PersistenceFailedException($entity, ['save']);
1966
        }
1967
1968
        return $saved;
1969
    }
1970
1971
    /**
1972
     * Performs the actual saving of an entity based on the passed options.
1973
     *
1974
     * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
1975
     * @param \ArrayObject $options the options to use for the save operation
1976
     * @return \Cake\Datasource\EntityInterface|bool
1977
     * @throws \RuntimeException When an entity is missing some of the primary keys.
1978
     * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction
1979
     *   is aborted in the afterSave event.
1980
     */
1981
    protected function _processSave($entity, $options)
1982
    {
1983
        $primaryColumns = (array)$this->getPrimaryKey();
1984
1985
        if ($options['checkExisting'] && $primaryColumns && $entity->isNew() && $entity->has($primaryColumns)) {
1986
            $alias = $this->getAlias();
1987
            $conditions = [];
1988
            foreach ($entity->extract($primaryColumns) as $k => $v) {
1989
                $conditions["$alias.$k"] = $v;
1990
            }
1991
            $entity->isNew(!$this->exists($conditions));
1992
        }
1993
1994
        $mode = $entity->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE;
1995
        if ($options['checkRules'] && !$this->checkRules($entity, $mode, $options)) {
1996
            return false;
1997
        }
1998
1999
        $options['associated'] = $this->_associations->normalizeKeys($options['associated']);
2000
        $event = $this->dispatchEvent('Model.beforeSave', compact('entity', 'options'));
2001
2002
        if ($event->isStopped()) {
2003
            return $event->getResult();
2004
        }
2005
2006
        $saved = $this->_associations->saveParents(
2007
            $this,
2008
            $entity,
2009
            $options['associated'],
2010
            ['_primary' => false] + $options->getArrayCopy()
2011
        );
2012
2013
        if (!$saved && $options['atomic']) {
2014
            return false;
2015
        }
2016
2017
        $data = $entity->extract($this->getSchema()->columns(), true);
2018
        $isNew = $entity->isNew();
2019
2020
        if ($isNew) {
2021
            $success = $this->_insert($entity, $data);
2022
        } else {
2023
            $success = $this->_update($entity, $data);
2024
        }
2025
2026
        if ($success) {
2027
            $success = $this->_onSaveSuccess($entity, $options);
2028
        }
2029
2030
        if (!$success && $isNew) {
2031
            $entity->unsetProperty($this->getPrimaryKey());
2032
            $entity->isNew(true);
2033
        }
2034
2035
        return $success ? $entity : false;
2036
    }
2037
2038
    /**
2039
     * Handles the saving of children associations and executing the afterSave logic
2040
     * once the entity for this table has been saved successfully.
2041
     *
2042
     * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
2043
     * @param \ArrayObject $options the options to use for the save operation
2044
     * @return bool True on success
2045
     * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction
2046
     *   is aborted in the afterSave event.
2047
     */
2048
    protected function _onSaveSuccess($entity, $options)
2049
    {
2050
        $success = $this->_associations->saveChildren(
2051
            $this,
2052
            $entity,
2053
            $options['associated'],
2054
            ['_primary' => false] + $options->getArrayCopy()
2055
        );
2056
2057
        if (!$success && $options['atomic']) {
2058
            return false;
2059
        }
2060
2061
        $this->dispatchEvent('Model.afterSave', compact('entity', 'options'));
2062
2063
        if ($options['atomic'] && !$this->getConnection()->inTransaction()) {
2064
            throw new RolledbackTransactionException(['table' => get_class($this)]);
2065
        }
2066
2067 View Code Duplication
        if (!$options['atomic'] && !$options['_primary']) {
2068
            $entity->clean();
2069
            $entity->isNew(false);
2070
            $entity->setSource($this->getRegistryAlias());
2071
        }
2072
2073
        return true;
2074
    }
2075
2076
    /**
2077
     * Auxiliary function to handle the insert of an entity's data in the table
2078
     *
2079
     * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted
2080
     * @param array $data The actual data that needs to be saved
2081
     * @return \Cake\Datasource\EntityInterface|bool
2082
     * @throws \RuntimeException if not all the primary keys where supplied or could
2083
     * be generated when the table has composite primary keys. Or when the table has no primary key.
2084
     */
2085
    protected function _insert($entity, $data)
2086
    {
2087
        $primary = (array)$this->getPrimaryKey();
2088
        if (empty($primary)) {
2089
            $msg = sprintf(
2090
                'Cannot insert row in "%s" table, it has no primary key.',
2091
                $this->getTable()
2092
            );
2093
            throw new RuntimeException($msg);
2094
        }
2095
        $keys = array_fill(0, count($primary), null);
2096
        $id = (array)$this->_newId($primary) + $keys;
2097
2098
        // Generate primary keys preferring values in $data.
2099
        $primary = array_combine($primary, $id);
2100
        $primary = array_intersect_key($data, $primary) + $primary;
2101
2102
        $filteredKeys = array_filter($primary, function ($v) {
2103
            return $v !== null;
2104
        });
2105
        $data += $filteredKeys;
2106
2107
        if (count($primary) > 1) {
2108
            $schema = $this->getSchema();
2109
            foreach ($primary as $k => $v) {
2110
                if (!isset($data[$k]) && empty($schema->getColumn($k)['autoIncrement'])) {
2111
                    $msg = 'Cannot insert row, some of the primary key values are missing. ';
2112
                    $msg .= sprintf(
2113
                        'Got (%s), expecting (%s)',
2114
                        implode(', ', $filteredKeys + $entity->extract(array_keys($primary))),
2115
                        implode(', ', array_keys($primary))
2116
                    );
2117
                    throw new RuntimeException($msg);
2118
                }
2119
            }
2120
        }
2121
2122
        $success = false;
2123
        if (empty($data)) {
2124
            return $success;
2125
        }
2126
2127
        $statement = $this->query()->insert(array_keys($data))
2128
            ->values($data)
2129
            ->execute();
2130
2131
        if ($statement->rowCount() !== 0) {
2132
            $success = $entity;
2133
            $entity->set($filteredKeys, ['guard' => false]);
2134
            $schema = $this->getSchema();
2135
            $driver = $this->getConnection()->getDriver();
2136
            foreach ($primary as $key => $v) {
2137
                if (!isset($data[$key])) {
2138
                    $id = $statement->lastInsertId($this->getTable(), $key);
2139
                    $type = $schema->getColumnType($key);
2140
                    $entity->set($key, Type::build($type)->toPHP($id, $driver));
2141
                    break;
2142
                }
2143
            }
2144
        }
2145
        $statement->closeCursor();
2146
2147
        return $success;
2148
    }
2149
2150
    /**
2151
     * Generate a primary key value for a new record.
2152
     *
2153
     * By default, this uses the type system to generate a new primary key
2154
     * value if possible. You can override this method if you have specific requirements
2155
     * for id generation.
2156
     *
2157
     * Note: The ORM will not generate primary key values for composite primary keys.
2158
     * You can overwrite _newId() in your table class.
2159
     *
2160
     * @param string[] $primary The primary key columns to get a new ID for.
2161
     * @return string|null Either null or the primary key value or a list of primary key values.
2162
     */
2163
    protected function _newId($primary)
2164
    {
2165
        if (!$primary || count((array)$primary) > 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $primary of type string[] 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...
2166
            return null;
2167
        }
2168
        $typeName = $this->getSchema()->getColumnType($primary[0]);
2169
        $type = Type::build($typeName);
2170
2171
        return $type->newId();
2172
    }
2173
2174
    /**
2175
     * Auxiliary function to handle the update of an entity's data in the table
2176
     *
2177
     * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted
2178
     * @param array $data The actual data that needs to be saved
2179
     * @return \Cake\Datasource\EntityInterface|bool
2180
     * @throws \InvalidArgumentException When primary key data is missing.
2181
     */
2182
    protected function _update($entity, $data)
2183
    {
2184
        $primaryColumns = (array)$this->getPrimaryKey();
2185
        $primaryKey = $entity->extract($primaryColumns);
2186
2187
        $data = array_diff_key($data, $primaryKey);
2188
        if (empty($data)) {
2189
            return $entity;
2190
        }
2191
2192
        if (count($primaryColumns) === 0) {
2193
            $entityClass = get_class($entity);
2194
            $table = $this->getTable();
2195
            $message = "Cannot update `$entityClass`. The `$table` has no primary key.";
2196
            throw new InvalidArgumentException($message);
2197
        }
2198
2199
        if (!$entity->has($primaryColumns)) {
2200
            $message = 'All primary key value(s) are needed for updating, ';
2201
            $message .= get_class($entity) . ' is missing ' . implode(', ', $primaryColumns);
2202
            throw new InvalidArgumentException($message);
2203
        }
2204
2205
        $query = $this->query();
2206
        $statement = $query->update()
2207
            ->set($data)
2208
            ->where($primaryKey)
2209
            ->execute();
2210
2211
        $success = false;
2212
        if ($statement->errorCode() === '00000') {
2213
            $success = $entity;
2214
        }
2215
        $statement->closeCursor();
2216
2217
        return $success;
2218
    }
2219
2220
    /**
2221
     * Persists multiple entities of a table.
2222
     *
2223
     * The records will be saved in a transaction which will be rolled back if
2224
     * any one of the records fails to save due to failed validation or database
2225
     * error.
2226
     *
2227
     * @param \Cake\Datasource\EntityInterface[]|\Cake\Datasource\ResultSetInterface $entities Entities to save.
2228
     * @param array|\ArrayAccess $options Options used when calling Table::save() for each entity.
2229
     * @return bool|\Cake\Datasource\EntityInterface[]|\Cake\Datasource\ResultSetInterface False on failure, entities list on success.
2230
     * @throws \Exception
2231
     */
2232
    public function saveMany($entities, $options = [])
2233
    {
2234
        $isNew = [];
2235
        $cleanup = function ($entities) use (&$isNew) {
2236
            foreach ($entities as $key => $entity) {
2237
                if (isset($isNew[$key]) && $isNew[$key]) {
2238
                    $entity->unsetProperty($this->getPrimaryKey());
2239
                    $entity->isNew(true);
2240
                }
2241
            }
2242
        };
2243
2244
        try {
2245
            $return = $this->getConnection()
2246
                ->transactional(function () use ($entities, $options, &$isNew) {
2247
                    foreach ($entities as $key => $entity) {
2248
                        $isNew[$key] = $entity->isNew();
2249
                        if ($this->save($entity, $options) === false) {
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 2232 can also be of type object<ArrayAccess>; however, Cake\ORM\Table::save() does only seem to accept array, 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...
2250
                            return false;
2251
                        }
2252
                    }
2253
                });
2254
        } catch (\Exception $e) {
2255
            $cleanup($entities);
2256
2257
            throw $e;
2258
        }
2259
2260
        if ($return === false) {
2261
            $cleanup($entities);
2262
2263
            return false;
2264
        }
2265
2266
        return $entities;
2267
    }
2268
2269
    /**
2270
     * {@inheritDoc}
2271
     *
2272
     * For HasMany and HasOne associations records will be removed based on
2273
     * the dependent option. Join table records in BelongsToMany associations
2274
     * will always be removed. You can use the `cascadeCallbacks` option
2275
     * when defining associations to change how associated data is deleted.
2276
     *
2277
     * ### Options
2278
     *
2279
     * - `atomic` Defaults to true. When true the deletion happens within a transaction.
2280
     * - `checkRules` Defaults to true. Check deletion rules before deleting the record.
2281
     *
2282
     * ### Events
2283
     *
2284
     * - `Model.beforeDelete` Fired before the delete occurs. If stopped the delete
2285
     *   will be aborted. Receives the event, entity, and options.
2286
     * - `Model.afterDelete` Fired after the delete has been successful. Receives
2287
     *   the event, entity, and options.
2288
     * - `Model.afterDeleteCommit` Fired after the transaction is committed for
2289
     *   an atomic delete. Receives the event, entity, and options.
2290
     *
2291
     * The options argument will be converted into an \ArrayObject instance
2292
     * for the duration of the callbacks, this allows listeners to modify
2293
     * the options used in the delete operation.
2294
     *
2295
     */
2296
    public function delete(EntityInterface $entity, $options = [])
2297
    {
2298
        $options = new ArrayObject((array)$options + [
2299
            'atomic' => true,
2300
            'checkRules' => true,
2301
            '_primary' => true,
2302
        ]);
2303
2304
        $success = $this->_executeTransaction(function () use ($entity, $options) {
2305
            return $this->_processDelete($entity, $options);
2306
        }, $options['atomic']);
2307
2308
        if ($success && $this->_transactionCommitted($options['atomic'], $options['_primary'])) {
2309
            $this->dispatchEvent('Model.afterDeleteCommit', [
2310
                'entity' => $entity,
2311
                'options' => $options,
2312
            ]);
2313
        }
2314
2315
        return $success;
2316
    }
2317
2318
    /**
2319
     * Try to delete an entity or throw a PersistenceFailedException if the entity is new,
2320
     * has no primary key value, application rules checks failed or the delete was aborted by a callback.
2321
     *
2322
     * @param \Cake\Datasource\EntityInterface $entity The entity to remove.
2323
     * @param array|\ArrayAccess $options The options for the delete.
2324
     * @return bool success
2325
     * @throws \Cake\ORM\Exception\PersistenceFailedException
2326
     * @see \Cake\ORM\Table::delete()
2327
     */
2328 View Code Duplication
    public function deleteOrFail(EntityInterface $entity, $options = [])
2329
    {
2330
        $deleted = $this->delete($entity, $options);
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 2328 can also be of type object<ArrayAccess>; however, Cake\ORM\Table::delete() does only seem to accept array, 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...
2331
        if ($deleted === false) {
2332
            throw new PersistenceFailedException($entity, ['delete']);
2333
        }
2334
2335
        return $deleted;
2336
    }
2337
2338
    /**
2339
     * Perform the delete operation.
2340
     *
2341
     * Will delete the entity provided. Will remove rows from any
2342
     * dependent associations, and clear out join tables for BelongsToMany associations.
2343
     *
2344
     * @param \Cake\Datasource\EntityInterface $entity The entity to delete.
2345
     * @param \ArrayObject $options The options for the delete.
2346
     * @throws \InvalidArgumentException if there are no primary key values of the
2347
     * passed entity
2348
     * @return bool success
2349
     */
2350
    protected function _processDelete($entity, $options)
2351
    {
2352
        if ($entity->isNew()) {
2353
            return false;
2354
        }
2355
2356
        $primaryKey = (array)$this->getPrimaryKey();
2357
        if (!$entity->has($primaryKey)) {
2358
            $msg = 'Deleting requires all primary key values.';
2359
            throw new InvalidArgumentException($msg);
2360
        }
2361
2362
        if ($options['checkRules'] && !$this->checkRules($entity, RulesChecker::DELETE, $options)) {
2363
            return false;
2364
        }
2365
2366
        $event = $this->dispatchEvent('Model.beforeDelete', [
2367
            'entity' => $entity,
2368
            'options' => $options,
2369
        ]);
2370
2371
        if ($event->isStopped()) {
2372
            return $event->getResult();
2373
        }
2374
2375
        $this->_associations->cascadeDelete(
2376
            $entity,
2377
            ['_primary' => false] + $options->getArrayCopy()
2378
        );
2379
2380
        $query = $this->query();
2381
        $conditions = (array)$entity->extract($primaryKey);
2382
        $statement = $query->delete()
2383
            ->where($conditions)
2384
            ->execute();
2385
2386
        $success = $statement->rowCount() > 0;
2387
        if (!$success) {
2388
            return $success;
2389
        }
2390
2391
        $this->dispatchEvent('Model.afterDelete', [
2392
            'entity' => $entity,
2393
            'options' => $options,
2394
        ]);
2395
2396
        return $success;
2397
    }
2398
2399
    /**
2400
     * Returns true if the finder exists for the table
2401
     *
2402
     * @param string $type name of finder to check
2403
     *
2404
     * @return bool
2405
     */
2406
    public function hasFinder($type)
2407
    {
2408
        $finder = 'find' . $type;
2409
2410
        return method_exists($this, $finder) || ($this->_behaviors && $this->_behaviors->hasFinder($type));
2411
    }
2412
2413
    /**
2414
     * Calls a finder method directly and applies it to the passed query,
2415
     * if no query is passed a new one will be created and returned
2416
     *
2417
     * @param string $type name of the finder to be called
2418
     * @param \Cake\ORM\Query $query The query object to apply the finder options to
2419
     * @param array $options List of options to pass to the finder
2420
     * @return \Cake\ORM\Query
2421
     * @throws \BadMethodCallException
2422
     */
2423
    public function callFinder($type, Query $query, array $options = [])
2424
    {
2425
        $query->applyOptions($options);
2426
        $options = $query->getOptions();
2427
        $finder = 'find' . $type;
2428
        if (method_exists($this, $finder)) {
2429
            return $this->{$finder}($query, $options);
2430
        }
2431
2432
        if ($this->_behaviors && $this->_behaviors->hasFinder($type)) {
2433
            return $this->_behaviors->callFinder($type, [$query, $options]);
2434
        }
2435
2436
        throw new BadMethodCallException(
2437
            sprintf('Unknown finder method "%s"', $type)
2438
        );
2439
    }
2440
2441
    /**
2442
     * Provides the dynamic findBy and findByAll methods.
2443
     *
2444
     * @param string $method The method name that was fired.
2445
     * @param array $args List of arguments passed to the function.
2446
     * @return mixed
2447
     * @throws \BadMethodCallException when there are missing arguments, or when
2448
     *  and & or are combined.
2449
     */
2450
    protected function _dynamicFinder($method, $args)
2451
    {
2452
        $method = Inflector::underscore($method);
2453
        preg_match('/^find_([\w]+)_by_/', $method, $matches);
2454
        if (empty($matches)) {
2455
            // find_by_ is 8 characters.
2456
            $fields = substr($method, 8);
2457
            $findType = 'all';
2458
        } else {
2459
            $fields = substr($method, strlen($matches[0]));
2460
            $findType = Inflector::variable($matches[1]);
2461
        }
2462
        $hasOr = strpos($fields, '_or_');
2463
        $hasAnd = strpos($fields, '_and_');
2464
2465
        $makeConditions = function ($fields, $args) {
2466
            $conditions = [];
2467
            if (count($args) < count($fields)) {
2468
                throw new BadMethodCallException(sprintf(
2469
                    'Not enough arguments for magic finder. Got %s required %s',
2470
                    count($args),
2471
                    count($fields)
2472
                ));
2473
            }
2474
            foreach ($fields as $field) {
2475
                $conditions[$this->aliasField($field)] = array_shift($args);
2476
            }
2477
2478
            return $conditions;
2479
        };
2480
2481
        if ($hasOr !== false && $hasAnd !== false) {
2482
            throw new BadMethodCallException(
2483
                'Cannot mix "and" & "or" in a magic finder. Use find() instead.'
2484
            );
2485
        }
2486
2487
        $conditions = [];
2488
        if ($hasOr === false && $hasAnd === false) {
2489
            $conditions = $makeConditions([$fields], $args);
2490
        } elseif ($hasOr !== false) {
2491
            $fields = explode('_or_', $fields);
2492
            $conditions = [
2493
            'OR' => $makeConditions($fields, $args),
2494
            ];
2495
        } elseif ($hasAnd !== false) {
2496
            $fields = explode('_and_', $fields);
2497
            $conditions = $makeConditions($fields, $args);
2498
        }
2499
2500
        return $this->find($findType, [
2501
            'conditions' => $conditions,
2502
        ]);
2503
    }
2504
2505
    /**
2506
     * Handles behavior delegation + dynamic finders.
2507
     *
2508
     * If your Table uses any behaviors you can call them as if
2509
     * they were on the table object.
2510
     *
2511
     * @param string $method name of the method to be invoked
2512
     * @param array $args List of arguments passed to the function
2513
     * @return mixed
2514
     * @throws \BadMethodCallException
2515
     */
2516
    public function __call($method, $args)
2517
    {
2518
        if ($this->_behaviors && $this->_behaviors->hasMethod($method)) {
2519
            return $this->_behaviors->call($method, $args);
2520
        }
2521
        if (preg_match('/^find(?:\w+)?By/', $method) > 0) {
2522
            return $this->_dynamicFinder($method, $args);
2523
        }
2524
2525
        throw new BadMethodCallException(
2526
            sprintf('Unknown method "%s"', $method)
2527
        );
2528
    }
2529
2530
    /**
2531
     * Returns the association named after the passed value if exists, otherwise
2532
     * throws an exception.
2533
     *
2534
     * @param string $property the association name
2535
     * @return \Cake\ORM\Association
2536
     * @throws \RuntimeException if no association with such name exists
2537
     */
2538
    public function __get($property)
2539
    {
2540
        $association = $this->_associations->get($property);
2541
        if (!$association) {
2542
            throw new RuntimeException(sprintf(
2543
                'Undefined property `%s`. ' .
2544
                'You have not defined the `%s` association on `%s`.',
2545
                $property,
2546
                $property,
2547
                static::class
2548
            ));
2549
        }
2550
2551
        return $association;
2552
    }
2553
2554
    /**
2555
     * Returns whether an association named after the passed value
2556
     * exists for this table.
2557
     *
2558
     * @param string $property the association name
2559
     * @return bool
2560
     */
2561
    public function __isset($property)
2562
    {
2563
        return $this->_associations->has($property);
2564
    }
2565
2566
    /**
2567
     * Get the object used to marshal/convert array data into objects.
2568
     *
2569
     * Override this method if you want a table object to use custom
2570
     * marshalling logic.
2571
     *
2572
     * @return \Cake\ORM\Marshaller
2573
     * @see \Cake\ORM\Marshaller
2574
     */
2575
    public function marshaller()
2576
    {
2577
        return new Marshaller($this);
2578
    }
2579
2580
    /**
2581
     * {@inheritDoc}
2582
     *
2583
     * By default all the associations on this table will be hydrated. You can
2584
     * limit which associations are built, or include deeper associations
2585
     * using the options parameter:
2586
     *
2587
     * ```
2588
     * $article = $this->Articles->newEntity(
2589
     *   $this->request->getData(),
2590
     *   ['associated' => ['Tags', 'Comments.Users']]
2591
     * );
2592
     * ```
2593
     *
2594
     * You can limit fields that will be present in the constructed entity by
2595
     * passing the `fields` option, which is also accepted for associations:
2596
     *
2597
     * ```
2598
     * $article = $this->Articles->newEntity($this->request->getData(), [
2599
     *  'fields' => ['title', 'body', 'tags', 'comments'],
2600
     *  'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
2601
     * ]
2602
     * );
2603
     * ```
2604
     *
2605
     * The `fields` option lets remove or restrict input data from ending up in
2606
     * the entity. If you'd like to relax the entity's default accessible fields,
2607
     * you can use the `accessibleFields` option:
2608
     *
2609
     * ```
2610
     * $article = $this->Articles->newEntity(
2611
     *   $this->request->getData(),
2612
     *   ['accessibleFields' => ['protected_field' => true]]
2613
     * );
2614
     * ```
2615
     *
2616
     * By default, the data is validated before being passed to the new entity. In
2617
     * the case of invalid fields, those will not be present in the resulting object.
2618
     * The `validate` option can be used to disable validation on the passed data:
2619
     *
2620
     * ```
2621
     * $article = $this->Articles->newEntity(
2622
     *   $this->request->getData(),
2623
     *   ['validate' => false]
2624
     * );
2625
     * ```
2626
     *
2627
     * You can also pass the name of the validator to use in the `validate` option.
2628
     * If `null` is passed to the first param of this function, no validation will
2629
     * be performed.
2630
     *
2631
     * You can use the `Model.beforeMarshal` event to modify request data
2632
     * before it is converted into entities.
2633
     */
2634
    public function newEntity($data = null, array $options = [])
2635
    {
2636
        if ($data === null) {
2637
            $class = $this->getEntityClass();
2638
2639
            return new $class([], ['source' => $this->getRegistryAlias()]);
2640
        }
2641
        if (!isset($options['associated'])) {
2642
            $options['associated'] = $this->_associations->keys();
2643
        }
2644
        $marshaller = $this->marshaller();
2645
2646
        return $marshaller->one($data, $options);
2647
    }
2648
2649
    /**
2650
     * {@inheritDoc}
2651
     *
2652
     * By default all the associations on this table will be hydrated. You can
2653
     * limit which associations are built, or include deeper associations
2654
     * using the options parameter:
2655
     *
2656
     * ```
2657
     * $articles = $this->Articles->newEntities(
2658
     *   $this->request->getData(),
2659
     *   ['associated' => ['Tags', 'Comments.Users']]
2660
     * );
2661
     * ```
2662
     *
2663
     * You can limit fields that will be present in the constructed entities by
2664
     * passing the `fields` option, which is also accepted for associations:
2665
     *
2666
     * ```
2667
     * $articles = $this->Articles->newEntities($this->request->getData(), [
2668
     *  'fields' => ['title', 'body', 'tags', 'comments'],
2669
     *  'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
2670
     *  ]
2671
     * );
2672
     * ```
2673
     *
2674
     * You can use the `Model.beforeMarshal` event to modify request data
2675
     * before it is converted into entities.
2676
     */
2677 View Code Duplication
    public function newEntities(array $data, array $options = [])
2678
    {
2679
        if (!isset($options['associated'])) {
2680
            $options['associated'] = $this->_associations->keys();
2681
        }
2682
        $marshaller = $this->marshaller();
2683
2684
        return $marshaller->many($data, $options);
2685
    }
2686
2687
    /**
2688
     * {@inheritDoc}
2689
     *
2690
     * When merging HasMany or BelongsToMany associations, all the entities in the
2691
     * `$data` array will appear, those that can be matched by primary key will get
2692
     * the data merged, but those that cannot, will be discarded.
2693
     *
2694
     * You can limit fields that will be present in the merged entity by
2695
     * passing the `fields` option, which is also accepted for associations:
2696
     *
2697
     * ```
2698
     * $article = $this->Articles->patchEntity($article, $this->request->getData(), [
2699
     *  'fields' => ['title', 'body', 'tags', 'comments'],
2700
     *  'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
2701
     *  ]
2702
     * );
2703
     * ```
2704
     *
2705
     * By default, the data is validated before being passed to the entity. In
2706
     * the case of invalid fields, those will not be assigned to the entity.
2707
     * The `validate` option can be used to disable validation on the passed data:
2708
     *
2709
     * ```
2710
     * $article = $this->patchEntity($article, $this->request->getData(),[
2711
     *  'validate' => false
2712
     * ]);
2713
     * ```
2714
     *
2715
     * You can use the `Model.beforeMarshal` event to modify request data
2716
     * before it is converted into entities.
2717
     *
2718
     * When patching scalar values (null/booleans/string/integer/float), if the property
2719
     * presently has an identical value, the setter will not be called, and the
2720
     * property will not be marked as dirty. This is an optimization to prevent unnecessary field
2721
     * updates when persisting entities.
2722
     */
2723 View Code Duplication
    public function patchEntity(EntityInterface $entity, array $data, array $options = [])
2724
    {
2725
        if (!isset($options['associated'])) {
2726
            $options['associated'] = $this->_associations->keys();
2727
        }
2728
        $marshaller = $this->marshaller();
2729
2730
        return $marshaller->merge($entity, $data, $options);
2731
    }
2732
2733
    /**
2734
     * {@inheritDoc}
2735
     *
2736
     * Those entries in `$entities` that cannot be matched to any record in
2737
     * `$data` will be discarded. Records in `$data` that could not be matched will
2738
     * be marshalled as a new entity.
2739
     *
2740
     * When merging HasMany or BelongsToMany associations, all the entities in the
2741
     * `$data` array will appear, those that can be matched by primary key will get
2742
     * the data merged, but those that cannot, will be discarded.
2743
     *
2744
     * You can limit fields that will be present in the merged entities by
2745
     * passing the `fields` option, which is also accepted for associations:
2746
     *
2747
     * ```
2748
     * $articles = $this->Articles->patchEntities($articles, $this->request->getData(), [
2749
     *  'fields' => ['title', 'body', 'tags', 'comments'],
2750
     *  'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
2751
     *  ]
2752
     * );
2753
     * ```
2754
     *
2755
     * You can use the `Model.beforeMarshal` event to modify request data
2756
     * before it is converted into entities.
2757
     */
2758 View Code Duplication
    public function patchEntities($entities, array $data, array $options = [])
2759
    {
2760
        if (!isset($options['associated'])) {
2761
            $options['associated'] = $this->_associations->keys();
2762
        }
2763
        $marshaller = $this->marshaller();
2764
2765
        return $marshaller->mergeMany($entities, $data, $options);
2766
    }
2767
2768
    /**
2769
     * Validator method used to check the uniqueness of a value for a column.
2770
     * This is meant to be used with the validation API and not to be called
2771
     * directly.
2772
     *
2773
     * ### Example:
2774
     *
2775
     * ```
2776
     * $validator->add('email', [
2777
     *  'unique' => ['rule' => 'validateUnique', 'provider' => 'table']
2778
     * ])
2779
     * ```
2780
     *
2781
     * Unique validation can be scoped to the value of another column:
2782
     *
2783
     * ```
2784
     * $validator->add('email', [
2785
     *  'unique' => [
2786
     *      'rule' => ['validateUnique', ['scope' => 'site_id']],
2787
     *      'provider' => 'table'
2788
     *  ]
2789
     * ]);
2790
     * ```
2791
     *
2792
     * In the above example, the email uniqueness will be scoped to only rows having
2793
     * the same site_id. Scoping will only be used if the scoping field is present in
2794
     * the data to be validated.
2795
     *
2796
     * @param mixed $value The value of column to be checked for uniqueness.
2797
     * @param array $options The options array, optionally containing the 'scope' key.
2798
     *   May also be the validation context, if there are no options.
2799
     * @param array|null $context Either the validation context or null.
2800
     * @return bool True if the value is unique, or false if a non-scalar, non-unique value was given.
2801
     */
2802
    public function validateUnique($value, array $options, array $context = null)
2803
    {
2804
        if ($context === null) {
2805
            $context = $options;
2806
        }
2807
        $entity = new Entity(
2808
            $context['data'],
2809
            [
2810
                'useSetters' => false,
2811
                'markNew' => $context['newRecord'],
2812
                'source' => $this->getRegistryAlias(),
2813
            ]
2814
        );
2815
        $fields = array_merge(
2816
            [$context['field']],
2817
            isset($options['scope']) ? (array)$options['scope'] : []
2818
        );
2819
        $values = $entity->extract($fields);
2820
        foreach ($values as $field) {
2821
            if ($field !== null && !is_scalar($field)) {
2822
                return false;
2823
            }
2824
        }
2825
        $class = static::IS_UNIQUE_CLASS;
2826
        $rule = new $class($fields, $options);
2827
2828
        return $rule($entity, ['repository' => $this]);
2829
    }
2830
2831
    /**
2832
     * Get the Model callbacks this table is interested in.
2833
     *
2834
     * By implementing the conventional methods a table class is assumed
2835
     * to be interested in the related event.
2836
     *
2837
     * Override this method if you need to add non-conventional event listeners.
2838
     * Or if you want you table to listen to non-standard events.
2839
     *
2840
     * The conventional method map is:
2841
     *
2842
     * - Model.beforeMarshal => beforeMarshal
2843
     * - Model.buildValidator => buildValidator
2844
     * - Model.beforeFind => beforeFind
2845
     * - Model.beforeSave => beforeSave
2846
     * - Model.afterSave => afterSave
2847
     * - Model.afterSaveCommit => afterSaveCommit
2848
     * - Model.beforeDelete => beforeDelete
2849
     * - Model.afterDelete => afterDelete
2850
     * - Model.afterDeleteCommit => afterDeleteCommit
2851
     * - Model.beforeRules => beforeRules
2852
     * - Model.afterRules => afterRules
2853
     *
2854
     * @return array
2855
     */
2856
    public function implementedEvents()
2857
    {
2858
        $eventMap = [
2859
            'Model.beforeMarshal' => 'beforeMarshal',
2860
            'Model.buildValidator' => 'buildValidator',
2861
            'Model.beforeFind' => 'beforeFind',
2862
            'Model.beforeSave' => 'beforeSave',
2863
            'Model.afterSave' => 'afterSave',
2864
            'Model.afterSaveCommit' => 'afterSaveCommit',
2865
            'Model.beforeDelete' => 'beforeDelete',
2866
            'Model.afterDelete' => 'afterDelete',
2867
            'Model.afterDeleteCommit' => 'afterDeleteCommit',
2868
            'Model.beforeRules' => 'beforeRules',
2869
            'Model.afterRules' => 'afterRules',
2870
        ];
2871
        $events = [];
2872
2873
        foreach ($eventMap as $event => $method) {
2874
            if (!method_exists($this, $method)) {
2875
                continue;
2876
            }
2877
            $events[$event] = $method;
2878
        }
2879
2880
        return $events;
2881
    }
2882
2883
    /**
2884
     * {@inheritDoc}
2885
     *
2886
     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
2887
     * @return \Cake\ORM\RulesChecker
2888
     */
2889
    public function buildRules(RulesChecker $rules)
2890
    {
2891
        return $rules;
2892
    }
2893
2894
    /**
2895
     * Gets a SaveOptionsBuilder instance.
2896
     *
2897
     * @param array $options Options to parse by the builder.
2898
     * @return \Cake\ORM\SaveOptionsBuilder
2899
     */
2900
    public function getSaveOptionsBuilder(array $options = [])
2901
    {
2902
        return new SaveOptionsBuilder($this, $options);
2903
    }
2904
2905
    /**
2906
     * Loads the specified associations in the passed entity or list of entities
2907
     * by executing extra queries in the database and merging the results in the
2908
     * appropriate properties.
2909
     *
2910
     * ### Example:
2911
     *
2912
     * ```
2913
     * $user = $usersTable->get(1);
2914
     * $user = $usersTable->loadInto($user, ['Articles.Tags', 'Articles.Comments']);
2915
     * echo $user->articles[0]->title;
2916
     * ```
2917
     *
2918
     * You can also load associations for multiple entities at once
2919
     *
2920
     * ### Example:
2921
     *
2922
     * ```
2923
     * $users = $usersTable->find()->where([...])->toList();
2924
     * $users = $usersTable->loadInto($users, ['Articles.Tags', 'Articles.Comments']);
2925
     * echo $user[1]->articles[0]->title;
2926
     * ```
2927
     *
2928
     * The properties for the associations to be loaded will be overwritten on each entity.
2929
     *
2930
     * @param \Cake\Datasource\EntityInterface|array $entities a single entity or list of entities
2931
     * @param array $contain A `contain()` compatible array.
2932
     * @see \Cake\ORM\Query::contain()
2933
     * @return \Cake\Datasource\EntityInterface|array
2934
     */
2935
    public function loadInto($entities, array $contain)
2936
    {
2937
        return (new LazyEagerLoader())->loadInto($entities, $contain, $this);
2938
    }
2939
2940
    /**
2941
     * {@inheritDoc}
2942
     */
2943
    protected function validationMethodExists($method)
2944
    {
2945
        return method_exists($this, $method) || $this->behaviors()->hasMethod($method);
2946
    }
2947
2948
    /**
2949
     * Returns an array that can be used to describe the internal state of this
2950
     * object.
2951
     *
2952
     * @return array
2953
     */
2954
    public function __debugInfo()
2955
    {
2956
        $conn = $this->getConnection();
2957
        $associations = $this->_associations;
2958
        $behaviors = $this->_behaviors;
2959
2960
        return [
2961
            'registryAlias' => $this->getRegistryAlias(),
2962
            'table' => $this->getTable(),
2963
            'alias' => $this->getAlias(),
2964
            'entityClass' => $this->getEntityClass(),
2965
            'associations' => $associations ? $associations->keys() : false,
2966
            'behaviors' => $behaviors ? $behaviors->loaded() : false,
2967
            'defaultConnection' => static::defaultConnectionName(),
2968
            'connectionName' => $conn ? $conn->configName() : null,
2969
        ];
2970
    }
2971
}
2972