Skip to content

Commit f405a86

Browse files
committed
Merge branch 'sync-prevent-recursion'
2 parents b3bbb16 + 30256e6 commit f405a86

File tree

13 files changed

+149
-76
lines changed

13 files changed

+149
-76
lines changed

lk-util/Command/Generate/Concept/GenerateCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ abstract class GenerateCommand extends Command
200200
protected string $InputClassType;
201201

202202
/**
203-
* @var Introspector<object,Provider,Entity,ProviderContext>
203+
* @var Introspector<object,Provider,Entity,ProviderContext<Provider,Entity>>
204204
*/
205205
protected Introspector $InputIntrospector;
206206

src/Util/Concept/Builder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ protected static function getTerminators(): array
4040
protected ContainerInterface $Container;
4141

4242
/**
43-
* @var Introspector<object,Provider,Entity,ProviderContext>
43+
* @var Introspector<object,Provider,Entity,ProviderContext<Provider,Entity>>
4444
*/
4545
private Introspector $Introspector;
4646

src/Util/Concept/Entity.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
/**
99
* Base class for entities
1010
*
11-
* @implements IProviderEntity<Provider,ProviderContext>
11+
* @implements IProviderEntity<Provider,ProviderContext<Provider,self>>
1212
*/
1313
abstract class Entity implements IProviderEntity {}

src/Util/Concept/Provider.php

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
use Lkrms\Container\ContainerInterface;
66
use Lkrms\Contract\IProvider;
7+
use Lkrms\Contract\IProviderContext;
78
use Lkrms\Support\Date\DateFormatterInterface;
89
use Lkrms\Support\ProviderContext;
910
use Salient\Core\Exception\MethodNotImplementedException;
1011

