Skip to content

Commit 485746e

Browse files
authored
fix(autorefresh): don't use clone to get the id values (#980)
1 parent 21b659b commit 485746e

File tree

6 files changed

+161
-7
lines changed

6 files changed

+161
-7
lines changed

src/Persistence/PersistenceManager.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Zenstruck\Foundry\Persistence\Exception\ObjectHasUnsavedChanges;
2323
use Zenstruck\Foundry\Persistence\Exception\ObjectNoLongerExist;
2424
use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed;
25+
use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker;
2526
use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata;
2627
use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
2728

@@ -85,6 +86,8 @@ public function save(object $object): object
8586
$this->flush($om);
8687
}
8788

89+
PersistedObjectsTracker::updateIds();
90+
8891
return $object;
8992
}
9093

@@ -148,18 +151,16 @@ public function flush(ObjectManager $om): void
148151
*
149152
* @param T $object
150153
*/
151-
public function autorefresh(object $object, object $clone): void
154+
public function autorefresh(object $object, mixed $id, object $clone): void
152155
{
153156
$strategy = $this->strategyFor($object::class);
154157
$om = $strategy->objectManagerFor($object::class);
155158

156-
$id = $strategy->getIdentifierValues($clone);
157-
158159
if ($id) {
159160
try {
160161
$om->refresh($object);
161162

162-
if ($strategy->getIdentifierValues($object)) {
163+
if ($this->getIdentifierValues($object)) {
163164
// no identifier values means the object no longer exists
164165
return;
165166
}
@@ -398,6 +399,11 @@ public function resetDatabaseManager(): ResetDatabaseManager
398399
return $this->resetDatabaseManager;
399400
}
400401

402+
public function getIdentifierValues(object $object): mixed
403+
{
404+
return $this->strategyFor($object::class)->getIdentifierValues($object);
405+
}
406+
401407
public static function isOrmOnly(): bool
402408
{
403409
static $isOrmOnly = null;

src/Persistence/Proxy/PersistedObjectsTracker.php

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,17 @@ final class PersistedObjectsTracker
2323
*
2424
* @var list<\WeakReference<object>>
2525
*/
26-
private static $buffer = [];
26+
private static array $buffer = [];
27+
28+
/**
29+
* @var \WeakMap<object, mixed>
30+
*/
31+
private static \WeakMap $ids;
32+
33+
public function __construct()
34+
{
35+
self::$ids ??= new \WeakMap();
36+
}
2737

2838
public function refresh(): void
2939
{
@@ -34,12 +44,33 @@ public function add(object ...$objects): void
3444
{
3545
foreach ($objects as $object) {
3646
self::$buffer[] = \WeakReference::create($object);
47+
48+
$id = Configuration::instance()->persistence()->getIdentifierValues($object);
49+
if ($id) {
50+
self::$ids[$object] = $id;
51+
}
52+
}
53+
}
54+
55+
public static function updateIds(): void
56+
{
57+
foreach (self::$buffer as $reference) {
58+
if (null === $object = $reference->get()) {
59+
continue;
60+
}
61+
62+
if (self::$ids->offsetExists($object)) {
63+
continue;
64+
}
65+
66+
self::$ids[$object] = Configuration::instance()->persistence()->getIdentifierValues($object);
3767
}
3868
}
3969

4070
public static function reset(): void
4171
{
4272
self::$buffer = [];
73+
self::$ids = new \WeakMap();
4374
}
4475

4576
public static function countObjects(): int
@@ -51,6 +82,10 @@ public static function countObjects(): int
5182

5283
private static function proxifyObjects(): void
5384
{
85+
if (!Configuration::isBooted()) {
86+
return;
87+
}
88+
5489
self::$buffer = \array_values(
5590
\array_map(
5691
static function(\WeakReference $weakRef) {
@@ -64,7 +99,9 @@ static function(\WeakReference $weakRef) {
6499

65100
$clone = clone $object;
66101
$reflector->resetAsLazyGhost($object, function($object) use ($clone) {
67-
Configuration::instance()->persistence()->autorefresh($object, $clone);
102+
$id = self::$ids[$object] ?? throw new \LogicException('Canot find the id for object');
103+
104+
Configuration::instance()->persistence()->autorefresh($object, $id, $clone);
68105
});
69106

70107
return \WeakReference::create($object);
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\Entity\EdgeCases;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
7+
#[ORM\Entity]
8+
#[ORM\Table('entity_with_clone_method')]
9+
class EntityWithCloneMethod
10+
{
11+
#[ORM\Id]
12+
#[ORM\GeneratedValue]
13+
#[ORM\Column(type: 'integer')]
14+
public int|null $id = null;
15+
16+
#[ORM\Column(nullable: true)]
17+
public string|null $prop = null;
18+
19+
public function __clone()
20+
{
21+
$this->id = null;
22+
}
23+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Zenstruck\Foundry\Tests\Integration;
4+
5+
use PHPUnit\Framework\Attributes\Depends;
6+
use PHPUnit\Framework\Attributes\Test;
7+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
8+
use Zenstruck\Foundry\Test\Factories;
9+
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory;
10+
11+
final class GetWebTestClientIsNotBrokenTest extends WebTestCase
12+
{
13+
use Factories, RequiresORM;
14+
15+
/**
16+
* @test
17+
*/
18+
#[Test]
19+
public function boots_kernel_and_get_client(): void
20+
{
21+
$client = self::createClient();
22+
23+
$object = GenericEntityFactory::createOne();
24+
$client->request('GET', "/orm/update/{$object->id}");
25+
self::assertResponseIsSuccessful();
26+
}
27+
28+
/**
29+
* @test
30+
* @depends boots_kernel_and_get_client
31+
*/
32+
#[Test]
33+
#[Depends('boots_kernel_and_get_client')]
34+
public function assert_test_starts_with_a_non_booted_kernel(): void
35+
{
36+
self::assertFalse(self::$booted);
37+
38+
// ensure we can get a client without the error:
39+
// Booting the kernel before calling "WebTestCase::createClient()" is not supported
40+
self::createClient();
41+
}
42+
43+
}

tests/Integration/ORM/AutoRefreshTest.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use Doctrine\ORM\EntityManagerInterface;
1717
use PHPUnit\Framework\Attributes\DataProvider;
18+
use PHPUnit\Framework\Attributes\Depends;
1819
use PHPUnit\Framework\Attributes\IgnorePhpunitWarnings;
1920
use PHPUnit\Framework\Attributes\RequiresEnvironmentVariable;
2021
use PHPUnit\Framework\Attributes\RequiresPhp;
@@ -26,13 +27,15 @@
2627
use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist;
2728
use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships;
2829
use Zenstruck\Foundry\Tests\Fixture\Entity\Contact;
30+
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\EntityWithCloneMethod;
2931
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\AddressFactory;
3032
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CategoryFactory;
3133
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory;
3234
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory;
3335
use Zenstruck\Foundry\Tests\Integration\Persistence\AutoRefreshTestCase;
3436
use Zenstruck\Foundry\Tests\Integration\RequiresORM;
3537

38+
use function Zenstruck\Foundry\Persistence\persistent_factory;
3639
use function Zenstruck\Foundry\Persistence\refresh_all;
3740

3841
/**
@@ -122,8 +125,26 @@ public function it_can_refresh_objects_with_relationships(): void
122125
self::assertSame('city', $contact->getAddress()->getCity());
123126
}
124127

128+
/**
129+
* The previous test creates entities with circular dependencies,
130+
* so the PersistedObjectsTracker still have references to them.
131+
*
132+
* Let's ensure we don't leave the test in an invalide state when the container is reset
133+
*/
134+
#[Test]
135+
#[Depends('it_can_refresh_objects_with_relationships')]
136+
public function assert_test_starts_with_a_non_booted_kernel(): void
137+
{
138+
self::assertSame(0, PersistedObjectsTracker::countObjects());
139+
140+
self::assertFalse(self::$booted);
141+
142+
// ensure we can get a client without the error:
143+
// Booting the kernel before calling "WebTestCase::createClient()" is not supported
144+
self::createClient();
145+
}
146+
125147
#[Test]
126-
#[IgnorePhpunitWarnings(EdgeCasesRelationshipTest::DATA_PROVIDER_WARNING_REGEX)]
127148
public function it_can_refresh_with_doctrine_proxies(): void
128149
{
129150
$contact = ContactFactory::createOne();
@@ -151,6 +172,22 @@ public function it_can_refresh_with_doctrine_proxies(): void
151172
self::assertSame('foo', $category->getName());
152173
}
153174

175+
#[Test]
176+
public function it_can_refresh_entity_which_removes_its_id_in_clone(): void
177+
{
178+
$entity = persistent_factory(EntityWithCloneMethod::class)->create();
179+
$entityId = $entity->id;
180+
181+
$this->objectManager()->getConnection()->executeQuery('UPDATE entity_with_clone_method SET prop = \'foo\' WHERE id = ?', [$entity->id]);
182+
183+
self::ensureKernelShutdown();
184+
185+
self::assertTrue((new \ReflectionClass($entity))->isUninitializedLazyObject($entity));
186+
187+
self::assertSame('foo', $entity->prop);
188+
self::assertSame($entityId, $entity->id);
189+
}
190+
154191
protected static function factory(): PersistentObjectFactory
155192
{
156193
return GenericEntityFactory::new();

tests/Integration/Persistence/AutoRefreshTestCase.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public function it_can_refresh_after_kernel_shutdown(): void
6262

6363
// service reset did clear the EM, thus the object is not managed anymore
6464
self::assertFalse($this->objectManager()->contains($object));
65+
self::assertSame($objectId, $object->id);
6566
}
6667

6768
#[Test]
@@ -80,6 +81,9 @@ public function it_can_refresh_many_objects(): void
8081

8182
self::assertSame('foo', $object1->getProp1());
8283
self::assertSame('foo', $object2->getProp1());
84+
85+
self::assertSame($objectId1, $object1->id);
86+
self::assertSame($objectId2, $object2->id);
8387
}
8488

8589
#[Test]
@@ -88,6 +92,8 @@ public function it_can_refresh_after_update_with_browser(): void
8892
$client = self::createClient();
8993

9094
$object = $this->factory()->create();
95+
$objectId = $object->id;
96+
9197
self::assertSame('default1', $object->getProp1());
9298
self::assertFalse((new \ReflectionClass($object))->isUninitializedLazyObject($object));
9399

@@ -100,6 +106,8 @@ public function it_can_refresh_after_update_with_browser(): void
100106
assert_persisted($object);
101107
self::assertSame('foo', $object->getProp1());
102108
self::assertFalse((new \ReflectionClass($object))->isUninitializedLazyObject($object));
109+
110+
self::assertSame($objectId, $object->id);
103111
}
104112

105113
#[Test]

0 commit comments

Comments
 (0)