Marshaller::merge()   F
last analyzed

Complexity

Conditions 21
Paths 168

Size

Total Lines 73

Duplication

Lines 5
Ratio 6.85 %

Importance

Changes 0
Metric Value
cc 21
nc 168
nop 3
dl 5
loc 73
rs 3.6
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * 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 Cake\Collection\Collection;
19
use Cake\Database\Expression\TupleComparison;
20
use Cake\Database\Type;
21
use Cake\Datasource\EntityInterface;
22
use Cake\Datasource\InvalidPropertyInterface;
23
use Cake\ORM\Association\BelongsToMany;
24
use RuntimeException;
25
26
/**
27
 * Contains logic to convert array data into entities.
28
 *
29
 * Useful when converting request data into entities.
30
 *
31
 * @see \Cake\ORM\Table::newEntity()
32
 * @see \Cake\ORM\Table::newEntities()
33
 * @see \Cake\ORM\Table::patchEntity()
34
 * @see \Cake\ORM\Table::patchEntities()
35
 */
36
class Marshaller
37
{
38
    use AssociationsNormalizerTrait;
39
40
    /**
41
     * The table instance this marshaller is for.
42
     *
43
     * @var \Cake\ORM\Table
44
     */
45
    protected $_table;
46
47
    /**
48
     * Constructor.
49
     *
50
     * @param \Cake\ORM\Table $table The table this marshaller is for.
51
     */
52
    public function __construct(Table $table)
53
    {
54
        $this->_table = $table;
55
    }
56
57
    /**
58
     * Build the map of property => marshalling callable.
59
     *
60
     * @param array $data The data being marshalled.
61
     * @param array $options List of options containing the 'associated' key.
62
     * @throws \InvalidArgumentException When associations do not exist.
63
     * @return array
64
     */
65
    protected function _buildPropertyMap($data, $options)
66
    {
67
        $map = [];
68
        $schema = $this->_table->getSchema();
69
70
        // Is a concrete column?
71
        foreach (array_keys($data) as $prop) {
72
            $columnType = $schema->getColumnType($prop);
73
            if ($columnType) {
74
                $map[$prop] = function ($value, $entity) use ($columnType) {
75
                    return Type::build($columnType)->marshal($value);
76
                };
77
            }
78
        }
79
80
        // Map associations
81
        if (!isset($options['associated'])) {
82
            $options['associated'] = [];
83
        }
84
        $include = $this->_normalizeAssociations($options['associated']);
85
        foreach ($include as $key => $nested) {
86
            if (is_int($key) && is_scalar($nested)) {
87
                $key = $nested;
88
                $nested = [];
89
            }
90
            // If the key is not a special field like _ids or _joinData
91
            // it is a missing association that we should error on.
92
            if (!$this->_table->hasAssociation($key)) {
93
                if (substr($key, 0, 1) !== '_') {
94
                    throw new \InvalidArgumentException(sprintf(
95
                        'Cannot marshal data for "%s" association. It is not associated with "%s".',
96
                        $key,
97
                        $this->_table->getAlias()
98
                    ));
99
                }
100
                continue;
101
            }
102
            $assoc = $this->_table->getAssociation($key);
103
104
            if (isset($options['forceNew'])) {
105
                $nested['forceNew'] = $options['forceNew'];
106
            }
107
            if (isset($options['isMerge'])) {
108
                $callback = function ($value, $entity) use ($assoc, $nested) {
109
                    /** @var \Cake\Datasource\EntityInterface $entity */
110
                    $options = $nested + ['associated' => [], 'association' => $assoc];
111
112
                    return $this->_mergeAssociation($entity->get($assoc->getProperty()), $assoc, $value, $options);
113
                };
114
            } else {
115
                $callback = function ($value, $entity) use ($assoc, $nested) {
116
                    $options = $nested + ['associated' => []];
117
118
                    return $this->_marshalAssociation($assoc, $value, $options);
119
                };
120
            }
121
            $map[$assoc->getProperty()] = $callback;
122
        }
123
124
        $behaviors = $this->_table->behaviors();
125
        foreach ($behaviors->loaded() as $name) {
126
            $behavior = $behaviors->get($name);
127
            if ($behavior instanceof PropertyMarshalInterface) {
128
                $map += $behavior->buildMarshalMap($this, $map, $options);
129
            }
130
        }
131
132
        return $map;
133
    }
134
135
    /**
136
     * Hydrate one entity and its associated data.
137
     *
138
     * ### Options:
139
     *
140
     * - validate: Set to false to disable validation. Can also be a string of the validator ruleset to be applied.
141
     *   Defaults to true/default.
142
     * - associated: Associations listed here will be marshalled as well. Defaults to null.
143
     * - fieldList: (deprecated) Since 3.4.0. Use fields instead.
144
     * - fields: A whitelist of fields to be assigned to the entity. If not present,
145
     *   the accessible fields list in the entity will be used. Defaults to null.
146
     * - accessibleFields: A list of fields to allow or deny in entity accessible fields. Defaults to null
147
     * - forceNew: When enabled, belongsToMany associations will have 'new' entities created
148
     *   when primary key values are set, and a record does not already exist. Normally primary key
149
     *   on missing entities would be ignored. Defaults to false.
150
     *
151
     * The above options can be used in each nested `associated` array. In addition to the above
152
     * options you can also use the `onlyIds` option for HasMany and BelongsToMany associations.
153
     * When true this option restricts the request data to only be read from `_ids`.
154
     *
155
     * ```
156
     * $result = $marshaller->one($data, [
157
     *   'associated' => ['Tags' => ['onlyIds' => true]]
158
     * ]);
159
     * ```
160
     *
161
     * @param array $data The data to hydrate.
162
     * @param array $options List of options
163
     * @return \Cake\Datasource\EntityInterface
164
     * @see \Cake\ORM\Table::newEntity()
165
     * @see \Cake\ORM\Entity::$_accessible
166
     */
167
    public function one(array $data, array $options = [])
168
    {
169
        list($data, $options) = $this->_prepareDataAndOptions($data, $options);
170
171
        $primaryKey = (array)$this->_table->getPrimaryKey();
172
        $entityClass = $this->_table->getEntityClass();
173
        /** @var \Cake\Datasource\EntityInterface $entity */
174
        $entity = new $entityClass();
175
        $entity->setSource($this->_table->getRegistryAlias());
176
177 View Code Duplication
        if (isset($options['accessibleFields'])) {
178
            foreach ((array)$options['accessibleFields'] as $key => $value) {
179
                $entity->setAccess($key, $value);
180
            }
181
        }
182
        $errors = $this->_validate($data, $options, true);
183
184
        $options['isMerge'] = false;
185
        $propertyMap = $this->_buildPropertyMap($data, $options);
186
        $properties = [];
187
        foreach ($data as $key => $value) {
188
            if (!empty($errors[$key])) {
189
                if ($entity instanceof InvalidPropertyInterface) {
190
                    $entity->setInvalidField($key, $value);
191
                }
192
                continue;
193
            }
194
195
            if ($value === '' && in_array($key, $primaryKey, true)) {
196
                // Skip marshalling '' for pk fields.
197
                continue;
198
            }
199
            if (isset($propertyMap[$key])) {
200
                $properties[$key] = $propertyMap[$key]($value, $entity);
201
            } else {
202
                $properties[$key] = $value;
203
            }
204
        }
205
206
        if (isset($options['fields'])) {
207
            foreach ((array)$options['fields'] as $field) {
208
                if (array_key_exists($field, $properties)) {
209
                    $entity->set($field, $properties[$field]);
210
                }
211
            }
212
        } else {
213
            $entity->set($properties);
214
        }
215
216
        // Don't flag clean association entities as
217
        // dirty so we don't persist empty records.
218
        foreach ($properties as $field => $value) {
219
            if ($value instanceof EntityInterface) {
220
                $entity->setDirty($field, $value->isDirty());
221
            }
222
        }
223
224
        $entity->setErrors($errors);
0 ignored issues
show
Bug introduced by
The method setErrors() 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...
225
226
        return $entity;
227
    }
228
229
    /**
230
     * Returns the validation errors for a data set based on the passed options
231
     *
232
     * @param array $data The data to validate.
233
     * @param array $options The options passed to this marshaller.
234
     * @param bool $isNew Whether it is a new entity or one to be updated.
235
     * @return array The list of validation errors.
236
     * @throws \RuntimeException If no validator can be created.
237
     */
238
    protected function _validate($data, $options, $isNew)
239
    {
240
        if (!$options['validate']) {
241
            return [];
242
        }
243
244
        $validator = null;
245
        if ($options['validate'] === true) {
246
            $validator = $this->_table->getValidator();
247
        } elseif (is_string($options['validate'])) {
248
            $validator = $this->_table->getValidator($options['validate']);
249
        } elseif (is_object($options['validate'])) {
250
            /** @var \Cake\Validation\Validator $validator */
251
            $validator = $options['validate'];
252
        }
253
254
        if ($validator === null) {
255
            throw new RuntimeException(
256
                sprintf('validate must be a boolean, a string or an object. Got %s.', getTypeName($options['validate']))
257
            );
258
        }
259
260
        return $validator->errors($data, $isNew);
261
    }
262
263
    /**
264
     * Returns data and options prepared to validate and marshall.
265
     *
266
     * @param array $data The data to prepare.
267
     * @param array $options The options passed to this marshaller.
268
     * @return array An array containing prepared data and options.
269
     */
270
    protected function _prepareDataAndOptions($data, $options)
271
    {
272
        $options += ['validate' => true];
273
274
        if (!isset($options['fields']) && isset($options['fieldList'])) {
275
            deprecationWarning(
276
                'The `fieldList` option for marshalling is deprecated. Use the `fields` option instead.'
277
            );
278
            $options['fields'] = $options['fieldList'];
279
            unset($options['fieldList']);
280
        }
281
282
        $tableName = $this->_table->getAlias();
283
        if (isset($data[$tableName])) {
284
            $data += $data[$tableName];
285
            unset($data[$tableName]);
286
        }
287
288
        $data = new ArrayObject($data);
289
        $options = new ArrayObject($options);
290
        $this->_table->dispatchEvent('Model.beforeMarshal', compact('data', 'options'));
291
292
        return [(array)$data, (array)$options];
293
    }
294
295
    /**
296
     * Create a new sub-marshaller and marshal the associated data.
297
     *
298
     * @param \Cake\ORM\Association $assoc The association to marshall
299
     * @param array $value The data to hydrate
300
     * @param array $options List of options.
301
     * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null
302
     */
303
    protected function _marshalAssociation($assoc, $value, $options)
304
    {
305
        if (!is_array($value)) {
306
            return null;
307
        }
308
        $targetTable = $assoc->getTarget();
309
        $marshaller = $targetTable->marshaller();
310
        $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE];
311
        if (in_array($assoc->type(), $types, true)) {
312
            return $marshaller->one($value, (array)$options);
313
        }
314 View Code Duplication
        if ($assoc->type() === Association::ONE_TO_MANY || $assoc->type() === Association::MANY_TO_MANY) {
315
            $hasIds = array_key_exists('_ids', $value);
316
            $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];
317
318
            if ($hasIds && is_array($value['_ids'])) {
319
                return $this->_loadAssociatedByIds($assoc, $value['_ids']);
320
            }
321
            if ($hasIds || $onlyIds) {
322
                return [];
323
            }
324
        }
325
        if ($assoc->type() === Association::MANY_TO_MANY) {
326
            return $marshaller->_belongsToMany($assoc, $value, (array)$options);
0 ignored issues
show
Compatibility introduced by
$assoc of type object<Cake\ORM\Association> is not a sub-type of object<Cake\ORM\Association\BelongsToMany>. It seems like you assume a child class of the class Cake\ORM\Association to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
327
        }
328
329
        return $marshaller->many($value, (array)$options);
330
    }
331
332
    /**
333
     * Hydrate many entities and their associated data.
334
     *
335
     * ### Options:
336
     *
337
     * - validate: Set to false to disable validation. Can also be a string of the validator ruleset to be applied.
338
     *   Defaults to true/default.
339
     * - associated: Associations listed here will be marshalled as well. Defaults to null.
340
     * - fieldList: (deprecated) Since 3.4.0. Use fields instead
341
     * - fields: A whitelist of fields to be assigned to the entity. If not present,
342
     *   the accessible fields list in the entity will be used. Defaults to null.
343
     * - accessibleFields: A list of fields to allow or deny in entity accessible fields. Defaults to null
344
     * - forceNew: When enabled, belongsToMany associations will have 'new' entities created
345
     *   when primary key values are set, and a record does not already exist. Normally primary key
346
     *   on missing entities would be ignored. Defaults to false.
347
     *
348
     * @param array $data The data to hydrate.
349
     * @param array $options List of options
350
     * @return \Cake\Datasource\EntityInterface[] An array of hydrated records.
351
     * @see \Cake\ORM\Table::newEntities()
352
     * @see \Cake\ORM\Entity::$_accessible
353
     */
