Skip to content

Commit 17d28b5

Browse files
authored
Merge pull request #11917 from stof/lazy_ghost_postload
Fix the initialization of lazy-ghost proxies with postLoad listeners
2 parents cc29ae0 + a2d510c commit 17d28b5

File tree

9 files changed

+223
-26
lines changed

9 files changed

+223
-26
lines changed

src/Proxy/ProxyFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi
378378
$class = $entityPersister->getClassMetadata();
379379

380380
foreach ($class->getReflectionProperties() as $property) {
381-
if (isset($identifier[$property->name]) || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
381+
if (isset($identifier[$property->name])) {
382382
continue;
383383
}
384384

@@ -448,7 +448,7 @@ private function getProxyFactory(string $className): Closure
448448
foreach ($reflector->getProperties($filter) as $property) {
449449
$name = $property->name;
450450

451-
if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
451+
if ($property->isStatic() || ! isset($identifiers[$name])) {
452452
continue;
453453
}
454454

src/UnitOfWork.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
use Exception;
5050
use InvalidArgumentException;
5151
use RuntimeException;
52+
use Symfony\Component\VarExporter\Hydrator;
5253
use UnexpectedValueException;
5354

5455
use function array_chunk;
@@ -2944,6 +2945,11 @@ public function createEntity($className, array $data, &$hints = [])
29442945

29452946
if ($this->isUninitializedObject($entity)) {
29462947
$entity->__setInitialized(true);
2948+
2949+
if ($this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
2950+
// Initialize properties that have default values to their default value (similar to what
2951+
Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
2952+
}
29472953
} else {
29482954
if (
29492955
! isset($hints[Query::HINT_REFRESH])
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\GH11524;
6+
7+
use Doctrine\ORM\Mapping as ORM;
8+
9+
/**
10+
* @ORM\Entity
11+
* @ORM\Table(name="gh11524_entities")
12+
*/
13+
class GH11524Entity
14+
{
15+
/**
16+
* @ORM\Id
17+
* @ORM\Column(type="integer")
18+
* @ORM\GeneratedValue
19+
*
20+
* @var int|null
21+
*/
22+
public $id = null;
23+
24+
/**
25+
* @ORM\ManyToOne(targetEntity="GH11524Relation")
26+
* @ORM\JoinColumn(name="relation_id", referencedColumnName="id", nullable=true)
27+
*
28+
* @var GH11524Relation|null
29+
*/
30+
public $relation = null;
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\GH11524;
6+
7+
use Doctrine\ORM\Event\PostLoadEventArgs;
8+
9+
class GH11524Listener
10+
{
11+
public function postLoad(PostloadEventArgs $eventArgs): void
12+
{
13+
$object = $eventArgs->getObject();
14+
15+
if (! $object instanceof GH11524Relation) {
16+
return;
17+
}
18+
19+
$object->setCurrentLocale('en');
20+
}
21+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\GH11524;
6+
7+
use Doctrine\ORM\Mapping as ORM;
8+
use LogicException;
9+
10+
/**
11+
* @ORM\Entity
12+
* @ORM\Table(name="gh11524_relations")
13+
*/
14+
class GH11524Relation
15+
{
16+
/**
17+
* @ORM\Id
18+
* @ORM\Column(type="integer")
19+
* @ORM\GeneratedValue
20+
*
21+
* @var int|null
22+
*/
23+
public $id;
24+
25+
/**
26+
* @ORM\Column(type="string")
27+
*
28+
* @var string
29+
*/
30+
public $name;
31+
32+
/**
33+
* @var string|null
34+
*/
35+
private $currentLocale;
36+
37+
public function setCurrentLocale(string $locale): void
38+
{
39+
$this->currentLocale = $locale;
40+
}
41+
42+
public function getTranslation(): string
43+
{
44+
if ($this->currentLocale === null) {
45+
throw new LogicException('The current locale must be set to retrieve translation.');
46+
}
47+
48+
return 'fake';
49+
}
50+
}

tests/Tests/ORM/Functional/LifecycleCallbackTest.php

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ public function testPreSavePostSaveCallbacksAreInvoked(): void
6666
self::assertTrue($entity->postPersistCallbackInvoked);
6767

6868
$this->_em->clear();
69+
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
6970

7071
$query = $this->_em->createQuery('select e from Doctrine\Tests\ORM\Functional\LifecycleCallbackTestEntity e');
7172
$result = $query->getResult();
72-
self::assertTrue($result[0]->postLoadCallbackInvoked);
73+
self::assertTrue($result[0]::$postLoadCallbackInvoked);
7374

7475
$result[0]->value = 'hello again';
7576

@@ -130,12 +131,14 @@ public function testGetReferenceWithPostLoadEventIsDelayedUntilProxyTrigger(): v
130131
$id = $entity->getId();
131132

132133
$this->_em->clear();
134+
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
133135

134136
$reference = $this->_em->getReference(LifecycleCallbackTestEntity::class, $id);
135-
self::assertFalse($reference->postLoadCallbackInvoked);
137+
self::assertFalse($reference::$postLoadCallbackInvoked);
138+
$this->assertTrue($this->isUninitializedObject($reference));
136139

137140
$reference->getValue(); // trigger proxy load
138-
self::assertTrue($reference->postLoadCallbackInvoked);
141+
self::assertTrue($reference::$postLoadCallbackInvoked);
139142
}
140143

141144
/** @group DDC-958 */
@@ -148,13 +151,14 @@ public function testPostLoadTriggeredOnRefresh(): void
148151
$id = $entity->getId();
149152

150153
$this->_em->clear();
154+
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
151155

152156
$reference = $this->_em->find(LifecycleCallbackTestEntity::class, $id);
153-
self::assertTrue($reference->postLoadCallbackInvoked);
154-
$reference->postLoadCallbackInvoked = false;
157+
self::assertTrue($reference::$postLoadCallbackInvoked);
158+
$reference::$postLoadCallbackInvoked = false;
155159

156160
$this->_em->refresh($reference);
157-
self::assertTrue($reference->postLoadCallbackInvoked, 'postLoad should be invoked when refresh() is called.');
161+
self::assertTrue($reference::$postLoadCallbackInvoked, 'postLoad should be invoked when refresh() is called.');
158162
}
159163

160164
/** @group DDC-113 */
@@ -197,6 +201,7 @@ public function testCascadedEntitiesLoadedInPostLoad(): void
197201

198202
$this->_em->flush();
199203
$this->_em->clear();
204+
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
200205

201206
$dql = <<<'DQL'
202207
SELECT
@@ -214,9 +219,9 @@ public function testCascadedEntitiesLoadedInPostLoad(): void
214219
->createQuery(sprintf($dql, $e1->getId(), $e2->getId()))
215220
->getResult();
216221

217-
self::assertTrue(current($entities)->postLoadCallbackInvoked);
222+
self::assertTrue(current($entities)::$postLoadCallbackInvoked);
218223
self::assertTrue(current($entities)->postLoadCascaderNotNull);
219-
self::assertTrue(current($entities)->cascader->postLoadCallbackInvoked);
224+
self::assertTrue(current($entities)->cascader::$postLoadCallbackInvoked);
220225
self::assertEquals(current($entities)->cascader->postLoadEntitiesCount, 2);
221226
}
222227

@@ -239,6 +244,8 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIteration(): void
239244

240245
$this->_em->flush();
241246
$this->_em->clear();
247+
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
248+
LifecycleCallbackCascader::$postLoadCallbackInvoked = false;
242249

243250
$dql = <<<'DQL'
244251
SELECT
@@ -256,7 +263,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIteration(): void
256263
$result = iterator_to_array($query->iterate());
257264

258265
foreach ($result as $entity) {
259-
self::assertTrue($entity[0]->postLoadCallbackInvoked);
266+
self::assertTrue($entity[0]::$postLoadCallbackInvoked);
260267
self::assertFalse($entity[0]->postLoadCascaderNotNull);
261268

262269
break;
@@ -265,7 +272,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIteration(): void
265272
$iterableResult = iterator_to_array($query->toIterable());
266273

267274
foreach ($iterableResult as $entity) {
268-
self::assertTrue($entity->postLoadCallbackInvoked);
275+
self::assertTrue($entity::$postLoadCallbackInvoked);
269276
self::assertFalse($entity->postLoadCascaderNotNull);
270277

271278
break;
@@ -283,6 +290,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIterationWithSimple
283290

284291
$this->_em->flush();
285292
$this->_em->clear();
293+
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
286294

287295
$query = $this->_em->createQuery(
288296
'SELECT e FROM Doctrine\Tests\ORM\Functional\LifecycleCallbackTestEntity AS e'
@@ -291,7 +299,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIterationWithSimple
291299
$result = iterator_to_array($query->iterate(null, Query::HYDRATE_SIMPLEOBJECT));
292300

293301
foreach ($result as $entity) {
294-
self::assertTrue($entity[0]->postLoadCallbackInvoked);
302+
self::assertTrue($entity[0]::$postLoadCallbackInvoked);
295303
self::assertFalse($entity[0]->postLoadCascaderNotNull);
296304

297305
break;
@@ -300,7 +308,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIterationWithSimple
300308
$result = iterator_to_array($query->toIterable([], Query::HYDRATE_SIMPLEOBJECT));
301309

302310
foreach ($result as $entity) {
303-
self::assertTrue($entity->postLoadCallbackInvoked);
311+
self::assertTrue($entity::$postLoadCallbackInvoked);
304312
self::assertFalse($entity->postLoadCascaderNotNull);
305313

306314
break;
@@ -325,6 +333,8 @@ public function testPostLoadIsInvokedOnFetchJoinedEntities(): void
325333

326334
$this->_em->flush();
327335
$this->_em->clear();
336+
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
337+
LifecycleCallbackCascader::$postLoadCallbackInvoked = false;
328338

329339
$dql = <<<'DQL'
330340
SELECT
@@ -342,9 +352,9 @@ public function testPostLoadIsInvokedOnFetchJoinedEntities(): void
342352
->createQuery($dql)->setParameter('entA_id', $entA->getId())
343353
->getOneOrNullResult();
344354

345-
self::assertTrue($fetchedA->postLoadCallbackInvoked);
355+
self::assertTrue($fetchedA::$postLoadCallbackInvoked);
346356
foreach ($fetchedA->entities as $fetchJoinedEntB) {
347-
self::assertTrue($fetchJoinedEntB->postLoadCallbackInvoked);
357+
self::assertTrue($fetchJoinedEntB::$postLoadCallbackInvoked);
348358
}
349359
}
350360

@@ -492,7 +502,7 @@ class LifecycleCallbackTestEntity
492502
public $postPersistCallbackInvoked = false;
493503

494504
/** @var bool */
495-
public $postLoadCallbackInvoked = false;
505+
public static $postLoadCallbackInvoked = false;
496506

497507
/** @var bool */
498508
public $postLoadCascaderNotNull = false;
@@ -546,7 +556,7 @@ public function doStuffOnPostPersist(): void
546556
/** @PostLoad */
547557
public function doStuffOnPostLoad(): void
548558
{
549-
$this->postLoadCallbackInvoked = true;
559+
self::$postLoadCallbackInvoked = true;
550560
$this->postLoadCascaderNotNull = isset($this->cascader);
551561
}
552562

@@ -572,7 +582,7 @@ class LifecycleCallbackCascader
572582
{
573583
/* test stuff */
574584
/** @var bool */
575-
public $postLoadCallbackInvoked = false;
585+
public static $postLoadCallbackInvoked = false;
576586

577587
/** @var int */
578588
public $postLoadEntitiesCount = 0;
@@ -599,7 +609,7 @@ public function __construct()
599609
/** @PostLoad */
600610
public function doStuffOnPostLoad(): void
601611
{
602-
$this->postLoadCallbackInvoked = true;
612+
self::$postLoadCallbackInvoked = true;
603613
$this->postLoadEntitiesCount = count($this->entities);
604614
}
605615

tests/Tests/ORM/Functional/Ticket/DDC1690Test.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,20 @@ public function testChangeTracking(): void
5151
$parentId = $parent->getId();
5252
$childId = $child->getId();
5353
unset($parent, $child);
54+
DDC1690Parent::$addPropertyChangedListenerInvoked = false;
55+
DDC1690Child::$addPropertyChangedListenerInvoked = false;
5456

5557
$parent = $this->_em->find(DDC1690Parent::class, $parentId);
5658
$child = $this->_em->find(DDC1690Child::class, $childId);
5759

60+
self::assertTrue($parent::$addPropertyChangedListenerInvoked);
5861
self::assertEquals(1, count($parent->listeners));
59-
self::assertCount(0, $child->listeners);
62+
$this->assertTrue($this->isUninitializedObject($child));
63+
self::assertFalse($child::$addPropertyChangedListenerInvoked);
6064

6165
$this->_em->getUnitOfWork()->initializeObject($child);
6266

67+
self::assertTrue($child::$addPropertyChangedListenerInvoked);
6368
self::assertCount(1, $child->listeners);
6469
unset($parent, $child);
6570

@@ -106,6 +111,11 @@ protected function onPropertyChanged($propName, $oldValue, $newValue): void
106111
*/
107112
class DDC1690Parent extends NotifyBaseEntity
108113
{
114+
/**
115+
* @var bool
116+
*/
117+
public static $addPropertyChangedListenerInvoked = false;
118+
109119
/**
110120
* @var int
111121
* @Id
@@ -151,11 +161,23 @@ public function getChild(): DDC1690Child
151161
{
152162
return $this->child;
153163
}
164+
165+
public function addPropertyChangedListener(PropertyChangedListener $listener): void
166+
{
167+
self::$addPropertyChangedListenerInvoked = true;
168+
169+
parent::addPropertyChangedListener($listener);
170+
}
154171
}
155172

156173
/** @Entity */
157174
class DDC1690Child extends NotifyBaseEntity
158175
{
176+
/**
177+
* @var bool
178+
*/
179+
public static $addPropertyChangedListenerInvoked = false;
180+
159181
/**
160182
* @var int
161183
* @Id
@@ -201,4 +223,11 @@ public function getParent(): DDC1690Parent
201223
{
202224
return $this->parent;
203225
}
226+
227+
public function addPropertyChangedListener(PropertyChangedListener $listener): void
228+
{
229+
self::$addPropertyChangedListenerInvoked = true;
230+
231+
parent::addPropertyChangedListener($listener);
232+
}
204233
}

0 commit comments

Comments
 (0)