1: <?php declare(strict_types=1);
2:
3: namespace Salient\Sync\Support;
4:
5: use Salient\Contract\Sync\DeferredEntityInterface;
6: use Salient\Contract\Sync\SyncContextInterface;
7: use Salient\Contract\Sync\SyncEntityInterface;
8: use Salient\Contract\Sync\SyncEntityLinkType as LinkType;
9: use Salient\Contract\Sync\SyncProviderInterface;
10: use Salient\Contract\Sync\SyncStoreInterface;
11: use Closure;
12: use LogicException;
13:
14: /**
15: * The promise of a sync entity that hasn't been retrieved yet
16: *
17: * @template TEntity of SyncEntityInterface
18: *
19: * @mixin TEntity
20: *
21: * @implements DeferredEntityInterface<TEntity>
22: */
23: final class DeferredEntity implements DeferredEntityInterface
24: {
25: private SyncProviderInterface $Provider;
26: private ?SyncContextInterface $Context;
27: /** @var class-string<TEntity> */
28: private string $Entity;
29: /** @var int|string */
30: private $EntityId;
31: /** @var TEntity|static|null */
32: private $Replace = null;
33: /** @var (Closure(TEntity): void)|null */
34: private ?Closure $Callback = null;
35: /** @var TEntity|null */
36: private ?SyncEntityInterface $Resolved = null;
37:
38: /**
39: * Creates a new DeferredEntity object
40: *
41: * @param class-string<TEntity> $entity
42: * @param int|string $entityId
43: * @param TEntity|static|null $replace
44: * @param (Closure(TEntity): void)|null $callback
45: */
46: private function __construct(
47: SyncProviderInterface $provider,
48: ?SyncContextInterface $context,
49: string $entity,
50: $entityId,
51: &$replace,
52: ?Closure $callback = null
53: ) {
54: $this->Provider = $provider;
55: $this->Context = $context;
56: $this->Entity = $entity;
57: $this->EntityId = $entityId;
58:
59: if ($callback) {
60: $this->Callback = $callback;
61: } else {
62: $this->Replace = &$replace;
63: $this->Replace = $this;
64: }
65:
66: $this
67: ->getStore()
68: ->registerEntity($entity)
69: ->deferEntity(
70: $this->Provider->getProviderId(),
71: $entity,
72: $entityId,
73: $this,
74: );
75: }
76:
77: /**
78: * @inheritDoc
79: */
80: public function toLink(int $type = LinkType::DEFAULT, bool $compact = true): array
81: {
82: switch ($type) {
83: case LinkType::DEFAULT:
84: case LinkType::FRIENDLY:
85: return [
86: '@type' => $this->getTypeUri($compact),
87: '@id' => $this->EntityId,
88: ];
89:
90: case LinkType::COMPACT:
91: return [
92: '@id' => $this->getUri($compact),
93: ];
94:
95: default:
96: throw new LogicException("Invalid link type: $type");
97: }
98: }
99:
100: /**
101: * @inheritDoc
102: */
103: public function getUri(bool $compact = true): string
104: {
105: return sprintf('%s/%s', $this->getTypeUri($compact), $this->EntityId);
106: }
107:
108: private function getTypeUri(bool $compact): string
109: {
110: $typeUri = $this->getStore()->getEntityUri($this->Entity, $compact);
111:
112: return $typeUri
113: ?? '/' . str_replace('\\', '/', ltrim($this->Entity, '\\'));
114: }
115:
116: /**
117: * @inheritDoc
118: */
119: public function resolve(): SyncEntityInterface
120: {
121: if ($this->Resolved !== null) {
122: return $this->Resolved;
123: }
124:
125: return $this
126: ->Provider
127: ->with($this->Entity, $this->Context)
128: ->get($this->EntityId);
129: }
130:
131: /**
132: * @inheritDoc
133: */
134: public function replace(SyncEntityInterface $entity): void
135: {
136: if ($this->Resolved !== null) {
137: // @codeCoverageIgnoreStart
138: throw new LogicException('Entity already resolved');
139: // @codeCoverageIgnoreEnd
140: }
141:
142: $this->Resolved = $entity;
143:
144: if ($this->Callback) {
145: ($this->Callback)($entity);
146: return;
147: }
148:
149: $this->Replace = $entity;
150: unset($this->Replace);
151: }
152:
153: /**
154: * @inheritDoc
155: */
156: public static function defer(
157: SyncProviderInterface $provider,
158: ?SyncContextInterface $context,
159: string $entity,
160: $entityId,
161: &$replace = null,
162: ?Closure $callback = null
163: ): void {
164: new self(
165: $provider,
166: $context,
167: $entity,
168: $entityId,
169: $replace,
170: $callback,
171: );
172: }
173:
174: /**
175: * @inheritDoc
176: */
177: public static function deferList(
178: SyncProviderInterface $provider,
179: ?SyncContextInterface $context,
180: string $entity,
181: array $entityIds,
182: &$replace = null,
183: ?Closure $callback = null
184: ): void {
185: if ($callback) {
186: foreach ($entityIds as $entityId) {
187: /** @disregard P1008 */
188: new self(
189: $provider,
190: $context,
191: $entity,
192: $entityId,
193: $null,
194: $callback,
195: );
196: }
197: return;
198: }
199:
200: $list = [];
201: $i = 0;
202: foreach ($entityIds as $entityId) {
203: new self(
204: $provider,
205: $context,
206: $entity,
207: $entityId,
208: // @phpstan-ignore offsetAccess.notFound
209: $list[$i++],
210: );
211: }
212: $replace = $list;
213: }
214:
215: /**
216: * @inheritDoc
217: */
218: public function getContext(): ?SyncContextInterface
219: {
220: return $this->Context;
221: }
222:
223: private function getStore(): SyncStoreInterface
224: {
225: return $this->Provider->getStore();
226: }
227:
228: /**
229: * @param mixed $value
230: */
231: public function __set(string $name, $value): void
232: {
233: $entity = $this->resolve();
234: $entity->{$name} = $value;
235: }
236:
237: /**
238: * @return mixed
239: */
240: public function __get(string $name)
241: {
242: $entity = $this->resolve();
243: return $entity->{$name};
244: }
245:
246: public function __isset(string $name): bool
247: {
248: $entity = $this->resolve();
249: return isset($entity->{$name});
250: }
251:
252: public function __unset(string $name): void
253: {
254: $entity = $this->resolve();
255: unset($entity->{$name});
256: }
257: }
258: