1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Sync\Support; |
4: | |
5: | use Salient\Contract\Container\ContainerInterface; |
6: | use Salient\Contract\Core\Entity\Relatable; |
7: | use Salient\Contract\Core\Entity\Treeable; |
8: | use Salient\Contract\Core\Provider\Providable; |
9: | use Salient\Contract\Core\DateFormatterInterface; |
10: | use Salient\Contract\Sync\HydrationPolicy; |
11: | use Salient\Contract\Sync\SyncContextInterface; |
12: | use Salient\Contract\Sync\SyncEntityInterface; |
13: | use Salient\Contract\Sync\SyncProviderInterface; |
14: | use Salient\Core\Facade\Sync; |
15: | use Salient\Core\Introspector; |
16: | use Salient\Core\IntrospectorKeyTargets; |
17: | use Salient\Sync\Reflection\ReflectionSyncProvider; |
18: | use Salient\Sync\SyncUtil; |
19: | use Salient\Utility\Arr; |
20: | use Salient\Utility\Get; |
21: | use Salient\Utility\Regex; |
22: | use Salient\Utility\Str; |
23: | use Closure; |
24: | use LogicException; |
25: | |
26: | |
27: | |
28: | |
29: | |
30: | |
31: | |
32: | |
33: | final class SyncIntrospector extends Introspector |
34: | { |
35: | private const ID_KEY = 0; |
36: | private const PARENT_KEY = 1; |
37: | private const CHILDREN_KEY = 2; |
38: | private const ID_PROPERTY = 'Id'; |
39: | |
40: | |
41: | protected $_Class; |
42: | |
43: | |
44: | |
45: | |
46: | |
47: | |
48: | |
49: | public static function getService(ContainerInterface $container, string $service) |
50: | { |
51: | return new static( |
52: | $service, |
53: | $container->getName($service), |
54: | SyncProviderInterface::class, |
55: | SyncEntityInterface::class, |
56: | SyncContextInterface::class, |
57: | ); |
58: | } |
59: | |
60: | |
61: | |
62: | |
63: | |
64: | |
65: | |
66: | public static function get(string $class) |
67: | { |
68: | return new static( |
69: | $class, |
70: | $class, |
71: | SyncProviderInterface::class, |
72: | SyncEntityInterface::class, |
73: | SyncContextInterface::class, |
74: | ); |
75: | } |
76: | |
77: | |
78: | |
79: | |
80: | |
81: | protected function getIntrospectionClass(string $class): SyncIntrospectionClass |
82: | { |
83: | return new SyncIntrospectionClass($class); |
84: | } |
85: | |
86: | |
87: | |
88: | |
89: | |
90: | |
91: | |
92: | |
93: | |
94: | |
95: | |
96: | |
97: | public function getCreateSyncEntityFromClosure(bool $strict = false): Closure |
98: | { |
99: | $closure = |
100: | $this->_Class->CreateSyncEntityFromClosures[(int) $strict] |
101: | ?? null; |
102: | |
103: | if ($closure) { |
104: | return $closure; |
105: | } |
106: | |
107: | $closure = |
108: | function ( |
109: | array $array, |
110: | SyncProviderInterface $provider, |
111: | SyncContextInterface $context |
112: | ) use ($strict) { |
113: | $keys = array_keys($array); |
114: | $closure = $this->getCreateSyncEntityFromSignatureClosure($keys, $strict); |
115: | return $closure($array, $provider, $context); |
116: | }; |
117: | |
118: | $this->_Class->CreateSyncEntityFromClosures[(int) $strict] = $closure; |
119: | |
120: | return $closure; |
121: | } |
122: | |
123: | |
124: | |
125: | |
126: | |
127: | |
128: | |
129: | |
130: | |
131: | |
132: | public function getCreateSyncEntityFromSignatureClosure(array $keys, bool $strict = false): Closure |
133: | { |
134: | $sig = implode("\0", $keys); |
135: | |
136: | $closure = |
137: | $this->_Class->CreateSyncEntityFromSignatureClosures[$sig][(int) $strict] |
138: | ?? null; |
139: | |
140: | if (!$closure) { |
141: | $closure = $this->_getCreateFromSignatureSyncClosure($keys, $strict); |
142: | $this->_Class->CreateSyncEntityFromSignatureClosures[$sig][(int) $strict] = $closure; |
143: | |
144: | |
145: | |
146: | if ($strict) { |
147: | $this->_Class->CreateSyncEntityFromSignatureClosures[$sig][(int) false] = $closure; |
148: | } |
149: | } |
150: | |
151: | |
152: | $service = $this->_Service; |
153: | |
154: | return |
155: | static function ( |
156: | array $array, |
157: | SyncProviderInterface $provider, |
158: | SyncContextInterface $context |
159: | ) use ($closure, $service) { |
160: | return $closure( |
161: | $array, |
162: | $service, |
163: | $context->getContainer(), |
164: | $provider, |
165: | $context, |
166: | $provider->getDateFormatter(), |
167: | $context->getParent(), |
168: | ); |
169: | }; |
170: | } |
171: | |
172: | |
173: | |
174: | |
175: | |
176: | |
177: | |
178: | |
179: | |
180: | |
181: | |
182: | |
183: | |
184: | |
185: | |
186: | public function getMagicSyncOperationClosure(string $method, SyncProviderInterface $provider): ?Closure |
187: | { |
188: | if (!$this->_Class->IsSyncProvider) { |
189: | return null; |
190: | } |
191: | |
192: | $method = Str::lower($method); |
193: | $closure = $this->_Class->MagicSyncOperationClosures[$method] ?? false; |
194: | |
195: | if ($closure === false) { |
196: | |
197: | $class = $this->_Class->Class; |
198: | $operation = (new ReflectionSyncProvider($class)) |
199: | ->getSyncOperationMagicMethods()[$method] ?? null; |
200: | if ($operation) { |
201: | $entity = $operation[1]; |
202: | $operation = $operation[0]; |
203: | $closure = |
204: | function (SyncContextInterface $ctx, ...$args) use ($entity, $operation) { |
205: | |
206: | return $this->with($entity, $ctx)->run($operation, ...$args); |
207: | }; |
208: | } |
209: | $this->_Class->MagicSyncOperationClosures[$method] = $closure ?: null; |
210: | } |
211: | |
212: | return $closure ? $closure->bindTo($provider) : null; |
213: | } |
214: | |
215: | |
216: | |
217: | |
218: | |
219: | private function _getCreateFromSignatureSyncClosure(array $keys, bool $strict = false): Closure |
220: | { |
221: | $sig = implode("\0", $keys); |
222: | |
223: | $closure = |
224: | $this->_Class->CreateFromSignatureSyncClosures[$sig] |
225: | ?? null; |
226: | |
227: | if ($closure) { |
228: | return $closure; |
229: | } |
230: | |
231: | $targets = $this->getKeyTargets($keys, true, $strict); |
232: | $constructor = $this->_getConstructor($targets); |
233: | $updater = $this->_getUpdater($targets); |
234: | $resolver = $this->_getResolver($targets); |
235: | $idKey = $targets->CustomKeys[self::ID_KEY] ?? null; |
236: | |
237: | $updateTargets = $this->getKeyTargets($keys, false, $strict); |
238: | $existingUpdater = $this->_getUpdater($updateTargets); |
239: | $existingResolver = $this->_getResolver($updateTargets); |
240: | |
241: | if ($idKey === null) { |
242: | $closure = static function ( |
243: | array $array, |
244: | ?string $service, |
245: | ContainerInterface $container, |
246: | ?SyncProviderInterface $provider, |
247: | ?SyncContextInterface $context, |
248: | ?DateFormatterInterface $dateFormatter, |
249: | ?Treeable $parent |
250: | ) use ($constructor, $updater, $resolver) { |
251: | |
252: | $obj = $constructor($array, $service, $container); |
253: | $obj = $updater($array, $obj, $container, $provider, $context, $dateFormatter, $parent); |
254: | $obj = $resolver($array, $service, $obj, $provider, $context); |
255: | if ($obj instanceof Providable) { |
256: | $obj->postLoad(); |
257: | } |
258: | return $obj; |
259: | }; |
260: | } else { |
261: | |
262: | $entityType = $this->_Class->Class; |
263: | $closure = static function ( |
264: | array $array, |
265: | ?string $service, |
266: | ContainerInterface $container, |
267: | ?SyncProviderInterface $provider, |
268: | ?SyncContextInterface $context, |
269: | ?DateFormatterInterface $dateFormatter, |
270: | ?Treeable $parent |
271: | ) use ( |
272: | $constructor, |
273: | $updater, |
274: | $resolver, |
275: | $existingUpdater, |
276: | $existingResolver, |
277: | $idKey, |
278: | $entityType |
279: | ) { |
280: | $id = $array[$idKey]; |
281: | |
282: | |
283: | if ($id === null || !$provider) { |
284: | $obj = $constructor($array, $service, $container); |
285: | $obj = $updater($array, $obj, $container, $provider, $context, $dateFormatter, $parent); |
286: | $obj = $resolver($array, $service, $obj, $provider, $context); |
287: | if ($obj instanceof Providable) { |
288: | $obj->postLoad(); |
289: | } |
290: | return $obj; |
291: | } |
292: | |
293: | $store = $provider->getStore()->registerEntityType($service ?? $entityType); |
294: | $providerId = $provider->getProviderId(); |
295: | $obj = $store->getEntity($providerId, $service ?? $entityType, $id, $context->getOffline()); |
296: | |
297: | if ($obj) { |
298: | $obj = $existingUpdater($array, $obj, $container, $provider, $context, $dateFormatter, $parent); |
299: | $obj = $existingResolver($array, $service, $obj, $provider, $context); |
300: | if ($obj instanceof Providable) { |
301: | $obj->postLoad(); |
302: | } |
303: | return $obj; |
304: | } |
305: | |
306: | $obj = $constructor($array, $service, $container); |
307: | |
308: | $obj = $updater($array, $obj, $container, $provider, $context, $dateFormatter, $parent); |
309: | $store->setEntity($providerId, $service ?? $entityType, $id, $obj); |
310: | $obj = $resolver($array, $service, $obj, $provider, $context); |
311: | if ($obj instanceof Providable) { |
312: | $obj->postLoad(); |
313: | } |
314: | return $obj; |
315: | }; |
316: | } |
317: | |
318: | $this->_Class->CreateFromSignatureSyncClosures[$sig] = $closure; |
319: | return $closure; |
320: | } |
321: | |
322: | protected function getKeyTargets( |
323: | array $keys, |
324: | bool $forNewInstance, |
325: | bool $strict, |
326: | bool $normalised = false, |
327: | array $customKeys = [], |
328: | array $keyClosures = [] |
329: | ): IntrospectorKeyTargets { |
330: | |
331: | $keys = $this->_Class->Normaliser |
332: | ? Arr::combine(array_map($this->_Class->CarefulNormaliser, $keys), $keys) |
333: | : Arr::combine($keys, $keys); |
334: | |
335: | foreach ([ |
336: | self::ID_KEY => self::ID_PROPERTY, |
337: | self::PARENT_KEY => $this->_Class->ParentProperty, |
338: | self::CHILDREN_KEY => $this->_Class->ChildrenProperty, |
339: | ] as $key => $property) { |
340: | if ($property === null) { |
341: | continue; |
342: | } |
343: | |
344: | if ($key === self::ID_KEY) { |
345: | $property = $this->_Class->Normaliser |
346: | ? ($this->_Class->CarefulNormaliser)($property) |
347: | : $property; |
348: | } |
349: | |
350: | |
351: | |
352: | $customKey = $keys[$property] ?? null; |
353: | if ($customKey !== null) { |
354: | $customKeys[$key] = $customKey; |
355: | } |
356: | } |
357: | |
358: | $idKey = $customKeys[self::ID_KEY] ?? null; |
359: | |
360: | |
361: | |
362: | if ($this->_Class->IsSyncEntity |
363: | && ($this->_Class->OneToOneRelationships |
364: | || $this->_Class->OneToManyRelationships)) { |
365: | $missing = null; |
366: | foreach ([ |
367: | $this->_Class->OneToOneRelationships, |
368: | $this->_Class->OneToManyRelationships, |
369: | ] as $list => $relationships) { |
370: | if ($list) { |
371: | $missing = array_diff_key($relationships, $keys); |
372: | } |
373: | $relationships = array_intersect_key($relationships, $keys); |
374: | |
375: | if (!$relationships) { |
376: | continue; |
377: | } |
378: | |
379: | foreach ($relationships as $match => $relationship) { |
380: | if (!is_a($relationship, SyncEntityInterface::class, true)) { |
381: | throw new LogicException(sprintf( |
382: | '%s does not implement %s', |
383: | $relationship, |
384: | SyncEntityInterface::class, |
385: | )); |
386: | } |
387: | |
388: | $key = $keys[$match]; |
389: | $list = (bool) $list; |
390: | $isParent = $match === $this->_Class->ParentProperty; |
391: | $isChildren = $match === $this->_Class->ChildrenProperty; |
392: | |
393: | |
394: | $property = $this->_Class->Properties[$match] ?? $match; |
395: | $keyClosures[$match] = $this->getRelationshipClosure( |
396: | $key, |
397: | $list, |
398: | $relationship, |
399: | $property, |
400: | $isParent, |
401: | $isChildren, |
402: | ); |
403: | } |
404: | } |
405: | |
406: | |
407: | if ($missing && $idKey !== null && $forNewInstance) { |
408: | foreach ($missing as $key => $relationship) { |
409: | if (!is_a($relationship, SyncEntityInterface::class, true)) { |
410: | throw new LogicException(sprintf( |
411: | '%s does not implement %s', |
412: | $relationship, |
413: | SyncEntityInterface::class, |
414: | )); |
415: | } |
416: | |
417: | $isChildren = $key === $this->_Class->ChildrenProperty; |
418: | $filter = |
419: | $isChildren |
420: | ? $this->_Class->ParentProperty |
421: | : null; |
422: | $property = $this->_Class->Properties[$key] ?? $key; |
423: | $keyClosures[$key] = $this->getHydrator( |
424: | $idKey, |
425: | $relationship, |
426: | $property, |
427: | $filter, |
428: | $isChildren, |
429: | ); |
430: | } |
431: | } |
432: | } |
433: | |
434: | |
435: | |
436: | $unclaimed = array_diff_key( |
437: | $keys, |
438: | $this->_Class->Parameters, |
439: | array_flip($this->_Class->NormalisedKeys), |
440: | ); |
441: | |
442: | if (!$unclaimed) { |
443: | return parent::getKeyTargets( |
444: | $keys, |
445: | $forNewInstance, |
446: | $strict, |
447: | true, |
448: | $customKeys, |
449: | $keyClosures, |
450: | ); |
451: | } |
452: | |
453: | |
454: | |
455: | foreach ($unclaimed as $normalisedKey => $key) { |
456: | if (!Regex::match('/^(.+)(?:_|\b|(?<=[[:lower:]])(?=[[:upper:]]))id(s?)$/i', $key, $matches)) { |
457: | continue; |
458: | } |
459: | |
460: | $match = $this->_Class->Normaliser |
461: | ? ($this->_Class->CarefulNormaliser)($matches[1]) |
462: | : $matches[1]; |
463: | |
464: | |
465: | if (isset($keys[$match]) || isset($keyClosures[$match])) { |
466: | continue; |
467: | } |
468: | |
469: | if (!in_array($match, $this->_Class->NormalisedKeys, true)) { |
470: | continue; |
471: | } |
472: | |
473: | |
474: | |
475: | $list = $matches[2] !== ''; |
476: | |
477: | |
478: | |
479: | $relationship = |
480: | $this->_Class->IsSyncEntity && $this->_Class->IsRelatable |
481: | ? ($list |
482: | ? ($this->_Class->OneToManyRelationships[$match] ?? null) |
483: | : ($this->_Class->OneToOneRelationships[$match] ?? null)) |
484: | : null; |
485: | |
486: | if ($relationship !== null |
487: | && !is_a($relationship, SyncEntityInterface::class, true)) { |
488: | throw new LogicException(sprintf( |
489: | '%s does not implement %s', |
490: | $relationship, |
491: | SyncEntityInterface::class, |
492: | )); |
493: | } |
494: | |
495: | |
496: | |
497: | $property = $this->_Class->Properties[$match] ?? $match; |
498: | $isParent = $match === $this->_Class->ParentProperty; |
499: | $isChildren = $match === $this->_Class->ChildrenProperty; |
500: | $keyClosures[$match] = $this->getRelationshipClosure( |
501: | $key, |
502: | $list, |
503: | $relationship, |
504: | $property, |
505: | $isParent, |
506: | $isChildren, |
507: | ); |
508: | |
509: | |
510: | unset($keys[$normalisedKey]); |
511: | } |
512: | |
513: | return parent::getKeyTargets( |
514: | $keys, |
515: | $forNewInstance, |
516: | $strict, |
517: | true, |
518: | $customKeys, |
519: | $keyClosures, |
520: | ); |
521: | } |
522: | |
523: | |
524: | |
525: | |
526: | |
527: | private function getRelationshipClosure( |
528: | string $key, |
529: | bool $isList, |
530: | ?string $relationship, |
531: | string $property, |
532: | bool $isParent, |
533: | bool $isChildren |
534: | ): Closure { |
535: | if ($relationship === null) { |
536: | return |
537: | static function ( |
538: | array $data, |
539: | ?string $service, |
540: | $entity |
541: | ) use ($key, $property): void { |
542: | $entity->{$property} = $data[$key]; |
543: | }; |
544: | } |
545: | |
546: | return |
547: | static function ( |
548: | array $data, |
549: | ?string $service, |
550: | $entity, |
551: | ?SyncProviderInterface $provider, |
552: | ?SyncContextInterface $context |
553: | ) use ( |
554: | $key, |
555: | $isList, |
556: | $relationship, |
557: | $property, |
558: | $isParent, |
559: | $isChildren |
560: | ): void { |
561: | if ( |
562: | $data[$key] === null |
563: | || (Arr::isList($data[$key]) xor $isList) |
564: | || !$entity instanceof SyncEntityInterface |
565: | || !$provider instanceof SyncProviderInterface |
566: | || !$context instanceof SyncContextInterface |
567: | ) { |
568: | $entity->{$property} = $data[$key]; |
569: | return; |
570: | } |
571: | |
572: | if ($isList) { |
573: | if (is_scalar($data[$key][0])) { |
574: | if (!$isChildren) { |
575: | DeferredEntity::deferList( |
576: | $provider, |
577: | $context->pushEntity($entity, true), |
578: | $relationship, |
579: | $data[$key], |
580: | $entity->{$property}, |
581: | ); |
582: | return; |
583: | } |
584: | |
585: | |
586: | |
587: | DeferredEntity::deferList( |
588: | $provider, |
589: | $context->pushEntity($entity, true), |
590: | $relationship, |
591: | $data[$key], |
592: | $replace, |
593: | static function ($child) use ($entity): void { |
594: | |
595: | $entity->addChild($child); |
596: | }, |
597: | ); |
598: | return; |
599: | } |
600: | |
601: | $entities = |
602: | $relationship::provideMultiple( |
603: | $data[$key], |
604: | $provider, |
605: | $context->getConformity(), |
606: | $context->pushEntity($entity), |
607: | )->toArray(); |
608: | |
609: | if (!$isChildren) { |
610: | $entity->{$property} = $entities; |
611: | return; |
612: | } |
613: | |
614: | |
615: | foreach ($entities as $child) { |
616: | |
617: | $entity->addChild($child); |
618: | } |
619: | return; |
620: | } |
621: | |
622: | if (is_scalar($data[$key])) { |
623: | if (!$isParent) { |
624: | DeferredEntity::defer( |
625: | $provider, |
626: | $context->pushEntity($entity, true), |
627: | $relationship, |
628: | $data[$key], |
629: | $entity->{$property}, |
630: | ); |
631: | return; |
632: | } |
633: | |
634: | |
635: | |
636: | DeferredEntity::defer( |
637: | $provider, |
638: | $context->pushEntity($entity, true), |
639: | $relationship, |
640: | $data[$key], |
641: | $replace, |
642: | static function ($parent) use ($entity): void { |
643: | |
644: | $entity->setParent($parent); |
645: | }, |
646: | ); |
647: | return; |
648: | } |
649: | |
650: | $related = |
651: | $relationship::provide( |
652: | $data[$key], |
653: | $provider, |
654: | $context->pushEntity($entity), |
655: | ); |
656: | |
657: | if (!$isParent) { |
658: | $entity->{$property} = $related; |
659: | return; |
660: | } |
661: | |
662: | |
663: | |
664: | |
665: | |
666: | $entity->setParent($related); |
667: | }; |
668: | } |
669: | |
670: | |
671: | |
672: | |
673: | |
674: | private function getHydrator( |
675: | string $idKey, |
676: | string $relationship, |
677: | string $property, |
678: | ?string $filter, |
679: | bool $isChildren |
680: | ): Closure { |
681: | $entityType = $this->_Class->Class; |
682: | $entityProvider = null; |
683: | |
684: | return |
685: | static function ( |
686: | array $data, |
687: | ?string $service, |
688: | $entity, |
689: | ?SyncProviderInterface $provider, |
690: | ?SyncContextInterface $context |
691: | ) use ( |
692: | $idKey, |
693: | $relationship, |
694: | $property, |
695: | $filter, |
696: | $isChildren, |
697: | $entityType, |
698: | &$entityProvider |
699: | ): void { |
700: | if ( |
701: | !$context instanceof SyncContextInterface |
702: | || !$provider instanceof SyncProviderInterface |
703: | || !is_a($provider, $entityProvider ??= SyncUtil::getEntityTypeProvider($relationship, SyncUtil::getStore($context->getContainer()))) |
704: | || $data[$idKey] === null |
705: | ) { |
706: | return; |
707: | } |
708: | |
709: | $policy = $context->getHydrationPolicy($relationship); |
710: | if ($policy === HydrationPolicy::SUPPRESS) { |
711: | return; |
712: | } |
713: | |
714: | if ($filter !== null) { |
715: | $filter = [$filter => $data[$idKey]]; |
716: | } |
717: | |
718: | if (!$isChildren) { |
719: | DeferredRelationship::defer( |
720: | $provider, |
721: | $context->pushEntity($entity, true), |
722: | $relationship, |
723: | $service ?? $entityType, |
724: | $property, |
725: | $data[$idKey], |
726: | $filter, |
727: | $entity->{$property}, |
728: | ); |
729: | return; |
730: | } |
731: | |
732: | |
733: | |
734: | DeferredRelationship::defer( |
735: | $provider, |
736: | $context->pushEntity($entity, true), |
737: | $relationship, |
738: | $service ?? $entityType, |
739: | $property, |
740: | $data[$idKey], |
741: | $filter, |
742: | $replace, |
743: | static function ($entities) use ($entity, $property): void { |
744: | if (!$entities) { |
745: | $entity->{$property} = []; |
746: | return; |
747: | } |
748: | foreach ($entities as $child) { |
749: | |
750: | $entity->addChild($child); |
751: | } |
752: | }, |
753: | ); |
754: | }; |
755: | } |
756: | } |
757: | |