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