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: * Generates closures that perform sync-related operations on a class
28: *
29: * @property-read string $EntityNoun
30: * @property-read string|null $EntityPlural Not set if the plural class name is the same as the singular one
31: *
32: * @template TClass of object
33: *
34: * @extends Introspector<TClass,SyncProviderInterface,SyncEntityInterface,SyncContextInterface>
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: /** @var SyncIntrospectionClass<TClass> */
44: protected $_Class;
45:
46: /**
47: * Check if a sync operation is CREATE_LIST, READ_LIST, UPDATE_LIST or
48: * DELETE_LIST
49: *
50: * @param SyncOperation::* $operation
51: * @return ($operation is SyncOperation::*_LIST ? true : false)
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: * Check if a sync operation is READ or READ_LIST
65: *
66: * @param SyncOperation::* $operation
67: * @return ($operation is SyncOperation::READ* ? true : false)
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: * Check if a sync operation is CREATE, UPDATE, DELETE, CREATE_LIST,
79: * UPDATE_LIST or DELETE_LIST
80: *
81: * @param SyncOperation::* $operation
82: * @return ($operation is SyncOperation::READ* ? false : true)
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: * Get the name of a sync entity's provider interface
98: *
99: * @param class-string<SyncEntityInterface> $entity
100: * @return class-string<SyncProviderInterface>
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: * Get the names of sync entities serviced by a provider interface
118: *
119: * @param class-string<SyncProviderInterface> $provider
120: * @return array<class-string<SyncEntityInterface>>
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: * @template T of object
154: *
155: * @param class-string<T> $service
156: * @return static<T>
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: * @template T of object
171: *
172: * @param class-string<T> $class
173: * @return static<T>
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: * @param class-string<TClass> $class
188: * @return SyncIntrospectionClass<TClass>
189: */
190: protected function getIntrospectionClass(string $class): SyncIntrospectionClass
191: {
192: return new SyncIntrospectionClass($class);
193: }
194:
195: /**
196: * Get a list of SyncProviderInterface interfaces implemented by the
197: * provider
198: *
199: * @return array<class-string<SyncProviderInterface>>
200: */
201: public function getSyncProviderInterfaces(): array
202: {
203: $this->assertIsProvider();
204:
205: return $this->_Class->SyncProviderInterfaces;
206: }
207:
208: /**
209: * Get a list of SyncEntityInterface classes serviced by the provider
210: *
211: * @return array<class-string<SyncEntityInterface>>
212: */
213: public function getSyncProviderEntities(): array
214: {
215: $this->assertIsProvider();
216:
217: return $this->_Class->SyncProviderEntities;
218: }
219:
220: /**
221: * Get an array that maps unambiguous lowercase entity basenames to
222: * SyncEntityInterface classes serviced by the provider
223: *
224: * @return array<string,class-string<SyncEntityInterface>>
225: */
226: public function getSyncProviderEntityBasenames(): array
227: {
228: $this->assertIsProvider();
229:
230: return $this->_Class->SyncProviderEntityBasenames;
231: }
232:
233: /**
234: * Get a closure that creates SyncProviderInterface-serviced instances of the class
235: * from arrays
236: *
237: * Wraps {@see SyncIntrospector::getCreateSyncEntityFromSignatureClosure()}
238: * in a closure that resolves array signatures to closures on-demand.
239: *
240: * @param bool $strict If `true`, the closure will throw an exception if it
241: * receives any data that would be discarded.
242: * @return Closure(mixed[], SyncProviderInterface, SyncContextInterface): TClass
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: * Get a closure that creates SyncProviderInterface-serviced instances of the class
272: * from arrays with a given signature
273: *
274: * @param string[] $keys
275: * @param bool $strict If `true`, throw an exception if any data would be
276: * discarded.
277: * @return Closure(mixed[], SyncProviderInterface, SyncContextInterface): TClass
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: // If the closure was created successfully in strict mode, use it
292: // for non-strict purposes too
293: if ($strict) {
294: $this->_Class->CreateSyncEntityFromSignatureClosures[$sig][(int) false] = $closure;
295: }
296: }
297:
298: // Return a closure that injects this introspector's service
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: * Get the provider method that implements a sync operation for an entity
321: *
322: * Returns `null` if the provider doesn't implement the given operation via
323: * a declared method, otherwise creates a closure for the operation and
324: * binds it to `$provider`.
325: *
326: * @template T of SyncEntityInterface
327: *
328: * @param SyncOperation::* $operation
329: * @param class-string<T>|static<T> $entity
330: * @return (Closure(SyncContextInterface, mixed...): (iterable<T>|T))|null
331: * @throws LogicException if the {@see SyncIntrospector} and `$entity` don't
332: * respectively represent a {@see SyncProviderInterface} and
333: * {@see SyncEntityInterface}.
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: // Use strict comparison with `false` because null closures are cached
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: * Get a closure to perform sync operations on behalf of a provider's
366: * "magic" method
367: *
368: * Returns `null` if:
369: * - the {@see SyncIntrospector} was not created for a
370: * {@see SyncProviderInterface},
371: * - the {@see SyncProviderInterface} class already has `$method`, or
372: * - `$method` doesn't resolve to an unambiguous sync operation on a
373: * {@see SyncEntityInterface} class serviced by the
374: * {@see SyncProviderInterface} class
375: *
376: * @return Closure(SyncContextInterface, mixed...)|null
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: // Use strict comparison with `false` because null closures are cached
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: /** @var SyncProviderInterface $this */
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: * @param string[] $keys
406: * @return Closure(mixed[], string|null, ContainerInterface, SyncProviderInterface|null, SyncContextInterface|null, DateFormatterInterface|null, Treeable|null): TClass
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: /** @var class-string<SyncEntityInterface>|null $service */
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: /** @var class-string<TClass&SyncEntityInterface> */
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: /** @var class-string<SyncEntityInterface>|null $service */
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: /** @var TClass&SyncEntityInterface */
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: /** @var array<string,string> Normalised key => original key */
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: // If receiving values for this property, add the relevant key to
542: // $customKeys
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: // Check for relationships to honour by applying deferred entities
552: // instead of raw data
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: // If $match doesn't resolve to a declared property, it will
584: // resolve to a magic method
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: // Check for absent one-to-many relationships to hydrate
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: // Get keys left behind by constructor parameters, declared properties
626: // and magic methods
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: // Check for any that end with `_id`, `_ids` or similar that would match
645: // a property or magic method otherwise
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: // Don't use the same key twice
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: // Require a list of values if the key is plural (`_ids` as opposed
666: // to `_id`)
667: $list = $matches[2] !== '';
668:
669: // Check the property or magic method for a relationship to honour
670: // by applying deferred entities instead of raw data
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: // As above, if $match doesn't resolve to a declared property, it
688: // will resolve to a magic method
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: // Prevent duplication of the key as a meta value
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: * @param class-string<SyncEntityInterface&Relatable>|null $relationship
717: * @return Closure(mixed[], ?string, TClass, ?SyncProviderInterface, ?SyncContextInterface): void
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: /** @var SyncEntityInterface&Treeable $entity */
778: /** @disregard P1008 */
779: DeferredEntity::deferList(
780: $provider,
781: $context->pushWithRecursionCheck($entity),
782: $relationship,
783: $data[$key],
784: $replace,
785: static function ($child) use ($entity): void {
786: /** @var SyncEntityInterface&Treeable $child */
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: /** @var array<SyncEntityInterface&Treeable> $entities */
807: foreach ($entities as $child) {
808: /** @var SyncEntityInterface&Treeable $entity */
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: /** @var SyncEntityInterface&Treeable $entity */
827: /** @disregard P1008 */
828: DeferredEntity::defer(
829: $provider,
830: $context->pushWithRecursionCheck($entity),
831: $relationship,
832: $data[$key],
833: $replace,
834: static function ($parent) use ($entity): void {
835: /** @var SyncEntityInterface&Treeable $parent */
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: * @var SyncEntityInterface&Treeable $entity
856: * @var SyncEntityInterface&Treeable $related
857: */
858: $entity->setParent($related);
859: };
860: }
861:
862: /**
863: * @param class-string<SyncEntityInterface&Relatable> $relationship
864: * @return Closure(mixed[], ?string, TClass, ?SyncProviderInterface, ?SyncContextInterface): void
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: /** @var SyncEntityInterface&Treeable $entity */
925: /** @disregard P1008 */
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: /** @var SyncEntityInterface&Treeable $child */
942: $entity->addChild($child);
943: }
944: },
945: );
946: };
947: }
948:
949: /**
950: * @param SyncOperation::* $operation
951: * @param static<SyncEntityInterface> $entity
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: