Skip to content

Commit 9bd9a76

Browse files
committed
fix: review
1 parent 76df287 commit 9bd9a76

11 files changed

+151
-22
lines changed

config/persistence.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
44

5+
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
56
use Symfony\Component\HttpKernel\Event\TerminateEvent;
7+
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
68
use Zenstruck\Foundry\Persistence\PersistenceManager;
79
use Zenstruck\Foundry\Persistence\Proxy\KernelTerminateListener;
810
use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
@@ -24,6 +26,8 @@
2426
if (PHP_VERSION_ID >= 80400) {
2527
$container->services()->set('.foundry.proxy.kernel_terminate_listener', KernelTerminateListener::class)
2628
->tag('kernel.event_listener', ['event' => TerminateEvent::class, 'method' => '__invoke'])
29+
->tag('kernel.event_listener', ['event' => ConsoleTerminateEvent::class, 'method' => '__invoke'])
30+
->tag('kernel.event_listener', ['event' => WorkerMessageHandledEvent::class, 'method' => '__invoke']) // @phpstan-ignore class.notFound
2731
;
2832
}
2933
};

phpstan.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ parameters:
6969
- identifier: missingType.callable
7070
path: tests/Fixture/Maker/expected/
7171

72+
# we're currently running static analysis with PHP 8.3
73+
- message: '#Call to an undefined method ReflectionClass\<(.*)\>::isUninitializedLazyObject\(\).#'
74+
- message: '#Call to an undefined method ReflectionClass\<(.*)\>::resetAsLazyProxy\(\).#'
75+
7276
excludePaths:
7377
- tests/Fixture/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php
7478
- tests/Fixture/Maker/expected/can_create_factory_interactively.php

src/Persistence/PersistenceManager.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata;
2323
use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
2424

25+
use function Zenstruck\Foundry\set;
26+
2527
/**
2628
* @author Kevin Bond <[email protected]>
2729
*
@@ -147,7 +149,7 @@ public function flush(ObjectManager $om): void
147149
*
148150
* @return T
149151
*/
150-
public function refresh(object &$object, bool $force = false): object
152+
public function refresh(object &$object, bool $force = false, bool $allowRefreshDeletedObject = false): object
151153
{
152154
if (!$this->flush && !$force) {
153155
return $object;
@@ -183,11 +185,23 @@ public function refresh(object &$object, bool $force = false): object
183185

184186
$id = $om->getClassMetadata($object::class)->getIdentifierValues($object);
185187

186-
if (!$id || !($object = $om->find($object::class, $id))) { // @phpstan-ignore parameterByRef.type
188+
if (!$id || !($objectFromDb = $om->find($object::class, $id))) {
189+
if ($allowRefreshDeletedObject) {
190+
$identifierFields = $om->getClassMetadata($object::class)->getIdentifierFieldNames();
191+
foreach ($identifierFields as $field) {
192+
try {
193+
set($object, $field, null);
194+
} catch (\Throwable) {
195+
}
196+
}
197+
198+
return $object;
199+
}
200+
187201
throw RefreshObjectFailed::objectNoLongExists();
188202
}
189203

190-
return $object;
204+
return $object = $objectFromDb;
191205
}
192206

193207
public function isPersisted(object $object): bool

src/Persistence/PersistentObjectFactory.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -456,14 +456,14 @@ final protected function initializeInternal(): static
456456
return parent::initializeInternal()
457457
->afterInstantiate(
458458
static function(object $object, array $parameters, PersistentObjectFactory $factoryUsed): void {
459-
if (PHP_VERSION_ID >= 80400) {
460-
CreatedObjectsTracker::add($object);
461-
}
462-
463459
if (!$factoryUsed->isPersisting()) {
464460
return;
465461
}
466462

463+
if (PHP_VERSION_ID >= 80400 && !$factoryUsed instanceof PersistentProxyObjectFactory) {
464+
CreatedObjectsTracker::add($object);
465+
}
466+
467467
$afterPersistCallbacks = [];
468468

469469
foreach ($factoryUsed->afterPersist as $afterPersist) {

src/Persistence/Proxy/CreatedObjectsTracker.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44

55
use WeakReference;
66

7+
use Zenstruck\Foundry\Configuration;
8+
use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed;
9+
710
use function Zenstruck\Foundry\Persistence\refresh;
811

12+
/**
13+
* @internal
14+
*/
915
final class CreatedObjectsTracker
1016
{
1117
/** @var list<\WeakReference<object>> */
@@ -22,8 +28,17 @@ public static function proxifyObjects(): void
2228
array_map(
2329
static function (WeakReference $weakRef) {
2430
$object = $weakRef->get() ?? throw new \LogicException('Object cannot be null.');
31+
32+
$reflector = new \ReflectionClass($object);
33+
34+
if ($reflector->isUninitializedLazyObject($object)) {
35+
return \WeakReference::create($object);
36+
}
37+
2538
$clone = clone $object;
26-
(new \ReflectionClass($object))->resetAsLazyProxy($object, fn() => refresh($clone));
39+
$reflector->resetAsLazyProxy($object, function () use ($clone) {
40+
return Configuration::instance()->persistence()->refresh($clone, allowRefreshDeletedObject: true);
41+
});
2742

2843
return \WeakReference::create($object);
2944
},

src/Persistence/Proxy/KernelTerminateListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
final class KernelTerminateListener
88
{
9-
public function __invoke(TerminateEvent $event): void
9+
public function __invoke(object $event): void
1010
{
1111
CreatedObjectsTracker::proxifyObjects();
1212
}

src/Persistence/functions.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Doctrine\Persistence\ObjectRepository;
1515
use Zenstruck\Foundry\AnonymousFactoryGenerator;
1616
use Zenstruck\Foundry\Configuration;
17+
use Zenstruck\Foundry\Persistence\Proxy\CreatedObjectsTracker;
1718

1819
/**
1920
* @template T of object
@@ -136,6 +137,14 @@ function refresh(object &$object): object
136137
return Configuration::instance()->persistence()->refresh($object);
137138
}
138139

140+
/**
141+
* For refreshing all persistent objects created by Foundry, that are currently in use.
142+
*/
143+
function refresh_all(): void
144+
{
145+
CreatedObjectsTracker::proxifyObjects();
146+
}
147+
139148
/**
140149
* @template T of object
141150
*
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Zenstruck\Foundry\Tests\Fixture\Controller;
4+
5+
use Doctrine\ORM\EntityManagerInterface;
6+
use Symfony\Component\HttpFoundation\Response;
7+
use Symfony\Component\HttpKernel\Attribute\AsController;
8+
use Symfony\Component\Routing\Attribute\Route;
9+
use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity;
10+
11+
#[AsController]
12+
final class DeleteGenericEntity
13+
{
14+
#[Route('/delete/{id}')]
15+
public function __invoke(EntityManagerInterface $entityManager, int $id): Response
16+
{
17+
$genericEntity = $entityManager->find(GenericEntity::class, $id) ?? throw new \RuntimeException('Entity not found');
18+
$entityManager->remove($genericEntity);
19+
$entityManager->flush();
20+
21+
return new Response();
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Zenstruck\Foundry\Tests\Fixture\Controller;
4+
5+
use Doctrine\ORM\EntityManagerInterface;
6+
use Symfony\Component\HttpFoundation\Response;
7+
use Symfony\Component\HttpKernel\Attribute\AsController;
8+
use Symfony\Component\Routing\Attribute\Route;
9+
use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity;
10+
11+
#[AsController]
12+
final class UpdateGenericEntity
13+
{
14+
#[Route('/update/{id}')]
15+
public function __invoke(EntityManagerInterface $entityManager, int $id): Response
16+
{
17+
$genericEntity = $entityManager->find(GenericEntity::class, $id);
18+
$genericEntity?->setProp1('foo');
19+
$entityManager->flush();
20+
21+
return new Response();
22+
}
23+
}

tests/Fixture/TestKernel.php

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@
1111

1212
namespace Zenstruck\Foundry\Tests\Fixture;
1313

14-
use Doctrine\ORM\EntityManagerInterface;
1514
use Symfony\Bundle\MakerBundle\MakerBundle;
1615
use Symfony\Component\Config\Loader\LoaderInterface;
1716
use Symfony\Component\DependencyInjection\ContainerBuilder;
18-
use Symfony\Component\HttpFoundation\Response;
19-
use Symfony\Component\Routing\Attribute\Route;
17+
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
2018
use Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode;
21-
use Zenstruck\Foundry\Tests\Fixture\Entity\Address;
22-
use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity;
19+
use Zenstruck\Foundry\Tests\Fixture\Controller\DeleteGenericEntity;
20+
use Zenstruck\Foundry\Tests\Fixture\Controller\UpdateGenericEntity;
2321
use Zenstruck\Foundry\Tests\Fixture\Factories\ArrayFactory;
2422
use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory;
2523
use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryAddressRepository;
@@ -60,15 +58,13 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
6058
$c->register(ServiceStory::class)->setAutowired(true)->setAutoconfigured(true);
6159
$c->register(InMemoryAddressRepository::class)->setAutowired(true)->setAutoconfigured(true);
6260
$c->register(InMemoryContactRepository::class)->setAutowired(true)->setAutoconfigured(true);
61+
62+
$c->register(DeleteGenericEntity::class)->setAutowired(true)->setAutoconfigured(true)->addTag('controller.service_arguments');
63+
$c->register(UpdateGenericEntity::class)->setAutowired(true)->setAutoconfigured(true)->addTag('controller.service_arguments');
6364
}
6465

65-
#[Route('/update/{id}', name: 'test')]
66-
public function __invoke(EntityManagerInterface $entityManager, int $id): Response
66+
protected function configureRoutes(RoutingConfigurator $routes): void
6767
{
68-
$genericEntity = $entityManager->find(GenericEntity::class, $id);
69-
$genericEntity?->setProp1('foo');
70-
$entityManager->flush();
71-
72-
return new Response();
68+
$routes->import(__DIR__.'/Controller/*.php', 'attribute');
7369
}
7470
}

tests/Integration/ORM/ProxyPHP84Test.php

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
final class ProxyPHP84Test extends WebTestCase
1515
{
1616
use Factories;
17-
17+
1818
/**
1919
* @test
2020
* @requires PHP >= 8.4
@@ -31,6 +31,7 @@ public function it_can_refresh_objects_with_php84_proxies(): void
3131

3232
$client = self::createClient();
3333
$client->request('GET', "/update/{$object->id}");
34+
self::assertResponseIsSuccessful();
3435

3536
self::assertTrue((new \ReflectionClass($object))->isUninitializedLazyObject($object));
3637
self::assertSame('foo', $object->getProp1());
@@ -91,4 +92,44 @@ public function tracker_only_keep_reference_for_objects_in_current_scope(): void
9192
// unsetting the generic entity will remove it from the tracker as well
9293
self::assertSame(0, CreatedObjectsTracker::countObjects());
9394
}
95+
96+
/**
97+
* @test
98+
* @requires PHP >= 8.4
99+
*/
100+
#[Test]
101+
#[RequiresPhp('>= 8.4')]
102+
public function deleting_an_object_does_not_create_a_refresh_error(): void
103+
{
104+
$object = GenericEntityFactory::createOne();
105+
self::ensureKernelShutdown();
106+
107+
self::assertSame('default1', $object->getProp1());
108+
self::assertFalse((new \ReflectionClass($object))->isUninitializedLazyObject($object));
109+
110+
$client = self::createClient();
111+
$client->request('GET', "/delete/{$object->id}");
112+
self::assertResponseIsSuccessful();
113+
114+
self::assertTrue((new \ReflectionClass($object))->isUninitializedLazyObject($object));
115+
self::assertNull($object->id);
116+
self::assertFalse((new \ReflectionClass($object))->isUninitializedLazyObject($object));
117+
}
118+
119+
/**
120+
* @test
121+
* @requires PHP >= 8.4
122+
*/
123+
#[Test]
124+
#[RequiresPhp('>= 8.4')]
125+
public function it_can_refresh_the_same_object_multiple_times(): void
126+
{
127+
$object = GenericEntityFactory::createOne();
128+
129+
CreatedObjectsTracker::proxifyObjects();
130+
CreatedObjectsTracker::proxifyObjects();
131+
CreatedObjectsTracker::proxifyObjects();
132+
133+
self::assertTrue((new \ReflectionClass($object))->isUninitializedLazyObject($object));
134+
}
94135
}

0 commit comments

Comments
 (0)