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