1112
/**
1213
* Base class for providers
1314
*
14-
* @implements IProvider<ProviderContext>
15+
* @implements IProvider<ProviderContext<static,Entity>>
1516
*/
1617
abstract class Provider implements IProvider
1718
{
@@ -39,11 +40,9 @@ abstract protected function getDateFormatter(): DateFormatterInterface;
3940
/**
4041
* @inheritDoc
4142
*/
42-
public function getContext(?ContainerInterface $container = null): ProviderContext
43+
public function getContext(?ContainerInterface $container = null): IProviderContext
4344
{
44-
if (!$container) {
45-
$container = $this->App;
46-
}
45+
$container ??= $this->App;
4746

4847
return $container->get(ProviderContext::class, [$this]);
4948
}
@@ -81,17 +80,15 @@ final public function container(): ContainerInterface
8180
*/
8281
final public function dateFormatter(): DateFormatterInterface
8382
{
84-
return $this->DateFormatter
85-
?? ($this->DateFormatter = $this->getDateFormatter());
83+
return $this->DateFormatter ??= $this->getDateFormatter();
8684
}
8785

8886
/**
89-
* Get the date formatter cached by dateFormatter(), or null if it hasn't
90-
* been cached
87+
* Check if the date formatter returned by dateFormatter() has been cached
9188
*/
92-
final protected function getCachedDateFormatter(): ?DateFormatterInterface
89+
final protected function hasDateFormatter(): bool
9390
{
94-
return $this->DateFormatter ?? null;
91+
return isset($this->DateFormatter);
9592
}
9693

9794
/**

src/Util/Support/Introspector.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ class Introspector
107107
* @template T of object
108108
*
109109
* @param class-string<T> $service
110-
* @return static<T,Provider,Entity,ProviderContext>
110+
* @return static<T,Provider,Entity,ProviderContext<Provider,Entity>>
111111
*/
112112
public static function getService(ContainerInterface $container, string $service)
113113
{
@@ -126,7 +126,7 @@ public static function getService(ContainerInterface $container, string $service
126126
* @template T of object
127127
*
128128
* @param class-string<T> $class
129-
* @return static<T,Provider,Entity,ProviderContext>
129+
* @return static<T,Provider,Entity,ProviderContext<Provider,Entity>>
130130
*/
131131
public static function get(string $class)
132132
{

src/Util/Support/ProviderContext.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@
1414
use Salient\Core\Utility\Str;
1515

1616
/**
17-
* The context within which an entity is instantiated by a provider
17+
* The context within which entities of a given type are instantiated by a
18+
* provider
1819
*
19-
* @implements IProviderContext<IProvider<static>,IProvidable<IProvider<static>,static>>
20+
* @template TProvider of IProvider
21+
* @template TEntity of IProvidable
22+
*
23+
* @implements IProviderContext<TProvider,TEntity>
2024
*/
2125
class ProviderContext implements IProviderContext
2226
{
@@ -25,12 +29,12 @@ class ProviderContext implements IProviderContext
2529
protected ContainerInterface $Container;
2630

2731
/**
28-
* @var IProvider<static>
32+
* @var TProvider
2933
*/
3034
protected IProvider $Provider;
3135

3236
/**
33-
* @var array<IProvidable<IProvider<static>,static>>
37+
* @var TEntity[]
3438
*/
3539
protected array $Stack = [];
3640

@@ -40,7 +44,7 @@ class ProviderContext implements IProviderContext
4044
protected array $Values = [];
4145

4246
/**
43-
* @var (IProvidable<IProvider<static>,static>&ITreeable)|null
47+
* @var (TEntity&ITreeable)|null
4448
*/
4549
protected ?ITreeable $Parent = null;
4650

@@ -52,7 +56,7 @@ class ProviderContext implements IProviderContext
5256
/**
5357
* Creates a new ProviderContext object
5458
*
55-
* @param IProvider<static> $provider
59+
* @param TProvider $provider
5660
*/
5761
public function __construct(
5862
ContainerInterface $container,

src/Util/Sync/Command/GetSyncEntities.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Lkrms\Cli\Exception\CliInvalidArgumentsException;
77
use Lkrms\Cli\CliOption;
88
use Lkrms\Facade\Console;
9+
use Lkrms\Sync\Catalog\DeferralPolicy;
910
use Lkrms\Sync\Catalog\HydrationPolicy;
1011
use Lkrms\Sync\Contract\ISyncEntity;
1112
use Lkrms\Sync\Contract\ISyncProvider;
@@ -84,7 +85,7 @@ protected function getOptionList(): array
8485
->bindTo($this->Filter),
8586
CliOption::build()
8687
->long('shallow')
87-
->description('Do not hydrate entity relationships')
88+
->description('Do not resolve entity relationships')
8889
->bindTo($this->Shallow),
8990
CliOption::build()
9091
->long('include-canonical-id')
@@ -148,7 +149,9 @@ protected function run(string ...$args)
148149

149150
$context = $provider->getContext();
150151
if ($this->Shallow || $this->Csv) {
151-
$context = $context->withHydrationPolicy(HydrationPolicy::SUPPRESS);
152+
$context = $context
153+
->withDeferralPolicy(DeferralPolicy::DO_NOT_RESOLVE)
154+
->withHydrationPolicy(HydrationPolicy::SUPPRESS);
152155
}
153156

154157
$result = $this->EntityId !== null

src/Util/Sync/Concept/SyncDefinition.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -325,12 +325,13 @@ final public function getSyncOperationClosure($operation): ?Closure
325325

326326
// If a method has been declared for this operation, use it, even if
327327
// it's not in $this->Operations
328-
if ($closure =
329-
$this->ProviderIntrospector->getDeclaredSyncOperationClosure(
330-
$operation,
331-
$this->EntityIntrospector,
332-
$this->Provider
333-
)) {
328+
$closure = $this->ProviderIntrospector->getDeclaredSyncOperationClosure(
329+
$operation,
330+
$this->EntityIntrospector,
331+
$this->Provider
332+
);
333+
334+
if ($closure) {
334335
return $this->Closures[$operation] =
335336
fn(ISyncContext $ctx, ...$args) =>
336337
$closure(
@@ -344,10 +345,9 @@ final public function getSyncOperationClosure($operation): ?Closure
344345
($closure = $this->getSyncOperationClosure(OP::READ_LIST))) {
345346
return $this->Closures[$operation] =
346347
function (ISyncContext $ctx, $id, ...$args) use ($closure) {
347-
$entity =
348-
$this
349-
->getFluentIterator($closure($ctx, ...$args))
350-
->nextWithValue('Id', $id);
348+
$entity = $this
349+
->getFluentIterator($closure($ctx, ...$args))
350+
->nextWithValue('Id', $id);
351351
if ($entity === null) {
352352
throw new SyncEntityNotFoundException($this->Provider, $this->Entity, $id);
353353
}

src/Util/Sync/Concept/SyncProvider.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public function __construct(ContainerInterface $app, SyncStore $store)
8383
/**
8484
* @inheritDoc
8585
*/
86-
public function getContext(?ContainerInterface $container = null): SyncContext
86+
public function getContext(?ContainerInterface $container = null): ISyncContext
8787
{
8888
if (!$container) {
8989
$container = $this->App;
@@ -249,10 +249,14 @@ final public static function getServices(): array
249249
*/
250250
final public function with(string $entity, $context = null): SyncEntityProvider
251251
{
252-
/** @var ContainerInterface */
253-
$container = $context instanceof ISyncContext
254-
? $context->container()
255-
: ($context ?: $this->App);
252+
if ($context instanceof ISyncContext) {
253+
$context->maybeThrowRecursionException();
254+
$container = $context->container();
255+
} else {
256+
/** @var ContainerInterface */
257+
$container = $context ?? $this->App;
258+
}
259+
256260
$container = $container->inContextOf(static::class);
257261

258262
$context = $context instanceof ISyncContext

src/Util/Sync/Contract/ISyncContext.php

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
use Lkrms\Sync\Catalog\HydrationPolicy;
1010
use Lkrms\Sync\Catalog\SyncOperation;
1111
use Lkrms\Sync\Concept\SyncProvider;
12+
use Lkrms\Sync\Exception\SyncEntityRecursionException;
1213
use Lkrms\Sync\Exception\SyncInvalidFilterException;
1314

1415
/**
15-
* The context within which a sync entity is instantiated by a provider
16+
* The context within which sync entities are instantiated by a provider
1617
*
1718
* @extends IProviderContext<ISyncProvider,ISyncEntity>
1819
*/
@@ -62,7 +63,7 @@ interface ISyncContext extends IProviderContext
6263
* @param SyncOperation::* $operation
6364
* @param mixed ...$args Sync operation arguments, NOT including the
6465
* {@see ISyncContext} argument.
65-
* @return $this
66+
* @return static
6667
*/
6768
public function withArgs($operation, ...$args);
6869

@@ -76,14 +77,14 @@ public function withArgs($operation, ...$args);
7677
* @see ISyncContext::applyFilterPolicy()
7778
*
7879
* @param (callable(ISyncContext, ?bool &$returnEmpty, array{}|null &$empty): void)|null $callback
79-
* @return $this
80+
* @return static
8081
*/
8182
public function withFilterPolicyCallback(?callable $callback);
8283

8384
/**
8485
* Reject entities from the local entity store
8586
*
86-
* @return $this
87+
* @return static
8788
*/
8889
public function online();
8990

@@ -93,7 +94,7 @@ public function online();
9394
* An exception is thrown if the local entity store is unable to satisfy
9495
* subsequent entity requests.
9596
*
96-
* @return $this
97+
* @return static
9798
*/
9899
public function offline();
99100

@@ -103,15 +104,15 @@ public function offline();
103104
*
104105
* This is the default behaviour.
105106
*
106-
* @return $this
107+
* @return static
107108
*/
108109
public function offlineFirst();
109110

110111
/**
111112
* Apply the given deferral policy to the context
112113
*
113114
* @param DeferralPolicy::* $policy
114-
* @return $this
115+
* @return static
115116
*/
116117
public function withDeferralPolicy($policy);
117118

@@ -123,14 +124,33 @@ public function withDeferralPolicy($policy);
123124
* change to an entity and its subclasses.
124125
* @param array<int<1,max>>|int<1,max>|null $depth Limit the scope of the
125126
* change to entities at a given `$depth` from the current context.
126-
* @return $this
127+
* @return static
127128
*/
128129
public function withHydrationPolicy(
129130
int $policy,
130131
?string $entity = null,
131132
$depth = null
132133
);
133134

135+
/**
136+
* Push the entity propagating the context onto the stack after checking if
137+
* it is already present
138+
*
139+
* {@see ISyncContext::maybeThrowRecursionException()} fails with an
140+
* exception if `$entity` is already in the stack.
141+
*
142+
* @return static
143+
*/
144+
public function pushWithRecursionCheck(ISyncEntity $entity);
145+
146+
/**
147+
* Throw an exception if recursion is detected
148+
*
149+
* @throws SyncEntityRecursionException if
150+
* {@see ISyncContext::pushWithRecursionCheck()} detected recursion.
151+
*/
152+
public function maybeThrowRecursionException(): void;
153+
134154
/**
135155
* Run the unclaimed filter policy callback
136156
*
@@ -163,7 +183,7 @@ public function withHydrationPolicy(
163183
*
164184
* @param array{}|null $empty
165185
*/
166-
public function applyFilterPolicy(?bool &$returnEmpty, &$empty): void;
186+
public function applyFilterPolicy(?bool &$returnEmpty, ?array &$empty): void;
167187

168188
/**
169189
* Get the filters passed to the context via optional sync operation
@@ -196,7 +216,7 @@ public function getFilter(string $key, bool $orValue = true);
196216
*
197217
* Unlike other {@see ISyncContext} methods,
198218
* {@see ISyncContext::claimFilter()} modifies the object it is called on
199-
* instead of returning a modified clone.
219+
* instead of returning a modified instance.
200220
*
201221
* If `$orValue` is `true` and a value for `$key` has been applied to the
202222
* context via {@see IProviderContext::withValue()}, it is returned if there

0 commit comments

Comments
 (0)