Skip to content

Commit 1db1e88

Browse files
committed
DDC-2332: Ensure managed entities are always tracked by UOW
1 parent 30c5a00 commit 1db1e88

File tree

2 files changed

+62
-1
lines changed

2 files changed

+62
-1
lines changed

lib/Doctrine/ORM/UnitOfWork.php

+16-1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ class UnitOfWork implements PropertyChangedListener
104104
*/
105105
private $identityMap = [];
106106

107+
/**
108+
* Associate entities with OIDs to ensure the GC won't recycle a managed entity
109+
*
110+
* DDC-2332 / #3037
111+
*
112+
* @var array
113+
*/
114+
private $oidMap = array();
115+
107116
/**
108117
* Map of all identifiers of managed entities.
109118
* Keys are object ids (spl_object_hash).
@@ -1498,7 +1507,8 @@ public function isEntityScheduled($entity)
14981507
public function addToIdentityMap($entity)
14991508
{
15001509
$classMetadata = $this->em->getClassMetadata(get_class($entity));
1501-
$identifier = $this->entityIdentifiers[spl_object_hash($entity)];
1510+
$oid = spl_object_hash($entity);
1511+
$identifier = $this->entityIdentifiers[$oid];
15021512

15031513
if (empty($identifier) || in_array(null, $identifier, true)) {
15041514
throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
@@ -1507,6 +1517,7 @@ public function addToIdentityMap($entity)
15071517
$idHash = implode(' ', $identifier);
15081518
$className = $classMetadata->rootEntityName;
15091519

1520+
$this->oidMap[$oid] = $entity;
15101521
if (isset($this->identityMap[$className][$idHash])) {
15111522
return false;
15121523
}
@@ -1622,6 +1633,7 @@ public function removeFromIdentityMap($entity)
16221633

16231634
$className = $classMetadata->rootEntityName;
16241635

1636+
unset($this->oidMap[$oid]);
16251637
if (isset($this->identityMap[$className][$idHash])) {
16261638
unset($this->identityMap[$className][$idHash]);
16271639
unset($this->readOnlyObjects[$oid]);
@@ -2482,6 +2494,7 @@ public function clear($entityName = null)
24822494
{
24832495
if ($entityName === null) {
24842496
$this->identityMap =
2497+
$this->oidMap =
24852498
$this->entityIdentifiers =
24862499
$this->originalEntityData =
24872500
$this->entityChangeSets =
@@ -2656,6 +2669,7 @@ public function createEntity($className, array $data, &$hints = [])
26562669
$this->entityStates[$oid] = self::STATE_MANAGED;
26572670
$this->originalEntityData[$oid] = $data;
26582671

2672+
$this->oidMap[$oid] = $entity;
26592673
$this->identityMap[$class->rootEntityName][$idHash] = $entity;
26602674

26612675
if ($entity instanceof NotifyPropertyChanged) {
@@ -2806,6 +2820,7 @@ public function createEntity($className, array $data, &$hints = [])
28062820
// PERF: Inlined & optimized code from UnitOfWork#registerManaged()
28072821
$newValueOid = spl_object_hash($newValue);
28082822
$this->entityIdentifiers[$newValueOid] = $associatedId;
2823+
$this->oidMap[$newValueOid] = $newValue;
28092824
$this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
28102825

28112826
if (

tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php

+46
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Doctrine\ORM\Query;
66
use Doctrine\Tests\Models\CMS\CmsAddress;
7+
use Doctrine\Tests\Models\CMS\CmsArticle;
78
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
89
use Doctrine\Tests\Models\CMS\CmsUser;
910
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -254,5 +255,50 @@ public function testCollectionValuedAssociationIdentityMapBehaviorWithRefresh()
254255
// Now the collection should be refreshed with correct count
255256
$this->assertEquals(4, count($user2->getPhonenumbers()));
256257
}
258+
259+
/**
260+
* @group HashCollision
261+
*/
262+
public function testHashCollision() {
263+
$user = new CmsUser();
264+
$user->username = "Test";
265+
$user->name = "Test";
266+
$this->_em->persist($user);
267+
$this->_em->flush();
268+
269+
$articles = [];
270+
for ($i = 0; $i < 100; $i++) {
271+
$article = new CmsArticle();
272+
$article->topic = "Test";
273+
$article->text = "Test";
274+
$article->setAuthor($this->_em->merge($user));
275+
$this->_em->persist($article);
276+
$this->_em->flush();
277+
$this->_em->clear();
278+
$articles [] = $article;
279+
}
280+
281+
$user = $this->_em->merge($user);
282+
foreach ($articles as $article) {
283+
$article = $this->_em->merge($article);
284+
$article->setAuthor($user);
285+
}
286+
287+
unset($article);
288+
gc_collect_cycles();
289+
290+
$keep = [];
291+
for ($x = 0; $x < 1000; $x++) {
292+
$keep[] = $article = new CmsArticle();
293+
294+
$article->topic = "Test";
295+
$article->text = "Test";
296+
$article->setAuthor($this->_em->merge($user));
297+
298+
$this->_em->persist($article);
299+
$this->_em->flush();
300+
$this->assertNotNull($article->id, "Article wasn't persisted on iteration $x");
301+
}
302+
}
257303
}
258304

0 commit comments

Comments
 (0)