Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/Mongo/MongoPersistenceStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,32 @@ public function isScheduledForInsert(object $object): bool

return $uow->isScheduledForInsert($object) || $uow->isScheduledForUpsert($object);
}

public function findBy(string $class, array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
$qb = $this->objectManagerFor($class)
->getRepository($class)
->createQueryBuilder()
->refresh();

foreach ($criteria as $field => $value) {
$qb->field($field)->equals($value);
}

if ($orderBy) {
foreach ($orderBy as $field => $direction) {
$qb->sort($field, $direction);
}
}

if ($limit) {
$qb->limit($limit);
}

if ($offset) {
$qb->skip($offset);
}

return $qb->getQuery()->execute()->toArray(); // @phpstan-ignore method.nonObject
}
}
30 changes: 30 additions & 0 deletions src/ORM/AbstractORMPersistenceStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
use Doctrine\ORM\Query;
use Doctrine\Persistence\Mapping\MappingException;
use Zenstruck\Foundry\Persistence\PersistenceStrategy;

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

return \array_values(\array_merge(...$namespaces));
}

final public function findBy(string $class, array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
$qb = $this->objectManagerFor($class)->getRepository($class)->createQueryBuilder('o');

foreach ($criteria as $field => $value) {
$paramName = str_replace('.', '_', $field);
$qb->andWhere('o.'.$field.' = :'.$paramName);
$qb->setParameter($paramName, $value);
}

if ($orderBy) {
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy('o.'.$field, $direction);
}
}

if ($limit) {
$qb->setMaxResults($limit);
}

if ($offset) {
$qb->setFirstResult($offset);
}

return $qb->getQuery()
->setHint(Query::HINT_REFRESH, true)
->getResult();
}
}
17 changes: 17 additions & 0 deletions src/Persistence/PersistenceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,23 @@ public static function isOrmOnly(): bool
})();
}

/**
* @template T of object
*
* @param class-string<T> $class
* @param array<string, mixed> $criteria
* @param array<string, string>|null $orderBy
* @phpstan-param array<string, 'asc'|'desc'|'ASC'|'DESC'>|null $orderBy
*
* @return list<T>
*/
public function findBy(string $class, array $criteria = [], ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
$class = ProxyGenerator::unwrap($class);

return $this->strategyFor($class)->findBy($class, $criteria, $orderBy, $limit, $offset);
}

private function flushAllStrategies(): void
{
foreach ($this->strategies as $strategy) {
Expand Down
14 changes: 14 additions & 0 deletions src/Persistence/PersistenceStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ public function getIdentifierValues(object $object): array
*/
abstract public function managedNamespaces(): array;

/**
* Uses a query builder to be able to pass hints to UoW and to force Doctrine to return fresh objects
*
* @template T of object
*
* @param class-string<T> $class
* @param array<string, mixed> $criteria
* @param array<string, string>|null $orderBy
* @phpstan-param array<string, 'asc'|'desc'|'ASC'|'DESC'>|null $orderBy
*
* @return list<T>
*/
abstract public function findBy(string $class, array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;

/**
* @param class-string $owner
*
Expand Down
72 changes: 25 additions & 47 deletions src/Persistence/Proxy/PersistedObjectsTracker.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,13 @@ final class PersistedObjectsTracker
/**
* This buffer of objects needs to be static to be kept between two kernel.reset events.
*
* @var list<\WeakReference<object>>
* @var \WeakMap<object, mixed> keys: objects, values: value ids
*/
private static array $buffer = [];

/**
* @var \WeakMap<object, mixed>
*/
private static \WeakMap $ids;
private static \WeakMap $buffer;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finally I'm using a weak map 😄

this prevents to have duplicates in the buffer


public function __construct()
{
self::$ids ??= new \WeakMap();
self::$buffer ??= new \WeakMap();
}

public function refresh(): void
Expand All @@ -43,41 +38,33 @@ public function refresh(): void
public function add(object ...$objects): void
{
foreach ($objects as $object) {
self::$buffer[] = \WeakReference::create($object);

$id = Configuration::instance()->persistence()->getIdentifierValues($object);
if ($id) {
self::$ids[$object] = $id;
if (self::$buffer->offsetExists($object) && self::$buffer[$object]) {
continue;
}

self::$buffer[$object] = Configuration::instance()->persistence()->getIdentifierValues($object);
}
}

public static function updateIds(): void
{
foreach (self::$buffer as $reference) {
if (null === $object = $reference->get()) {
continue;
}

if (self::$ids->offsetExists($object)) {
foreach (self::$buffer as $object => $id) {
if ($id) {
continue;
}

self::$ids[$object] = Configuration::instance()->persistence()->getIdentifierValues($object);
self::$buffer[$object] = Configuration::instance()->persistence()->getIdentifierValues($object);
}
}

public static function reset(): void
{
self::$buffer = [];
self::$ids = new \WeakMap();
self::$buffer = new \WeakMap();
}

public static function countObjects(): int
{
return \count(
\array_filter(self::$buffer, static fn(\WeakReference $weakRef) => null !== $weakRef->get())
);
return \count(self::$buffer);
}

private static function proxifyObjects(): void
Expand All @@ -86,30 +73,21 @@ private static function proxifyObjects(): void
return;
}

self::$buffer = \array_values(
\array_map(
static function(\WeakReference $weakRef) {
$object = $weakRef->get() ?? throw new \LogicException('Object cannot be null.');

$reflector = new \ReflectionClass($object);

if ($reflector->isUninitializedLazyObject($object)) {
return \WeakReference::create($object);
}

$clone = clone $object;
$reflector->resetAsLazyGhost($object, function($object) use ($clone) {
$id = self::$ids[$object] ?? throw new \LogicException('Canot find the id for object');
foreach (self::$buffer as $object => $id) {
if (!$id) {
continue;
}

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

return \WeakReference::create($object);
},
if ($reflector->isUninitializedLazyObject($object)) {
continue;
}

// remove all empty references
\array_filter(self::$buffer, static fn(\WeakReference $weakRef) => null !== $weakRef->get()),
)
);
$clone = clone $object;
$reflector->resetAsLazyGhost($object, function($object) use ($clone, $id) {
Configuration::instance()->persistence()->autorefresh($object, $id, $clone);
});
}
}
}
12 changes: 7 additions & 5 deletions src/Persistence/RepositoryAssertions.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ public function countLessThanOrEqual(int $expected, array $criteria = [], string

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

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

return $this;
}
Expand Down
32 changes: 16 additions & 16 deletions src/Persistence/RepositoryDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public function find($id): ?object
/** @var T|null $object */
$object = $this->inner()->find(ProxyGenerator::unwrap($id));

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

Expand All @@ -120,24 +120,30 @@ public function findOrFail(mixed $id): object
*/
public function findAll(): array
{
$objects = \array_values($this->inner()->findAll());

Configuration::instance()->persistedObjectsTracker?->add(...$objects);

return $objects;
return $this->findBy([]);
}

/**
* @param array<string, string>|null $orderBy
* @phpstan-param array<string, 'asc'|'desc'|'ASC'|'DESC'>|null $orderBy
* @param ?int $limit
* @param ?int $offset
*
* @return list<T>
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
$objects = \array_values($this->inner()->findBy($this->normalize($criteria), $orderBy, $limit, $offset));
if ($this->inMemory) {
$results = $this->inner()->findBy($this->normalize($criteria), $orderBy, $limit, $offset);
} else {
$results = Configuration::instance()->persistence()->findBy($this->class, $this->normalize($criteria), $orderBy, $limit, $offset);
}

$objects = \array_values($results);

Configuration::instance()->persistedObjectsTracker?->add(...$objects);
if (!$this instanceof ProxyRepositoryDecorator) {
Configuration::instance()->persistedObjectsTracker?->add(...$objects);
}

return $objects;
}
Expand All @@ -147,13 +153,7 @@ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $
*/
public function findOneBy(array $criteria): ?object
{
$object = $this->inner()->findOneBy($this->normalize($criteria));

if ($object) {
Configuration::instance()->persistedObjectsTracker?->add($object);
}

return $object;
return $this->findBy($criteria, limit: 1)[0] ?? null;
}

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

return \count($this->findBy($criteria));
return \count($this->inner()->findBy($criteria));
}

public function truncate(): void
Expand Down
24 changes: 24 additions & 0 deletions tests/Integration/Persistence/AutoRefreshTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,30 @@ public function it_can_enable_autorefresh_when_disabled_globally(): void
self::assertSame('foo', $object->getProp1());
}

#[Test]
public function repository_method_returns_up_to_date_objects(): void
{
[$object1, $object2] = $this->factory()->many(2)->create();

self::assertSame(2, PersistedObjectsTracker::countObjects());

$this->updateObject($object1->id);
$this->updateObject($object2->id);

[$newObject1, $newObject2] = $this->factory()::all();

self::assertSame(2, PersistedObjectsTracker::countObjects());

self::assertSame($object1, $newObject1);
self::assertSame($object2, $newObject2);

self::assertSame('foo', $newObject1->getProp1());
self::assertSame('foo', $newObject2->getProp1());

self::assertSame('foo', $object1->getProp1());
self::assertSame('foo', $object2->getProp1());
}

/**
* @return PersistentObjectFactory<GenericModel>
*/
Expand Down