1: <?php declare(strict_types=1);
2:
3: namespace Salient\Sync\Reflection;
4:
5: use Salient\Contract\Sync\SyncContextInterface;
6: use Salient\Contract\Sync\SyncEntityInterface;
7: use Salient\Contract\Sync\SyncOperation;
8: use Salient\Contract\Sync\SyncProviderInterface;
9: use Salient\Contract\Sync\SyncStoreInterface;
10: use Salient\Core\Reflection\ClassReflection;
11: use Salient\Sync\SyncUtil;
12: use Salient\Utility\Str;
13: use Closure;
14: use ReflectionClass;
15: use ReflectionException;
16:
17: /**
18: * @template TProvider of SyncProviderInterface
19: *
20: * @extends ClassReflection<TProvider>
21: */
22: class SyncProviderReflection extends ClassReflection
23: {
24: use SyncReflectionTrait;
25:
26: /** @var array<class-string<SyncProviderInterface>,array<class-string<SyncProviderInterface>>> */
27: private static array $Interfaces = [];
28: /** @var array<class-string<SyncProviderInterface>,array<class-string<SyncEntityInterface>>> */
29: private static array $EntityTypes = [];
30: /** @var array<class-string<SyncProviderInterface>,array<string,class-string<SyncEntityInterface>>> */
31: private static array $EntityTypeBasenames = [];
32: /** @var array<class-string<SyncProviderInterface>,array<string,array{SyncOperation::*,class-string<SyncEntityInterface>}>> */
33: private static array $Methods = [];
34: /** @var array<class-string<SyncProviderInterface>,array<string,array{SyncOperation::*,class-string<SyncEntityInterface>}>> */
35: private static array $MagicMethods = [];
36: /** @var array<class-string<SyncProviderInterface>,array<class-string<SyncEntityInterface>,array<SyncOperation::*,(Closure(SyncContextInterface, mixed...): (iterable<SyncEntityInterface>|SyncEntityInterface))|false>>> */
37: private static array $Closures = [];
38: private SyncStoreInterface $Store;
39:
40: /**
41: * @param TProvider|class-string<TProvider> $provider
42: */
43: public function __construct($provider, ?SyncStoreInterface $store = null)
44: {
45: $this->assertImplements($provider, SyncProviderInterface::class);
46:
47: $this->Store = $store
48: ?? ($provider instanceof SyncProviderInterface ? $provider->getStore() : null)
49: ?? SyncUtil::getStore();
50:
51: parent::__construct($provider);
52: }
53:
54: /**
55: * Get names of interfaces that extend SyncProviderInterface
56: *
57: * @return array<class-string<SyncProviderInterface>>
58: */
59: public function getSyncProviderInterfaces(): array
60: {
61: return self::$Interfaces[$this->name] ??=
62: array_keys($this->getSyncProviderReflectionInterfaces());
63: }
64:
65: /**
66: * Get interfaces that extend SyncProviderInterface
67: *
68: * @return array<class-string<SyncProviderInterface>,ReflectionClass<SyncProviderInterface>>
69: */
70: public function getSyncProviderReflectionInterfaces(): array
71: {
72: foreach ($this->getInterfaces() as $name => $interface) {
73: if ($interface->isSubclassOf(SyncProviderInterface::class)) {
74: /** @var class-string<SyncProviderInterface> $name */
75: $interfaces[$name] = $interface;
76: }
77: }
78: return $interfaces ?? [];
79: }
80:
81: /**
82: * Get names of entity types serviced via sync provider interfaces
83: *
84: * @return array<class-string<SyncEntityInterface>>
85: */
86: public function getSyncProviderEntityTypes(): array
87: {
88: return self::$EntityTypes[$this->name] ??=
89: array_keys($this->getSyncProviderReflectionEntities());
90: }
91:
92: /**
93: * Get an array that maps unambiguous kebab-case basenames to qualified
94: * names for entity types serviced via sync provider interfaces
95: *
96: * @return array<string,class-string<SyncEntityInterface>>
97: */
98: public function getSyncProviderEntityTypeBasenames(): array
99: {
100: return self::$EntityTypeBasenames[$this->name] ??=
101: $this->doGetSyncProviderEntityTypeBasenames();
102: }
103:
104: /**
105: * @return array<string,class-string<SyncEntityInterface>>
106: */
107: private function doGetSyncProviderEntityTypeBasenames(): array
108: {
109: foreach ($this->getSyncProviderReflectionEntities() as $name => $entity) {
110: $basename = Str::kebab($entity->getShortName());
111: if (isset($basenames[$basename])) {
112: $basenames[$basename] = false;
113: continue;
114: }
115: $basenames[$basename] = $name;
116: }
117: return array_filter($basenames ?? []);
118: }
119:
120: /**
121: * Get entity types serviced via sync provider interfaces
122: *
123: * @return array<class-string<SyncEntityInterface>,SyncEntityReflection<SyncEntityInterface>>
124: */
125: public function getSyncProviderReflectionEntities(): array
126: {
127: foreach ($this->getSyncProviderInterfaces() as $interface) {
128: foreach (SyncUtil::getProviderEntityTypes($interface, $this->Store) as $entityType) {
129: $entity = new SyncEntityReflection($entityType);
130: $entities[$entity->name] = $entity;
131: }
132: }
133: return $entities ?? [];
134: }
135:
136: /**
137: * Check if the provider services a sync entity type
138: *
139: * @template T of SyncEntityInterface
140: *
141: * @param ReflectionClass<T>|class-string<T>|T $entity
142: */
143: public function isSyncEntityProvider($entity): bool
144: {
145: if ($entity instanceof ReflectionClass) {
146: /** @var class-string<T> */
147: $entity = $entity->name;
148: } elseif ($entity instanceof SyncEntityInterface) {
149: $entity = get_class($entity);
150: }
151:
152: $interface = SyncUtil::getEntityTypeProvider($entity, $this->Store);
153:
154: return interface_exists($interface)
155: && $this->implementsInterface($interface);
156: }
157:
158: /**
159: * Get a closure for a sync operation the provider has implemented via a
160: * declared method
161: *
162: * @template T of SyncEntityInterface
163: *
164: * @param SyncOperation::* $operation
165: * @param SyncEntityReflection<T>|class-string<T> $entity
166: * @param TProvider $provider
167: * @return (Closure(SyncContextInterface, mixed...): (iterable<T>|T))|null
168: */
169: public function getSyncOperationClosure(int $operation, $entity, SyncProviderInterface $provider): ?Closure
170: {
171: if (!$entity instanceof SyncEntityReflection) {
172: $entity = new SyncEntityReflection($entity);
173: }
174:
175: $closure = self::$Closures[$this->name][$entity->name][$operation] ?? null;
176: if ($closure === null) {
177: $method = $this->getSyncOperationMethod($operation, $entity);
178: if ($method !== null) {
179: $closure = fn(...$args) => $this->$method(...$args);
180: }
181: self::$Closures[$this->name][$entity->name][$operation] = $closure ?? false;
182: }
183:
184: return $closure
185: ? $closure->bindTo($provider)
186: : null;
187: }
188:
189: /**
190: * Get the name of the method that implements a sync operation if declared
191: * by the provider
192: *
193: * @param SyncOperation::* $operation
194: * @param SyncEntityReflection<SyncEntityInterface>|class-string<SyncEntityInterface> $entity
195: */
196: public function getSyncOperationMethod(int $operation, $entity): ?string
197: {
198: if (!$entity instanceof SyncEntityReflection) {
199: $entity = new SyncEntityReflection($entity);
200: }
201:
202: if (SyncUtil::isListOperation($operation)) {
203: $plural = $entity->getPluralName();
204: if ($plural !== null) {
205: $methods[] = [
206: SyncOperation::CREATE_LIST => 'create',
207: SyncOperation::READ_LIST => 'get',
208: SyncOperation::UPDATE_LIST => 'update',
209: SyncOperation::DELETE_LIST => 'delete',
210: ][$operation] . Str::lower($plural);
211: }
212: }
213:
214: $name = Str::lower($entity->getShortName());
215: switch ($operation) {
216: case SyncOperation::CREATE:
217: $methods[] = 'create' . $name;
218: $methods[] = 'create_' . $name;
219: break;
220:
221: case SyncOperation::READ:
222: $methods[] = 'get' . $name;
223: $methods[] = 'get_' . $name;
224: break;
225:
226: case SyncOperation::UPDATE:
227: $methods[] = 'update' . $name;
228: $methods[] = 'update_' . $name;
229: break;
230:
231: case SyncOperation::DELETE:
232: $methods[] = 'delete' . $name;
233: $methods[] = 'delete_' . $name;
234: break;
235:
236: case SyncOperation::CREATE_LIST:
237: $methods[] = 'createlist_' . $name;
238: break;
239:
240: case SyncOperation::READ_LIST:
241: $methods[] = 'getlist_' . $name;
242: break;
243:
244: case SyncOperation::UPDATE_LIST:
245: $methods[] = 'updatelist_' . $name;
246: break;
247:
248: case SyncOperation::DELETE_LIST:
249: $methods[] = 'deletelist_' . $name;
250: break;
251: }
252:
253: $methods = array_intersect_key(
254: $this->getSyncOperationMethods(),
255: array_flip($methods),
256: );
257:
258: if (!$methods) {
259: return null;
260: }
261:
262: if (count($methods) > 1) {
263: throw new ReflectionException(sprintf(
264: '%s has multiple implementations of one operation: %s()',
265: $this->name,
266: implode('(), ', array_keys($methods)),
267: ));
268: }
269:
270: [, $methodEntity] = reset($methods);
271: $method = key($methods);
272: if ($methodEntity !== $entity->name) {
273: throw new ReflectionException(sprintf(
274: '%s::%s() does not operate on %s',
275: $this->name,
276: $method,
277: $entity->name,
278: ));
279: }
280:
281: return $method;
282: }
283:
284: /**
285: * Get declared methods that implement sync operations
286: *
287: * @return array<string,array{SyncOperation::*,class-string<SyncEntityInterface>}>
288: */
289: public function getSyncOperationMethods(): array
290: {
291: return self::$Methods[$this->name] ??=
292: $this->filterUniqueSyncOperationMethods(true);
293: }
294:
295: /**
296: * Get methods that are not declared but could be used to implement sync
297: * operations via method overloading
298: *
299: * @return array<string,array{SyncOperation::*,class-string<SyncEntityInterface>}>
300: */
301: public function getSyncOperationMagicMethods(): array
302: {
303: return self::$MagicMethods[$this->name] ??=
304: $this->filterUniqueSyncOperationMethods(false);
305: }
306:
307: /**
308: * @return array<string,array{SyncOperation::*,class-string<SyncEntityInterface>}>
309: */
310: private function filterUniqueSyncOperationMethods(bool $visible): array
311: {
312: foreach ($this->getUniqueSyncOperationMethods() as $method => $operation) {
313: if (!($visible xor (
314: $this->hasMethod($method)
315: && $this->getMethod($method)->isPublic()
316: ))) {
317: $methods[$method] = $operation;
318: }
319: }
320: return $methods ?? [];
321: }
322:
323: /**
324: * @return array<string,array{SyncOperation::*,class-string<SyncEntityInterface>}>
325: */
326: private function getUniqueSyncOperationMethods(): array
327: {
328: foreach ($this->getPossibleSyncOperationMethods() as $method => $operation) {
329: if (isset($methods[$method])) {
330: $methods[$method] = false;
331: continue;
332: }
333: $methods[$method] = $operation;
334: }
335: return array_filter($methods ?? []);
336: }
337:
338: /**
339: * @return iterable<string,array{SyncOperation::*,class-string<SyncEntityInterface>}>
340: */
341: private function getPossibleSyncOperationMethods(): iterable
342: {
343: foreach ($this->getSyncProviderReflectionEntities() as $entity) {
344: $plural = $entity->getPluralName();
345: if ($plural !== null) {
346: $plural = Str::lower($plural);
347: yield from [
348: 'create' . $plural => [SyncOperation::CREATE_LIST, $entity->name],
349: 'get' . $plural => [SyncOperation::READ_LIST, $entity->name],
350: 'update' . $plural => [SyncOperation::UPDATE_LIST, $entity->name],
351: 'delete' . $plural => [SyncOperation::DELETE_LIST, $entity->name],
352: ];
353: }
354:
355: $name = Str::lower($entity->getShortName());
356: yield from [
357: 'create' . $name => [SyncOperation::CREATE, $entity->name],
358: 'create_' . $name => [SyncOperation::CREATE, $entity->name],
359: 'get' . $name => [SyncOperation::READ, $entity->name],
360: 'get_' . $name => [SyncOperation::READ, $entity->name],
361: 'update' . $name => [SyncOperation::UPDATE, $entity->name],
362: 'update_' . $name => [SyncOperation::UPDATE, $entity->name],
363: 'delete' . $name => [SyncOperation::DELETE, $entity->name],
364: 'delete_' . $name => [SyncOperation::DELETE, $entity->name],
365: 'createlist_' . $name => [SyncOperation::CREATE_LIST, $entity->name],
366: 'getlist_' . $name => [SyncOperation::READ_LIST, $entity->name],
367: 'updatelist_' . $name => [SyncOperation::UPDATE_LIST, $entity->name],
368: 'deletelist_' . $name => [SyncOperation::DELETE_LIST, $entity->name],
369: ];
370: }
371: }
372: }
373: