1: <?php declare(strict_types=1);
2:
3: namespace Salient\Sync;
4:
5: use Salient\Container\RequiresContainer;
6: use Salient\Contract\Container\ContainerInterface;
7: use Salient\Contract\Core\Entity\Readable;
8: use Salient\Contract\Core\Entity\Writable;
9: use Salient\Contract\Core\Provider\ProviderContextInterface;
10: use Salient\Contract\Core\Provider\ProviderInterface;
11: use Salient\Contract\Core\DateFormatterInterface;
12: use Salient\Contract\Core\Flushable;
13: use Salient\Contract\Core\HasDescription;
14: use Salient\Contract\Core\ListConformity;
15: use Salient\Contract\Core\NormaliserFlag;
16: use Salient\Contract\Core\TextComparisonAlgorithm as Algorithm;
17: use Salient\Contract\Core\TextComparisonFlag as Flag;
18: use Salient\Contract\Iterator\FluentIteratorInterface;
19: use Salient\Contract\Sync\DeferredEntityInterface;
20: use Salient\Contract\Sync\DeferredRelationshipInterface;
21: use Salient\Contract\Sync\EntityState;
22: use Salient\Contract\Sync\LinkType;
23: use Salient\Contract\Sync\SyncContextInterface;
24: use Salient\Contract\Sync\SyncEntityInterface;
25: use Salient\Contract\Sync\SyncEntityProviderInterface;
26: use Salient\Contract\Sync\SyncProviderInterface;
27: use Salient\Contract\Sync\SyncSerializeRulesInterface;
28: use Salient\Contract\Sync\SyncStoreInterface;
29: use Salient\Core\Concern\ConstructibleTrait;
30: use Salient\Core\Concern\ExtensibleTrait;
31: use Salient\Core\Concern\HasNormaliser;
32: use Salient\Core\Concern\HasReadableProperties;
33: use Salient\Core\Concern\HasWritableProperties;
34: use Salient\Core\Concern\ProvidableTrait;
35: use Salient\Core\Facade\Sync;
36: use Salient\Core\AbstractEntity;
37: use Salient\Core\DateFormatter;
38: use Salient\Iterator\IterableIterator;
39: use Salient\Sync\Exception\SyncEntityNotFoundException;
40: use Salient\Sync\Support\SyncIntrospector;
41: use Salient\Utility\Arr;
42: use Salient\Utility\Get;
43: use Salient\Utility\Inflect;
44: use Salient\Utility\Regex;
45: use Salient\Utility\Str;
46: use DateTimeInterface;
47: use Generator;
48: use LogicException;
49: use ReflectionClass;
50: use UnexpectedValueException;
51:
52: /**
53: * Base class for entities serviced by sync providers
54: *
55: * {@see AbstractSyncEntity} implements {@see Readable} and {@see Writable}, but
56: * `protected` properties are not accessible by default. Override
57: * {@see AbstractSyncEntity::getReadableProperties()} and/or
58: * {@see AbstractSyncEntity::getWritableProperties()} to change this.
59: *
60: * The following "magic" property methods are discovered automatically and don't
61: * need to be returned by {@see AbstractSyncEntity::getReadableProperties()} or
62: * {@see AbstractSyncEntity::getWritableProperties()}:
63: * - `protected function _get<PropertyName>()`
64: * - `protected function _isset<PropertyName>()` (optional; falls back to
65: * `_get<PropertyName>()` if not declared)
66: * - `protected function _set<PropertyName>($value)`
67: * - `protected function _unset<PropertyName>()` (optional; falls back to
68: * `_set<PropertyName>(null)` if not declared)
69: *
70: * Accessible properties are mapped to associative arrays with snake_case keys
71: * when {@see AbstractSyncEntity} objects are serialized. Override
72: * {@see AbstractSyncEntity::buildSerializeRules()} to provide serialization
73: * rules for nested entities.
74: */
75: abstract class AbstractSyncEntity extends AbstractEntity implements
76: SyncEntityInterface,
77: Flushable
78: {
79: use ConstructibleTrait;
80: use HasReadableProperties;
81: use HasWritableProperties;
82: use ExtensibleTrait {
83: ExtensibleTrait::__set insteadof HasWritableProperties;
84: ExtensibleTrait::__get insteadof HasReadableProperties;
85: ExtensibleTrait::__isset insteadof HasReadableProperties;
86: ExtensibleTrait::__unset insteadof HasWritableProperties;
87: }
88: /** @use ProvidableTrait<SyncProviderInterface,SyncContextInterface> */
89: use ProvidableTrait;
90: use HasNormaliser;
91: use RequiresContainer;
92:
93: /**
94: * The unique identifier assigned to the entity by its provider
95: *
96: * @see SyncEntityInterface::getId()
97: *
98: * @var int|string|null
99: */
100: public $Id;
101:
102: /**
103: * The unique identifier assigned to the entity by its canonical backend
104: *
105: * @see SyncEntityInterface::canonicalId()
106: *
107: * @var int|string|null
108: */
109: public $CanonicalId;
110:
111: /** @var SyncProviderInterface|null */
112: private $Provider;
113: /** @var SyncContextInterface|null */
114: private $Context;
115: /** @var int-mask-of<EntityState::*> */
116: private $State = 0;
117:
118: /**
119: * Entity => "/^<pattern>_/" | false
120: *
121: * @var array<class-string<self>,string|false>
122: */
123: private static array $RemovablePrefixRegex = [];
124:
125: /**
126: * Entity => [ property name => normalised property name ]
127: *
128: * @var array<class-string<self>,array<string,string>>
129: */
130: private static array $NormalisedPropertyMap = [];
131:
132: /**
133: * @inheritDoc
134: */
135: public static function getPlural(): ?string
136: {
137: return Inflect::plural(Get::basename(static::class));
138: }
139:
140: /**
141: * @inheritDoc
142: */
143: public static function getRelationships(): array
144: {
145: return [];
146: }
147:
148: /**
149: * @inheritDoc
150: */
151: public static function getDateProperties(): array
152: {
153: return [];
154: }
155:
156: /**
157: * @inheritDoc
158: */
159: public static function flushStatic(): void
160: {
161: unset(
162: self::$RemovablePrefixRegex[static::class],
163: self::$NormalisedPropertyMap[static::class],
164: );
165: }
166:
167: /**
168: * @inheritDoc
169: */
170: public function getName(): string
171: {
172: return
173: SyncIntrospector::get(static::class)
174: ->getGetNameClosure()($this);
175: }
176:
177: /**
178: * Override to specify how object graphs below entities of this type should
179: * be serialized
180: *
181: * To prevent infinite recursion when entities of this type are serialized,
182: * return a {@see SyncSerializeRulesBuilder} object configured to remove or
183: * replace circular references.
184: *
185: * @param SyncSerializeRulesBuilder<static> $rulesB
186: * @return SyncSerializeRulesBuilder<static>
187: */
188: protected static function buildSerializeRules(SyncSerializeRulesBuilder $rulesB): SyncSerializeRulesBuilder
189: {
190: return $rulesB;
191: }
192:
193: /**
194: * Override to specify prefixes to remove when normalising property names
195: *
196: * Entity names are removed by default, e.g. for an
197: * {@see AbstractSyncEntity} subclass called `User`, "User" is removed to
198: * ensure fields like "USER_ID" and "USER_NAME" match properties like "Id"
199: * and "Name". For a subclass of `User` called `AdminUser`, both "User" and
200: * "AdminUser" are removed.
201: *
202: * Return `null` to suppress prefix removal, otherwise use
203: * {@see AbstractSyncEntity::normalisePrefixes()} or
204: * {@see AbstractSyncEntity::expandPrefixes()} to normalise the return
205: * value.
206: *
207: * @return string[]|null
208: */
209: protected static function getRemovablePrefixes(): ?array
210: {
211: $current = new ReflectionClass(static::class);
212: do {
213: $prefixes[] = Get::basename($current->getName());
214: $current = $current->getParentClass();
215: } while ($current && $current->isSubclassOf(self::class));
216:
217: return self::expandPrefixes($prefixes);
218: }
219:
220: // --
221:
222: /**
223: * @inheritDoc
224: */
225: final public function getId()
226: {
227: return $this->Id;
228: }
229:
230: /**
231: * @inheritDoc
232: */
233: final public function getCanonicalId()
234: {
235: return $this->CanonicalId;
236: }
237:
238: /**
239: * @inheritDoc
240: */
241: final public static function getDefaultProvider(ContainerInterface $container): SyncProviderInterface
242: {
243: return $container->get(SyncUtil::getEntityTypeProvider(static::class, SyncUtil::getStore($container)));
244: }
245:
246: /**
247: * @inheritDoc
248: */
249: final public static function withDefaultProvider(ContainerInterface $container, ?SyncContextInterface $context = null): SyncEntityProviderInterface
250: {
251: return static::getDefaultProvider($container)->with(static::class, $context);
252: }
253:
254: /**
255: * @inheritDoc
256: */
257: final public static function getSerializeRules(): SyncSerializeRulesInterface
258: {
259: return static::buildSerializeRules(
260: SyncSerializeRules::build()
261: ->entity(static::class)
262: )->build();
263: }
264:
265: /**
266: * Get the serialization rules of the entity's parent class
267: *
268: * @return SyncSerializeRules<static>|null
269: */
270: final protected static function getParentSerializeRules(): ?SyncSerializeRules
271: {
272: $class = get_parent_class(get_called_class());
273:
274: if (
275: $class === false
276: || !is_a($class, self::class, true)
277: || $class === self::class
278: ) {
279: return null;
280: }
281:
282: /** @var SyncSerializeRules<static> */
283: return $class::buildSerializeRules(
284: SyncSerializeRules::build()
285: ->entity($class)
286: )->build();
287: }
288:
289: /**
290: * Normalise a property name
291: *
292: * 1. `$name` is converted to snake_case
293: * 2. If the result is one of the given `$hints`, it is returned
294: * 3. If `$greedy` is `true`, prefixes returned by
295: * {@see AbstractSyncEntity::getRemovablePrefixes()} are removed
296: */
297: final public static function normaliseProperty(
298: string $name,
299: bool $greedy = true,
300: string ...$hints
301: ): string {
302: $regex = self::$RemovablePrefixRegex[static::class]
303: ??= self::getRemovablePrefixRegex();
304:
305: if ($regex === false) {
306: return self::$NormalisedPropertyMap[static::class][$name]
307: ??= Str::snake($name);
308: }
309:
310: if ($greedy && !$hints) {
311: return self::$NormalisedPropertyMap[static::class][$name]
312: ??= Regex::replace($regex, '', Str::snake($name));
313: }
314:
315: $_name = Str::snake($name);
316: if (!$greedy || in_array($_name, $hints)) {
317: return $_name;
318: }
319:
320: return Regex::replace($regex, '', $_name);
321: }
322:
323: /**
324: * @return string|false
325: */
326: private static function getRemovablePrefixRegex()
327: {
328: $prefixes = static::getRemovablePrefixes();
329:
330: return $prefixes
331: ? sprintf(
332: count($prefixes) > 1 ? '/^(?:%s)_/' : '/^%s_/',
333: implode('|', $prefixes),
334: )
335: : false;
336: }
337:
338: /**
339: * @inheritDoc
340: */
341: final public function toArray(?SyncStoreInterface $store = null): array
342: {
343: /** @var SyncSerializeRulesInterface<self> */
344: $rules = static::getSerializeRules();
345: return $this->_toArray($rules, $store);
346: }
347:
348: /**
349: * @inheritDoc
350: */
351: final public function toArrayWith(SyncSerializeRulesInterface $rules, ?SyncStoreInterface $store = null): array
352: {
353: /** @var SyncSerializeRulesInterface<self> $rules */
354: return $this->_toArray($rules, $store);
355: }
356:
357: /**
358: * @inheritDoc
359: */
360: final public function toLink(?SyncStoreInterface $store = null, int $type = LinkType::DEFAULT, bool $compact = true): array
361: {
362: switch ($type) {
363: case LinkType::DEFAULT:
364: return [
365: '@type' => $this->getTypeUri($store, $compact),
366: '@id' => $this->Id === null
367: ? spl_object_id($this)
368: : $this->Id,
369: ];
370:
371: case LinkType::COMPACT:
372: return [
373: '@id' => $this->getUri($store, $compact),
374: ];
375:
376: case LinkType::FRIENDLY:
377: return Arr::whereNotEmpty([
378: '@type' => $this->getTypeUri($store, $compact),
379: '@id' => $this->Id === null
380: ? spl_object_id($this)
381: : $this->Id,
382: '@name' => $this->getName(),
383: '@description' =>
384: $this instanceof HasDescription
385: ? $this->getDescription()
386: : null,
387: ]);
388:
389: default:
390: throw new LogicException("Invalid link type: $type");
391: }
392: }
393:
394: /**
395: * @inheritDoc
396: */
397: final public function getUri(?SyncStoreInterface $store = null, bool $compact = true): string
398: {
399: return sprintf(
400: '%s/%s',
401: $this->getTypeUri($store, $compact),
402: $this->Id === null
403: ? spl_object_id($this)
404: : $this->Id
405: );
406: }
407:
408: /**
409: * Get the current state of the entity
410: *
411: * @return int-mask-of<EntityState::*>
412: */
413: final public function state(): int
414: {
415: return $this->State;
416: }
417:
418: /**
419: * Convert prefixes to snake_case for removal from property names
420: *
421: * e.g. `['AdminUserGroup']` becomes `['admin_user_group']`.
422: *
423: * @param string[] $prefixes
424: * @return string[]
425: */
426: final protected static function normalisePrefixes(array $prefixes): array
427: {
428: if (!$prefixes) {
429: return [];
430: }
431:
432: return Arr::snakeCase($prefixes);
433: }
434:
435: /**
436: * Convert prefixes to snake_case and expand them for removal from property
437: * names
438: *
439: * e.g. `['AdminUserGroup']` becomes `['admin_user_group', 'user_group',
440: * 'group']`.
441: *
442: * @param string[] $prefixes
443: * @return string[]
444: */
445: final protected static function expandPrefixes(array $prefixes): array
446: {
447: if (!$prefixes) {
448: return [];
449: }
450:
451: foreach ($prefixes as $prefix) {
452: $prefix = Str::snake($prefix);
453: $expanded[$prefix] = true;
454: $prefix = explode('_', $prefix);
455: while (array_shift($prefix)) {
456: $expanded[implode('_', $prefix)] = true;
457: }
458: }
459:
460: return array_keys($expanded);
461: }
462:
463: private function getTypeUri(?SyncStoreInterface $store, bool $compact): string
464: {
465: /** @var class-string<self> */
466: $service = $this->getService();
467: $store ??= $this->Provider ? $this->Provider->getStore() : null;
468: return SyncUtil::getEntityTypeUri($service, $compact, $store);
469: }
470:
471: /**
472: * @param SyncSerializeRulesInterface<self> $rules
473: * @return array<string,mixed>
474: */
475: private function _toArray(SyncSerializeRulesInterface $rules, ?SyncStoreInterface $store): array
476: {
477: /** @var SyncSerializeRulesInterface<self> $rules */
478: if ($rules->getDateFormatter() === null) {
479: $rules = $rules->withDateFormatter(
480: $this->Provider
481: ? $this->Provider->getDateFormatter()
482: : new DateFormatter()
483: );
484: }
485:
486: $array = $this;
487: $this->_serialize($array, [], $rules, $store);
488:
489: return (array) $array;
490: }
491:
492: /**
493: * @param AbstractSyncEntity|DeferredEntityInterface<AbstractSyncEntity>|DeferredRelationshipInterface<AbstractSyncEntity>|DateTimeInterface|mixed[] $node
494: * @param string[] $path
495: * @param SyncSerializeRulesInterface<self> $rules
496: * @param array<int,true> $parents
497: */
498: private function _serialize(
499: &$node,
500: array $path,
501: SyncSerializeRulesInterface $rules,
502: ?SyncStoreInterface $store,
503: bool $nodeIsList = false,
504: array $parents = []
505: ): void {
506: $maxDepth = $rules->getMaxDepth();
507: if ($maxDepth !== null && count($path) > $maxDepth) {
508: throw new UnexpectedValueException('In too deep: ' . implode('.', $path));
509: }
510:
511: if ($node instanceof DateTimeInterface) {
512: /** @var DateFormatterInterface */
513: $formatter = $rules->getDateFormatter();
514: $node = $formatter->format($node);
515: return;
516: }
517:
518: // Now is not the time to resolve deferred entities
519: if ($node instanceof DeferredEntityInterface) {
520: $node = $node->toLink(LinkType::DEFAULT);
521: return;
522: }
523:
524: /** @todo Serialize deferred relationships */
525: if ($node instanceof DeferredRelationshipInterface) {
526: $node = null;
527: return;
528: }
529:
530: if ($node instanceof AbstractSyncEntity) {
531: if ($path && $rules->getForSyncStore()) {
532: $node = $node->toLink($store, LinkType::DEFAULT);
533: return;
534: }
535:
536: if ($rules->getDetectRecursion()) {
537: // Prevent infinite recursion by replacing each sync entity
538: // descended from itself with a link
539: if ($parents[spl_object_id($node)] ?? false) {
540: $node = $node->toLink($store, LinkType::DEFAULT);
541: $node['@why'] = 'Circular reference detected';
542: return;
543: }
544: $parents[spl_object_id($node)] = true;
545: }
546:
547: $class = get_class($node);
548: $node = $node->serialize($rules);
549: }
550:
551: $delete = $rules->getRemovableKeys($class ?? null, null, $path);
552: $replace = $rules->getReplaceableKeys($class ?? null, null, $path);
553:
554: // Don't delete values returned in both lists
555: $delete = array_diff_key($delete, $replace);
556:
557: if ($delete) {
558: $node = array_diff_key($node, $delete);
559: }
560:
561: $replace = array_intersect_key($replace, $node + ['[]' => null]);
562:
563: foreach ($replace as $key => [$newKey, $closure]) {
564: if ($key !== '[]') {
565: if ($newKey !== null && $key !== $newKey) {
566: $node = Arr::rename($node, $key, $newKey);
567: $key = $newKey;
568: }
569:
570: if ($closure) {
571: if ($node[$key] !== null) {
572: $node[$key] = $closure($node[$key], $store);
573: }
574: continue;
575: }
576:
577: $_path = $path;
578: $_path[] = (string) $key;
579: $this->_serializeId($node[$key], $_path);
580: continue;
581: }
582:
583: if (!$nodeIsList) {
584: continue;
585: }
586:
587: if ($closure) {
588: foreach ($node as &$child) {
589: if ($child !== null) {
590: $child = $closure($child, $store);
591: }
592: }
593: unset($child);
594: continue;
595: }
596:
597: $_path = $path;
598: $last = array_pop($_path) . '[]';
599: $_path[] = $last;
600: foreach ($node as &$child) {
601: $this->_serializeId($child, $_path);
602: }
603: unset($child);
604: }
605:
606: $isList = false;
607: if (Arr::isIndexed($node)) {
608: $isList = true;
609: $last = array_pop($path) . '[]';
610: $path[] = $last;
611: }
612:
613: unset($_path);
614: foreach ($node as $key => &$child) {
615: if ($child === null || is_scalar($child)) {
616: continue;
617: }
618: if (!$isList) {
619: $_path = $path;
620: $_path[] = (string) $key;
621: }
622: $this->_serialize(
623: $child,
624: $_path ?? $path,
625: $rules,
626: $store,
627: $isList,
628: $parents,
629: );
630: }
631: }
632:
633: /**
634: * @param AbstractSyncEntity[]|AbstractSyncEntity|null $node
635: * @param string[] $path
636: */
637: private function _serializeId(&$node, array $path): void
638: {
639: if ($node === null) {
640: return;
641: }
642:
643: if (Arr::of($node, AbstractSyncEntity::class, true) && Arr::isIndexed($node, true)) {
644: /** @var AbstractSyncEntity $child */
645: foreach ($node as &$child) {
646: $child = $child->Id;
647: }
648:
649: return;
650: }
651:
652: if (!$node instanceof AbstractSyncEntity) {
653: throw new UnexpectedValueException('Cannot replace (not an AbstractSyncEntity): ' . implode('.', $path));
654: }
655:
656: $node = $node->Id;
657: }
658:
659: /**
660: * Convert the entity to an associative array
661: *
662: * Nested objects and lists are returned as-is. Only the top-level entity is
663: * replaced.
664: *
665: * @param SyncSerializeRulesInterface<static> $rules
666: * @return array<string,mixed>
667: */
668: private function serialize(SyncSerializeRulesInterface $rules): array
669: {
670: $clone = clone $this;
671: $clone->State |= EntityState::SERIALIZING;
672: $array = SyncIntrospector::get(static::class)
673: ->getSerializeClosure($rules)($clone);
674:
675: if (!$rules->getIncludeCanonicalId()) {
676: unset($array[
677: SyncIntrospector::get(static::class)
678: ->maybeNormalise('CanonicalId', NormaliserFlag::CAREFUL)
679: ]);
680: }
681:
682: return $array;
683: }
684:
685: /**
686: * @internal
687: *
688: * @return array<string,mixed>
689: */
690: final public function jsonSerialize(): array
691: {
692: return $this->toArray();
693: }
694:
695: /**
696: * @param SyncProviderInterface $provider
697: * @param SyncContextInterface|null $context
698: */
699: final public static function provide(
700: array $data,
701: ProviderInterface $provider,
702: ?ProviderContextInterface $context = null
703: ) {
704: $container = $context
705: ? $context->getContainer()
706: : $provider->getContainer();
707: $container = $container->inContextOf(get_class($provider));
708:
709: $context = ($context ?? $provider->getContext())->withContainer($container);
710:
711: $closure = SyncIntrospector::getService(
712: $container, static::class
713: )->getCreateSyncEntityFromClosure();
714:
715: return $closure($data, $provider, $context);
716: }
717:
718: /**
719: * @param SyncProviderInterface $provider
720: * @param SyncContextInterface|null $context
721: */
722: final public static function provideMultiple(
723: iterable $list,
724: ProviderInterface $provider,
725: int $conformity = ListConformity::NONE,
726: ?ProviderContextInterface $context = null
727: ): FluentIteratorInterface {
728: return IterableIterator::from(
729: self::_provideMultiple($list, $provider, $conformity, $context)
730: );
731: }
732:
733: /**
734: * @inheritDoc
735: */
736: final public static function idFromNameOrId(
737: $nameOrId,
738: $providerOrContext,
739: ?float $uncertaintyThreshold = null,
740: ?string $nameProperty = null,
741: ?float &$uncertainty = null
742: ) {
743: if ($nameOrId === null) {
744: $uncertainty = null;
745: return null;
746: }
747:
748: if ($providerOrContext instanceof SyncProviderInterface) {
749: $provider = $providerOrContext;
750: $context = $provider->getContext();
751: } else {
752: $context = $providerOrContext;
753: $provider = $context->getProvider();
754: }
755:
756: if ($provider->isValidIdentifier($nameOrId, static::class)) {
757: $uncertainty = 0.0;
758: return $nameOrId;
759: }
760:
761: $entity = $provider
762: ->with(static::class, $context)
763: ->getResolver(
764: $nameProperty,
765: Algorithm::SAME | Algorithm::CONTAINS | Algorithm::NGRAM_SIMILARITY | Flag::NORMALISE,
766: $uncertaintyThreshold,
767: null,
768: true,
769: )
770: ->getByName((string) $nameOrId, $uncertainty);
771:
772: if ($entity) {
773: return $entity->Id;
774: }
775:
776: throw new SyncEntityNotFoundException(
777: $provider,
778: static::class,
779: $nameProperty === null
780: ? ['name' => $nameOrId]
781: : [$nameProperty => $nameOrId],
782: );
783: }
784:
785: /**
786: * @inheritDoc
787: */
788: public function postLoad(): void {}
789:
790: /**
791: * @return array<string,mixed>
792: */
793: public function __serialize(): array
794: {
795: foreach ([
796: ...SyncIntrospector::get(static::class)->SerializableProperties,
797: 'MetaProperties',
798: 'MetaPropertyNames',
799: ] as $property) {
800: $data[$property] = $this->{$property};
801: }
802:
803: $data['Provider'] = $this->Provider === null
804: ? null
805: : $this->Provider->getStore()->getProviderSignature($this->Provider);
806:
807: return $data;
808: }
809:
810: /**
811: * @param array<string,mixed> $data
812: */
813: public function __unserialize(array $data): void
814: {
815: foreach ($data as $property => $value) {
816: if ($property === 'Provider' && $value !== null) {
817: $value = is_string($value) && Sync::isLoaded() && Sync::hasProvider($value)
818: ? Sync::getProvider($value)
819: : null;
820: if ($value === null) {
821: throw new UnexpectedValueException('Cannot unserialize missing provider');
822: }
823: }
824: $this->{$property} = $value;
825: }
826: }
827:
828: /**
829: * @param iterable<array-key,mixed[]> $list
830: * @param SyncProviderInterface $provider
831: * @param ListConformity::* $conformity
832: * @param SyncContextInterface|null $context
833: * @return Generator<array-key,static>
834: */
835: private static function _provideMultiple(
836: iterable $list,
837: ProviderInterface $provider,
838: $conformity,
839: ?ProviderContextInterface $context
840: ): Generator {
841: $container = $context
842: ? $context->getContainer()
843: : $provider->getContainer();
844: $container = $container->inContextOf(get_class($provider));
845:
846: $conformity = $context
847: ? max($context->getConformity(), $conformity)
848: : $conformity;
849:
850: $context = ($context ?? $provider->getContext())->withContainer($container);
851:
852: $introspector = SyncIntrospector::getService($container, static::class);
853:
854: foreach ($list as $key => $data) {
855: if (!isset($closure)) {
856: $closure = $conformity === ListConformity::PARTIAL || $conformity === ListConformity::COMPLETE
857: ? $introspector->getCreateSyncEntityFromSignatureClosure(array_keys($data))
858: : $introspector->getCreateSyncEntityFromClosure();
859: }
860:
861: yield $key => $closure($data, $provider, $context);
862: }
863: }
864: }
865: