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