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 SyncIntrospector::get(static::class)
173: ->getGetNameClosure()($this);
174: }
175:
176: /**
177: * Override to specify how object graphs below entities of this type should
178: * be serialized
179: *
180: * To prevent infinite recursion when entities of this type are serialized,
181: * return a {@see SyncSerializeRulesBuilder} object configured to remove or
182: * replace circular references.
183: *
184: * @param SyncSerializeRulesBuilder<static> $rulesB
185: * @return SyncSerializeRulesBuilder<static>
186: */
187: protected static function buildSerializeRules(SyncSerializeRulesBuilder $rulesB): SyncSerializeRulesBuilder
188: {
189: return $rulesB;
190: }
191:
192: /**
193: * Override to specify prefixes to remove when normalising property names
194: *
195: * Entity names are removed by default, e.g. for an
196: * {@see AbstractSyncEntity} subclass called `User`, "User" is removed to
197: * ensure fields like "USER_ID" and "USER_NAME" match properties like "Id"
198: * and "Name". For a subclass of `User` called `AdminUser`, both "User" and
199: * "AdminUser" are removed.
200: *
201: * Return `null` to suppress prefix removal, otherwise use
202: * {@see AbstractSyncEntity::normalisePrefixes()} or
203: * {@see AbstractSyncEntity::expandPrefixes()} to normalise the return
204: * value.
205: *
206: * @return string[]|null
207: */
208: protected static function getRemovablePrefixes(): ?array
209: {
210: $current = new ReflectionClass(static::class);
211: do {
212: $prefixes[] = Get::basename($current->getName());
213: $current = $current->getParentClass();
214: } while ($current && $current->isSubclassOf(self::class));
215:
216: return self::expandPrefixes($prefixes);
217: }
218:
219: // --
220:
221: /**
222: * @inheritDoc
223: */
224: final public function getId()
225: {
226: return $this->Id;
227: }
228:
229: /**
230: * @inheritDoc
231: */
232: final public function getCanonicalId()
233: {
234: return $this->CanonicalId;
235: }
236:
237: /**
238: * @inheritDoc
239: */
240: final public static function getDefaultProvider(ContainerInterface $container): SyncProviderInterface
241: {
242: return $container->get(SyncUtil::getEntityTypeProvider(static::class, SyncUtil::getStore($container)));
243: }
244:
245: /**
246: * @inheritDoc
247: */
248: final public static function withDefaultProvider(ContainerInterface $container, ?SyncContextInterface $context = null): SyncEntityProviderInterface
249: {
250: return static::getDefaultProvider($container)->with(static::class, $context);
251: }
252:
253: /**
254: * @inheritDoc
255: */
256: final public static function getSerializeRules(): SyncSerializeRulesInterface
257: {
258: return static::buildSerializeRules(
259: SyncSerializeRules::build()
260: ->entity(static::class)
261: )->build();
262: }
263:
264: /**
265: * Get the serialization rules of the entity's parent class
266: *
267: * @return SyncSerializeRules<static>|null
268: */
269: final protected static function getParentSerializeRules(): ?SyncSerializeRules
270: {
271: $class = get_parent_class(get_called_class());
272:
273: if (
274: $class === false
275: || !is_a($class, self::class, true)
276: || $class === self::class
277: ) {
278: return null;
279: }
280:
281: /** @var SyncSerializeRules<static> */
282: return $class::buildSerializeRules(
283: SyncSerializeRules::build()
284: ->entity($class)
285: )->build();
286: }
287:
288: /**
289: * Normalise a property name
290: *
291: * 1. `$name` is converted to snake_case
292: * 2. If the result is one of the given `$hints`, it is returned
293: * 3. If `$greedy` is `true`, prefixes returned by
294: * {@see AbstractSyncEntity::getRemovablePrefixes()} are removed
295: */
296: final public static function normaliseProperty(
297: string $name,
298: bool $greedy = true,
299: string ...$hints
300: ): string {
301: $regex = self::$RemovablePrefixRegex[static::class] ??=
302: self::getRemovablePrefixRegex();
303:
304: if ($regex === false) {
305: return self::$NormalisedPropertyMap[static::class][$name] ??=
306: Str::snake($name);
307: }
308:
309: if ($greedy && !$hints) {
310: return self::$NormalisedPropertyMap[static::class][$name] ??=
311: Regex::replace($regex, '', Str::snake($name));
312: }
313:
314: $_name = Str::snake($name);
315: if (!$greedy || in_array($_name, $hints)) {
316: return $_name;
317: }
318:
319: return Regex::replace($regex, '', $_name);
320: }
321:
322: /**
323: * @return string|false
324: */
325: private static function getRemovablePrefixRegex()
326: {
327: $prefixes = static::getRemovablePrefixes();
328:
329: return $prefixes
330: ? sprintf(
331: count($prefixes) > 1 ? '/^(?:%s)_/' : '/^%s_/',
332: implode('|', $prefixes),
333: )
334: : false;
335: }
336:
337: /**
338: * @inheritDoc
339: */
340: final public function toArray(?SyncStoreInterface $store = null): array
341: {
342: /** @var SyncSerializeRulesInterface<self> */
343: $rules = static::getSerializeRules();
344: return $this->_toArray($rules, $store);
345: }
346:
347: /**
348: * @inheritDoc
349: */
350: final public function toArrayWith(SyncSerializeRulesInterface $rules, ?SyncStoreInterface $store = null): array
351: {
352: /** @var SyncSerializeRulesInterface<self> $rules */
353: return $this->_toArray($rules, $store);
354: }
355:
356: /**
357: * @inheritDoc
358: */
359: final public function toLink(?SyncStoreInterface $store = null, int $type = LinkType::DEFAULT, bool $compact = true): array
360: {
361: switch ($type) {
362: case LinkType::DEFAULT:
363: return [
364: '@type' => $this->getTypeUri($store, $compact),
365: '@id' => $this->Id === null
366: ? spl_object_id($this)
367: : $this->Id,
368: ];
369:
370: case LinkType::COMPACT:
371: return [
372: '@id' => $this->getUri($store, $compact),
373: ];
374:
375: case LinkType::FRIENDLY:
376: return Arr::whereNotEmpty([
377: '@type' => $this->getTypeUri($store, $compact),
378: '@id' => $this->Id === null
379: ? spl_object_id($this)
380: : $this->Id,
381: '@name' => $this->getName(),
382: '@description' =>
383: $this instanceof HasDescription
384: ? $this->getDescription()
385: : null,
386: ]);
387:
388: default:
389: throw new LogicException("Invalid link type: $type");
390: }
391: }
392:
393: /**
394: * @inheritDoc
395: */
396: final public function getUri(?SyncStoreInterface $store = null, bool $compact = true): string
397: {
398: return sprintf(
399: '%s/%s',
400: $this->getTypeUri($store, $compact),
401: $this->Id === null
402: ? spl_object_id($this)
403: : $this->Id
404: );
405: }
406:
407: /**
408: * Get the current state of the entity
409: *
410: * @return int-mask-of<EntityState::*>
411: */
412: final public function state(): int
413: {
414: return $this->State;
415: }
416:
417: /**
418: * Convert prefixes to snake_case for removal from property names
419: *
420: * e.g. `['AdminUserGroup']` becomes `['admin_user_group']`.
421: *
422: * @param string[] $prefixes
423: * @return string[]
424: */
425: final protected static function normalisePrefixes(array $prefixes): array
426: {
427: if (!$prefixes) {
428: return [];
429: }
430:
431: return Arr::snakeCase($prefixes);
432: }
433:
434: /**
435: * Convert prefixes to snake_case and expand them for removal from property
436: * names
437: *
438: * e.g. `['AdminUserGroup']` becomes `['admin_user_group', 'user_group',
439: * 'group']`.
440: *
441: * @param string[] $prefixes
442: * @return string[]
443: */
444: final protected static function expandPrefixes(array $prefixes): array
445: {
446: if (!$prefixes) {
447: return [];
448: }
449:
450: foreach ($prefixes as $prefix) {
451: $prefix = Str::snake($prefix);
452: $expanded[$prefix] = true;
453: $prefix = explode('_', $prefix);
454: while (array_shift($prefix)) {
455: $expanded[implode('_', $prefix)] = true;
456: }
457: }
458:
459: return array_keys($expanded);
460: }
461:
462: private function getTypeUri(?SyncStoreInterface $store, bool $compact): string
463: {
464: /** @var class-string<self> */
465: $service = $this->getService();
466: $store ??= $this->Provider ? $this->Provider->getStore() : null;
467: return SyncUtil::getEntityTypeUri($service, $compact, $store);
468: }
469:
470: /**
471: * @param SyncSerializeRulesInterface<self> $rules
472: * @return array<string,mixed>
473: */
474: private function _toArray(SyncSerializeRulesInterface $rules, ?SyncStoreInterface $store): array
475: {
476: /** @var SyncSerializeRulesInterface<self> $rules */
477: if ($rules->getDateFormatter() === null) {
478: $rules = $rules->withDateFormatter(
479: $this->Provider
480: ? $this->Provider->getDateFormatter()
481: : new DateFormatter()
482: );
483: }
484:
485: $array = $this;
486: $this->_serialize($array, [], $rules, $store);
487:
488: return (array) $array;
489: }
490:
491: /**
492: * @param self|DeferredEntityInterface<self>|DeferredRelationshipInterface<self>|DateTimeInterface|mixed[] $node
493: * @param string[] $path
494: * @param SyncSerializeRulesInterface<self> $rules
495: * @param array<int,true> $parents
496: * @param-out ($node is self ? array<string,mixed> : mixed[]|string|null) $node
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 (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: /** @var self[]|self|null */
578: $child = &$node[$key];
579: $this->_serializeId($child);
580: unset($child);
581: continue;
582: }
583:
584: if (!$nodeIsList) {
585: continue;
586: }
587:
588: if ($closure) {
589: foreach ($node as &$child) {
590: if ($child !== null) {
591: $child = $closure($child, $store);
592: }
593: }
594: unset($child);
595: continue;
596: }
597:
598: $_path = $path;
599: $last = array_pop($_path) . '[]';
600: $_path[] = $last;
601: /** @var self|null $child */
602: foreach ($node as &$child) {
603: $this->_serializeId($child);
604: }
605: unset($child);
606: }
607:
608: $isList = false;
609: if (Arr::isIndexed($node)) {
610: $isList = true;
611: $last = array_pop($path) . '[]';
612: $path[] = $last;
613: }
614:
615: unset($_path);
616: foreach ($node as $key => &$child) {
617: if ($child === null || is_scalar($child)) {
618: continue;
619: }
620: if (!$isList) {
621: $_path = $path;
622: $_path[] = (string) $key;
623: }
624: /** @var self|DeferredEntityInterface<self>|DeferredRelationshipInterface<self>|DateTimeInterface|mixed[] $child */
625: $this->_serialize(
626: $child,
627: $_path ?? $path,
628: $rules,
629: $store,
630: $isList,
631: $parents,
632: );
633: }
634: }
635:
636: /**
637: * @param self[]|self|null $node
638: * @param-out ($node is self[] ? array<int|string|null> : ($node is self ? int|string|null : null)) $node
639: */
640: private function _serializeId(&$node): void
641: {
642: if ($node === null) {
643: return;
644: }
645:
646: if (is_array($node)) {
647: foreach ($node as $key => $child) {
648: $children[$key] = $child->Id;
649: }
650: $node = $children ?? [];
651: return;
652: }
653:
654: $node = $node->Id;
655: }
656:
657: /**
658: * Convert the entity to an associative array
659: *
660: * Nested objects and lists are returned as-is. Only the top-level entity is
661: * replaced.
662: *
663: * @param SyncSerializeRulesInterface<static> $rules
664: * @return array<string,mixed>
665: */
666: private function serialize(SyncSerializeRulesInterface $rules): array
667: {
668: $clone = clone $this;
669: $clone->State |= EntityState::SERIALIZING;
670: $array = SyncIntrospector::get(static::class)
671: ->getSerializeClosure($rules)($clone);
672:
673: if (!$rules->getIncludeCanonicalId()) {
674: unset($array[
675: SyncIntrospector::get(static::class)
676: ->maybeNormalise('CanonicalId', NormaliserFlag::CAREFUL)
677: ]);
678: }
679:
680: return $array;
681: }
682:
683: /**
684: * @internal
685: *
686: * @return array<string,mixed>
687: */
688: final public function jsonSerialize(): array
689: {
690: return $this->toArray();
691: }
692:
693: /**
694: * @param SyncProviderInterface $provider
695: * @param SyncContextInterface|null $context
696: */
697: final public static function provide(
698: array $data,
699: ProviderInterface $provider,
700: ?ProviderContextInterface $context = null
701: ) {
702: $container = $context
703: ? $context->getContainer()
704: : $provider->getContainer();
705: $container = $container->inContextOf(get_class($provider));
706:
707: $context = ($context ?? $provider->getContext())->withContainer($container);
708:
709: $closure = SyncIntrospector::getService(
710: $container, static::class
711: )->getCreateSyncEntityFromClosure();
712:
713: return $closure($data, $provider, $context);
714: }
715:
716: /**
717: * @param SyncProviderInterface $provider
718: * @param SyncContextInterface|null $context
719: */
720: final public static function provideMultiple(
721: iterable $list,
722: ProviderInterface $provider,
723: int $conformity = ListConformity::NONE,
724: ?ProviderContextInterface $context = null
725: ): FluentIteratorInterface {
726: return IterableIterator::from(
727: self::_provideMultiple($list, $provider, $conformity, $context)
728: );
729: }
730:
731: /**
732: * @inheritDoc
733: */
734: final public static function idFromNameOrId(
735: $nameOrId,
736: $providerOrContext,
737: ?float $uncertaintyThreshold = null,
738: ?string $nameProperty = null,
739: ?float &$uncertainty = null
740: ) {
741: if ($nameOrId === null) {
742: $uncertainty = null;
743: return null;
744: }
745:
746: if ($providerOrContext instanceof SyncProviderInterface) {
747: $provider = $providerOrContext;
748: $context = $provider->getContext();
749: } else {
750: $context = $providerOrContext;
751: $provider = $context->getProvider();
752: }
753:
754: if ($provider->isValidIdentifier($nameOrId, static::class)) {
755: $uncertainty = 0.0;
756: return $nameOrId;
757: }
758:
759: $entity = $provider
760: ->with(static::class, $context)
761: ->getResolver(
762: $nameProperty,
763: Algorithm::SAME | Algorithm::CONTAINS | Algorithm::NGRAM_SIMILARITY | Flag::NORMALISE,
764: $uncertaintyThreshold,
765: null,
766: true,
767: )
768: ->getByName((string) $nameOrId, $uncertainty);
769:
770: if ($entity) {
771: return $entity->Id;
772: }
773:
774: throw new SyncEntityNotFoundException(
775: $provider,
776: static::class,
777: $nameProperty === null
778: ? ['name' => $nameOrId]
779: : [$nameProperty => $nameOrId],
780: );
781: }
782:
783: /**
784: * @inheritDoc
785: */
786: public function postLoad(): void {}
787:
788: /**
789: * @return array<string,mixed>
790: */
791: public function __serialize(): array
792: {
793: foreach ([
794: ...SyncIntrospector::get(static::class)->SerializableProperties,
795: 'MetaProperties',
796: 'MetaPropertyNames',
797: ] as $property) {
798: $data[$property] = $this->{$property};
799: }
800:
801: $data['Provider'] = $this->Provider === null
802: ? null
803: : $this->Provider->getStore()->getProviderSignature($this->Provider);
804:
805: return $data;
806: }
807:
808: /**
809: * @param array<string,mixed> $data
810: */
811: public function __unserialize(array $data): void
812: {
813: foreach ($data as $property => $value) {
814: if ($property === 'Provider' && $value !== null) {
815: $value = is_string($value) && Sync::isLoaded() && Sync::hasProvider($value)
816: ? Sync::getProvider($value)
817: : null;
818: if ($value === null) {
819: throw new UnexpectedValueException('Cannot unserialize missing provider');
820: }
821: }
822: $this->{$property} = $value;
823: }
824: }
825:
826: /**
827: * @param iterable<array-key,mixed[]> $list
828: * @param SyncProviderInterface $provider
829: * @param ListConformity::* $conformity
830: * @param SyncContextInterface|null $context
831: * @return Generator<array-key,static>
832: */
833: private static function _provideMultiple(
834: iterable $list,
835: ProviderInterface $provider,
836: $conformity,
837: ?ProviderContextInterface $context
838: ): Generator {
839: $container = $context
840: ? $context->getContainer()
841: : $provider->getContainer();
842: $container = $container->inContextOf(get_class($provider));
843:
844: $conformity = $context
845: ? max($context->getConformity(), $conformity)
846: : $conformity;
847:
848: $context = ($context ?? $provider->getContext())->withContainer($container);
849:
850: $introspector = SyncIntrospector::getService($container, static::class);
851:
852: foreach ($list as $key => $data) {
853: if (!isset($closure)) {
854: $closure = $conformity === ListConformity::PARTIAL || $conformity === ListConformity::COMPLETE
855: ? $introspector->getCreateSyncEntityFromSignatureClosure(array_keys($data))
856: : $introspector->getCreateSyncEntityFromClosure();
857: }
858:
859: yield $key => $closure($data, $provider, $context);
860: }
861: }
862: }
863: