1: <?php declare(strict_types=1);
2:
3: namespace Salient\Sync\Support;
4:
5: use Salient\Contract\Container\ContainerInterface;
6: use Salient\Contract\Core\TextComparisonAlgorithm;
7: use Salient\Contract\Iterator\FluentIteratorInterface;
8: use Salient\Contract\Sync\DeferralPolicy;
9: use Salient\Contract\Sync\HydrationPolicy;
10: use Salient\Contract\Sync\SyncContextInterface;
11: use Salient\Contract\Sync\SyncDefinitionInterface;
12: use Salient\Contract\Sync\SyncEntityInterface;
13: use Salient\Contract\Sync\SyncEntityProviderInterface;
14: use Salient\Contract\Sync\SyncEntityResolverInterface;
15: use Salient\Contract\Sync\SyncOperation;
16: use Salient\Contract\Sync\SyncProviderInterface;
17: use Salient\Contract\Sync\SyncStoreInterface;
18: use Salient\Iterator\IterableIterator;
19: use Salient\Sync\Exception\SyncOperationNotImplementedException;
20: use Salient\Sync\AbstractSyncEntity;
21: use Generator;
22: use LogicException;
23:
24: /**
25: * An interface to a SyncProviderInterface's implementation of sync operations
26: * for an SyncEntityInterface class
27: *
28: * So you can do this:
29: *
30: * ```php
31: * <?php
32: * $faculties = $provider->with(Faculty::class)->getList();
33: * ```
34: *
35: * or, if a `Faculty` provider is bound to the current global container:
36: *
37: * ```php
38: * <?php
39: * $faculties = Faculty::withDefaultProvider()->getList();
40: * ```
41: *
42: * @template TEntity of SyncEntityInterface
43: * @template TProvider of SyncProviderInterface
44: *
45: * @implements SyncEntityProviderInterface<TEntity>
46: */
47: final class SyncEntityProvider implements SyncEntityProviderInterface
48: {
49: /** @var class-string<TEntity> */
50: private $Entity;
51:
52: /**
53: * @todo Remove `SyncProviderInterface&` when Intelephense generics issues
54: * are fixed
55: *
56: * @var SyncProviderInterface&TProvider
57: */
58: private $Provider;
59:
60: /** @var SyncDefinitionInterface<TEntity,TProvider> */
61: private $Definition;
62: /** @var SyncContextInterface */
63: private $Context;
64: /** @var SyncStoreInterface */
65: private $Store;
66:
67: /**
68: * @param class-string<TEntity> $entity
69: * @param TProvider $provider
70: */
71: public function __construct(
72: ContainerInterface $container,
73: string $entity,
74: SyncProviderInterface $provider,
75: ?SyncContextInterface $context = null
76: ) {
77: if (!is_a($entity, SyncEntityInterface::class, true)) {
78: throw new LogicException(sprintf(
79: 'Does not implement %s: %s',
80: SyncEntityInterface::class,
81: $entity,
82: ));
83: }
84:
85: if ($context && $context->getProvider() !== $provider) {
86: throw new LogicException(sprintf(
87: '%s has different provider (%s, expected %s)',
88: get_class($context),
89: $context->getProvider()->getName(),
90: $provider->getName(),
91: ));
92: }
93:
94: $_entity = $entity;
95: $checked = [];
96: do {
97: $entityProvider = SyncIntrospector::entityToProvider($entity, $container);
98: if (interface_exists($entityProvider)) {
99: break;
100: }
101: $checked[] = $entityProvider;
102: $entityProvider = null;
103: $entity = get_parent_class($entity);
104: if ($entity === false
105: || $entity === AbstractSyncEntity::class
106: || !is_a($entity, SyncEntityInterface::class, true)) {
107: break;
108: }
109: } while (true);
110:
111: if ($entityProvider === null) {
112: throw new LogicException(sprintf(
113: '%s does not have a provider interface (tried: %s)',
114: $_entity,
115: implode(', ', $checked),
116: ));
117: }
118:
119: if (!is_a($provider, $entityProvider)) {
120: throw new LogicException(sprintf(
121: '%s does not implement %s',
122: get_class($provider),
123: $entityProvider
124: ));
125: }
126:
127: /** @var class-string $entity */
128: if ($entity !== $_entity && $container->getName($entity) !== $_entity) {
129: throw new LogicException(sprintf(
130: '%s cannot be serviced by provider interface %s unless it is bound to the container as %s',
131: $_entity,
132: $entityProvider,
133: $entity,
134: ));
135: }
136:
137: /** @var class-string<TEntity> $entity */
138: $this->Entity = $entity;
139: $this->Provider = $provider;
140: $this->Definition = $provider->getDefinition($entity);
141: $this->Context = $context ?? $provider->getContext($container);
142: $this->Store = $provider->getStore();
143: }
144:
145: /**
146: * @inheritDoc
147: */
148: public function getProvider(): SyncProviderInterface
149: {
150: return $this->Provider;
151: }
152:
153: /**
154: * @inheritDoc
155: */
156: public function entity(): string
157: {
158: return $this->Entity;
159: }
160:
161: /**
162: * @param SyncOperation::* $operation
163: * @param mixed ...$args
164: * @return iterable<TEntity>|TEntity
165: * @phpstan-return (
166: * $operation is SyncOperation::*_LIST
167: * ? iterable<TEntity>
168: * : TEntity
169: * )
170: */
171: private function _run($operation, ...$args)
172: {
173: $closure =
174: $this
175: ->Definition
176: ->getOperationClosure($operation);
177:
178: if (!$closure) {
179: throw new SyncOperationNotImplementedException(
180: $this->Provider,
181: $this->Entity,
182: $operation
183: );
184: }
185:
186: return $closure(
187: $this->Context->withFilter($operation, ...$args),
188: ...$args
189: );
190: }
191:
192: /**
193: * @inheritDoc
194: */
195: public function run($operation, ...$args)
196: {
197: $fromCheckpoint = $this->Store->getDeferralCheckpoint();
198: $deferralPolicy = $this->Context->getDeferralPolicy();
199:
200: if (!SyncIntrospector::isListOperation($operation)) {
201: $result = $this->_run($operation, ...$args);
202:
203: if ($deferralPolicy === DeferralPolicy::RESOLVE_LATE) {
204: $this->Store->resolveDeferrals($fromCheckpoint);
205: }
206:
207: return $result;
208: }
209:
210: switch ($deferralPolicy) {
211: case DeferralPolicy::DO_NOT_RESOLVE:
212: case DeferralPolicy::RESOLVE_EARLY:
213: $result = $this->_run($operation, ...$args);
214: break;
215:
216: case DeferralPolicy::RESOLVE_LATE:
217: $result = $this->resolveDeferredEntitiesAfterRun(
218: $fromCheckpoint,
219: $operation,
220: ...$args,
221: );
222: break;
223:
224: default:
225: throw new LogicException(sprintf(
226: 'Invalid deferral policy: %d',
227: $deferralPolicy,
228: ));
229: }
230:
231: if (!$result instanceof FluentIteratorInterface) {
232: return new IterableIterator($result);
233: }
234:
235: return $result;
236: }
237:
238: /**
239: * @param SyncOperation::* $operation
240: * @param mixed ...$args
241: * @return Generator<TEntity>
242: */
243: private function resolveDeferredEntitiesAfterRun(int $fromCheckpoint, $operation, ...$args): Generator
244: {
245: yield from $this->_run($operation, ...$args);
246: $this->Store->resolveDeferrals($fromCheckpoint);
247: }
248:
249: /**
250: * Add an entity to the backend
251: *
252: * The underlying {@see SyncProviderInterface} must implement the
253: * {@see SyncOperation::CREATE} operation, e.g. one of the following for a
254: * `Faculty` entity:
255: *
256: * ```php
257: * <?php
258: * // 1.
259: * public function createFaculty(SyncContextInterface $ctx, Faculty $entity): Faculty;
260: *
261: * // 2.
262: * public function create_Faculty(SyncContextInterface $ctx, Faculty $entity): Faculty;
263: * ```
264: *
265: * The first parameter after `SyncContextInterface $ctx`:
266: * - must be defined
267: * - must have a native type declaration, which must be the class of the
268: * entity being created
269: * - must be required
270: */
271: public function create($entity, ...$args): SyncEntityInterface
272: {
273: return $this->run(SyncOperation::CREATE, $entity, ...$args);
274: }
275:
276: /**
277: * Get an entity from the backend
278: *
279: * The underlying {@see SyncProviderInterface} must implement the
280: * {@see SyncOperation::READ} operation, e.g. one of the following for a
281: * `Faculty` entity:
282: *
283: * ```php
284: * <?php
285: * // 1.
286: * public function getFaculty(SyncContextInterface $ctx, $id): Faculty;
287: *
288: * // 2.
289: * public function get_Faculty(SyncContextInterface $ctx, $id): Faculty;
290: * ```
291: *
292: * The first parameter after `SyncContextInterface $ctx`:
293: * - must be defined
294: * - must not have a native type declaration, but may be tagged as an
295: * `int|string|null` parameter for static analysis purposes
296: * - must be nullable
297: *
298: * @param int|string|null $id
299: */
300: public function get($id, ...$args): SyncEntityInterface
301: {
302: $offline = $this->Context->getOffline();
303: if ($offline !== false) {
304: if ($id === null) {
305: throw new LogicException('$id cannot be null when working offline');
306: }
307: $entity = $this->Store->registerEntity($this->Entity)->getEntity(
308: $this->Provider->getProviderId(),
309: $this->Entity,
310: $id,
311: $offline,
312: );
313: if ($entity) {
314: return $entity;
315: }
316: }
317:
318: return $this->run(SyncOperation::READ, $id, ...$args);
319: }
320:
321: /**
322: * Update an entity in the backend
323: *
324: * The underlying {@see SyncProviderInterface} must implement the
325: * {@see SyncOperation::UPDATE} operation, e.g. one of the following for a
326: * `Faculty` entity:
327: *
328: * ```php
329: * <?php
330: * // 1.
331: * public function updateFaculty(SyncContextInterface $ctx, Faculty $entity): Faculty;
332: *
333: * // 2.
334: * public function update_Faculty(SyncContextInterface $ctx, Faculty $entity): Faculty;
335: * ```
336: *
337: * The first parameter after `SyncContextInterface $ctx`:
338: * - must be defined
339: * - must have a native type declaration, which must be the class of the
340: * entity being updated
341: * - must be required
342: */
343: public function update($entity, ...$args): SyncEntityInterface
344: {
345: return $this->run(SyncOperation::UPDATE, $entity, ...$args);
346: }
347:
348: /**
349: * Delete an entity from the backend
350: *
351: * The underlying {@see SyncProviderInterface} must implement the
352: * {@see SyncOperation::DELETE} operation, e.g. one of the following for a
353: * `Faculty` entity:
354: *
355: * ```php
356: * <?php
357: * // 1.
358: * public function deleteFaculty(SyncContextInterface $ctx, Faculty $entity): Faculty;
359: *
360: * // 2.
361: * public function delete_Faculty(SyncContextInterface $ctx, Faculty $entity): Faculty;
362: * ```
363: *
364: * The first parameter after `SyncContextInterface $ctx`:
365: * - must be defined
366: * - must have a native type declaration, which must be the class of the
367: * entity being deleted
368: * - must be required
369: *
370: * The return value:
371: * - must represent the final state of the entity before it was deleted
372: */
373: public function delete($entity, ...$args): SyncEntityInterface
374: {
375: return $this->run(SyncOperation::DELETE, $entity, ...$args);
376: }
377:
378: /**
379: * Add a list of entities to the backend
380: *
381: * The underlying {@see SyncProviderInterface} must implement the
382: * {@see SyncOperation::CREATE_LIST} operation, e.g. one of the following
383: * for a `Faculty` entity:
384: *
385: * ```php
386: * <?php
387: * // 1. With a plural entity name
388: * public function createFaculties(SyncContextInterface $ctx, iterable $entities): iterable;
389: *
390: * // 2. With a singular name
391: * public function createList_Faculty(SyncContextInterface $ctx, iterable $entities): iterable;
392: * ```
393: *
394: * The first parameter after `SyncContextInterface $ctx`:
395: * - must be defined
396: * - must have a native type declaration, which must be `iterable`
397: * - must be required
398: *
399: * @param iterable<TEntity> $entities
400: * @return FluentIteratorInterface<array-key,TEntity>
401: */
402: public function createList(iterable $entities, ...$args): FluentIteratorInterface
403: {
404: return $this->run(SyncOperation::CREATE_LIST, $entities, ...$args);
405: }
406:
407: /**
408: * Get a list of entities from the backend
409: *
410: * The underlying {@see SyncProviderInterface} must implement the
411: * {@see SyncOperation::READ_LIST} operation, e.g. one of the following for
412: * a `Faculty` entity:
413: *
414: * ```php
415: * <?php
416: * // 1. With a plural entity name
417: * public function getFaculties(SyncContextInterface $ctx): iterable;
418: *
419: * // 2. With a singular name
420: * public function getList_Faculty(SyncContextInterface $ctx): iterable;
421: * ```
422: *
423: * @return FluentIteratorInterface<array-key,TEntity>
424: */
425: public function getList(...$args): FluentIteratorInterface
426: {
427: return $this->run(SyncOperation::READ_LIST, ...$args);
428: }
429:
430: /**
431: * Update a list of entities in the backend
432: *
433: * The underlying {@see SyncProviderInterface} must implement the
434: * {@see SyncOperation::UPDATE_LIST} operation, e.g. one of the following
435: * for a `Faculty` entity:
436: *
437: * ```php
438: * <?php
439: * // 1. With a plural entity name
440: * public function updateFaculties(SyncContextInterface $ctx, iterable $entities): iterable;
441: *
442: * // 2. With a singular name
443: * public function updateList_Faculty(SyncContextInterface $ctx, iterable $entities): iterable;
444: * ```
445: *
446: * The first parameter after `SyncContextInterface $ctx`:
447: * - must be defined
448: * - must have a native type declaration, which must be `iterable`
449: * - must be required
450: *
451: * @param iterable<TEntity> $entities
452: * @return FluentIteratorInterface<array-key,TEntity>
453: */
454: public function updateList(iterable $entities, ...$args): FluentIteratorInterface
455: {
456: return $this->run(SyncOperation::UPDATE_LIST, $entities, ...$args);
457: }
458:
459: /**
460: * Delete a list of entities from the backend
461: *
462: * The underlying {@see SyncProviderInterface} must implement the
463: * {@see SyncOperation::DELETE_LIST} operation, e.g. one of the following
464: * for a `Faculty` entity:
465: *
466: * ```php
467: * <?php
468: * // 1. With a plural entity name
469: * public function deleteFaculties(SyncContextInterface $ctx, iterable $entities): iterable;
470: *
471: * // 2. With a singular name
472: * public function deleteList_Faculty(SyncContextInterface $ctx, iterable $entities): iterable;
473: * ```
474: *
475: * The first parameter after `SyncContextInterface $ctx`:
476: * - must be defined
477: * - must have a native type declaration, which must be `iterable`
478: * - must be required
479: *
480: * The return value:
481: * - must represent the final state of the entities before they were deleted
482: *
483: * @param iterable<TEntity> $entities
484: * @return FluentIteratorInterface<array-key,TEntity>
485: */
486: public function deleteList(iterable $entities, ...$args): FluentIteratorInterface
487: {
488: return $this->run(SyncOperation::DELETE_LIST, $entities, ...$args);
489: }
490:
491: public function runA($operation, ...$args): array
492: {
493: if (!SyncIntrospector::isListOperation($operation)) {
494: throw new LogicException('Not a *_LIST operation: ' . $operation);
495: }
496:
497: $fromCheckpoint = $this->Store->getDeferralCheckpoint();
498: $deferralPolicy = $this->Context->getDeferralPolicy();
499:
500: $result = $this->_run($operation, ...$args);
501: if (!is_array($result)) {
502: $result = iterator_to_array($result, false);
503: }
504:
505: if ($deferralPolicy === DeferralPolicy::RESOLVE_LATE) {
506: $this->Store->resolveDeferrals($fromCheckpoint);
507: }
508:
509: return $result;
510: }
511:
512: public function createListA(iterable $entities, ...$args): array
513: {
514: return $this->runA(SyncOperation::CREATE_LIST, $entities, ...$args);
515: }
516:
517: public function getListA(...$args): array
518: {
519: return $this->runA(SyncOperation::READ_LIST, ...$args);
520: }
521:
522: public function updateListA(iterable $entities, ...$args): array
523: {
524: return $this->runA(SyncOperation::UPDATE_LIST, $entities, ...$args);
525: }
526:
527: public function deleteListA(iterable $entities, ...$args): array
528: {
529: return $this->runA(SyncOperation::DELETE_LIST, $entities, ...$args);
530: }
531:
532: /**
533: * @inheritDoc
534: */
535: public function online()
536: {
537: $this->Context = $this->Context->online();
538: return $this;
539: }
540:
541: /**
542: * @inheritDoc
543: */
544: public function offline()
545: {
546: $this->Context = $this->Context->offline();
547: return $this;
548: }
549:
550: /**
551: * @inheritDoc
552: */
553: public function offlineFirst()
554: {
555: $this->Context = $this->Context->offlineFirst();
556: return $this;
557: }
558:
559: /**
560: * @inheritDoc
561: */
562: public function doNotResolve()
563: {
564: $this->Context = $this->Context->withDeferralPolicy(
565: DeferralPolicy::DO_NOT_RESOLVE,
566: );
567: return $this;
568: }
569:
570: /**
571: * @inheritDoc
572: */
573: public function resolveEarly()
574: {
575: $this->Context = $this->Context->withDeferralPolicy(
576: DeferralPolicy::RESOLVE_EARLY,
577: );
578: return $this;
579: }
580:
581: /**
582: * @inheritDoc
583: */
584: public function resolveLate()
585: {
586: $this->Context = $this->Context->withDeferralPolicy(
587: DeferralPolicy::RESOLVE_LATE,
588: );
589: return $this;
590: }
591:
592: /**
593: * @inheritDoc
594: */
595: public function doNotHydrate()
596: {
597: $this->Context = $this->Context->withHydrationPolicy(
598: HydrationPolicy::SUPPRESS,
599: );
600:
601: return $this;
602: }
603:
604: /**
605: * @inheritDoc
606: */
607: public function hydrate(
608: int $policy = HydrationPolicy::EAGER,
609: ?string $entity = null,
610: $depth = null
611: ) {
612: $this->Context = $this->Context->withHydrationPolicy(
613: $policy,
614: $entity,
615: $depth,
616: );
617: return $this;
618: }
619:
620: /**
621: * @inheritDoc
622: */
623: public function getResolver(
624: ?string $nameProperty = null,
625: int $algorithm = TextComparisonAlgorithm::SAME,
626: $uncertaintyThreshold = null,
627: ?string $weightProperty = null,
628: bool $requireOneMatch = false
629: ): SyncEntityResolverInterface {
630: if ($nameProperty !== null
631: && $algorithm === TextComparisonAlgorithm::SAME
632: && $weightProperty === null
633: && !$requireOneMatch) {
634: return new SyncEntityResolver($this, $nameProperty);
635: }
636: return new SyncEntityFuzzyResolver(
637: $this,
638: $nameProperty,
639: $algorithm,
640: $uncertaintyThreshold,
641: $weightProperty,
642: $requireOneMatch,
643: );
644: }
645: }
646: