Skip to content

Commit 90866d2

Browse files
authored
fix(autorefresh): return fresh data from RepositoryDecorator methods (#983)
1 parent f1a0b86 commit 90866d2

File tree

8 files changed

+161
-68
lines changed

8 files changed

+161
-68
lines changed

src/Mongo/MongoPersistenceStrategy.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,32 @@ public function isScheduledForInsert(object $object): bool
9595

9696
return $uow->isScheduledForInsert($object) || $uow->isScheduledForUpsert($object);
9797
}
98+
99+
public function findBy(string $class, array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
100+
{
101+
$qb = $this->objectManagerFor($class)
102+
->getRepository($class)
103+
->createQueryBuilder()
104+
->refresh();
105+
106+
foreach ($criteria as $field => $value) {
107+
$qb->field($field)->equals($value);
108+
}
109+
110+
if ($orderBy) {
111+
foreach ($orderBy as $field => $direction) {
112+
$qb->sort($field, $direction);
113+
}
114+
}
115+
116+
if ($limit) {
117+
$qb->limit($limit);
118+
}
119+
120+
if ($offset) {
121+
$qb->skip($offset);
122+
}
123+
124+
return $qb->getQuery()->execute()->toArray(); // @phpstan-ignore method.nonObject
125+
}
98126
}

src/ORM/AbstractORMPersistenceStrategy.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Doctrine\ORM\EntityManagerInterface;
1515
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
16+
use Doctrine\ORM\Query;
1617
use Doctrine\Persistence\Mapping\MappingException;
1718
use Zenstruck\Foundry\Persistence\PersistenceStrategy;
1819

@@ -96,4 +97,33 @@ final public function managedNamespaces(): array
9697

9798
return \array_values(\array_merge(...$namespaces));
9899
}
100+
101+
final public function findBy(string $class, array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
102+
{
103+
$qb = $this->objectManagerFor($class)->getRepository($class)->createQueryBuilder('o');
104+
105+
foreach ($criteria as $field => $value) {
106+
$paramName = str_replace('.', '_', $field);
107+
$qb->andWhere('o.'.$field.' = :'.$paramName);
108+
$qb->setParameter($paramName, $value);
109+
}
110+
111+
if ($orderBy) {
112+
foreach ($orderBy as $field => $direction) {
113+
$qb->addOrderBy('o.'.$field, $direction);
114+
}
115+
}
116+
117+
if ($limit) {
118+
$qb->setMaxResults($limit);
119+
}
120+
121+
if ($offset) {
122+
$qb->setFirstResult($offset);
123+
}
124+
125+
return $qb->getQuery()
126+
->setHint(Query::HINT_REFRESH, true)
127+
->getResult();
128+
}
99129
}

src/Persistence/PersistenceManager.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,23 @@ public static function isOrmOnly(): bool
419419
})();
420420
}
421421

