diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index c79afd084ee..289f83fb2c5 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -104,6 +104,15 @@ class UnitOfWork implements PropertyChangedListener */ private $identityMap = []; + /** + * Associate entities with OIDs to ensure the GC won't recycle a managed entity + * + * DDC-2332 / #3037 + * + * @var array + */ + private $oidMap = array(); + /** * Map of all identifiers of managed entities. * Keys are object ids (spl_object_hash). @@ -1498,7 +1507,8 @@ public function isEntityScheduled($entity) public function addToIdentityMap($entity) { $classMetadata = $this->em->getClassMetadata(get_class($entity)); - $identifier = $this->entityIdentifiers[spl_object_hash($entity)]; + $oid = spl_object_hash($entity); + $identifier = $this->entityIdentifiers[$oid]; if (empty($identifier) || in_array(null, $identifier, true)) { throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity); @@ -1507,6 +1517,7 @@ public function addToIdentityMap($entity) $idHash = implode(' ', $identifier); $className = $classMetadata->rootEntityName; + $this->oidMap[$oid] = $entity; if (isset($this->identityMap[$className][$idHash])) { return false; } @@ -1622,6 +1633,7 @@ public function removeFromIdentityMap($entity) $className = $classMetadata->rootEntityName; + unset($this->oidMap[$oid]); if (isset($this->identityMap[$className][$idHash])) { unset($this->identityMap[$className][$idHash]); unset($this->readOnlyObjects[$oid]); @@ -2482,6 +2494,7 @@ public function clear($entityName = null) { if ($entityName === null) { $this->identityMap = + $this->oidMap = $this->entityIdentifiers = $this->originalEntityData = $this->entityChangeSets = @@ -2656,6 +2669,7 @@ public function createEntity($className, array $data, &$hints = []) $this->entityStates[$oid] = self::STATE_MANAGED; $this->originalEntityData[$oid] = $data; + $this->oidMap[$oid] = $entity; $this->identityMap[$class->rootEntityName][$idHash] = $entity; if ($entity instanceof NotifyPropertyChanged) { @@ -2806,6 +2820,7 @@ public function createEntity($className, array $data, &$hints = []) // PERF: Inlined & optimized code from UnitOfWork#registerManaged() $newValueOid = spl_object_hash($newValue); $this->entityIdentifiers[$newValueOid] = $associatedId; + $this->oidMap[$newValueOid] = $newValue; $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue; if ( diff --git a/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php b/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php index 15929ae270d..ef7748ddb5a 100644 --- a/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php @@ -4,6 +4,7 @@ use Doctrine\ORM\Query; use Doctrine\Tests\Models\CMS\CmsAddress; +use Doctrine\Tests\Models\CMS\CmsArticle; use Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\Tests\Models\CMS\CmsUser; use Doctrine\Tests\OrmFunctionalTestCase; @@ -254,5 +255,50 @@ public function testCollectionValuedAssociationIdentityMapBehaviorWithRefresh() // Now the collection should be refreshed with correct count $this->assertEquals(4, count($user2->getPhonenumbers())); } + + /** + * @group HashCollision + */ + public function testHashCollision() { + $user = new CmsUser(); + $user->username = "Test"; + $user->name = "Test"; + $this->_em->persist($user); + $this->_em->flush(); + + $articles = []; + for ($i = 0; $i < 100; $i++) { + $article = new CmsArticle(); + $article->topic = "Test"; + $article->text = "Test"; + $article->setAuthor($this->_em->merge($user)); + $this->_em->persist($article); + $this->_em->flush(); + $this->_em->clear(); + $articles [] = $article; + } + + $user = $this->_em->merge($user); + foreach ($articles as $article) { + $article = $this->_em->merge($article); + $article->setAuthor($user); + } + + unset($article); + gc_collect_cycles(); + + $keep = []; + for ($x = 0; $x < 1000; $x++) { + $keep[] = $article = new CmsArticle(); + + $article->topic = "Test"; + $article->text = "Test"; + $article->setAuthor($this->_em->merge($user)); + + $this->_em->persist($article); + $this->_em->flush(); + $this->assertNotNull($article->id, "Article wasn't persisted on iteration $x"); + } + } }