Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 993fe24

Browse files
committed
Avoid creating unmanaged proxy instances for referred-to entities during merge()
This PR tries to improve the situation/problem explained in doctrine#3037: Under certain conditions – there may be multiple and not all are known/well-understood – we may get inconsistencies between the `\Doctrine\ORM\UnitOfWork::$entityIdentifiers` and `\Doctrine\ORM\UnitOfWork::$identityMap` arrays. Since the `::$identityMap` is a plain array holding object references, objects contained in it cannot be garbage-collected. `::$entityIdentifiers`, however, is indexed by `spl_object_id` values. When those objects are destructed and/or garbage-collected, the OID may be reused and reassigned to other objects later on. When the OID re-assignment happens to be for another entity, the UoW may assume incorrect entity states and, for example, miss INSERT or UPDATE operations. One cause for such inconsistencies is _replacing_ identity map entries with other object instances: This makes it possible that the old object becomes GC'd, while its OID is not cleaned up. Since that is not a use case we need to support (IMHO), doctrine#10785 is about adding a safeguard against it. In this test shown here, the `merge()` operation is currently too eager in creating a proxy object for another referred-to entity. This proxy represents an entity already present in the identity map at that time, potentially leading to this problem later on.
1 parent fe8e313 commit 993fe24

2 files changed

Lines changed: 99 additions & 8 deletions

File tree

lib/Doctrine/ORM/UnitOfWork.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3640,14 +3640,18 @@ private function mergeEntityStateIntoManagedCopy($entity, $managedCopy): void
36403640
$targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
36413641
$relatedId = $targetClass->getIdentifierValues($other);
36423642

3643-
if ($targetClass->subClasses) {
3644-
$other = $this->em->find($targetClass->name, $relatedId);
3645-
} else {
3646-
$other = $this->em->getProxyFactory()->getProxy(
3647-
$assoc2['targetEntity'],
3648-
$relatedId
3649-
);
3650-
$this->registerManaged($other, $relatedId, []);
3643+
$other = $this->tryGetById($relatedId, $targetClass->name);
3644+
3645+
if (! $other) {
3646+
if ($targetClass->subClasses) {
3647+
$other = $this->em->find($targetClass->name, $relatedId);
3648+
} else {
3649+
$other = $this->em->getProxyFactory()->getProxy(
3650+
$assoc2['targetEntity'],
3651+
$relatedId
3652+
);
3653+
$this->registerManaged($other, $relatedId, []);
3654+
}
36513655
}
36523656
}
36533657

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket;
6+
7+
use Doctrine\ORM\UnitOfWork;
8+
use Doctrine\Tests\Models\CMS\CmsArticle;
9+
use Doctrine\Tests\Models\CMS\CmsUser;
10+
use Doctrine\Tests\OrmFunctionalTestCase;
11+
use ReflectionClass;
12+
13+
use function spl_object_id;
14+
15+
class GH7407Test extends OrmFunctionalTestCase
16+
{
17+
protected function setUp(): void
18+
{
19+
$this->useModelSet('cms');
20+
21+
parent::setUp();
22+
}
23+
24+
public function testMergingEntitiesDoesNotCreateUnmanagedProxyReferences(): void
25+
{
26+
// 1. Create an article with a user; persist, flush and clear the entity manager
27+
$user = new CmsUser();
28+
$user->username = 'Test';
29+
$user->name = 'Test';
30+
$this->_em->persist($user);
31+
32+
$article = new CmsArticle();
33+
$article->topic = 'Test';
34+
$article->text = 'Test';
35+
$article->setAuthor($user);
36+
$this->_em->persist($article);
37+
38+
$this->_em->flush();
39+
$this->_em->clear();
40+
41+
// 2. Merge the user object back in:
42+
// We get a new (different) entity object that represents the user instance
43+
// which is now (through this object instance) managed by the EM/UoW
44+
$mergedUser = $this->_em->merge($user);
45+
$mergedUserOid = spl_object_id($mergedUser);
46+
47+
// 3. Merge the article object back in,
48+
// the returned entity object is the article instance as it is managed by the EM/UoW
49+
$mergedArticle = $this->_em->merge($article);
50+
$mergedArticleOid = spl_object_id($mergedArticle);
51+
52+
self::assertSame($mergedUser, $mergedArticle->user, 'The $mergedArticle\'s #user property should hold the $mergedUser we obtained previously, since that\'s the only legitimate object instance representing the user from the UoW\'s point of view.');
53+
54+
// Inspect internal UoW state
55+
$uow = $this->_em->getUnitOfWork();
56+
$entityIdentifiers = $this->grabProperty('entityIdentifiers', $uow);
57+
$identityMap = $this->grabProperty('identityMap', $uow);
58+
$entityStates = $this->grabProperty('entityStates', $uow);
59+
60+
self::assertCount(2, $entityIdentifiers, 'UoW#entityIdentifiers contains exactly two OID -> ID value mapping entries one for the article, one for the user object');
61+
self::assertArrayHasKey($mergedArticleOid, $entityIdentifiers);
62+
self::assertArrayHasKey($mergedUserOid, $entityIdentifiers);
63+
64+
self::assertSame([
65+
$mergedUserOid => UnitOfWork::STATE_MANAGED,
66+
$mergedArticleOid => UnitOfWork::STATE_MANAGED,
67+
], $entityStates, 'UoW#entityStates contains two OID -> state entries, one for the article, one for the user object');
68+
69+
self::assertCount(2, $entityIdentifiers);
70+
self::assertArrayHasKey($mergedArticleOid, $entityIdentifiers);
71+
self::assertArrayHasKey($mergedUserOid, $entityIdentifiers);
72+
73+
self::assertSame([
74+
CmsUser::class => [$user->id => $mergedUser],
75+
CmsArticle::class => [$article->id => $mergedArticle],
76+
], $identityMap, 'The identity map contains exactly two objects, the article and the user.');
77+
}
78+
79+
private function grabProperty(string $name, UnitOfWork $uow)
80+
{
81+
$reflection = new ReflectionClass($uow);
82+
$property = $reflection->getProperty($name);
83+
$property->setAccessible(true);
84+
85+
return $property->getValue($uow);
86+
}
87+
}

0 commit comments

Comments
 (0)