CakePHP
  • Documentation
    • Book
    • API
    • Videos
    • Logos & Trademarks
  • Business Solutions
  • Swag
  • Road Trip
  • Team
  • Community
    • Community
    • Team
    • Issues (Github)
    • YouTube Channel
    • Get Involved
    • Bakery
    • Featured Resources
    • Newsletter
    • Certification
    • My CakePHP
    • CakeFest
    • Facebook
    • Twitter
    • Help & Support
    • Forum
    • Stack Overflow
    • IRC
    • Slack
    • Paid Support
CakePHP

C CakePHP 3.8 Red Velvet API

  • Overview
  • Tree
  • Deprecated
  • Version:
    • 3.8
      • 3.8
      • 3.7
      • 3.6
      • 3.5
      • 3.4
      • 3.3
      • 3.2
      • 3.1
      • 3.0
      • 2.10
      • 2.9
      • 2.8
      • 2.7
      • 2.6
      • 2.5
      • 2.4
      • 2.3
      • 2.2
      • 2.1
      • 2.0
      • 1.3
      • 1.2

Namespaces

  • Cake
    • Auth
      • Storage
    • Cache
      • Engine
    • Collection
      • Iterator
    • Command
    • Console
      • Exception
    • Controller
      • Component
      • Exception
    • Core
      • Configure
        • Engine
      • Exception
      • Retry
    • Database
      • Driver
      • Exception
      • Expression
      • Schema
      • Statement
      • Type
    • Datasource
      • Exception
    • Error
      • Middleware
    • Event
      • Decorator
    • Filesystem
    • Form
    • Http
      • Client
        • Adapter
        • Auth
      • Cookie
      • Exception
      • Middleware
      • Session
    • I18n
      • Formatter
      • Middleware
      • Parser
    • Log
      • Engine
    • Mailer
      • Exception
      • Transport
    • Network
      • Exception
    • ORM
      • Association
      • Behavior
        • Translate
      • Exception
      • Locator
      • Rule
    • Routing
      • Exception
      • Filter
      • Middleware
      • Route
    • Shell
      • Helper
      • Task
    • TestSuite
      • Fixture
      • Stub
    • Utility
      • Exception
    • Validation
    • View
      • Exception
      • Form
      • Helper
      • Widget
  • None

Classes

  • Association
  • AssociationCollection
  • Behavior
  • BehaviorRegistry
  • EagerLoader
  • Entity
  • Marshaller
  • Query
  • ResultSet
  • RulesChecker
  • SaveOptionsBuilder
  • Table
  • TableRegistry

Interfaces

  • PropertyMarshalInterface

Traits

  • AssociationsNormalizerTrait
  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:         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);
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:         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);
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:         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 ((is_scalar($value) && $original === $value) ||
590:                     ($value === null && $original === $value) ||
591:                     (is_object($value) && !($value instanceof EntityInterface) && $original == $value)
592:                 ) {
593:                     continue;
594:                 }
595:             }
596:             $properties[$key] = $value;
597:         }
598: 
599:         $entity->setErrors($errors);
600:         if (!isset($options['fields'])) {
601:             $entity->set($properties);
602: 
603:             foreach ($properties as $field => $value) {
604:                 if ($value instanceof EntityInterface) {
605:                     $entity->setDirty($field, $value->isDirty());
606:                 }
607:             }
608: 
609:             return $entity;
610:         }
611: 
612:         foreach ((array)$options['fields'] as $field) {
613:             if (!array_key_exists($field, $properties)) {
614:                 continue;
615:             }
616:             $entity->set($field, $properties[$field]);
617:             if ($properties[$field] instanceof EntityInterface) {
618:                 $entity->setDirty($field, $properties[$field]->isDirty());
619:             }
620:         }
621: 
622:         return $entity;
623:     }
624: 
625:     /**
626:      * Merges each of the elements from `$data` into each of the entities in `$entities`
627:      * and recursively does the same for each of the association names passed in
628:      * `$options`. When merging associations, if an entity is not present in the parent
629:      * entity for a given association, a new one will be created.
630:      *
631:      * Records in `$data` are matched against the entities using the primary key
632:      * column. Entries in `$entities` that cannot be matched to any record in
633:      * `$data` will be discarded. Records in `$data` that could not be matched will
634:      * be marshalled as a new entity.
635:      *
636:      * When merging HasMany or BelongsToMany associations, all the entities in the
637:      * `$data` array will appear, those that can be matched by primary key will get
638:      * the data merged, but those that cannot, will be discarded.
639:      *
640:      * ### Options:
641:      *
642:      * - validate: Whether or not to validate data before hydrating the entities. Can
643:      *   also be set to a string to use a specific validator. Defaults to true/default.
644:      * - associated: Associations listed here will be marshalled as well.
645:      * - fieldList: (deprecated) Since 3.4.0. Use fields instead
646:      * - fields: A whitelist of fields to be assigned to the entity. If not present,
647:      *   the accessible fields list in the entity will be used.
648:      * - accessibleFields: A list of fields to allow or deny in entity accessible fields.
649:      *
650:      * @param \Cake\Datasource\EntityInterface[]|\Traversable $entities the entities that will get the
651:      *   data merged in
652:      * @param array $data list of arrays to be merged into the entities
653:      * @param array $options List of options.
654:      * @return \Cake\Datasource\EntityInterface[]
655:      * @see \Cake\ORM\Entity::$_accessible
656:      */
657:     public function mergeMany($entities, array $data, array $options = [])
658:     {
659:         $primary = (array)$this->_table->getPrimaryKey();
660: 
661:         $indexed = (new Collection($data))
662:             ->groupBy(function ($el) use ($primary) {
663:                 $keys = [];
664:                 foreach ($primary as $key) {
665:                     $keys[] = isset($el[$key]) ? $el[$key] : '';
666:                 }
667: 
668:                 return implode(';', $keys);
669:             })
670:             ->map(function ($element, $key) {
671:                 return $key === '' ? $element : $element[0];
672:             })
673:             ->toArray();
674: 
675:         $new = isset($indexed[null]) ? $indexed[null] : [];
676:         unset($indexed[null]);
677:         $output = [];
678: 
679:         foreach ($entities as $entity) {
680:             if (!($entity instanceof EntityInterface)) {
681:                 continue;
682:             }
683: 
684:             $key = implode(';', $entity->extract($primary));
685:             if ($key === null || !isset($indexed[$key])) {
686:                 continue;
687:             }
688: 
689:             $output[] = $this->merge($entity, $indexed[$key], $options);
690:             unset($indexed[$key]);
691:         }
692: 
693:         $conditions = (new Collection($indexed))
694:             ->map(function ($data, $key) {
695:                 return explode(';', $key);
696:             })
697:             ->filter(function ($keys) use ($primary) {
698:                 return count(array_filter($keys, 'strlen')) === count($primary);
699:             })
700:             ->reduce(function ($conditions, $keys) use ($primary) {
701:                 $fields = array_map([$this->_table, 'aliasField'], $primary);
702:                 $conditions['OR'][] = array_combine($fields, $keys);
703: 
704:                 return $conditions;
705:             }, ['OR' => []]);
706:         $maybeExistentQuery = $this->_table->find()->where($conditions);
707: 
708:         if (!empty($indexed) && count($maybeExistentQuery->clause('where'))) {
709:             foreach ($maybeExistentQuery as $entity) {
710:                 $key = implode(';', $entity->extract($primary));
711:                 if (isset($indexed[$key])) {
712:                     $output[] = $this->merge($entity, $indexed[$key], $options);
713:                     unset($indexed[$key]);
714:                 }
715:             }
716:         }
717: 
718:         foreach ((new Collection($indexed))->append($new) as $value) {
719:             if (!is_array($value)) {
720:                 continue;
721:             }
722:             $output[] = $this->one($value, $options);
723:         }
724: 
725:         return $output;
726:     }
727: 
728:     /**
729:      * Creates a new sub-marshaller and merges the associated data.
730:      *
731:      * @param \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[] $original The original entity
732:      * @param \Cake\ORM\Association $assoc The association to merge
733:      * @param array $value The data to hydrate
734:      * @param array $options List of options.
735:      * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null
736:      */
737:     protected function _mergeAssociation($original, $assoc, $value, $options)
738:     {
739:         if (!$original) {
740:             return $this->_marshalAssociation($assoc, $value, $options);
741:         }
742:         if (!is_array($value)) {
743:             return null;
744:         }
745: 
746:         $targetTable = $assoc->getTarget();
747:         $marshaller = $targetTable->marshaller();
748:         $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE];
749:         if (in_array($assoc->type(), $types, true)) {
750:             return $marshaller->merge($original, $value, (array)$options);
751:         }
752:         if ($assoc->type() === Association::MANY_TO_MANY) {
753:             return $marshaller->_mergeBelongsToMany($original, $assoc, $value, (array)$options);
754:         }
755: 
756:         if ($assoc->type() === Association::ONE_TO_MANY) {
757:             $hasIds = array_key_exists('_ids', $value);
758:             $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];
759:             if ($hasIds && is_array($value['_ids'])) {
760:                 return $this->_loadAssociatedByIds($assoc, $value['_ids']);
761:             }
762:             if ($hasIds || $onlyIds) {
763:                 return [];
764:             }
765:         }
766: 
767:         return $marshaller->mergeMany($original, $value, (array)$options);
768:     }
769: 
770:     /**
771:      * Creates a new sub-marshaller and merges the associated data for a BelongstoMany
772:      * association.
773:      *
774:      * @param \Cake\Datasource\EntityInterface $original The original entity
775:      * @param \Cake\ORM\Association $assoc The association to marshall
776:      * @param array $value The data to hydrate
777:      * @param array $options List of options.
778:      * @return \Cake\Datasource\EntityInterface[]
779:      */
780:     protected function _mergeBelongsToMany($original, $assoc, $value, $options)
781:     {
782:         $associated = isset($options['associated']) ? $options['associated'] : [];
783: 
784:         $hasIds = array_key_exists('_ids', $value);
785:         $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];
786: 
787:         if ($hasIds && is_array($value['_ids'])) {
788:             return $this->_loadAssociatedByIds($assoc, $value['_ids']);
789:         }
790:         if ($hasIds || $onlyIds) {
791:             return [];
792:         }
793: 
794:         if (!empty($associated) && !in_array('_joinData', $associated, true) && !isset($associated['_joinData'])) {
795:             return $this->mergeMany($original, $value, $options);
796:         }
797: 
798:         return $this->_mergeJoinData($original, $assoc, $value, $options);
799:     }
800: 
801:     /**
802:      * Merge the special _joinData property into the entity set.
803:      *
804:      * @param \Cake\Datasource\EntityInterface $original The original entity
805:      * @param \Cake\ORM\Association\BelongsToMany $assoc The association to marshall
806:      * @param array $value The data to hydrate
807:      * @param array $options List of options.
808:      * @return \Cake\Datasource\EntityInterface[] An array of entities
809:      */
810:     protected function _mergeJoinData($original, $assoc, $value, $options)
811:     {
812:         $associated = isset($options['associated']) ? $options['associated'] : [];
813:         $extra = [];
814:         foreach ($original as $entity) {
815:             // Mark joinData as accessible so we can marshal it properly.
816:             $entity->setAccess('_joinData', true);
817: 
818:             $joinData = $entity->get('_joinData');
819:             if ($joinData && $joinData instanceof EntityInterface) {
820:                 $extra[spl_object_hash($entity)] = $joinData;
821:             }
822:         }
823: 
824:         $joint = $assoc->junction();
825:         $marshaller = $joint->marshaller();
826: 
827:         $nested = [];
828:         if (isset($associated['_joinData'])) {
829:             $nested = (array)$associated['_joinData'];
830:         }
831: 
832:         $options['accessibleFields'] = ['_joinData' => true];
833: 
834:         $records = $this->mergeMany($original, $value, $options);
835:         foreach ($records as $record) {
836:             $hash = spl_object_hash($record);
837:             $value = $record->get('_joinData');
838: 
839:             // Already an entity, no further marshalling required.
840:             if ($value instanceof EntityInterface) {
841:                 continue;
842:             }
843: 
844:             // Scalar data can't be handled
845:             if (!is_array($value)) {
846:                 $record->unsetProperty('_joinData');
847:                 continue;
848:             }
849: 
850:             // Marshal data into the old object, or make a new joinData object.
851:             if (isset($extra[$hash])) {
852:                 $record->set('_joinData', $marshaller->merge($extra[$hash], $value, $nested));
853:             } elseif (is_array($value)) {
854:                 $joinData = $marshaller->one($value, $nested);
855:                 $record->set('_joinData', $joinData);
856:             }
857:         }
858: 
859:         return $records;
860:     }
861: }
862: 
Follow @CakePHP
#IRC
OpenHub
Rackspace
  • Business Solutions
  • Showcase
  • Documentation
  • Book
  • API
  • Videos
  • Logos & Trademarks
  • Community
  • Team
  • Issues (Github)
  • YouTube Channel
  • Get Involved
  • Bakery
  • Featured Resources
  • Newsletter
  • Certification
  • My CakePHP
  • CakeFest
  • Facebook
  • Twitter
  • Help & Support
  • Forum
  • Stack Overflow
  • IRC
  • Slack
  • Paid Support

Generated using CakePHP API Docs