422+
/**
423+
* @template T of object
424+
*
425+
* @param class-string<T> $class
426+
* @param array<string, mixed> $criteria
427+
* @param array<string, string>|null $orderBy
428+
* @phpstan-param array<string, 'asc'|'desc'|'ASC'|'DESC'>|null $orderBy
429+
*
430+
* @return list<T>
431+
*/
432+
public function findBy(string $class, array $criteria = [], ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
433+
{
434+
$class = ProxyGenerator::unwrap($class);
435+
436+
return $this->strategyFor($class)->findBy($class, $criteria, $orderBy, $limit, $offset);
437+
}
438+
422439
private function flushAllStrategies(): void
423440
{
424441
foreach ($this->strategies as $strategy) {

src/Persistence/PersistenceStrategy.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ public function getIdentifierValues(object $object): array
9292
*/
9393
abstract public function managedNamespaces(): array;
9494

95+
/**
96+
* Uses a query builder to be able to pass hints to UoW and to force Doctrine to return fresh objects
97+
*
98+
* @template T of object
99+
*
100+
* @param class-string<T> $class
101+
* @param array<string, mixed> $criteria
102+
* @param array<string, string>|null $orderBy
103+
* @phpstan-param array<string, 'asc'|'desc'|'ASC'|'DESC'>|null $orderBy
104+
*
105+
* @return list<T>
106+
*/
107+
abstract public function findBy(string $class, array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
108+
95109
/**
96110
* @param class-string $owner
97111
*

src/Persistence/Proxy/PersistedObjectsTracker.php

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,13 @@ final class PersistedObjectsTracker
2121
/**
2222
* This buffer of objects needs to be static to be kept between two kernel.reset events.
2323
*
24-
* @var list<\WeakReference<object>>
24+
* @var \WeakMap<object, mixed> keys: objects, values: value ids
2525
*/
26-
private static array $buffer = [];
27-
28-
/**
29-
* @var \WeakMap<object, mixed>
30-
*/
31-
private static \WeakMap $ids;
26+
private static \WeakMap $buffer;
3227

3328
public function __construct()
3429
{
35-
self::$ids ??= new \WeakMap();
30+
self::$buffer ??= new \WeakMap();
3631
}
3732

3833
public function refresh(): void
@@ -43,41 +38,33 @@ public function refresh(): void
4338
public function add(object ...$objects): void
4439
{
4540
foreach ($objects as $object) {
46-
self::$buffer[] = \WeakReference::create($object);
47-
48-
$id = Configuration::instance()->persistence()->getIdentifierValues($object);
49-
if ($id) {
50-
self::$ids[$object] = $id;
41+
if (self::$buffer->offsetExists($object) && self::$buffer[$object]) {
42+
continue;
5143
}
44+
45+
self::$buffer[$object] = Configuration::instance()->persistence()->getIdentifierValues($object);
5246
}
5347
}
5448

5549
public static function updateIds(): void
5650
{
57-
foreach (self::$buffer as $reference) {
58-
if (null === $object = $reference->get()) {
59-
continue;
60-
}
61-
62-
if (self::$ids->offsetExists($object)) {
51+
foreach (self::$buffer as $object => $id) {
52+
if ($id) {
6353
continue;
6454
}
6555

66-
self::$ids[$object] = Configuration::instance()->persistence()->getIdentifierValues($object);
56+
self::$buffer[$object] = Configuration::instance()->persistence()->getIdentifierValues($object);
6757
}
6858
}
6959

7060
public static function reset(): void
7161
{
72-
self::$buffer = [];
73-
self::$ids = new \WeakMap();
62+
self::$buffer = new \WeakMap();
7463
}
7564

7665
public static function countObjects(): int
7766
{
78-
return \count(
79-
\array_filter(self::$buffer, static fn(\WeakReference $weakRef) => null !== $weakRef->get())
80-
);
67+
return \count(self::$buffer);
8168
}
8269

8370
private static function proxifyObjects(): void
@@ -86,30 +73,21 @@ private static function proxifyObjects(): void
8673
return;
8774
}
8875

89-
self::$buffer = \array_values(
90-
\array_map(
91-
static function(\WeakReference $weakRef) {
92-
$object = $weakRef->get() ?? throw new \LogicException('Object cannot be null.');
93-
94-
$reflector = new \ReflectionClass($object);
95-
96-
if ($reflector->isUninitializedLazyObject($object)) {
97-
return \WeakReference::create($object);
98-
}
99-
100-
$clone = clone $object;
101-
$reflector->resetAsLazyGhost($object, function($object) use ($clone) {
102-
$id = self::$ids[$object] ?? throw new \LogicException('Canot find the id for object');
76+
foreach (self::$buffer as $object => $id) {
77+
if (!$id) {
78+
continue;
79+
}
10380

104-
Configuration::instance()->persistence()->autorefresh($object, $id, $clone);
105-
});
81+
$reflector = new \ReflectionClass($object);
10682

107-
return \WeakReference::create($object);
108-
},
83+
if ($reflector->isUninitializedLazyObject($object)) {
84+
continue;
85+
}
10986

110-
// remove all empty references
111-
\array_filter(self::$buffer, static fn(\WeakReference $weakRef) => null !== $weakRef->get()),
112-
)
113-
);
87+
$clone = clone $object;
88+
$reflector->resetAsLazyGhost($object, function($object) use ($clone, $id) {
89+
Configuration::instance()->persistence()->autorefresh($object, $id, $clone);
90+
});
91+
}
11492
}
11593
}

src/Persistence/RepositoryAssertions.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ public function countLessThanOrEqual(int $expected, array $criteria = [], string
113113

114114
public function exists(mixed $criteria, string $message = 'Expected {entity} to exist but it does not.'): self
115115
{
116-
Assert::that($this->repository->find($criteria))->isNotEmpty($message, [
116+
Assert::that($this->repository->count($criteria))
117+
->isGreaterThan(0, $message, [
117118
'entity' => $this->repository->getClassName(),
118119
'criteria' => $criteria,
119120
]);
@@ -123,10 +124,11 @@ public function exists(mixed $criteria, string $message = 'Expected {entity} to
123124

124125
public function notExists(mixed $criteria, string $message = 'Expected {entity} to not exist but it does.'): self
125126
{
126-
Assert::that($this->repository->find($criteria))->isEmpty($message, [
127-
'entity' => $this->repository->getClassName(),
128-
'criteria' => $criteria,
129-
]);
127+
Assert::that($this->repository->count($criteria))
128+
->is(0, $message, [
129+
'entity' => $this->repository->getClassName(),
130+
'criteria' => $criteria,
131+
]);
130132

131133
return $this;
132134
}

src/Persistence/RepositoryDecorator.php

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public function find($id): ?object
100100
/** @var T|null $object */
101101
$object = $this->inner()->find(ProxyGenerator::unwrap($id));
102102

103-
if ($object) {
103+
if ($object && !$this instanceof ProxyRepositoryDecorator) {
104104
Configuration::instance()->persistedObjectsTracker?->add($object);
105105
}
106106

@@ -120,24 +120,30 @@ public function findOrFail(mixed $id): object
120120
*/
121121
public function findAll(): array
122122
{
123-
$objects = \array_values($this->inner()->findAll());
124-
125-
Configuration::instance()->persistedObjectsTracker?->add(...$objects);
126-
127-
return $objects;
123+
return $this->findBy([]);
128124
}
129125

130126
/**
127+
* @param array<string, string>|null $orderBy
128+
* @phpstan-param array<string, 'asc'|'desc'|'ASC'|'DESC'>|null $orderBy
131129
* @param ?int $limit
132130
* @param ?int $offset
133131
*
134132
* @return list<T>
135133
*/
136134
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
137135
{
138-
$objects = \array_values($this->inner()->findBy($this->normalize($criteria), $orderBy, $limit, $offset));
136+
if ($this->inMemory) {
137+
$results = $this->inner()->findBy($this->normalize($criteria), $orderBy, $limit, $offset);
138+
} else {
139+
$results = Configuration::instance()->persistence()->findBy($this->class, $this->normalize($criteria), $orderBy, $limit, $offset);
140+
}
141+
142+
$objects = \array_values($results);
139143

140-
Configuration::instance()->persistedObjectsTracker?->add(...$objects);
144+
if (!$this instanceof ProxyRepositoryDecorator) {
145+
Configuration::instance()->persistedObjectsTracker?->add(...$objects);
146+
}
141147

142148
return $objects;
143149
}
@@ -147,13 +153,7 @@ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $
147153
*/
148154
public function findOneBy(array $criteria): ?object
149155
{
150-
$object = $this->inner()->findOneBy($this->normalize($criteria));
151-
152-
if ($object) {
153-
Configuration::instance()->persistedObjectsTracker?->add($object);
154-
}
155-
156-
return $object;
156+
return $this->findBy($criteria, limit: 1)[0] ?? null;
157157
}
158158

159159
public function getClassName(): string
@@ -173,7 +173,7 @@ public function count(array $criteria = []): int
173173
return $inner->count($this->normalize($criteria));
174174
}
175175

176-
return \count($this->findBy($criteria));
176+
return \count($this->inner()->findBy($criteria));
177177
}
178178

179179
public function truncate(): void

tests/Integration/Persistence/AutoRefreshTestCase.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,30 @@ public function it_can_enable_autorefresh_when_disabled_globally(): void
329329
self::assertSame('foo', $object->getProp1());
330330
}
331331

332+
#[Test]
333+
public function repository_method_returns_up_to_date_objects(): void
334+
{
335+
[$object1, $object2] = $this->factory()->many(2)->create();
336+
337+
self::assertSame(2, PersistedObjectsTracker::countObjects());
338+
339+
$this->updateObject($object1->id);
340+
$this->updateObject($object2->id);
341+
342+
[$newObject1, $newObject2] = $this->factory()::all();
343+
344+
self::assertSame(2, PersistedObjectsTracker::countObjects());
345+
346+
self::assertSame($object1, $newObject1);
347+
self::assertSame($object2, $newObject2);
348+
349+
self::assertSame('foo', $newObject1->getProp1());
350+
self::assertSame('foo', $newObject2->getProp1());
351+
352+
self::assertSame('foo', $object1->getProp1());
353+
self::assertSame('foo', $object2->getProp1());
354+
}
355+
332356
/**
333357
* @return PersistentObjectFactory<GenericModel>
334358
*/

0 commit comments

Comments
 (0)