Skip to content

Commit dadec86

Browse files
committed
Sync: defer registration of entity types, fix bootstrapping issues
- Add `Sync::getProviderId()`, which returns a value even if it has to start a run to do so - In `DeferredSyncEntity`, register entity types before deferring them - Update `Sync` facade
1 parent 25c378a commit dadec86

File tree

4 files changed

+113
-23
lines changed

4 files changed

+113
-23
lines changed

src/Facade/Sync.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Lkrms\Sync\Contract\ISyncClassResolver;
88
use Lkrms\Sync\Contract\ISyncEntity;
99
use Lkrms\Sync\Contract\ISyncProvider;
10+
use Lkrms\Sync\Support\DeferredSyncEntity;
1011
use Lkrms\Sync\Support\SyncError;
1112
use Lkrms\Sync\Support\SyncErrorBuilder as ErrorBuilder;
1213
use Lkrms\Sync\Support\SyncErrorCollection;
@@ -21,13 +22,17 @@
2122
* @method static void unload() Clear the underlying SyncStore instance
2223
* @method static SyncStore checkHeartbeats(int $ttl = 300, bool $failEarly = true, ISyncProvider ...$providers) Throw an exception if a provider has an unreachable backend (see {@see SyncStore::checkHeartbeats()})
2324
* @method static SyncStore close(?int $exitStatus = 0) Terminate the current run and close the database
24-
* @method static SyncStore entityType(class-string<ISyncEntity> $entity) Register a sync entity type and set its ID (unless already registered)
25+
* @method static SyncStore deferredEntity(int $providerId, class-string<ISyncEntity> $entityType, int|string $entityId, DeferredSyncEntity $deferred) Register a deferred sync entity (see {@see SyncStore::deferredEntity()})
26+
* @method static SyncStore entity(int $providerId, class-string<ISyncEntity> $entityType, int|string $entityId, ISyncEntity $entity) Register a sync entity (see {@see SyncStore::entity()})
27+
* @method static SyncStore entityType(class-string<ISyncEntity> $entity) Register a sync entity type and set its ID (unless already registered) (see {@see SyncStore::entityType()})
2528
* @method static SyncStore error(SyncError|ErrorBuilder $error, bool $deduplicate = false, bool $toConsole = false) Report an error that occurred during a sync operation
29+
* @method static ISyncEntity|null getEntity(int $providerId, class-string<ISyncEntity> $entityType, int|string $entityId) Get a previously registered sync entity
2630
* @method static string|null getEntityTypeNamespace(class-string<ISyncEntity> $entity) Get the namespace of a sync entity type (see {@see SyncStore::getEntityTypeNamespace()})
2731
* @method static string|null getEntityTypeUri(class-string<ISyncEntity> $entity, bool $compact = true) Get the canonical URI of a sync entity type (see {@see SyncStore::getEntityTypeUri()})
2832
* @method static SyncErrorCollection getErrors() Get sync operation errors recorded so far
2933
* @method static string|null getFilename() Get the filename of the database
3034
* @method static class-string<ISyncClassResolver>|null getNamespaceResolver(class-string<ISyncEntity|ISyncProvider> $class) Get the class resolver for an entity or provider's namespace
35+
* @method static int getProviderId(ISyncProvider $provider) Get the provider ID of a registered sync provider, starting a run if necessary
3136
* @method static int getRunId() Get the run ID of the current run
3237
* @method static string getRunUuid(bool $binary = false) Get the UUID of the current run (see {@see SyncStore::getRunUuid()})
3338
* @method static bool isOpen() Check if a database is open