354
    public function many(array $data, array $options = [])
355
    {
356
        $output = [];
357
        foreach ($data as $record) {
358
            if (!is_array($record)) {
359
                continue;
360
            }
361
            $output[] = $this->one($record, $options);
362
        }
363
364
        return $output;
365
    }
366
367
    /**
368
     * Marshals data for belongsToMany associations.
369
     *
370
     * Builds the related entities and handles the special casing
371
     * for junction table entities.
372
     *
373
     * @param \Cake\ORM\Association\BelongsToMany $assoc The association to marshal.
374
     * @param array $data The data to convert into entities.
375
     * @param array $options List of options.
376
     * @return \Cake\Datasource\EntityInterface[] An array of built entities.
377
     * @throws \BadMethodCallException
378
     * @throws \InvalidArgumentException
379
     * @throws \RuntimeException
380
     */
381
    protected function _belongsToMany(BelongsToMany $assoc, array $data, $options = [])
382
    {
383
        $associated = isset($options['associated']) ? $options['associated'] : [];
384
        $forceNew = isset($options['forceNew']) ? $options['forceNew'] : false;
385
386
        $data = array_values($data);
387
388
        $target = $assoc->getTarget();
389
        $primaryKey = array_flip((array)$target->getPrimaryKey());
390
        $records = $conditions = [];
391
        $primaryCount = count($primaryKey);
392
        $conditions = [];
393
394
        foreach ($data as $i => $row) {
395
            if (!is_array($row)) {
396
                continue;
397
            }
398
            if (array_intersect_key($primaryKey, $row) === $primaryKey) {
399
                $keys = array_intersect_key($row, $primaryKey);
400
                if (count($keys) === $primaryCount) {
401
                    $rowConditions = [];
402
                    foreach ($keys as $key => $value) {
403
                        $rowConditions[][$target->aliasField($key)] = $value;
404
                    }
405
406
                    if ($forceNew && !$target->exists($rowConditions)) {
407
                        $records[$i] = $this->one($row, $options);
408
                    }
409
410
                    $conditions = array_merge($conditions, $rowConditions);
411
                }
412
            } else {
413
                $records[$i] = $this->one($row, $options);
414
            }
415
        }
416
417
        if (!empty($conditions)) {
418
            $query = $target->find();
419
            $query->andWhere(function ($exp) use ($conditions) {
420
                /** @var \Cake\Database\Expression\QueryExpression $exp */
421
                return $exp->or($conditions);
422
            });
423
424
            $keyFields = array_keys($primaryKey);
425
426
            $existing = [];
427
            foreach ($query as $row) {
428
                $k = implode(';', $row->extract($keyFields));
429
                $existing[$k] = $row;
430
            }
431
432
            foreach ($data as $i => $row) {
433
                $key = [];
434
                foreach ($keyFields as $k) {
435
                    if (isset($row[$k])) {
436
                        $key[] = $row[$k];
437
                    }
438
                }
439
                $key = implode(';', $key);
440
441
                // Update existing record and child associations
442
                if (isset($existing[$key])) {
443
                    $records[$i] = $this->merge($existing[$key], $data[$i], $options);
444
                }
445
            }
446
        }
447
448
        $jointMarshaller = $assoc->junction()->marshaller();
449
450
        $nested = [];
451
        if (isset($associated['_joinData'])) {
452
            $nested = (array)$associated['_joinData'];
453
        }
454
455
        foreach ($records as $i => $record) {
456
            // Update junction table data in _joinData.
457
            if (isset($data[$i]['_joinData'])) {
458
                $joinData = $jointMarshaller->one($data[$i]['_joinData'], $nested);
459
                $record->set('_joinData', $joinData);
460
            }
461
        }
462
463
        return $records;
464
    }
