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\Providable; |
8: | use Salient\Contract\Core\Entity\Temporal; |
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\Sync\DeferredEntityInterface; |
15: | use Salient\Contract\Sync\DeferredRelationshipInterface; |
16: | use Salient\Contract\Sync\EntityState; |
17: | use Salient\Contract\Sync\LinkType; |
18: | use Salient\Contract\Sync\SyncContextInterface; |
19: | use Salient\Contract\Sync\SyncEntityInterface; |
20: | use Salient\Contract\Sync\SyncEntityProviderInterface; |
21: | use Salient\Contract\Sync\SyncProviderInterface; |
22: | use Salient\Contract\Sync\SyncSerializeRulesInterface; |
23: | use Salient\Contract\Sync\SyncStoreInterface; |
24: | use Salient\Core\Concern\ConstructibleTrait; |
25: | use Salient\Core\Concern\ExtensibleTrait; |
26: | use Salient\Core\Concern\ProvidableTrait; |
27: | use Salient\Core\Concern\ReadableTrait; |
28: | use Salient\Core\Concern\WritableTrait; |
29: | use Salient\Core\Date\DateFormatter; |
30: | use Salient\Core\Facade\Sync; |
31: | use Salient\Sync\Exception\SyncEntityNotFoundException; |
32: | use Salient\Sync\Reflection\SyncEntityReflection; |
33: | use Salient\Sync\Support\SyncIntrospector; |
34: | use Salient\Utility\Arr; |
35: | use Salient\Utility\Get; |
36: | use Salient\Utility\Inflect; |
37: | use Salient\Utility\Regex; |
38: | use Salient\Utility\Str; |
39: | use DateTimeInterface; |
40: | use LogicException; |
41: | use ReflectionClass; |
42: | use UnexpectedValueException; |
43: | |
44: | abstract class AbstractSyncEntity implements |
45: | SyncEntityInterface, |
46: | Temporal, |
47: | Flushable |
48: | { |
49: | use ConstructibleTrait; |
50: | use ExtensibleTrait; |
51: | use ReadableTrait; |
52: | use WritableTrait; |
53: | |
54: | use ProvidableTrait; |
55: | use RequiresContainer; |
56: | |
57: | |
58: | |
59: | |
60: | |
61: | |
62: | public $Id; |
63: | |
64: | |
65: | |
66: | |
67: | |
68: | |
69: | public $CanonicalId; |
70: | |
71: | |
72: | |
73: | |
74: | private ?ProviderInterface $Provider = null; |
75: | |
76: | private ?ProviderContextInterface $Context = null; |
77: | |
78: | private int $State = 0; |
79: | |
80: | |
81: | |
82: | |
83: | |
84: | |
85: | private static array $RemovablePrefixRegex = []; |
86: | |
87: | |
88: | |
89: | |
90: | |
91: | |
92: | private static array $NormalisedNames = []; |
93: | |
94: | |
95: | |
96: | |
97: | |
98: | |
99: | private static array $SerializableNames; |
100: | |
101: | |
102: | |
103: | |
104: | |
105: | |
106: | public static function getPlural(): ?string |
107: | { |
108: | return Inflect::plural(Get::basename(static::class)); |
109: | } |
110: | |
111: | |
112: | |
113: | |
114: | public static function getRelationships(): array |
115: | { |
116: | return []; |
117: | } |
118: | |
119: | |
120: | |
121: | |
122: | public static function getDateProperties(): array |
123: | { |
124: | return []; |
125: | } |
126: | |
127: | |
128: | |
129: | |
130: | public static function flushStatic(): void |
131: | { |
132: | unset( |
133: | self::$RemovablePrefixRegex[static::class], |
134: | self::$NormalisedNames[static::class], |
135: | self::$SerializableNames[static::class], |
136: | ); |
137: | } |
138: | |
139: | |
140: | |
141: | |
142: | public function getName(): string |
143: | { |
144: | return SyncIntrospector::get(static::class) |
145: | ->getGetNameClosure()($this); |
146: | } |
147: | |
148: | |
149: | |
150: | |
151: | |
152: | |
153: | |
154: | |
155: | |
156: | |
157: | |
158: | |
159: | protected static function buildSerializeRules(SyncSerializeRulesBuilder $rulesB): SyncSerializeRulesBuilder |
160: | { |
161: | return $rulesB; |
162: | } |
163: | |
164: | |
165: | |
166: | |
167: | |
168: | |
169: | |
170: | |
171: | |
172: | |
173: | |
174: | |
175: | |
176: | |
177: | |
178: | |
179: | |
180: | protected static function getRemovablePrefixes(): ?array |
181: | { |
182: | $current = new ReflectionClass(static::class); |
183: | do { |
184: | $prefixes[] = Get::basename($current->getName()); |
185: | $current = $current->getParentClass(); |
186: | } while ($current && $current->isSubclassOf(self::class)); |
187: | |
188: | return self::expandPrefixes($prefixes); |
189: | } |
190: | |
191: | |
192: | |
193: | |
194: | |
195: | |
196: | final public function getId() |
197: | { |
198: | return $this->Id; |
199: | } |
200: | |
201: | |
202: | |
203: | |
204: | final public function getCanonicalId() |
205: | { |
206: | return $this->CanonicalId; |
207: | } |
208: | |
209: | |
210: | |
211: | |
212: | final public static function getDefaultProvider(ContainerInterface $container): SyncProviderInterface |
213: | { |
214: | return $container->get(SyncUtil::getEntityTypeProvider(static::class, SyncUtil::getStore($container))); |
215: | } |
216: | |
217: | |
218: | |
219: | |
220: | final public static function withDefaultProvider(ContainerInterface $container, ?SyncContextInterface $context = null): SyncEntityProviderInterface |
221: | { |
222: | return self::getDefaultProvider($container)->with(static::class, $context); |
223: | } |
224: | |
225: | |
226: | |
227: | |
228: | final public static function getSerializeRules(): SyncSerializeRulesInterface |
229: | { |
230: | return static::buildSerializeRules( |
231: | SyncSerializeRules::build() |
232: | ->entity(static::class) |
233: | )->build(); |
234: | } |
235: | |
236: | |
237: | |
238: | |
239: | |
240: | |
241: | final protected static function getParentSerializeRules(): ?SyncSerializeRules |
242: | { |
243: | $class = get_parent_class(get_called_class()); |
244: | |
245: | if ( |
246: | $class === false |
247: | || !is_a($class, self::class, true) |
248: | || $class === self::class |
249: | ) { |
250: | return null; |
251: | } |
252: | |
253: | |
254: | return $class::buildSerializeRules( |
255: | SyncSerializeRules::build() |
256: | ->entity($class) |
257: | )->build(); |
258: | } |
259: | |
260: | |
261: | |
262: | |
263: | |
264: | |
265: | |
266: | |
267: | |
268: | |
269: | final public static function normaliseProperty( |
270: | string $name, |
271: | bool $fromData = true, |
272: | string ...$declaredName |
273: | ): string { |
274: | if (isset(self::$NormalisedNames[static::class][$name])) { |
275: | return self::$NormalisedNames[static::class][$name]; |
276: | } |
277: | |
278: | $_name = Str::snake($name); |
279: | |
280: | if ( |
281: | $fromData |
282: | && (!$declaredName || !in_array($_name, $declaredName)) |
283: | && ($regex = self::$RemovablePrefixRegex[static::class] ??= |
284: | self::getRemovablePrefixRegex()) !== false |
285: | ) { |
286: | $_name = Regex::replace($regex, '', $_name); |
287: | } |
288: | |
289: | return self::$NormalisedNames[static::class][$name] = $_name; |
290: | } |
291: | |
292: | |
293: | |
294: | |
295: | private static function getRemovablePrefixRegex() |
296: | { |
297: | $prefixes = static::getRemovablePrefixes(); |
298: | |
299: | return $prefixes |
300: | ? sprintf( |
301: | count($prefixes) > 1 ? '/^(?:%s)_/' : '/^%s_/', |
302: | implode('|', $prefixes), |
303: | ) |
304: | : false; |
305: | } |
306: | |
307: | |
308: | |
309: | |
310: | final public function toArray(?SyncStoreInterface $store = null): array |
311: | { |
312: | |
313: | $rules = self::getSerializeRules(); |
314: | return $this->_toArray($rules, $store); |
315: | } |
316: | |
317: | |
318: | |
319: | |
320: | final public function toArrayWith(SyncSerializeRulesInterface $rules, ?SyncStoreInterface $store = null): array |
321: | { |
322: | |
323: | return $this->_toArray($rules, $store); |
324: | } |
325: | |
326: | |
327: | |
328: | |
329: | final public function toLink(?SyncStoreInterface $store = null, int $type = LinkType::DEFAULT, bool $compact = true): array |
330: | { |
331: | switch ($type) { |
332: | case LinkType::DEFAULT: |
333: | return [ |
334: | '@type' => $this->getTypeUri($store, $compact), |
335: | '@id' => $this->Id === null |
336: | ? spl_object_id($this) |
337: | : $this->Id, |
338: | ]; |
339: | |
340: | case LinkType::COMPACT: |
341: | return [ |
342: | '@id' => $this->getUri($store, $compact), |
343: | ]; |
344: | |
345: | case LinkType::FRIENDLY: |
346: | return Arr::whereNotEmpty([ |
347: | '@type' => $this->getTypeUri($store, $compact), |
348: | '@id' => $this->Id === null |
349: | ? spl_object_id($this) |
350: | : $this->Id, |
351: | '@name' => $this->getName(), |
352: | '@description' => |
353: | $this instanceof HasDescription |
354: | ? $this->getDescription() |
355: | : null, |
356: | ]); |
357: | |
358: | default: |
359: | throw new LogicException("Invalid link type: $type"); |
360: | } |
361: | } |
362: | |
363: | |
364: | |
365: | |
366: | final public function getUri(?SyncStoreInterface $store = null, bool $compact = true): string |
367: | { |
368: | return sprintf( |
369: | '%s/%s', |
370: | $this->getTypeUri($store, $compact), |
371: | $this->Id === null |
372: | ? spl_object_id($this) |
373: | : $this->Id |
374: | ); |
375: | } |
376: | |
377: | |
378: | |
379: | |
380: | |
381: | |
382: | final public function state(): int |
383: | { |
384: | return $this->State; |
385: | } |
386: | |
387: | |
388: | |
389: | |
390: | |
391: | |
392: | |
393: | |
394: | |
395: | final protected static function normalisePrefixes(array $prefixes): array |
396: | { |
397: | if (!$prefixes) { |
398: | return []; |
399: | } |
400: | |
401: | return Arr::snakeCase($prefixes); |
402: | } |
403: | |
404: | |
405: | |
406: | |
407: | |
408: | |
409: | |
410: | |
411: | |
412: | |
413: | |
414: | final protected static function expandPrefixes(array $prefixes): array |
415: | { |
416: | if (!$prefixes) { |
417: | return []; |
418: | } |
419: | |
420: | foreach ($prefixes as $prefix) { |
421: | $prefix = Str::snake($prefix); |
422: | $expanded[$prefix] = true; |
423: | $prefix = explode('_', $prefix); |
424: | while (array_shift($prefix)) { |
425: | $expanded[implode('_', $prefix)] = true; |
426: | } |
427: | } |
428: | |
429: | return array_keys($expanded); |
430: | } |
431: | |
432: | private function getTypeUri(?SyncStoreInterface $store, bool $compact): string |
433: | { |
434: | |
435: | $service = $this->getService(); |
436: | $store ??= $this->Provider ? $this->Provider->getStore() : null; |
437: | return SyncUtil::getEntityTypeUri($service, $compact, $store); |
438: | } |
439: | |
440: | |
441: | |
442: | |
443: | |
444: | private function _toArray(SyncSerializeRulesInterface $rules, ?SyncStoreInterface $store): array |
445: | { |
446: | |
447: | if ($rules->getDateFormatter() === null) { |
448: | $rules = $rules->withDateFormatter( |
449: | $this->Provider |
450: | ? $this->Provider->getDateFormatter() |
451: | : new DateFormatter() |
452: | ); |
453: | } |
454: | |
455: | $array = $this; |
456: | $this->_serialize($array, [], $rules, $store); |
457: | |
458: | return (array) $array; |
459: | } |
460: | |
461: | |
462: | |
463: | |
464: | |
465: | |
466: | |
467: | |
468: | private function _serialize( |
469: | &$node, |
470: | array $path, |
471: | SyncSerializeRulesInterface $rules, |
472: | ?SyncStoreInterface $store, |
473: | bool $nodeIsList = false, |
474: | array $parents = [] |
475: | ): void { |
476: | $maxDepth = $rules->getMaxDepth(); |
477: | if (count($path) > $maxDepth) { |
478: | throw new UnexpectedValueException('In too deep: ' . implode('.', $path)); |
479: | } |
480: | |
481: | if ($node instanceof DateTimeInterface) { |
482: | |
483: | $formatter = $rules->getDateFormatter(); |
484: | $node = $formatter->format($node); |
485: | return; |
486: | } |
487: | |
488: | |
489: | if ($node instanceof DeferredEntityInterface) { |
490: | $node = $node->toLink(LinkType::DEFAULT); |
491: | return; |
492: | } |
493: | |
494: | |
495: | if ($node instanceof DeferredRelationshipInterface) { |
496: | $node = null; |
497: | return; |
498: | } |
499: | |
500: | if ($node instanceof AbstractSyncEntity) { |
501: | if ($path && $rules->getForSyncStore()) { |
502: | $node = $node->toLink($store, LinkType::DEFAULT); |
503: | return; |
504: | } |
505: | |
506: | if ($rules->getDetectRecursion()) { |
507: | |
508: | |
509: | if ($parents[spl_object_id($node)] ?? false) { |
510: | $node = $node->toLink($store, LinkType::DEFAULT); |
511: | $node['@why'] = 'Circular reference detected'; |
512: | return; |
513: | } |
514: | $parents[spl_object_id($node)] = true; |
515: | } |
516: | |
517: | $class = get_class($node); |
518: | $node = $node->serialize($rules); |
519: | } |
520: | |
521: | $delete = $rules->getRemovableKeys($class ?? null, null, $path); |
522: | $replace = $rules->getReplaceableKeys($class ?? null, null, $path); |
523: | |
524: | |
525: | $delete = array_diff_key($delete, $replace); |
526: | |
527: | if ($delete) { |
528: | $node = array_diff_key($node, $delete); |
529: | } |
530: | |
531: | $replace = array_intersect_key($replace, $node + ['[]' => null]); |
532: | |
533: | foreach ($replace as $key => [$newKey, $closure]) { |
534: | if ($key !== '[]') { |
535: | if ($newKey !== null && $key !== $newKey) { |
536: | $node = Arr::rename($node, $key, $newKey); |
537: | $key = $newKey; |
538: | } |
539: | |
540: | if ($closure) { |
541: | if ($node[$key] !== null) { |
542: | $node[$key] = $closure($node[$key], $store); |
543: | } |
544: | continue; |
545: | } |
546: | |
547: | |
548: | $child = &$node[$key]; |
549: | $this->_serializeId($child); |
550: | unset($child); |
551: | continue; |
552: | } |
553: | |
554: | if (!$nodeIsList) { |
555: | continue; |
556: | } |
557: | |
558: | if ($closure) { |
559: | foreach ($node as &$child) { |
560: | if ($child !== null) { |
561: | $child = $closure($child, $store); |
562: | } |
563: | } |
564: | unset($child); |
565: | continue; |
566: | } |
567: | |
568: | $_path = $path; |
569: | $last = array_pop($_path) . '[]'; |
570: | $_path[] = $last; |
571: | |
572: | foreach ($node as &$child) { |
573: | $this->_serializeId($child); |
574: | } |
575: | unset($child); |
576: | } |
577: | |
578: | $isList = false; |
579: | if (Arr::hasNumericKeys($node)) { |
580: | $isList = true; |
581: | $last = array_pop($path) . '[]'; |
582: | $path[] = $last; |
583: | } |
584: | |
585: | unset($_path); |
586: | foreach ($node as $key => &$child) { |
587: | if ($child === null || is_scalar($child)) { |
588: | continue; |
589: | } |
590: | if (!$isList) { |
591: | $_path = $path; |
592: | $_path[] = (string) $key; |
593: | } |
594: | |
595: | $this->_serialize( |
596: | $child, |
597: | $_path ?? $path, |
598: | $rules, |
599: | $store, |
600: | $isList, |
601: | $parents, |
602: | ); |
603: | } |
604: | } |
605: | |
606: | |
607: | |
608: | |
609: | |
610: | private function _serializeId(&$node): void |
611: | { |
612: | if ($node === null) { |
613: | return; |
614: | } |
615: | |
616: | if (is_array($node)) { |
617: | foreach ($node as $key => $child) { |
618: | $children[$key] = $child->Id; |
619: | } |
620: | $node = $children ?? []; |
621: | return; |
622: | } |
623: | |
624: | $node = $node->Id; |
625: | } |
626: | |
627: | |
628: | |
629: | |
630: | |
631: | |
632: | |
633: | |
634: | |
635: | |
636: | private function serialize(SyncSerializeRulesInterface $rules): array |
637: | { |
638: | $clone = clone $this; |
639: | $clone->State |= EntityState::SERIALIZING; |
640: | $array = SyncIntrospector::get(static::class) |
641: | ->getSerializeClosure($rules)($clone); |
642: | |
643: | if (!$rules->getCanonicalId()) { |
644: | unset($array[self::normaliseProperty('CanonicalId', false)]); |
645: | } |
646: | |
647: | return $array; |
648: | } |
649: | |
650: | |
651: | |
652: | |
653: | |
654: | |
655: | final public function jsonSerialize(): array |
656: | { |
657: | return $this->toArray(); |
658: | } |
659: | |
660: | |
661: | |
662: | |
663: | final public static function provide( |
664: | array $data, |
665: | ProviderContextInterface $context |
666: | ) { |
667: | $provider = $context->getProvider(); |
668: | $container = $context |
669: | ->getContainer() |
670: | ->inContextOf(get_class($provider)); |
671: | $context = $context->withContainer($container); |
672: | |
673: | $closure = SyncIntrospector::getService($container, static::class) |
674: | ->getCreateSyncEntityFromClosure(); |
675: | |
676: | return $closure($data, $provider, $context); |
677: | } |
678: | |
679: | |
680: | |
681: | |
682: | final public static function provideMultiple( |
683: | iterable $data, |
684: | ProviderContextInterface $context, |
685: | int $conformity = Providable::CONFORMITY_NONE |
686: | ): iterable { |
687: | $provider = $context->getProvider(); |
688: | $container = $context |
689: | ->getContainer() |
690: | ->inContextOf(get_class($provider)); |
691: | $context = $context->withContainer($container); |
692: | $conformity = max($context->getConformity(), $conformity); |
693: | $introspector = SyncIntrospector::getService($container, static::class); |
694: | |
695: | foreach ($data as $key => $data) { |
696: | if (!isset($closure)) { |
697: | $closure = $conformity === self::CONFORMITY_PARTIAL || $conformity === self::CONFORMITY_COMPLETE |
698: | ? $introspector->getCreateSyncEntityFromSignatureClosure(array_keys($data)) |
699: | : $introspector->getCreateSyncEntityFromClosure(); |
700: | } |
701: | |
702: | yield $key => $closure($data, $provider, $context); |
703: | } |
704: | } |
705: | |
706: | |
707: | |
708: | |
709: | final public static function idFromNameOrId( |
710: | $nameOrId, |
711: | $providerOrContext, |
712: | ?float $uncertaintyThreshold = null, |
713: | ?string $nameProperty = null, |
714: | ?float &$uncertainty = null |
715: | ) { |
716: | if ($nameOrId === null) { |
717: | $uncertainty = null; |
718: | return null; |
719: | } |
720: | |
721: | if ($providerOrContext instanceof SyncProviderInterface) { |
722: | $provider = $providerOrContext; |
723: | $context = $provider->getContext(); |
724: | } else { |
725: | $context = $providerOrContext; |
726: | $provider = $context->getProvider(); |
727: | } |
728: | |
729: | if ($provider->isValidIdentifier($nameOrId, static::class)) { |
730: | $uncertainty = 0.0; |
731: | return $nameOrId; |
732: | } |
733: | |
734: | $entity = $provider |
735: | ->with(static::class, $context) |
736: | ->getResolver( |
737: | $nameProperty, |
738: | SyncEntityProviderInterface::ALGORITHM_SAME |
739: | | SyncEntityProviderInterface::ALGORITHM_CONTAINS |
740: | | SyncEntityProviderInterface::ALGORITHM_NGRAM_SIMILARITY |
741: | | SyncEntityProviderInterface::NORMALISE, |
742: | $uncertaintyThreshold, |
743: | null, |
744: | true, |
745: | ) |
746: | ->getByName((string) $nameOrId, $uncertainty); |
747: | |
748: | if ($entity) { |
749: | return $entity->Id; |
750: | } |
751: | |
752: | throw new SyncEntityNotFoundException( |
753: | $provider, |
754: | static::class, |
755: | $nameProperty === null |
756: | ? ['name' => $nameOrId] |
757: | : [$nameProperty => $nameOrId], |
758: | ); |
759: | } |
760: | |
761: | |
762: | |
763: | |
764: | public function __serialize(): array |
765: | { |
766: | $properties = self::$SerializableNames[static::class] ??= [ |
767: | ...(new SyncEntityReflection(static::class))->getSerializableNames(), |
768: | static::getDynamicPropertiesProperty(), |
769: | static::getDynamicPropertyNamesProperty(), |
770: | ]; |
771: | |
772: | foreach ($properties as $property) { |
773: | $data[$property] = $this->{$property}; |
774: | } |
775: | |
776: | $data['Provider'] = $this->Provider === null |
777: | ? null |
778: | : $this->Provider->getStore()->getProviderSignature($this->Provider); |
779: | |
780: | return $data; |
781: | } |
782: | |
783: | |
784: | |
785: | |
786: | public function __unserialize(array $data): void |
787: | { |
788: | foreach ($data as $property => $value) { |
789: | if ($property === 'Provider' && $value !== null) { |
790: | $value = is_string($value) && Sync::isLoaded() && Sync::hasProvider($value) |
791: | ? Sync::getProvider($value) |
792: | : null; |
793: | if ($value === null) { |
794: | throw new UnexpectedValueException('Cannot unserialize missing provider'); |
795: | } |
796: | } |
797: | $this->{$property} = $value; |
798: | } |
799: | } |
800: | } |
801: | |