src/Sync/Concept/SyncProvider.php

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
abstract class SyncProvider extends Provider implements ISyncProvider, IService
2727
{
2828
/**
29-
* Get a dependency subtitution map for the class
29+
* Get a dependency subtitution map for the provider
3030
*
3131
* {@inheritDoc}
3232
*
33-
* Bind any {@see ISyncEntity} classes customised for this provider to their
34-
* generic parent classes by overriding this method, e.g.:
33+
* Override this method to bind any {@see ISyncEntity} classes customised
34+
* for the provider to their generic parent classes, e.g.:
3535
*
3636
* ```php
3737
* <?php
@@ -65,24 +65,43 @@ public static function getContextualBindings(): array
6565
*/
6666
private $MagicMethodClosures = [];
6767

68+
/**
69+
* Creates a new provider object
70+
*
71+
* Creating an instance of the provider registers it with the entity store
72+
* injected by the container.
73+
*/
6874
public function __construct(IContainer $app, Env $env, SyncStore $store)
6975
{
7076
parent::__construct($app, $env);
7177
$this->Store = $store;
72-
7378
$this->Store->provider($this);
7479
}
7580

81+
/**
82+
* @inheritDoc
83+
*/
84+
final public function store(): SyncStore
85+
{
86+
return $this->Store;
87+
}
88+
89+
/**
90+
* @inheritDoc
91+
*/
7692
final public function setProviderId(int $providerId)
7793
{
7894
$this->Id = $providerId;
79-
8095
return $this;
8196
}
8297

83-
final public function store(): SyncStore
98+
/**
99+
* @inheritDoc
100+
*/
101+
final public function getProviderId(): ?int
84102
{
85-
return $this->Store;
103+
return $this->Id
104+
?? $this->Store->getProviderId($this);
86105
}
87106

88107
/**
@@ -108,12 +127,17 @@ final protected function buildSerializeRules(string $entity): SerializeRulesBuil
108127
->entity($entity);
109128
}
110129

130+
/**
131+
* @inheritDoc
132+
*/
111133
final public static function getServices(): array
112134
{
113135
return SyncIntrospector::get(static::class)->getSyncProviderInterfaces();
114136
}
115137

116138
/**
139+
* @inheritDoc
140+
*
117141
* @template TEntity of ISyncEntity
118142
* @param class-string<TEntity> $entity
119143
* @return SyncEntityProvider<TEntity,static>
@@ -147,9 +171,4 @@ final public function __call(string $name, array $arguments)
147171

148172
throw new LogicException('Call to undefined method: ' . static::class . "::$name()");
149173
}
150-
151-
final public function getProviderId(): ?int
152-
{
153-
return $this->Id;
154-
}
155174
}

src/Sync/Support/DeferredSyncEntity.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ private function __construct(ISyncProvider $provider, ISyncContext $context, $en
6060
$this->Replace = $this;
6161

6262
if (is_string($entity)) {
63-
$this->store()->deferredEntity($this->Provider->getProviderId(), $entity, $deferred, $this);
63+
$this->store()
64+
->entityType($entity)
65+
->deferredEntity($this->Provider->getProviderId(), $entity, $deferred, $this);
6466
}
6567
}
6668

src/Sync/Support/SyncStore.php

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ final class SyncStore extends SqliteStore
136136
*/
137137
private $DeferredProviders = [];
138138

139+
/**
140+
* Deferred entity type registrations
141+
*
142+
* @var array<class-string<ISyncEntity>,true>
143+
*/
144+
private $DeferredEntityTypes = [];
145+
139146
/**
140147
* Deferred namespace registrations
141148
*
@@ -146,6 +153,8 @@ final class SyncStore extends SqliteStore
146153
private $DeferredNamespaces = [];
147154

148155
/**
156+
* Creates a new SyncStore object
157+
*
149158
* @param string $command The canonical name of the command performing sync
150159
* operations (e.g. a qualified class and/or method name).
151160
* @param string[] $arguments Arguments passed to the command.
@@ -298,7 +307,9 @@ public function getRunUuid(bool $binary = false): string
298307
{
299308
$this->check();
300309

301-
return $binary ? $this->RunUuid : Convert::uuidToHex($this->RunUuid);
310+
return $binary
311+
? $this->RunUuid
312+
: Convert::uuidToHex($this->RunUuid);
302313
}
303314

304315
/**
@@ -323,7 +334,7 @@ public function provider(ISyncProvider $provider)
323334
$hash = Compute::binaryHash($class, ...$provider->getBackendIdentifier());
324335

325336
if (($this->ProviderMap[$hash] ?? null) !== null) {
326-
throw new LogicException("Provider already registered: $class");
337+
throw new LogicException(sprintf('Provider already registered: %s', $class));
327338
}
328339

329340
// Update `last_seen` if the provider is already in the database
@@ -369,21 +380,61 @@ public function provider(ISyncProvider $provider)
369380
return $this;
370381
}
371382

383+
/**
384+
* Get the provider ID of a registered sync provider, starting a run if
385+
* necessary
386+
*/
387+
public function getProviderId(ISyncProvider $provider): int
388+
{
389+
if ($this->RunId === null) {
390+
$this->check();
391+
}
392+
393+
$class = get_class($provider);
394+
$hash = Compute::binaryHash($class, ...$provider->getBackendIdentifier());
395+
396+
$id = $this->ProviderMap[$hash] ?? null;
397+
if ($id === null) {
398+
throw new LogicException(sprintf('Provider not registered: %s', $class));
399+
}
400+
return $id;
401+
}
402+
372403
/**
373404
* Register a sync entity type and set its ID (unless already registered)
374405
*
406+
* If a sync run has started, the entity type is registered immediately and
407+
* its ID is passed to {@see ISyncEntity::setEntityTypeId()} before
408+
* {@see SyncStore::entityType()} returns. Otherwise, registration is
409+
* deferred until a sync run starts.
410+
*
375411
* @param class-string<ISyncEntity> $entity
376412
* @return $this
377413
*/
378414
public function entityType(string $entity)
379415
{
380-
if (($this->EntityTypes[$entity] ?? null) !== null) {
416+
if (isset($this->EntityTypes[$entity]) ||
417+
($this->RunId === null && isset($this->DeferredEntityTypes[$entity]))) {
381418
return $this;
382419
}
383420

384421
$class = new ReflectionClass($entity);
422+
$name = $class->getName();
423+
424+
if ($name !== $entity &&
425+
(isset($this->EntityTypes[$name]) ||
426+
($this->RunId === null && isset($this->DeferredEntityTypes[$name])))) {
427+
return $this;
428+
}
429+
385430
if (!$class->implementsInterface(ISyncEntity::class)) {
386-
throw new LogicException("Does not implement ISyncEntity: $entity");
431+
throw new LogicException(sprintf('Does not implement ISyncEntity: %s', $entity));
432+
}
433+
434+
// Don't start a run just to register an entity type
435+
if ($this->RunId === null) {
436+
$this->DeferredEntityTypes[$name] = true;
437+
return $this;
387438
}
388439

389440
// Update `last_seen` if the entity type is already in the database
@@ -398,7 +449,7 @@ public function entityType(string $entity)
398449
last_seen = CURRENT_TIMESTAMP;
399450
SQL;
400451
$stmt = $db->prepare($sql);
401-
$stmt->bindValue(':entity_type_class', $class->name, SQLITE3_TEXT);
452+
$stmt->bindValue(':entity_type_class', $name, SQLITE3_TEXT);
402453
$stmt->execute();
403454
$stmt->close();
404455

@@ -411,7 +462,7 @@ public function entityType(string $entity)
411462
entity_type_class = :entity_type_class;
412463
SQL;
413464
$stmt = $db->prepare($sql);
414-
$stmt->bindValue(':entity_type_class', $class->name, SQLITE3_TEXT);
465+
$stmt->bindValue(':entity_type_class', $name, SQLITE3_TEXT);
415466
$result = $stmt->execute();
416467
$row = $result->fetchArray(SQLITE3_NUM);
417468
$stmt->close();
@@ -421,7 +472,7 @@ public function entityType(string $entity)
421472
}
422473

423474
$class->getMethod('setEntityTypeId')->invoke(null, $row[0]);
424-
$this->EntityTypes[$entity] = $row[0];
475+
$this->EntityTypes[$name] = $row[0];
425476

426477
return $this;
427478
}
@@ -449,13 +500,13 @@ public function entityType(string $entity)
449500
public function namespace(string $prefix, string $uri, string $namespace, ?string $resolver = null)
450501
{
451502
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*$/', $prefix)) {
452-
throw new LogicException("Invalid prefix: $prefix");
503+
throw new LogicException(sprintf('Invalid prefix: %s', $prefix));
453504
}
454505

455506
$prefix = strtolower($prefix);
456507
if (($this->RegisteredNamespaces[$prefix] ?? null) ||
457508
($this->RunId === null && ($this->DeferredNamespaces[$prefix] ?? null))) {
458-
throw new LogicException("Prefix already registered: $prefix");
509+
throw new LogicException(sprintf('Prefix already registered: %s', $prefix));
459510
}
460511

461512
$uri = rtrim($uri, '/') . '/';
@@ -607,6 +658,10 @@ private function classToNamespace(
607658
*/
608659
public function entity(int $providerId, string $entityType, $entityId, ISyncEntity $entity)
609660
{
661+
if ($this->RunId === null) {
662+
$this->check();
663+
}
664+
610665
$entityTypeId = $this->EntityTypes[$entityType];
611666
if (isset($this->Entities[$providerId][$entityTypeId][$entityId])) {
612667
throw new LogicException('Entity already registered');
@@ -651,6 +706,10 @@ public function getEntity(int $providerId, string $entityType, $entityId): ?ISyn
651706
*/
652707
public function deferredEntity(int $providerId, string $entityType, $entityId, DeferredSyncEntity $deferred)
653708
{
709+
if ($this->RunId === null) {
710+
$this->check();
711+
}
712+
654713
$entityTypeId = $this->EntityTypes[$entityType];
655714
$entity = $this->Entities[$providerId][$entityTypeId][$entityId] ?? null;
656715
if ($entity !== null) {
@@ -809,6 +868,11 @@ protected function check()
809868
}
810869
unset($this->DeferredProviders);
811870

871+
foreach (array_keys($this->DeferredEntityTypes) as $entity) {
872+
$this->entityType($entity);
873+
}
874+
unset($this->DeferredEntityTypes);
875+
812876
foreach ($this->DeferredNamespaces as $prefix => [$uri, $namespace, $resolver]) {
813877
$this->namespace($prefix, $uri, $namespace, $resolver);
814878
}

0 commit comments

Comments
 (0)