1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Sync\Support; |
4: | |
5: | use Salient\Contract\Container\ContainerInterface; |
6: | use Salient\Contract\Core\DateFormatterInterface; |
7: | use Salient\Contract\Core\Providable; |
8: | use Salient\Contract\Core\Relatable; |
9: | use Salient\Contract\Core\Treeable; |
10: | use Salient\Contract\Sync\HydrationPolicy; |
11: | use Salient\Contract\Sync\SyncContextInterface; |
12: | use Salient\Contract\Sync\SyncEntityInterface; |
13: | use Salient\Contract\Sync\SyncOperation; |
14: | use Salient\Contract\Sync\SyncProviderInterface; |
15: | use Salient\Contract\Sync\SyncStoreInterface; |
16: | use Salient\Core\Facade\Sync; |
17: | use Salient\Core\Introspector; |
18: | use Salient\Core\IntrospectorKeyTargets; |
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: | |
34: | |
35: | |
36: | final class SyncIntrospector extends Introspector |
37: | { |
38: | private const ID_KEY = 0; |
39: | private const PARENT_KEY = 1; |
40: | private const CHILDREN_KEY = 2; |
41: | private const ID_PROPERTY = 'Id'; |
42: | |
43: | |
44: | protected $_Class; |
45: | |
46: | |
47: | |
48: | |
49: | |
50: | |
51: | |
52: | |
53: | public static function isListOperation($operation): bool |
54: | { |
55: | return [ |
56: | SyncOperation::CREATE_LIST => true, |
57: | SyncOperation::READ_LIST => true, |
58: | SyncOperation::UPDATE_LIST => true, |
59: | SyncOperation::DELETE_LIST => true, |
60: | ][$operation] ?? false; |
61: | } |
62: | |
63: | |
64: | |
65: | |
66: | |
67: | |
68: | |
69: | public static function isReadOperation($operation): bool |
70: | { |
71: | return [ |
72: | SyncOperation::READ => true, |
73: | SyncOperation::READ_LIST => true, |
74: | ][$operation] ?? false; |
75: | } |
76: | |
77: | |
78: | |
79: | |
80: | |
81: | |
82: | |
83: | |
84: | public static function isWriteOperation($operation): bool |
85: | { |
86: | return [ |
87: | SyncOperation::CREATE => true, |
88: | SyncOperation::UPDATE => true, |
89: | SyncOperation::DELETE => true, |
90: | SyncOperation::CREATE_LIST => true, |
91: | SyncOperation::UPDATE_LIST => true, |
92: | SyncOperation::DELETE_LIST => true, |
93: | ][$operation] ?? false; |
94: | } |
95: | |
96: | |
97: | |
98: | |
99: | |
100: | |
101: | |
102: | public static function entityToProvider(string $entity, ?ContainerInterface $container = null): string |
103: | { |
104: | if (($store = self::maybeGetStore($container)) |
105: | && ($resolver = $store->getClassResolver($entity))) { |
106: | return $resolver->entityToProvider($entity); |
107: | } |
108: | |
109: | return sprintf( |
110: | '%s\Provider\%sProvider', |
111: | Get::namespace($entity), |
112: | Get::basename($entity) |
113: | ); |
114: | } |
115: | |
116: | |
117: | |
118: | |
119: | |
120: | |
121: | |
122: | public static function providerToEntity(string $provider, ?ContainerInterface $container = null): array |
123: | { |
124: | if (($store = self::maybeGetStore($container)) |
125: | && ($resolver = $store->getClassResolver($provider))) { |
126: | return $resolver->providerToEntity($provider); |
127: | } |
128: | |
129: | if (Regex::match( |
130: | '/^(?<namespace>' . Regex::PHP_TYPE . '\\\\)?Provider\\\\' |
131: | . '(?<class>' . Regex::PHP_IDENTIFIER . ')?Provider$/U', |
132: | $provider, |
133: | $matches |
134: | )) { |
135: | return [$matches['namespace'] . $matches['class']]; |
136: | } |
137: | |
138: | return []; |
139: | } |
140: | |
141: | private static function maybeGetStore(?ContainerInterface $container = null): ?SyncStoreInterface |
142: | { |
143: | if ($container && $container->hasInstance(SyncStoreInterface::class)) { |
144: | return $container->get(SyncStoreInterface::class); |
145: | } |
146: | if (Sync::isLoaded()) { |
147: | return Sync::getInstance(); |
148: | } |
149: | return null; |
150: | } |
151: | |
152: | |
153: | |
154: | |
155: | |
156: | |
157: | |
158: | public static function getService(ContainerInterface $container, string $service) |
159: | { |
160: | return new static( |
161: | $service, |
162: | $container->getName($service), |
163: | SyncProviderInterface::class, |
164: | SyncEntityInterface::class, |
165: | SyncContextInterface::class, |
166: | ); |
167: | } |
168: | |
169: | |
170: | |
171: | |
172: | |
173: | |
174: | |
175: | public static function get(string $class) |
176: | { |
177: | return new static( |
178: | $class, |
179: | $class, |
180: | SyncProviderInterface::class, |
181: | SyncEntityInterface::class, |
182: | SyncContextInterface::class, |
183: | ); |
184: | } |
185: | |
186: | |
187: | |
188: | |
189: | |
190: | protected function getIntrospectionClass(string $class): SyncIntrospectionClass |
191: | { |
192: | return new SyncIntrospectionClass($class); |
193: | } |
194: | |
195: | |
196: | |
197: | |
198: | |
199: | |
200: | |
201: | public function getSyncProviderInterfaces(): array |
202: | { |
203: | $this->assertIsProvider(); |
204: | |
205: | return $this->_Class->SyncProviderInterfaces; |
206: | } |
207: | |
208: | |
209: | |
210: | |
211: | |
212: | |
213: | public function getSyncProviderEntities(): array |
214: | { |
215: | $this->assertIsProvider(); |
216: | |
217: | return $this->_Class->SyncProviderEntities; |
218: | } |
219: | |
220: | |
221: | |
222: | |
223: | |
224: | |
225: | |
226: | public function getSyncProviderEntityBasenames(): array |
227: | { |
228: | $this->assertIsProvider(); |
229: | |
230: | return $this->_Class->SyncProviderEntityBasenames; |
231: | } |
232: | |
233: | |
234: | |
235: | |
236: | |
237: | |
238: | |
239: | |
240: | |
241: | |
242: | |
243: | |
244: | public function getCreateSyncEntityFromClosure(bool $strict = false): Closure |
245: | { |
246: | $closure = |
247: | $this->_Class->CreateSyncEntityFromClosures[(int) $strict] |
248: | ?? null; |
249: | |
250: | if ($closure) { |
251: | return $closure; |
252: | } |
253: | |
254: | $closure = |
255: | function ( |
256: | array $array, |
257: | SyncProviderInterface $provider, |
258: | SyncContextInterface $context |
259: | ) use ($strict) { |
260: | $keys = array_keys($array); |
261: | $closure = $this->getCreateSyncEntityFromSignatureClosure($keys, $strict); |
262: | return $closure($array, $provider, $context); |
263: | }; |
264: | |
265: | $this->_Class->CreateSyncEntityFromClosures[(int) $strict] = $closure; |
266: | |
267: | return $closure; |
268: | } |
269: | |
270: | |
271: | |
272: | |
273: | |
274: | |
275: | |
276: | |
277: | |
278: | |
279: | public function getCreateSyncEntityFromSignatureClosure(array $keys, bool $strict = false): Closure |
280: | { |
281: | $sig = implode("\0", $keys); |
282: | |
283: | $closure = |
284: | $this->_Class->CreateSyncEntityFromSignatureClosures[$sig][(int) $strict] |
285: | ?? null; |
286: | |
287: | if (!$closure) { |
288: | $closure = $this->_getCreateFromSignatureSyncClosure($keys, $strict); |
289: | $this->_Class->CreateSyncEntityFromSignatureClosures[$sig][(int) $strict] = $closure; |
290: | |
291: | |
292: | |
293: | if ($strict) { |
294: | $this->_Class->CreateSyncEntityFromSignatureClosures[$sig][(int) false] = $closure; |
295: | } |
296: | } |
297: | |
298: | |
299: | $service = $this->_Service; |
300: | |
301: | return |
302: | static function ( |
303: | array $array, |
304: | SyncProviderInterface $provider, |
305: | SyncContextInterface $context |
306: | ) use ($closure, $service) { |
307: | return $closure( |
308: | $array, |
309: | $service, |
310: | $context->getContainer(), |
311: | $provider, |
312: | $context, |
313: | $provider->getDateFormatter(), |
314: | $context->getParent(), |
315: | ); |
316: | }; |
317: | } |
318: | |
319: | |
320: | |
321: | |
322: | |
323: | |
324: | |
325: | |
326: | |
327: | |
328: | |
329: | |
330: | |
331: | |
332: | |
333: | |
334: | |
335: | public function getDeclaredSyncOperationClosure($operation, $entity, SyncProviderInterface $provider): ?Closure |
336: | { |
337: | if (!$entity instanceof SyncIntrospector) { |
338: | $entity = static::get($entity); |
339: | } |
340: | |
341: | $_entity = $entity->_Class; |
342: | $closure = $this->_Class->DeclaredSyncOperationClosures[$_entity->Class][$operation] ?? false; |
343: | |
344: | |
345: | if ($closure === false) { |
346: | $this->assertIsProvider(); |
347: | |
348: | if (!$_entity->IsSyncEntity) { |
349: | throw new LogicException( |
350: | sprintf('%s does not implement %s', $_entity->Class, SyncEntityInterface::class) |
351: | ); |
352: | } |
353: | |
354: | $method = $this->getSyncOperationMethod($operation, $entity); |
355: | if ($method) { |
356: | $closure = fn(...$args) => $this->$method(...$args); |
357: | } |
358: | $this->_Class->DeclaredSyncOperationClosures[$_entity->Class][$operation] = $closure ?: null; |
359: | } |
360: | |
361: | return $closure ? $closure->bindTo($provider) : null; |
362: | } |
363: | |
364: | |
365: | |
366: | |
367: | |
368: | |
369: | |
370: | |
371: | |
372: | |
373: | |
374: | |
375: | |
376: | |
377: | |
378: | public function getMagicSyncOperationClosure(string $method, SyncProviderInterface $provider): ?Closure |
379: | { |
380: | if (!$this->_Class->IsSyncProvider) { |
381: | return null; |
382: | } |
383: | |
384: | $method = Str::lower($method); |
385: | $closure = $this->_Class->MagicSyncOperationClosures[$method] ?? false; |
386: | |
387: | if ($closure === false) { |
388: | $operation = $this->_Class->SyncOperationMagicMethods[$method] ?? null; |
389: | if ($operation) { |
390: | $entity = $operation[1]; |
391: | $operation = $operation[0]; |
392: | $closure = |
393: | function (SyncContextInterface $ctx, ...$args) use ($entity, $operation) { |
394: | |
395: | return $this->with($entity, $ctx)->run($operation, ...$args); |
396: | }; |
397: | } |
398: | $this->_Class->MagicSyncOperationClosures[$method] = $closure ?: null; |
399: | } |
400: | |
401: | return $closure ? $closure->bindTo($provider) : null; |
402: | } |
403: | |
404: | |
405: | |
406: | |
407: | |
408: | private function _getCreateFromSignatureSyncClosure(array $keys, bool $strict = false): Closure |
409: | { |
410: | $sig = implode("\0", $keys); |
411: | |
412: | $closure = |
413: | $this->_Class->CreateFromSignatureSyncClosures[$sig] |
414: | ?? null; |
415: | |
416: | if ($closure) { |
417: | return $closure; |
418: | } |
419: | |
420: | $targets = $this->getKeyTargets($keys, true, $strict); |
421: | $constructor = $this->_getConstructor($targets); |
422: | $updater = $this->_getUpdater($targets); |
423: | $resolver = $this->_getResolver($targets); |
424: | $idKey = $targets->CustomKeys[self::ID_KEY] ?? null; |
425: | |
426: | $updateTargets = $this->getKeyTargets($keys, false, $strict); |
427: | $existingUpdater = $this->_getUpdater($updateTargets); |
428: | $existingResolver = $this->_getResolver($updateTargets); |
429: | |
430: | if ($idKey === null) { |
431: | $closure = static function ( |
432: | array $array, |
433: | ?string $service, |
434: | ContainerInterface $container, |
435: | ?SyncProviderInterface $provider, |
436: | ?SyncContextInterface $context, |
437: | ?DateFormatterInterface $dateFormatter, |
438: | ?Treeable $parent |
439: | ) use ($constructor, $updater, $resolver) { |
440: | |
441: | $obj = $constructor($array, $service, $container); |
442: | $obj = $updater($array, $obj, $container, $provider, $context, $dateFormatter, $parent); |
443: | $obj = $resolver($array, $service, $obj, $provider, $context); |
444: | if ($obj instanceof Providable) { |
445: | $obj->postLoad(); |
446: | } |
447: | return $obj; |
448: | }; |
449: | } else { |
450: | |
451: | $entityType = $this->_Class->Class; |
452: | $closure = static function ( |
453: | array $array, |
454: | ?string $service, |
455: | ContainerInterface $container, |
456: | ?SyncProviderInterface $provider, |
457: | ?SyncContextInterface $context, |
458: | ?DateFormatterInterface $dateFormatter, |
459: | ?Treeable $parent |
460: | ) use ( |
461: | $constructor, |
462: | $updater, |
463: | $resolver, |
464: | $existingUpdater, |
465: | $existingResolver, |
466: | $idKey, |
467: | $entityType |
468: | ) { |
469: | $id = $array[$idKey]; |
470: | |
471: | |
472: | if ($id === null || !$provider) { |
473: | $obj = $constructor($array, $service, $container); |
474: | $obj = $updater($array, $obj, $container, $provider, $context, $dateFormatter, $parent); |
475: | $obj = $resolver($array, $service, $obj, $provider, $context); |
476: | if ($obj instanceof Providable) { |
477: | $obj->postLoad(); |
478: | } |
479: | return $obj; |
480: | } |
481: | |
482: | $store = $provider->getStore()->registerEntity($service ?? $entityType); |
483: | $providerId = $provider->getProviderId(); |
484: | $obj = $store->getEntity($providerId, $service ?? $entityType, $id, $context->getOffline()); |
485: | |
486: | if ($obj) { |
487: | $obj = $existingUpdater($array, $obj, $container, $provider, $context, $dateFormatter, $parent); |
488: | $obj = $existingResolver($array, $service, $obj, $provider, $context); |
489: | if ($obj instanceof Providable) { |
490: | $obj->postLoad(); |
491: | } |
492: | return $obj; |
493: | } |
494: | |
495: | $obj = $constructor($array, $service, $container); |
496: | |
497: | $obj = $updater($array, $obj, $container, $provider, $context, $dateFormatter, $parent); |
498: | $store->setEntity($providerId, $service ?? $entityType, $id, $obj); |
499: | $obj = $resolver($array, $service, $obj, $provider, $context); |
500: | if ($obj instanceof Providable) { |
501: | $obj->postLoad(); |
502: | } |
503: | return $obj; |
504: | }; |
505: | } |
506: | |
507: | $this->_Class->CreateFromSignatureSyncClosures[$sig] = $closure; |
508: | return $closure; |
509: | } |
510: | |
511: | protected function getKeyTargets( |
512: | array $keys, |
513: | bool $forNewInstance, |
514: | bool $strict, |
515: | bool $normalised = false, |
516: | array $customKeys = [], |
517: | array $keyClosures = [] |
518: | ): IntrospectorKeyTargets { |
519: | |
520: | $keys = |
521: | $this->_Class->Normaliser |
522: | ? array_combine(array_map($this->_Class->CarefulNormaliser, $keys), $keys) |
523: | : array_combine($keys, $keys); |
524: | |
525: | foreach ([ |
526: | self::ID_KEY => self::ID_PROPERTY, |
527: | self::PARENT_KEY => $this->_Class->ParentProperty, |
528: | self::CHILDREN_KEY => $this->_Class->ChildrenProperty, |
529: | ] as $key => $property) { |
530: | if ($property === null) { |
531: | continue; |
532: | } |
533: | |
534: | if ($key === self::ID_KEY) { |
535: | $property = |
536: | $this->_Class->Normaliser |
537: | ? ($this->_Class->CarefulNormaliser)($property) |
538: | : $property; |
539: | } |
540: | |
541: | |
542: | |
543: | $customKey = $keys[$property] ?? null; |
544: | if ($customKey !== null) { |
545: | $customKeys[$key] = $customKey; |
546: | } |
547: | } |
548: | |
549: | $idKey = $customKeys[self::ID_KEY] ?? null; |
550: | |
551: | |
552: | |
553: | if ($this->_Class->IsSyncEntity |
554: | && ($this->_Class->OneToOneRelationships |
555: | || $this->_Class->OneToManyRelationships)) { |
556: | $missing = null; |
557: | foreach ([ |
558: | $this->_Class->OneToOneRelationships, |
559: | $this->_Class->OneToManyRelationships, |
560: | ] as $list => $relationships) { |
561: | if ($list) { |
562: | $missing = array_diff_key($relationships, $keys); |
563: | } |
564: | $relationships = array_intersect_key($relationships, $keys); |
565: | |
566: | if (!$relationships) { |
567: | continue; |
568: | } |
569: | |
570: | foreach ($relationships as $match => $relationship) { |
571: | if (!is_a($relationship, SyncEntityInterface::class, true)) { |
572: | throw new LogicException(sprintf( |
573: | '%s does not implement %s', |
574: | $relationship, |
575: | SyncEntityInterface::class, |
576: | )); |
577: | } |
578: | |
579: | $key = $keys[$match]; |
580: | $list = (bool) $list; |
581: | $isParent = $match === $this->_Class->ParentProperty; |
582: | $isChildren = $match === $this->_Class->ChildrenProperty; |
583: | |
584: | |
585: | $property = $this->_Class->Properties[$match] ?? $match; |
586: | $keyClosures[$match] = $this->getRelationshipClosure( |
587: | $key, |
588: | $list, |
589: | $relationship, |
590: | $property, |
591: | $isParent, |
592: | $isChildren, |
593: | ); |
594: | } |
595: | } |
596: | |
597: | |
598: | if ($missing && $idKey !== null && $forNewInstance) { |
599: | foreach ($missing as $key => $relationship) { |
600: | if (!is_a($relationship, SyncEntityInterface::class, true)) { |
601: | throw new LogicException(sprintf( |
602: | '%s does not implement %s', |
603: | $relationship, |
604: | SyncEntityInterface::class, |
605: | )); |
606: | } |
607: | |
608: | $isChildren = $key === $this->_Class->ChildrenProperty; |
609: | $filter = |
610: | $isChildren |
611: | ? $this->_Class->ParentProperty |
612: | : null; |
613: | $property = $this->_Class->Properties[$key] ?? $key; |
614: | $keyClosures[$key] = $this->getHydrator( |
615: | $idKey, |
616: | $relationship, |
617: | $property, |
618: | $filter, |
619: | $isChildren, |
620: | ); |
621: | } |
622: | } |
623: | } |
624: | |
625: | |
626: | |
627: | $unclaimed = array_diff_key( |
628: | $keys, |
629: | $this->_Class->Parameters, |
630: | array_flip($this->_Class->NormalisedKeys), |
631: | ); |
632: | |
633: | if (!$unclaimed) { |
634: | return parent::getKeyTargets( |
635: | $keys, |
636: | $forNewInstance, |
637: | $strict, |
638: | true, |
639: | $customKeys, |
640: | $keyClosures, |
641: | ); |
642: | } |
643: | |
644: | |
645: | |
646: | foreach ($unclaimed as $normalisedKey => $key) { |
647: | if (!Regex::match('/^(.+)(?:_|\b|(?<=[[:lower:]])(?=[[:upper:]]))id(s?)$/i', $key, $matches)) { |
648: | continue; |
649: | } |
650: | |
651: | $match = |
652: | $this->_Class->Normaliser |
653: | ? ($this->_Class->CarefulNormaliser)($matches[1]) |
654: | : $matches[1]; |
655: | |
656: | |
657: | if (isset($keys[$match]) || isset($keyClosures[$match])) { |
658: | continue; |
659: | } |
660: | |
661: | if (!in_array($match, $this->_Class->NormalisedKeys, true)) { |
662: | continue; |
663: | } |
664: | |
665: | |
666: | |
667: | $list = $matches[2] !== ''; |
668: | |
669: | |
670: | |
671: | $relationship = |
672: | $this->_Class->IsSyncEntity && $this->_Class->IsRelatable |
673: | ? ($list |
674: | ? ($this->_Class->OneToManyRelationships[$match] ?? null) |
675: | : ($this->_Class->OneToOneRelationships[$match] ?? null)) |
676: | : null; |
677: | |
678: | if ($relationship !== null |
679: | && !is_a($relationship, SyncEntityInterface::class, true)) { |
680: | throw new LogicException(sprintf( |
681: | '%s does not implement %s', |
682: | $relationship, |
683: | SyncEntityInterface::class, |
684: | )); |
685: | } |
686: | |
687: | |
688: | |
689: | $property = $this->_Class->Properties[$match] ?? $match; |
690: | $isParent = $match === $this->_Class->ParentProperty; |
691: | $isChildren = $match === $this->_Class->ChildrenProperty; |
692: | $keyClosures[$match] = $this->getRelationshipClosure( |
693: | $key, |
694: | $list, |
695: | $relationship, |
696: | $property, |
697: | $isParent, |
698: | $isChildren, |
699: | ); |
700: | |
701: | |
702: | unset($keys[$normalisedKey]); |
703: | } |
704: | |
705: | return parent::getKeyTargets( |
706: | $keys, |
707: | $forNewInstance, |
708: | $strict, |
709: | true, |
710: | $customKeys, |
711: | $keyClosures, |
712: | ); |
713: | } |
714: | |
715: | |
716: | |
717: | |
718: | |
719: | private function getRelationshipClosure( |
720: | string $key, |
721: | bool $isList, |
722: | ?string $relationship, |
723: | string $property, |
724: | bool $isParent, |
725: | bool $isChildren |
726: | ): Closure { |
727: | if ($relationship === null) { |
728: | return |
729: | static function ( |
730: | array $data, |
731: | ?string $service, |
732: | $entity |
733: | ) use ($key, $property): void { |
734: | $entity->{$property} = $data[$key]; |
735: | }; |
736: | } |
737: | |
738: | return |
739: | static function ( |
740: | array $data, |
741: | ?string $service, |
742: | $entity, |
743: | ?SyncProviderInterface $provider, |
744: | ?SyncContextInterface $context |
745: | ) use ( |
746: | $key, |
747: | $isList, |
748: | $relationship, |
749: | $property, |
750: | $isParent, |
751: | $isChildren |
752: | ): void { |
753: | if ( |
754: | $data[$key] === null |
755: | || (Arr::isList($data[$key]) xor $isList) |
756: | || !$entity instanceof SyncEntityInterface |
757: | || !$provider instanceof SyncProviderInterface |
758: | || !$context instanceof SyncContextInterface |
759: | ) { |
760: | $entity->{$property} = $data[$key]; |
761: | return; |
762: | } |
763: | |
764: | if ($isList) { |
765: | if (is_scalar($data[$key][0])) { |
766: | if (!$isChildren) { |
767: | DeferredEntity::deferList( |
768: | $provider, |
769: | $context->pushWithRecursionCheck($entity), |
770: | $relationship, |
771: | $data[$key], |
772: | $entity->{$property}, |
773: | ); |
774: | return; |
775: | } |
776: | |
777: | |
778: | |
779: | DeferredEntity::deferList( |
780: | $provider, |
781: | $context->pushWithRecursionCheck($entity), |
782: | $relationship, |
783: | $data[$key], |
784: | $replace, |
785: | static function ($child) use ($entity): void { |
786: | |
787: | $entity->addChild($child); |
788: | }, |
789: | ); |
790: | return; |
791: | } |
792: | |
793: | $entities = |
794: | $relationship::provideList( |
795: | $data[$key], |
796: | $provider, |
797: | $context->getConformity(), |
798: | $context->push($entity), |
799: | )->toArray(); |
800: | |
801: | if (!$isChildren) { |
802: | $entity->{$property} = $entities; |
803: | return; |
804: | } |
805: | |
806: | |
807: | foreach ($entities as $child) { |
808: | |
809: | $entity->addChild($child); |
810: | } |
811: | return; |
812: | } |
813: | |
814: | if (is_scalar($data[$key])) { |
815: | if (!$isParent) { |
816: | DeferredEntity::defer( |
817: | $provider, |
818: | $context->pushWithRecursionCheck($entity), |
819: | $relationship, |
820: | $data[$key], |
821: | $entity->{$property}, |
822: | ); |
823: | return; |
824: | } |
825: | |
826: | |
827: | |
828: | DeferredEntity::defer( |
829: | $provider, |
830: | $context->pushWithRecursionCheck($entity), |
831: | $relationship, |
832: | $data[$key], |
833: | $replace, |
834: | static function ($parent) use ($entity): void { |
835: | |
836: | $entity->setParent($parent); |
837: | }, |
838: | ); |
839: | return; |
840: | } |
841: | |
842: | $related = |
843: | $relationship::provide( |
844: | $data[$key], |
845: | $provider, |
846: | $context->push($entity), |
847: | ); |
848: | |
849: | if (!$isParent) { |
850: | $entity->{$property} = $related; |
851: | return; |
852: | } |
853: | |
854: | |
855: | |
856: | |
857: | |
858: | $entity->setParent($related); |
859: | }; |
860: | } |
861: | |
862: | |
863: | |
864: | |
865: | |
866: | private function getHydrator( |
867: | string $idKey, |
868: | string $relationship, |
869: | string $property, |
870: | ?string $filter, |
871: | bool $isChildren |
872: | ): Closure { |
873: | $entityType = $this->_Class->Class; |
874: | $entityProvider = self::entityToProvider($relationship); |
875: | |
876: | return |
877: | static function ( |
878: | array $data, |
879: | ?string $service, |
880: | $entity, |
881: | ?SyncProviderInterface $provider, |
882: | ?SyncContextInterface $context |
883: | ) use ( |
884: | $idKey, |
885: | $relationship, |
886: | $property, |
887: | $filter, |
888: | $isChildren, |
889: | $entityType, |
890: | $entityProvider |
891: | ): void { |
892: | if ( |
893: | !$context instanceof SyncContextInterface |
894: | || !$provider instanceof SyncProviderInterface |
895: | || !is_a($provider, $entityProvider) |
896: | || $data[$idKey] === null |
897: | ) { |
898: | return; |
899: | } |
900: | |
901: | $policy = $context->getHydrationPolicy($relationship); |
902: | if ($policy === HydrationPolicy::SUPPRESS) { |
903: | return; |
904: | } |
905: | |
906: | if ($filter !== null) { |
907: | $filter = [$filter => $data[$idKey]]; |
908: | } |
909: | |
910: | if (!$isChildren) { |
911: | DeferredRelationship::defer( |
912: | $provider, |
913: | $context->pushWithRecursionCheck($entity), |
914: | $relationship, |
915: | $service ?? $entityType, |
916: | $property, |
917: | $data[$idKey], |
918: | $filter, |
919: | $entity->{$property}, |
920: | ); |
921: | return; |
922: | } |
923: | |
924: | |
925: | |
926: | DeferredRelationship::defer( |
927: | $provider, |
928: | $context->pushWithRecursionCheck($entity), |
929: | $relationship, |
930: | $service ?? $entityType, |
931: | $property, |
932: | $data[$idKey], |
933: | $filter, |
934: | $replace, |
935: | static function ($entities) use ($entity, $property): void { |
936: | if (!$entities) { |
937: | $entity->{$property} = []; |
938: | return; |
939: | } |
940: | foreach ($entities as $child) { |
941: | |
942: | $entity->addChild($child); |
943: | } |
944: | }, |
945: | ); |
946: | }; |
947: | } |
948: | |
949: | |
950: | |
951: | |
952: | |
953: | private function getSyncOperationMethod($operation, SyncIntrospector $entity): ?string |
954: | { |
955: | $_entity = $entity->_Class; |
956: | $noun = Str::lower($_entity->EntityNoun); |
957: | $methods = []; |
958: | |
959: | if ($_entity->EntityPlural !== null) { |
960: | $plural = Str::lower($_entity->EntityPlural); |
961: | switch ($operation) { |
962: | case SyncOperation::CREATE_LIST: |
963: | $methods[] = 'create' . $plural; |
964: | break; |
965: | |
966: | case SyncOperation::READ_LIST: |
967: | $methods[] = 'get' . $plural; |
968: | break; |
969: | |
970: | case SyncOperation::UPDATE_LIST: |
971: | $methods[] = 'update' . $plural; |
972: | break; |
973: | |
974: | case SyncOperation::DELETE_LIST: |
975: | $methods[] = 'delete' . $plural; |
976: | break; |
977: | } |
978: | } |
979: | |
980: | switch ($operation) { |
981: | case SyncOperation::CREATE: |
982: | $methods[] = 'create' . $noun; |
983: | $methods[] = 'create_' . $noun; |
984: | break; |
985: | |
986: | case SyncOperation::READ: |
987: | $methods[] = 'get' . $noun; |
988: | $methods[] = 'get_' . $noun; |
989: | break; |
990: | |
991: | case SyncOperation::UPDATE: |
992: | $methods[] = 'update' . $noun; |
993: | $methods[] = 'update_' . $noun; |
994: | break; |
995: | |
996: | case SyncOperation::DELETE: |
997: | $methods[] = 'delete' . $noun; |
998: | $methods[] = 'delete_' . $noun; |
999: | break; |
1000: | |
1001: | case SyncOperation::CREATE_LIST: |
1002: | $methods[] = 'createlist_' . $noun; |
1003: | break; |
1004: | |
1005: | case SyncOperation::READ_LIST: |
1006: | $methods[] = 'getlist_' . $noun; |
1007: | break; |
1008: | |
1009: | case SyncOperation::UPDATE_LIST: |
1010: | $methods[] = 'updatelist_' . $noun; |
1011: | break; |
1012: | |
1013: | case SyncOperation::DELETE_LIST: |
1014: | $methods[] = 'deletelist_' . $noun; |
1015: | break; |
1016: | } |
1017: | |
1018: | $methods = array_intersect_key( |
1019: | $this->_Class->SyncOperationMethods, |
1020: | array_flip($methods) |
1021: | ); |
1022: | |
1023: | if (count($methods) > 1) { |
1024: | throw new LogicException(sprintf( |
1025: | 'Too many implementations: %s', |
1026: | implode(', ', $methods), |
1027: | )); |
1028: | } |
1029: | |
1030: | return reset($methods) ?: null; |
1031: | } |
1032: | |
1033: | private function assertIsProvider(): void |
1034: | { |
1035: | if (!$this->_Class->IsSyncProvider) { |
1036: | throw new LogicException( |
1037: | sprintf('%s does not implement %s', $this->_Class->Class, SyncProviderInterface::class) |
1038: | ); |
1039: | } |
1040: | } |
1041: | } |
1042: | |