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: use AssociationsNormalizerTrait;
39:
40: 41: 42: 43: 44:
45: protected $_table;
46:
47: 48: 49: 50: 51:
52: public function __construct(Table $table)
53: {
54: $this->_table = $table;
55: }
56:
57: 58: 59: 60: 61: 62: 63: 64:
65: protected function _buildPropertyMap($data, $options)
66: {
67: $map = [];
68: $schema = $this->_table->getSchema();
69:
70:
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:
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:
91:
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:
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: 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: 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:
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:
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:
217:
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: 231: 232: 233: 234: 235: 236: 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:
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: 265: 266: 267: 268: 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: 297: 298: 299: 300: 301: 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: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 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: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 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:
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:
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:
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: 468: 469: 470: 471: 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: 499: 500: 501: 502: 503: 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: 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: 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:
586:
587:
588:
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: 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: 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: 730: 731: 732: 733: 734: 735: 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: 772: 773: 774: 775: 776: 777: 778: 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: 803: 804: 805: 806: 807: 808: 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:
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:
840: if ($value instanceof EntityInterface) {
841: continue;
842: }
843:
844:
845: if (!is_array($value)) {
846: $record->unsetProperty('_joinData');
847: continue;
848: }
849:
850:
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: