Skip to content

Commit 51a08b6

Browse files
committed
Sync: review sync context handling
- Rename `claimFilterValue()` to `claimFilter()` - Rename `getFilter()` to `getFilters()` - Add `DeferredSyncEntityPolicy` - Add `withDeferredSyncEntityPolicy()` and `getDeferredSyncEntityPolicy()` to `ISyncContext` / `SyncContext` - Allow entities resolved by `SyncStore::resolveDeferredEntities()` to be scoped to entities deferred since a checkpoint returned by `SyncStore::getDeferredEntityCheckpoint()` - Apply deferred sync entity policies in `SyncEntityProvider`
1 parent 5eef59f commit 51a08b6

File tree

11 files changed

+260
-44
lines changed

11 files changed

+260
-44
lines changed

src/Facade/Sync.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
* @method static SyncStore entity(int $providerId, class-string<ISyncEntity> $entityType, int|string $entityId, ISyncEntity $entity) Register a sync entity (see {@see SyncStore::entity()})
2727
* @method static SyncStore entityType(class-string<ISyncEntity> $entity) Register a sync entity type and set its ID (unless already registered) (see {@see SyncStore::entityType()})
2828
* @method static SyncStore error(SyncError|SyncErrorBuilder $error, bool $deduplicate = false, bool $toConsole = false) Report an error that occurred during a sync operation
29+
* @method static int getDeferredEntityCheckpoint() Get a checkpoint to delineate between entities already in the deferred entity queue and any subsequently deferred sync entities (see {@see SyncStore::getDeferredEntityCheckpoint()})
2930
* @method static ISyncEntity|null getEntity(int $providerId, class-string<ISyncEntity> $entityType, int|string $entityId, bool|null $offline = null) Get a previously registered and/or stored sync entity (see {@see SyncStore::getEntity()})
3031
* @method static string|null getEntityTypeNamespace(class-string<ISyncEntity> $entity) Get the namespace of a sync entity type (see {@see SyncStore::getEntityTypeNamespace()})
3132
* @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()})
@@ -39,7 +40,7 @@
3940
* @method static SyncStore namespace(string $prefix, string $uri, string $namespace, class-string<ISyncClassResolver>|null $resolver = null) Register a sync entity namespace (see {@see SyncStore::namespace()})
4041
* @method static SyncStore provider(ISyncProvider $provider) Register a sync provider and set its provider ID (see {@see SyncStore::provider()})
4142
* @method static SyncStore reportErrors(string $successText = 'No sync errors recorded') Report sync operation errors to the console (see {@see SyncStore::reportErrors()})
42-
* @method static ISyncEntity[] resolveDeferredEntities(?int $providerId = null, class-string<ISyncEntity> $entityType = null, bool|null $offline = null) Resolve deferred sync entities from their respective providers and/or the local entity store (see {@see SyncStore::resolveDeferredEntities()})
43+
* @method static ISyncEntity[] resolveDeferredEntities(?int $fromCheckpoint = null, ?int $providerId = null, class-string<ISyncEntity> $entityType = null, bool|null $offline = null) Resolve deferred sync entities from their respective providers and/or the local entity store (see {@see SyncStore::resolveDeferredEntities()})
4344
*
4445
* @uses SyncStore
4546
*
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Lkrms\Sync\Catalog;
4+
5+
use Lkrms\Concept\Enumeration;
6+
use Lkrms\Sync\Support\DeferredSyncEntity;
7+
use Lkrms\Sync\Support\SyncStore;
8+
9+
/**
10+
* Policies for sync entity deferral
11+
*
12+
* @extends Enumeration<int>
13+
*/
14+
final class DeferredSyncEntityPolicy extends Enumeration
15+
{
16+
/**
17+
* Do not resolve deferred entities
18+
*
19+
* If {@see SyncStore::resolveDeferredEntities()} is not called manually,
20+
* unresolved {@see DeferredSyncEntity} instances may appear in object
21+
* graphs returned by sync operations.
22+
*/
23+
public const DO_NOT_RESOLVE = 0;
24+
25+
/**
26+
* Resolve deferred entities immediately
27+
*
28+
* This is the least efficient policy because it produces the most round
29+
* trips, but it does guarantee the return of fully resolved object graphs.
30+
*/
31+
public const RESOLVE_EARLY = 1;
32+
33+
/**
34+
* Resolve deferred entities after reaching the end of each stream of entity
35+
* data
36+
*
37+
* This policy minimises the number of round trips to the backend, but
38+
* unresolved {@see DeferredSyncEntity} instances may appear in object
39+
* graphs until they have been fully traversed.
40+
*/
41+
public const RESOLVE_LATE = 2;
42+
}

src/Sync/Command/GetSyncEntities.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
use Lkrms\Cli\CliOption;
88
use Lkrms\Facade\Console;
99
use Lkrms\Facade\File;
10-
use Lkrms\Sync\Contract\ISyncContext;
1110
use Lkrms\Sync\Contract\ISyncEntity;
1211
use Lkrms\Sync\Contract\ISyncProvider;
13-
use Lkrms\Sync\Support\SyncContext;
1412
use Lkrms\Sync\Support\SyncIntrospector;
1513
use Lkrms\Sync\Support\SyncSerializeRules;
1614
use Lkrms\Utility\Convert;

src/Sync/Concept/SyncDefinition.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
* @property-read TProvider $Provider The ISyncProvider servicing the entity
3434
* @property-read array<SyncOperation::*> $Operations A list of supported sync operations
3535
* @property-read ArrayKeyConformity::* $Conformity The conformity level of data returned by the provider for this entity
36-
* @property-read SyncFilterPolicy::* $FilterPolicy The action to take when filters are ignored by the provider
36+
* @property-read SyncFilterPolicy::* $FilterPolicy The action to take when filters are unclaimed by the provider
3737
* @property-read array<SyncOperation::*,Closure(ISyncDefinition<TEntity,TProvider>, SyncOperation::*, ISyncContext, mixed...): mixed> $Overrides An array that maps sync operations to closures that override any other implementations
3838
* @property-read IPipeline<mixed[],TEntity,array{0:int,1:ISyncContext,2?:int|string|TEntity|TEntity[]|null,...}>|null $PipelineFromBackend A pipeline that maps data from the provider to entity-compatible associative arrays, or `null` if mapping is not required
3939
* @property-read IPipeline<TEntity,mixed[],array{0:int,1:ISyncContext,2?:int|string|TEntity|TEntity[]|null,...}>|null $PipelineToBackend A pipeline that maps serialized entities to data compatible with the provider, or `null` if mapping is not required
@@ -101,13 +101,13 @@ abstract protected function getClosure(int $operation): ?Closure;
101101
protected $Conformity;
102102

103103
/**
104-
* The action to take when filters are ignored by the provider
104+
* The action to take when filters are unclaimed by the provider
105105
*
106106
* To prevent a request for entities that meet one or more criteria
107107
* inadvertently reaching the backend as a request for a larger set of
108108
* entities--if not all of them--the default policy if there are unclaimed
109109
* filters is {@see SyncFilterPolicy::THROW_EXCEPTION}. See
110-
* {@see SyncFilterPolicy} for alternative policies or
110+
* {@see SyncFilterPolicy} for alternative policies and
111111
* {@see ISyncContext::withArgs()} for more information about filters.
112112
*
113113
* @var SyncFilterPolicy::*
@@ -369,7 +369,7 @@ function (array $data, IPipeline $pipeline, $arg) use (&$ctx, &$closure) {
369369
}
370370

371371
/**
372-
* Enforce the ignored filter policy
372+
* Enforce the unclaimed filter policy
373373
*
374374
* @param SyncOperation::* $operation
375375
* @param array{}|null $empty
@@ -381,7 +381,7 @@ final protected function applyFilterPolicy(int $operation, ISyncContext $ctx, ?b
381381
$returnEmpty = false;
382382

383383
if (SyncFilterPolicy::IGNORE === $this->FilterPolicy ||
384-
!($filter = $ctx->getFilter())) {
384+
!($filter = $ctx->getFilters())) {
385385
return;
386386
}
387387

src/Sync/Contract/ISyncContext.php

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,25 @@
33
namespace Lkrms\Sync\Contract;
44

55
use Lkrms\Contract\IProviderContext;
6+
use Lkrms\Sync\Catalog\DeferredSyncEntityPolicy;
67
use Lkrms\Sync\Catalog\SyncFilterPolicy;
78
use Lkrms\Sync\Catalog\SyncOperation;
89
use Lkrms\Sync\Exception\SyncInvalidFilterException;
910

1011
/**
11-
* The context within which a sync entity is instantiated
12+
* The context within which a sync entity is instantiated by a provider
1213
*
1314
*/
1415
interface ISyncContext extends IProviderContext
1516
{
1617
/**
17-
* Convert non-mandatory sync operation arguments to a normalised filter and
18-
* add it to the context
18+
* Normalise non-mandatory sync operation arguments and add them to the
19+
* context
1920
*
2021
* If, after removing the operation's mandatory arguments from `$args`, the
2122
* remaining values match one of the following signatures, they are mapped
22-
* to an associative array and returned by {@see ISyncContext::getFilter()}
23-
* until updated by another call to {@see ISyncContext::withArgs()}.
23+
* to an associative array surfaced by {@see ISyncContext::getFilters()} and
24+
* {@see ISyncContext::claimFilter()}:
2425
*
2526
* 1. One array argument (`fn(...$mandatoryArgs, array $filter)`)
2627
* - Alphanumeric keys are converted to snake_case
@@ -41,9 +42,12 @@ interface ISyncContext extends IProviderContext
4142
* If `$args` doesn't match any of these, a
4243
* {@see SyncInvalidFilterException} is thrown.
4344
*
44-
* Using {@see ISyncContext::claimFilterValue()} to claim values from the
45-
* filter is recommended. Depending on the provider's
46-
* {@see SyncFilterPolicy}, unclaimed values may cause requests to fail.
45+
* Using {@see ISyncContext::claimFilter()} to claim filters is recommended.
46+
* Depending on the provider's {@see SyncFilterPolicy}, unclaimed filters
47+
* may cause requests to fail.
48+
*
49+
* When a filter is claimed, it is removed from the context.
50+
* {@see ISyncContext::getFilters()} only returns unclaimed filters.
4751
*
4852
* {@see ISyncEntity} objects are replaced with the return value of
4953
* {@see ISyncEntity::id()} when `$args` contains an array or a list of
@@ -57,27 +61,50 @@ interface ISyncContext extends IProviderContext
5761
public function withArgs(int $operation, ...$args);
5862

5963
/**
60-
* Use a callback to enforce the provider's ignored filter policy
64+
* Use a callback to enforce the provider's unclaimed filter policy
6165
*
6266
* Allows providers to enforce their {@see SyncFilterPolicy} by calling
6367
* {@see ISyncContext::maybeApplyFilterPolicy()} in scenarios where
6468
* enforcement before a sync operation starts isn't possible.
6569
*
70+
* @see ISyncContext::maybeApplyFilterPolicy()
71+
*
6672
* @param (callable(ISyncContext, ?bool &$returnEmpty, array{}|null &$empty): void)|null $callback
6773
* @return $this
6874
*/
6975
public function withFilterPolicyCallback(?callable $callback);
7076

7177
/**
72-
* Run the ignored filter policy callback (if provided)
78+
* Apply a deferred sync entity policy to the context
79+
*
80+
* @param DeferredSyncEntityPolicy::* $policy
81+
* @return $this
82+
*/
83+
public function withDeferredSyncEntityPolicy(int $policy);
84+
85+
/**
86+
* Run the unclaimed filter policy callback
7387
*
7488
* Example:
7589
*
7690
* ```php
7791
* <?php
78-
* $ctx->maybeApplyFilterPolicy($returnEmpty, $empty);
79-
* if ($returnEmpty) {
80-
* return $empty;
92+
* class Provider extends \Lkrms\Sync\Concept\HttpSyncProvider
93+
* {
94+
* public function getList_Entity(\Lkrms\Sync\Contract\ISyncContext $ctx): iterable
95+
* {
96+
* if ($ctx->claimFilter('pending')) {
97+
* $entryTypes[] = 0;
98+
* }
99+
* if ($ctx->claimFilter('completed')) {
100+
* $entryTypes[] = 1;
101+
* }
102+
* $ctx->maybeApplyFilterPolicy($returnEmpty, $empty);
103+
* if ($returnEmpty) {
104+
* return $empty;
105+
* }
106+
* // ...
107+
* }
81108
* }
82109
* ```
83110
*
@@ -92,19 +119,26 @@ public function maybeApplyFilterPolicy(?bool &$returnEmpty, &$empty): void;
92119
*
93120
* @return array<string,mixed>
94121
*/
95-
public function getFilter(): array;
122+
public function getFilters(): array;
96123

97124
/**
98125
* Get a value from the filter most recently passed via optional sync
99126
* operation arguments
100127
*
101128
* Unlike other {@see ISyncContext} methods,
102-
* {@see ISyncContext::claimFilterValue()} modifies the object it is called
129+
* {@see ISyncContext::claimFilter()} modifies the object it is called
103130
* on instead of returning a modified clone.
104131
*
105132
* @return mixed `null` if the value has already been claimed or wasn't
106133
* passed to the operation.
107134
* @see ISyncContext::withArgs()
108135
*/
109-
public function claimFilterValue(string $key);
136+
public function claimFilter(string $key);
137+
138+
/**
139+
* Get the deferred sync entity policy applied to the context
140+
*
141+
* @return DeferredSyncEntityPolicy::*
142+
*/
143+
public function getDeferredSyncEntityPolicy(): int;
110144
}

src/Sync/Support/DbSyncDefinitionBuilder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* @method $this operations(array<SyncOperation::*> $value) A list of supported sync operations
2727
* @method $this table(?string $value) Set DbSyncDefinition::$Table
2828
* @method $this conformity(ArrayKeyConformity::* $value) The conformity level of data returned by the provider for this entity (see {@see SyncDefinition::$Conformity})
29-
* @method $this filterPolicy(SyncFilterPolicy::* $value) The action to take when filters are ignored by the provider (see {@see SyncDefinition::$FilterPolicy})
29+
* @method $this filterPolicy(SyncFilterPolicy::* $value) The action to take when filters are unclaimed by the provider (see {@see SyncDefinition::$FilterPolicy})
3030
* @method $this overrides(array<SyncOperation::*,Closure(ISyncDefinition<TEntity,TProvider>, SyncOperation::*, ISyncContext, mixed...): mixed> $value) An array that maps sync operations to closures that override any other implementations (see {@see SyncDefinition::$Overrides})
3131
* @method $this pipelineFromBackend(IPipeline<mixed[],TEntity,array{0:int,1:ISyncContext,2?:int|string|TEntity|TEntity[]|null,...}>|null $value) A pipeline that maps data from the provider to entity-compatible associative arrays, or `null` if mapping is not required
3232
* @method $this pipelineToBackend(IPipeline<TEntity,mixed[],array{0:int,1:ISyncContext,2?:int|string|TEntity|TEntity[]|null,...}>|null $value) A pipeline that maps serialized entities to data compatible with the provider, or `null` if mapping is not required

src/Sync/Support/HttpSyncDefinitionBuilder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
* @method $this pager(?ICurlerPager $value) The pagination handler for the endpoint servicing the entity (see {@see HttpSyncDefinition::$Pager})
3333
* @method $this callback((callable(HttpSyncDefinition<TEntity,TProvider>, SyncOperation::*, ISyncContext, mixed...): HttpSyncDefinition<TEntity,TProvider>)|null $value) A callback applied to the definition before every sync operation (see {@see HttpSyncDefinition::$Callback})
3434
* @method $this conformity(ArrayKeyConformity::* $value) The conformity level of data returned by the provider for this entity (see {@see SyncDefinition::$Conformity})
35-
* @method $this filterPolicy(SyncFilterPolicy::* $value) The action to take when filters are ignored by the provider (see {@see SyncDefinition::$FilterPolicy})
35+
* @method $this filterPolicy(SyncFilterPolicy::* $value) The action to take when filters are unclaimed by the provider (see {@see SyncDefinition::$FilterPolicy})
3636
* @method $this expiry(?int $value) The time, in seconds, before responses from the provider expire (see {@see HttpSyncDefinition::$Expiry})
3737
* @method $this methodMap(array<SyncOperation::*,string> $value) An array that maps sync operations to HTTP request methods (see {@see HttpSyncDefinition::$MethodMap})
3838
* @method $this syncOneEntityPerRequest(bool $value = true) If true, perform CREATE_LIST, UPDATE_LIST and DELETE_LIST operations on one entity per HTTP request (default: false)

src/Sync/Support/SyncContext.php

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Lkrms\Sync\Support;
44

55
use Lkrms\Support\ProviderContext;
6+
use Lkrms\Sync\Catalog\DeferredSyncEntityPolicy;
67
use Lkrms\Sync\Catalog\SyncOperation;
78
use Lkrms\Sync\Contract\ISyncContext;
89
use Lkrms\Sync\Contract\ISyncEntity;
@@ -11,26 +12,37 @@
1112
use Lkrms\Utility\Test;
1213

1314
/**
14-
* The context within which a sync entity is instantiated
15+
* The context within which a sync entity is instantiated by a provider
1516
*
1617
*/
1718
final class SyncContext extends ProviderContext implements ISyncContext
1819
{
1920
/**
2021
* @var array<string,mixed>
2122
*/
22-
protected array $Filter = [];
23+
protected array $Filters = [];
2324

2425
/**
2526
* @var (callable(ISyncContext, ?bool &$returnEmpty, mixed &$empty): void)|null
2627
*/
2728
protected $FilterPolicyCallback;
2829

30+
/**
31+
* @var DeferredSyncEntityPolicy::*
32+
*/
33+
protected $DeferredSyncEntityPolicy = DeferredSyncEntityPolicy::RESOLVE_EARLY;
34+
35+
/**
36+
* @inheritDoc
37+
*/
2938
public function withFilterPolicyCallback(?callable $callback)
3039
{
3140
return $this->withPropertyValue('FilterPolicyCallback', $callback);
3241
}
3342

43+
/**
44+
* @inheritDoc
45+
*/
3446
public function maybeApplyFilterPolicy(?bool &$returnEmpty, &$empty): void
3547
{
3648
$returnEmpty = false;
@@ -40,6 +52,9 @@ public function maybeApplyFilterPolicy(?bool &$returnEmpty, &$empty): void
4052
}
4153
}
4254

55+
/**
56+
* @inheritDoc
57+
*/
4358
public function withArgs(int $operation, ...$args)
4459
{
4560
// READ_LIST is the only operation with no mandatory argument after
@@ -49,11 +64,11 @@ public function withArgs(int $operation, ...$args)
4964
}
5065

5166
if (empty($args)) {
52-
return $this->withPropertyValue('Filter', []);
67+
return $this->withPropertyValue('Filters', []);
5368
}
5469

5570
if (is_array($args[0]) && count($args) === 1) {
56-
return $this->withPropertyValue('Filter', array_combine(
71+
return $this->withPropertyValue('Filters', array_combine(
5772
array_map(
5873
fn($key) =>
5974
preg_match('/[^[:alnum:]_-]/', $key) ? $key : Convert::toSnakeCase($key),
@@ -68,7 +83,7 @@ public function withArgs(int $operation, ...$args)
6883
}
6984

7085
if (Test::isArrayOfArrayKey($args)) {
71-
return $this->withPropertyValue('Filter', ['id' => $args]);
86+
return $this->withPropertyValue('Filters', ['id' => $args]);
7287
}
7388

7489
if (Test::isArrayOf($args, ISyncEntity::class)) {
@@ -88,20 +103,42 @@ public function withArgs(int $operation, ...$args)
88103
throw new SyncInvalidFilterException(...$args);
89104
}
90105

91-
public function getFilter(): array
106+
/**
107+
* @inheritDoc
108+
*/
109+
public function withDeferredSyncEntityPolicy(int $policy)
92110
{
93-
return $this->Filter;
111+
return $this->withPropertyValue('DeferredSyncEntityPolicy', $policy);
94112
}
95113

96-
public function claimFilterValue(string $key)
114+
/**
115+
* @inheritDoc
116+
*/
117+
public function getFilters(): array
118+
{
119+
return $this->Filters;
120+
}
121+
122+
/**
123+
* @inheritDoc
124+
*/
125+
public function claimFilter(string $key)
97126
{
98-
if (array_key_exists($key, $this->Filter)) {
99-
$value = $this->Filter[$key];
100-
unset($this->Filter[$key]);
127+
if (array_key_exists($key, $this->Filters)) {
128+
$value = $this->Filters[$key];
129+
unset($this->Filters[$key]);
101130

102131
return $value;
103132
}
104133

105134
return null;
106135
}
136+
137+
/**
138+
* @inheritDoc
139+
*/
140+
public function getDeferredSyncEntityPolicy(): int
141+
{
142+
return $this->DeferredSyncEntityPolicy;
143+
}
107144
}

0 commit comments

Comments
 (0)