1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 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: 28: 29: 30: 31: 32: 33: 34: 35:
36: class Marshaller
37: {
38:
39: use AssociationsNormalizerTrait;
40:
41: 42: 43: 44: 45:
46: protected $_table;
47:
48: 49: 50: 51: 52:
53: public function __construct(Table $table)
54: {
55: $this->_table = $table;
56: }
57:
58: 59: 60: 61: 62: 63: 64: 65:
66: protected function _buildPropertyMap($data, $options)
67: {
68: $map = [];
69: $schema = $this->_table->getSchema();
70:
71:
72: foreach (array_keys($data) as $prop) {
73: $columnType = $schema->getColumnType($prop);
74: if ($columnType) {
75: $map[$prop] = function ($value, $entity) use ($columnType) {
76: return Type::build($columnType)->marshal($value);
77: };
78: }
79: }
80:
81:
82: if (!isset($options['associated'])) {
83: $options['associated'] = [];
84: }
85: $include = $this->_normalizeAssociations($options['associated']);
86: foreach ($include as $key => $nested) {
87: if (is_int($key) && is_scalar($nested)) {
88: $key = $nested;
89: $nested = [];
90: }
91:
92:
93: if (!$this->_table->hasAssociation($key)) {
94: if (substr($key, 0, 1) !== '_') {
95: throw new \InvalidArgumentException(sprintf(
96: 'Cannot marshal data for "%s" association. It is not associated with "%s".',
97: $key,
98: $this->_table->getAlias()
99: ));
100: }
101: continue;
102: }
103: $assoc = $this->_table->getAssociation($key);
104:
105: if (isset($options['forceNew'])) {
106: $nested['forceNew'] = $options['forceNew'];
107: }
108: if (isset($options['isMerge'])) {
109: $callback = function ($value, $entity) use ($assoc, $nested) {
110:
111: $options = $nested + ['associated' => [], 'association' => $assoc];
112:
113: return $this->_mergeAssociation($entity->get($assoc->getProperty()), $assoc, $value, $options);
114: };
115: } else {
116: $callback = function ($value, $entity) use ($assoc, $nested) {
117: $options = $nested + ['associated' => []];
118:
119: return $this->_marshalAssociation($assoc, $value, $options);
120: };
121: }
122: $map[$assoc->getProperty()] = $callback;
123: }
124:
125: $behaviors = $this->_table->behaviors();
126: foreach ($behaviors->loaded() as $name) {
127: $behavior = $behaviors->get($name);
128: if ($behavior instanceof PropertyMarshalInterface) {
129: $map += $behavior->buildMarshalMap($this, $map, $options);
130: }
131: }
132:
133: return $map;
134: }
135:
136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167:
168: public function one(array $data, array $options = [])
169: {
170: list($data, $options) = $this->_prepareDataAndOptions($data, $options);
171:
172: $primaryKey = (array)$this->_table->getPrimaryKey();
173: $entityClass = $this->_table->getEntityClass();
174:
175: $entity = new $entityClass();
176: $entity->setSource($this->_table->getRegistryAlias());
177:
178: if (isset($options['accessibleFields'])) {
179: foreach ((array)$options['accessibleFields'] as $key => $value) {
180: $entity->setAccess($key, $value);
181: }
182: }
183: $errors = $this->_validate($data, $options, true);
184:
185: $options['isMerge'] = false;
186: $propertyMap = $this->_buildPropertyMap($data, $options);
187: $properties = [];
188: foreach ($data as $key => $value) {
189: if (!empty($errors[$key])) {
190: if ($entity instanceof InvalidPropertyInterface) {
191: $entity->setInvalidField($key, $value);
192: }
193: continue;
194: }
195:
196: if ($value === '' && in_array($key, $primaryKey, true)) {
197:
198: continue;
199: }
200: if (isset($propertyMap[$key])) {
201: $properties[$key] = $propertyMap[$key]($value, $entity);
202: } else {
203: $properties[$key] = $value;
204: }
205: }
206:
207: if (isset($options['fields'])) {
208: foreach ((array)$options['fields'] as $field) {
209: if (array_key_exists($field, $properties)) {
210: $entity->set($field, $properties[$field]);
211: }
212: }
213: } else {
214: $entity->set($properties);
215: }
216:
217:
218:
219: foreach ($properties as $field => $value) {
220: if ($value instanceof EntityInterface) {
221: $entity->setDirty($field, $value->isDirty());
222: }
223: }
224:
225: $entity->setErrors($errors);
226:
227: return $entity;
228: }
229:
230: 231: 232: 233: 234: 235: 236: 237: 238:
239: protected function _validate($data, $options, $isNew)
240: {
241: if (!$options['validate']) {
242: return [];
243: }
244:
245: $validator = null;
246: if ($options['validate'] === true) {
247: $validator = $this->_table->getValidator();
248: } elseif (is_string($options['validate'])) {
249: $validator = $this->_table->getValidator($options['validate']);
250: } elseif (is_object($options['validate'])) {
251:
252: $validator = $options['validate'];
253: }
254:
255: if ($validator === null) {
256: throw new RuntimeException(
257: sprintf('validate must be a boolean, a string or an object. Got %s.', getTypeName($options['validate']))
258: );
259: }
260:
261: return $validator->errors($data, $isNew);
262: }
263:
264: 265: 266: 267: 268: 269: 270:
271: protected function _prepareDataAndOptions($data, $options)
272: {
273: $options += ['validate' => true];
274:
275: if (!isset($options['fields']) && isset($options['fieldList'])) {
276: deprecationWarning(
277: 'The `fieldList` option for marshalling is deprecated. Use the `fields` option instead.'
278: );
279: $options['fields'] = $options['fieldList'];
280: unset($options['fieldList']);
281: }
282:
283: $tableName = $this->_table->getAlias();
284: if (isset($data[$tableName])) {
285: $data += $data[$tableName];
286: unset($data[$tableName]);
287: }
288:
289: $data = new ArrayObject($data);
290: $options = new ArrayObject($options);
291: $this->_table->dispatchEvent('Model.beforeMarshal', compact('data', 'options'));
292:
293: return [(array)$data, (array)$options];
294: }
295:
296: 297: 298: 299: 300: 301: 302: 303:
304: protected function _marshalAssociation($assoc, $value, $options)
305: {
306: if (!is_array($value)) {
307: return null;
308: }
309: $targetTable = $assoc->getTarget();
310: $marshaller = $targetTable->marshaller();
311: $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE];
312: if (in_array($assoc->type(), $types)) {
313: return $marshaller->one($value, (array)$options);
314: }
315: if ($assoc->type() === Association::ONE_TO_MANY || $assoc->type() === Association::MANY_TO_MANY) {
316: $hasIds = array_key_exists('_ids', $value);
317: $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];
318:
319: if ($hasIds && is_array($value['_ids'])) {
320: return $this->_loadAssociatedByIds($assoc, $value['_ids']);
321: }
322: if ($hasIds || $onlyIds) {
323: return [];
324: }
325: }
326: if ($assoc->type() === Association::MANY_TO_MANY) {
327: return $marshaller->_belongsToMany($assoc, $value, (array)$options);
328: }
329:
330: return $marshaller->many($value, (array)$options);
331: }
332:
333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354:
355: public function many(array $data, array $options = [])
356: {
357: $output = [];
358: foreach ($data as $record) {
359: if (!is_array($record)) {
360: continue;
361: }
362: $output[] = $this->one($record, $options);
363: }
364:
365: return $output;
366: }
367:
368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381:
382: protected function _belongsToMany(BelongsToMany $assoc, array $data, $options = [])
383: {
384: $associated = isset($options['associated']) ? $options['associated'] : [];
385: $forceNew = isset($options['forceNew']) ? $options['forceNew'] : false;
386:
387: $data = array_values($data);
388:
389: $target = $assoc->getTarget();
390: $primaryKey = array_flip((array)$target->getPrimaryKey());
391: $records = $conditions = [];
392: $primaryCount = count($primaryKey);
393: $conditions = [];
394:
395: foreach ($data as $i => $row) {
396: if (!is_array($row)) {
397: continue;
398: }
399: if (array_intersect_key($primaryKey, $row) === $primaryKey) {
400: $keys = array_intersect_key($row, $primaryKey);
401: if (count($keys) === $primaryCount) {
402: $rowConditions = [];
403: foreach ($keys as $key => $value) {
404: $rowConditions[][$target->aliasField($key)] = $value;
405: }
406:
407: if ($forceNew && !$target->exists($rowConditions)) {
408: $records[$i] = $this->one($row, $options);
409: }
410:
411: $conditions = array_merge($conditions, $rowConditions);
412: }
413: } else {
414: $records[$i] = $this->one($row, $options);
415: }
416: }
417:
418: if (!empty($conditions)) {
419: $query = $target->find();
420: $query->andWhere(function ($exp) use ($conditions) {
421:
422: return $exp->or_($conditions);
423: });
424:
425: $keyFields = array_keys($primaryKey);
426:
427: $existing = [];
428: foreach ($query as $row) {
429: $k = implode(';', $row->extract($keyFields));
430: $existing[$k] = $row;
431: }
432:
433: foreach ($data as $i => $row) {
434: $key = [];
435: foreach ($keyFields as $k) {
436: if (isset($row[$k])) {
437: $key[] = $row[$k];
438: }
439: }
440: $key = implode(';', $key);
441:
442:
443: if (isset($existing[$key])) {
444: $records[$i] = $this->merge($existing[$key], $data[$i], $options);
445: }
446: }
447: }
448:
449: $jointMarshaller = $assoc->junction()->marshaller();
450:
451: $nested = [];
452: if (isset($associated['_joinData'])) {
453: $nested = (array)$associated['_joinData'];
454: }
455:
456: foreach ($records as $i => $record) {
457:
458: if (isset($data[$i]['_joinData'])) {
459: $joinData = $jointMarshaller->one($data[$i]['_joinData'], $nested);
460: $record->set('_joinData', $joinData);
461: }
462: }
463:
464: return $records;
465: }
466:
467: 468: 469: 470: 471: 472: 473:
474: protected function _loadAssociatedByIds($assoc, $ids)
475: {
476: if (empty($ids)) {
477: return [];
478: }
479:
480: $target = $assoc->getTarget();
481: $primaryKey = (array)$target->getPrimaryKey();
482: $multi = count($primaryKey) > 1;
483: $primaryKey = array_map([$target, 'aliasField'], $primaryKey);
484:
485: if ($multi) {
486: $first = current($ids);
487: if (!is_array($first) || count($first) !== count($primaryKey)) {
488: return [];
489: }
490: $filter = new TupleComparison($primaryKey, $ids, [], 'IN');
491: } else {
492: $filter = [$primaryKey[0] . ' IN' => $ids];
493: }
494:
495: return $target->find()->where($filter)->toArray();
496: }
497:
498: 499: 500: 501: 502: 503: 504: 505:
506: protected function _loadBelongsToMany($assoc, $ids)
507: {
508: deprecationWarning(
509: 'Marshaller::_loadBelongsToMany() is deprecated. Use _loadAssociatedByIds() instead.'
510: );
511:
512: return $this->_loadAssociatedByIds($assoc, $ids);
513: }
514:
515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552:
553: public function merge(EntityInterface $entity, array $data, array $options = [])
554: {
555: list($data, $options) = $this->_prepareDataAndOptions($data, $options);
556:
557: $isNew = $entity->isNew();
558: $keys = [];
559:
560: if (!$isNew) {
561: $keys = $entity->extract((array)$this->_table->getPrimaryKey());
562: }
563:
564: if (isset($options['accessibleFields'])) {
565: foreach ((array)$options['accessibleFields'] as $key => $value) {
566: $entity->setAccess($key, $value);
567: }
568: }
569:
570: $errors = $this->_validate($data + $keys, $options, $isNew);
571: $options['isMerge'] = true;
572: $propertyMap = $this->_buildPropertyMap($data, $options);
573: $properties = [];
574: foreach ($data as $key => $value) {
575: if (!empty($errors[$key])) {
576: if ($entity instanceof InvalidPropertyInterface) {
577: $entity->setInvalidField($key, $value);
578: }
579: continue;
580: }
581: $original = $entity->get($key);
582:
583: if (isset($propertyMap[$key])) {
584: $value = $propertyMap[$key]($value, $entity);
585:
586:
587:
588:
589:
590: if ((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);
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: 628: 629: 630: 631: 632: 633: 634: 635: 636: 637: 638: 639: 640: 641: 642: 643: 644: 645: 646: 647: 648: 649: 650: 651: 652: 653: 654: 655: 656: 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: 731: 732: 733: 734: 735: 736: 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)) {
751: return $marshaller->merge($original, $value, (array)$options);
752: }
753: if ($assoc->type() === Association::MANY_TO_MANY) {
754: return $marshaller->_mergeBelongsToMany($original, $assoc, $value, (array)$options);
755: }
756:
757: 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);
769: }
770:
771: 772: 773: 774: 775: 776: 777: 778: 779: 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) && !isset($associated['_joinData'])) {
796: return $this->mergeMany($original, $value, $options);
797: }
798:
799: return $this->_mergeJoinData($original, $assoc, $value, $options);
800: }
801:
802: 803: 804: 805: 806: 807: 808: 809: 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:
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);
836: foreach ($records as $record) {
837: $hash = spl_object_hash($record);
838: $value = $record->get('_joinData');
839:
840:
841: if ($value instanceof EntityInterface) {
842: continue;
843: }
844:
845:
846: if (!is_array($value)) {
847: $record->unsetProperty('_joinData');
848: continue;
849: }
850:
851:
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: