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: | |
54: | |
55: | |
56: | |
57: | |
58: | |
59: | |
60: | |
61: | |
62: | |
63: | |
64: | |
65: | |
66: | |
67: | |
68: | |
69: | |
70: | |
71: | |
72: | |
73: | |
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: | |
89: | use ProvidableTrait; |
90: | use HasNormaliser; |
91: | use RequiresContainer; |
92: | |
93: | |
94: | |
95: | |
96: | |
97: | |
98: | |
99: | |
100: | public $Id; |
101: | |
102: | |
103: | |
104: | |
105: | |
106: | |
107: | |
108: | |
109: | public $CanonicalId; |
110: | |
111: | |
112: | private $Provider; |
113: | |
114: | private $Context; |
115: | |
116: | private $State = 0; |
117: | |
118: | |
119: | |
120: | |
121: | |
122: | |
123: | private static array $RemovablePrefixRegex = []; |
124: | |
125: | |
126: | |
127: | |
128: | |
129: | |
130: | private static array $NormalisedPropertyMap = []; |
131: | |
132: | |
133: | |
134: | |
135: | public static function getPlural(): ?string |
136: | { |
137: | return Inflect::plural(Get::basename(static::class)); |
138: | } |
139: | |
140: | |
141: | |
142: | |
143: | public static function getRelationships(): array |
144: | { |
145: | return []; |
146: | } |
147: | |
148: | |
149: | |
150: | |
151: | public static function getDateProperties(): array |
152: | { |
153: | return []; |
154: | } |
155: | |
156: | |
157: | |
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: | |
169: | |
170: | public function getName(): string |
171: | { |
172: | return SyncIntrospector::get(static::class) |
173: | ->getGetNameClosure()($this); |
174: | } |
175: | |
176: | |
177: | |
178: | |
179: | |
180: | |
181: | |
182: | |
183: | |
184: | |
185: | |
186: | |
187: | protected static function buildSerializeRules(SyncSerializeRulesBuilder $rulesB): SyncSerializeRulesBuilder |
188: | { |
189: | return $rulesB; |
190: | } |
191: | |
192: | |
193: | |
194: | |
195: | |
196: | |
197: | |
198: | |
199: | |
200: | |
201: | |
202: | |
203: | |
204: | |
205: | |
206: | |
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: | |
223: | |
224: | final public function getId() |
225: | { |
226: | return $this->Id; |
227: | } |
228: | |
229: | |
230: | |
231: | |
232: | final public function getCanonicalId() |
233: | { |
234: | return $this->CanonicalId; |
235: | } |
236: | |
237: | |
238: | |
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: | |
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: | |
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: | |
266: | |
267: | |
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: | |
282: | return $class::buildSerializeRules( |
283: | SyncSerializeRules::build() |
284: | ->entity($class) |
285: | )->build(); |
286: | } |
287: | |
288: | |
289: | |
290: | |
291: | |
292: | |
293: | |
294: | |
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: | |
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: | |
339: | |
340: | final public function toArray(?SyncStoreInterface $store = null): array |
341: | { |
342: | |
343: | $rules = static::getSerializeRules(); |
344: | return $this->_toArray($rules, $store); |
345: | } |
346: | |
347: | |
348: | |
349: | |
350: | final public function toArrayWith(SyncSerializeRulesInterface $rules, ?SyncStoreInterface $store = null): array |
351: | { |
352: | |
353: | return $this->_toArray($rules, $store); |
354: | } |
355: | |
356: | |
357: | |
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: | |
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: | |
409: | |
410: | |
411: | |
412: | final public function state(): int |
413: | { |
414: | return $this->State; |
415: | } |
416: | |
417: | |
418: | |
419: | |
420: | |
421: | |
422: | |
423: | |
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: | |
436: | |
437: | |
438: | |
439: | |
440: | |
441: | |
442: | |
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: | |
465: | $service = $this->getService(); |
466: | $store ??= $this->Provider ? $this->Provider->getStore() : null; |
467: | return SyncUtil::getEntityTypeUri($service, $compact, $store); |
468: | } |
469: | |
470: | |
471: | |
472: | |
473: | |
474: | private function _toArray(SyncSerializeRulesInterface $rules, ?SyncStoreInterface $store): array |
475: | { |
476: | |
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: | |
493: | |
494: | |
495: | |
496: | |
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: | |
513: | $formatter = $rules->getDateFormatter(); |
514: | $node = $formatter->format($node); |
515: | return; |
516: | } |
517: | |
518: | |
519: | if ($node instanceof DeferredEntityInterface) { |
520: | $node = $node->toLink(LinkType::DEFAULT); |
521: | return; |
522: | } |
523: | |
524: | |
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: | |
538: | |
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: | |
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: | |
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: | |
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: | |
625: | $this->_serialize( |
626: | $child, |
627: | $_path ?? $path, |
628: | $rules, |
629: | $store, |
630: | $isList, |
631: | $parents, |
632: | ); |
633: | } |
634: | } |
635: | |
636: | |
637: | |
638: | |
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: | |
659: | |
660: | |
661: | |
662: | |
663: | |
664: | |
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: | |
685: | |
686: | |
687: | |
688: | final public function jsonSerialize(): array |
689: | { |
690: | return $this->toArray(); |
691: | } |
692: | |
693: | |
694: | |
695: | |
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: | |
718: | |
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: | |
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: | |
785: | |
786: | public function postLoad(): void {} |
787: | |
788: | |
789: | |
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: | |
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: | |
828: | |
829: | |
830: | |
831: | |
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: | |