465
466
    /**
467
     * Loads a list of belongs to many from ids.
468
     *
469
     * @param \Cake\ORM\Association $assoc The association class for the belongsToMany association.
470
     * @param array $ids The list of ids to load.
471
     * @return \Cake\Datasource\EntityInterface[] An array of entities.
472
     */
473
    protected function _loadAssociatedByIds($assoc, $ids)
474
    {
475
        if (empty($ids)) {
476
            return [];
477
        }
478
479
        $target = $assoc->getTarget();
480
        $primaryKey = (array)$target->getPrimaryKey();
481
        $multi = count($primaryKey) > 1;
482
        $primaryKey = array_map([$target, 'aliasField'], $primaryKey);
483
484
        if ($multi) {
485
            $first = current($ids);
486
            if (!is_array($first) || count($first) !== count($primaryKey)) {
487
                return [];
488
            }
489
            $filter = new TupleComparison($primaryKey, $ids, [], 'IN');
490
        } else {
491
            $filter = [$primaryKey[0] . ' IN' => $ids];
492
        }
493
494
        return $target->find()->where($filter)->toArray();
495
    }
496
497
    /**
498
     * Loads a list of belongs to many from ids.
499
     *
500
     * @param \Cake\ORM\Association $assoc The association class for the belongsToMany association.
501
     * @param array $ids The list of ids to load.
502
     * @return \Cake\Datasource\EntityInterface[] An array of entities.
503
     * @deprecated Use _loadAssociatedByIds()
504
     */
505
    protected function _loadBelongsToMany($assoc, $ids)
506
    {
507
        deprecationWarning(
508
            'Marshaller::_loadBelongsToMany() is deprecated. Use _loadAssociatedByIds() instead.'
509
        );
510
511
        return $this->_loadAssociatedByIds($assoc, $ids);
512
    }
513
514
    /**
515
     * Merges `$data` into `$entity` and recursively does the same for each one of
516
     * the association names passed in `$options`. When merging associations, if an
517
     * entity is not present in the parent entity for a given association, a new one
518
     * will be created.
519
     *
520
     * When merging HasMany or BelongsToMany associations, all the entities in the
521
     * `$data` array will appear, those that can be matched by primary key will get
522
     * the data merged, but those that cannot, will be discarded. `ids` option can be used
523
     * to determine whether the association must use the `_ids` format.
524
     *
525
     * ### Options:
526
     *
527
     * - associated: Associations listed here will be marshalled as well.
528
     * - validate: Whether or not to validate data before hydrating the entities. Can
529
     *   also be set to a string to use a specific validator. Defaults to true/default.
530
     * - fieldList: (deprecated) Since 3.4.0. Use fields instead
531
     * - fields: A whitelist of fields to be assigned to the entity. If not present
532
     *   the accessible fields list in the entity will be used.
533
     * - accessibleFields: A list of fields to allow or deny in entity accessible fields.
534
     *
535
     * The above options can be used in each nested `associated` array. In addition to the above
536
     * options you can also use the `onlyIds` option for HasMany and BelongsToMany associations.
537
     * When true this option restricts the request data to only be read from `_ids`.
538
     *
539
     * ```
540
     * $result = $marshaller->merge($entity, $data, [
541
     *   'associated' => ['Tags' => ['onlyIds' => true]]
542
     * ]);
543
     * ```
544
     *
545
     * @param \Cake\Datasource\EntityInterface $entity the entity that will get the
546
     * data merged in
547
     * @param array $data key value list of fields to be merged into the entity
548
     * @param array $options List of options.
549
     * @return \Cake\Datasource\EntityInterface
550
     * @see \Cake\ORM\Entity::$_accessible
551
     */
552
    public function merge(EntityInterface $entity, array $data, array $options = [])
553
    {
554
        list($data, $options) = $this->_prepareDataAndOptions($data, $options);
555
556
        $isNew = $entity->isNew();
557
        $keys = [];
558
559
        if (!$isNew) {
560
            $keys = $entity->extract((array)$this->_table->getPrimaryKey());
561
        }
562
563 View Code Duplication
        if (isset($options['accessibleFields'])) {
564
            foreach ((array)$options['accessibleFields'] as $key => $value) {
565
                $entity->setAccess($key, $value);
566
            }
567
        }
568
569
        $errors = $this->_validate($data + $keys, $options, $isNew);
570
        $options['isMerge'] = true;
571
        $propertyMap = $this->_buildPropertyMap($data, $options);
572
        $properties = [];
573
        foreach ($data as $key => $value) {
574
            if (!empty($errors[$key])) {
575
                if ($entity instanceof InvalidPropertyInterface) {
576
                    $entity->setInvalidField($key, $value);
577
                }
578
                continue;
579
            }
580
            $original = $entity->get($key);
581
582
            if (isset($propertyMap[$key])) {
583
                $value = $propertyMap[$key]($value, $entity);
584
585
                // Don't dirty scalar values and objects that didn't
586
                // change. Arrays will always be marked as dirty because
587
                // the original/updated list could contain references to the
588
                // same objects, even though those objects may have changed internally.
589
                if (
590
                    (is_scalar($value) && $original === $value) ||
591
                    ($value === null && $original === $value) ||
592
                    (is_object($value) && !($value instanceof EntityInterface) && $original == $value)
593
                ) {
594
                    continue;
595
                }
596
            }
597
            $properties[$key] = $value;
598
        }
599
600
        $entity->setErrors($errors);
0 ignored issues
show
Bug introduced by
The method setErrors() 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...
601
        if (!isset($options['fields'])) {
602
            $entity->set($properties);
603
604
            foreach ($properties as $field => $value) {
605
                if ($value instanceof EntityInterface) {
606
                    $entity->setDirty($field, $value->isDirty());
607
                }
608
            }
609
610
            return $entity;
611
        }
612
613
        foreach ((array)$options['fields'] as $field) {
614
            if (!array_key_exists($field, $properties)) {
615
                continue;
616
            }
617
            $entity->set($field, $properties[$field]);
618
            if ($properties[$field] instanceof EntityInterface) {
619
                $entity->setDirty($field, $properties[$field]->isDirty());
620
            }
621
        }
622
623
        return $entity;
624
    }
625
626
    /**
627
     * Merges each of the elements from `$data` into each of the entities in `$entities`
628
     * and recursively does the same for each of the association names passed in
629
     * `$options`. When merging associations, if an entity is not present in the parent
630
     * entity for a given association, a new one will be created.
631
     *
632
     * Records in `$data` are matched against the entities using the primary key
633
     * column. Entries in `$entities` that cannot be matched to any record in
634
     * `$data` will be discarded. Records in `$data` that could not be matched will
635
     * be marshalled as a new entity.
636
     *
637
     * When merging HasMany or BelongsToMany associations, all the entities in the
638
     * `$data` array will appear, those that can be matched by primary key will get
639
     * the data merged, but those that cannot, will be discarded.
640
     *
641
     * ### Options:
642
     *
643
     * - validate: Whether or not to validate data before hydrating the entities. Can
644
     *   also be set to a string to use a specific validator. Defaults to true/default.
645
     * - associated: Associations listed here will be marshalled as well.
646
     * - fieldList: (deprecated) Since 3.4.0. Use fields instead
647
     * - fields: A whitelist of fields to be assigned to the entity. If not present,
648
     *   the accessible fields list in the entity will be used.
649
     * - accessibleFields: A list of fields to allow or deny in entity accessible fields.
650
     *
651
     * @param \Cake\Datasource\EntityInterface[]|\Traversable $entities the entities that will get the
652
     *   data merged in
653
     * @param array $data list of arrays to be merged into the entities
654
     * @param array $options List of options.
655
     * @return \Cake\Datasource\EntityInterface[]
656
     * @see \Cake\ORM\Entity::$_accessible
657
     */
658
    public function mergeMany($entities, array $data, array $options = [])
659
    {
660
        $primary = (array)$this->_table->getPrimaryKey();
661
662
        $indexed = (new Collection($data))
663
            ->groupBy(function ($el) use ($primary) {
664
                $keys = [];
665
                foreach ($primary as $key) {
666
                    $keys[] = isset($el[$key]) ? $el[$key] : '';
667
                }
668
669
                return implode(';', $keys);
670
            })
671
            ->map(function ($element, $key) {
672
                return $key === '' ? $element : $element[0];
673
            })
674
            ->toArray();
675
676
        $new = isset($indexed[null]) ? $indexed[null] : [];
677
        unset($indexed[null]);
678
        $output = [];
679
680
        foreach ($entities as $entity) {
681
            if (!($entity instanceof EntityInterface)) {
682
                continue;
683
            }
684
685
            $key = implode(';', $entity->extract($primary));
686
            if ($key === null || !isset($indexed[$key])) {
687
                continue;
688
            }
689
690
            $output[] = $this->merge($entity, $indexed[$key], $options);
691
            unset($indexed[$key]);
692
        }
693
694
        $conditions = (new Collection($indexed))
695
            ->map(function ($data, $key) {
696
                return explode(';', $key);
697
            })
698
            ->filter(function ($keys) use ($primary) {
699
                return count(array_filter($keys, 'strlen')) === count($primary);
700
            })
701
            ->reduce(function ($conditions, $keys) use ($primary) {
702
                $fields = array_map([$this->_table, 'aliasField'], $primary);
703
                $conditions['OR'][] = array_combine($fields, $keys);
704
705
                return $conditions;
706
            }, ['OR' => []]);
707
        $maybeExistentQuery = $this->_table->find()->where($conditions);
708
709
        if (!empty($indexed) && count($maybeExistentQuery->clause('where'))) {
710
            foreach ($maybeExistentQuery as $entity) {
711
                $key = implode(';', $entity->extract($primary));
712
                if (isset($indexed[$key])) {
713
                    $output[] = $this->merge($entity, $indexed[$key], $options);
714
                    unset($indexed[$key]);
715
                }
716
            }
717
        }
718
719
        foreach ((new Collection($indexed))->append($new) as $value) {
720
            if (!is_array($value)) {
721
                continue;
722
            }
723
            $output[] = $this->one($value, $options);
724
        }
725
726
        return $output;
727
    }
728
729
    /**
730
     * Creates a new sub-marshaller and merges the associated data.
731
     *
732
     * @param \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[] $original The original entity
733
     * @param \Cake\ORM\Association $assoc The association to merge
734
     * @param array $value The data to hydrate
735
     * @param array $options List of options.
736
     * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null
737
     */
738
    protected function _mergeAssociation($original, $assoc, $value, $options)
739
    {
740
        if (!$original) {
741
            return $this->_marshalAssociation($assoc, $value, $options);
742
        }
743
        if (!is_array($value)) {
744
            return null;
745
        }
746
747
        $targetTable = $assoc->getTarget();
748
        $marshaller = $targetTable->marshaller();
749
        $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE];
750
        if (in_array($assoc->type(), $types, true)) {
751
            return $marshaller->merge($original, $value, (array)$options);
0 ignored issues
show
Bug introduced by
It seems like $original defined by parameter $original on line 738 can also be of type array<integer,object<Cak...ource\EntityInterface>>; however, Cake\ORM\Marshaller::merge() does only seem to accept object<Cake\Datasource\EntityInterface>, 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...
752
        }
753
        if ($assoc->type() === Association::MANY_TO_MANY) {
754
            return $marshaller->_mergeBelongsToMany($original, $assoc, $value, (array)$options);
0 ignored issues
show
Bug introduced by
It seems like $original defined by parameter $original on line 738 can also be of type array<integer,object<Cak...ource\EntityInterface>>; however, Cake\ORM\Marshaller::_mergeBelongsToMany() does only seem to accept object<Cake\Datasource\EntityInterface>, 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...
755
        }
756
757 View Code Duplication
        if ($assoc->type() === Association::ONE_TO_MANY) {
758
            $hasIds = array_key_exists('_ids', $value);
759
            $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];
760
            if ($hasIds && is_array($value['_ids'])) {
761
                return $this->_loadAssociatedByIds($assoc, $value['_ids']);
762
            }
763
            if ($hasIds || $onlyIds) {
764
                return [];
765
            }
766
        }
767
768
        return $marshaller->mergeMany($original, $value, (array)$options);
0 ignored issues
show
Bug introduced by
It seems like $original defined by parameter $original on line 738 can also be of type object<Cake\Datasource\EntityInterface>; however, Cake\ORM\Marshaller::mergeMany() does only seem to accept array<integer,object<Cak...e>>|object<Traversable>, 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...
769
    }
770
771
    /**
772
     * Creates a new sub-marshaller and merges the associated data for a BelongstoMany
773
     * association.
774
     *
775
     * @param \Cake\Datasource\EntityInterface $original The original entity
776
     * @param \Cake\ORM\Association $assoc The association to marshall
777
     * @param array $value The data to hydrate
778
     * @param array $options List of options.
779
     * @return \Cake\Datasource\EntityInterface[]
780
     */
781
    protected function _mergeBelongsToMany($original, $assoc, $value, $options)
782
    {
783
        $associated = isset($options['associated']) ? $options['associated'] : [];
784
785
        $hasIds = array_key_exists('_ids', $value);
786
        $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];
787
788
        if ($hasIds && is_array($value['_ids'])) {
789
            return $this->_loadAssociatedByIds($assoc, $value['_ids']);
790
        }
791
        if ($hasIds || $onlyIds) {
792
            return [];
793
        }
794
795
        if (!empty($associated) && !in_array('_joinData', $associated, true) && !isset($associated['_joinData'])) {
796
            return $this->mergeMany($original, $value, $options);
0 ignored issues
show
Documentation introduced by
$original is of type object<Cake\Datasource\EntityInterface>, but the function expects a array<integer,object<Cak...e>>|object<Traversable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
797
        }
798
799
        return $this->_mergeJoinData($original, $assoc, $value, $options);
0 ignored issues
show
Compatibility introduced by
$assoc of type object<Cake\ORM\Association> is not a sub-type of object<Cake\ORM\Association\BelongsToMany>. It seems like you assume a child class of the class Cake\ORM\Association to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
800
    }
801
802
    /**
803
     * Merge the special _joinData property into the entity set.
804
     *
805
     * @param \Cake\Datasource\EntityInterface $original The original entity
806
     * @param \Cake\ORM\Association\BelongsToMany $assoc The association to marshall
807
     * @param array $value The data to hydrate
808
     * @param array $options List of options.
809
     * @return \Cake\Datasource\EntityInterface[] An array of entities
810
     */
811
    protected function _mergeJoinData($original, $assoc, $value, $options)
812
    {
813
        $associated = isset($options['associated']) ? $options['associated'] : [];
814
        $extra = [];
815
        foreach ($original as $entity) {
816
            // Mark joinData as accessible so we can marshal it properly.
817
            $entity->setAccess('_joinData', true);
818
819
            $joinData = $entity->get('_joinData');
820
            if ($joinData && $joinData instanceof EntityInterface) {
821
                $extra[spl_object_hash($entity)] = $joinData;
822
            }
823
        }
824
825
        $joint = $assoc->junction();
826
        $marshaller = $joint->marshaller();
827
828
        $nested = [];
829
        if (isset($associated['_joinData'])) {
830
            $nested = (array)$associated['_joinData'];
831
        }
832
833
        $options['accessibleFields'] = ['_joinData' => true];
834
835
        $records = $this->mergeMany($original, $value, $options);
0 ignored issues
show
Documentation introduced by
$original is of type object<Cake\Datasource\EntityInterface>, but the function expects a array<integer,object<Cak...e>>|object<Traversable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
836
        foreach ($records as $record) {
837
            $hash = spl_object_hash($record);
838
            $value = $record->get('_joinData');
839
840
            // Already an entity, no further marshalling required.
841
            if ($value instanceof EntityInterface) {
842
                continue;
843
            }
844
845
            // Scalar data can't be handled
846
            if (!is_array($value)) {
847
                $record->unsetProperty('_joinData');
848
                continue;
849
            }
850
851
            // Marshal data into the old object, or make a new joinData object.
852
            if (isset($extra[$hash])) {
853
                $record->set('_joinData', $marshaller->merge($extra[$hash], $value, $nested));
854
            } elseif (is_array($value)) {
855
                $joinData = $marshaller->one($value, $nested);
856
                $record->set('_joinData', $joinData);
857
            }
858
        }
859
860
        return $records;
861
    }
862
}
863