diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 699926f43..c0e681bce 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1782,10 +1782,14 @@ class UnitOfWork implements PropertyChangedListener $oid = spl_object_hash($entity); if (isset($visited[$oid])) { - return $visited[$oid]; // Prevent infinite recursion - } + $managedCopy = $visited[$oid]; - $visited[$oid] = $entity; // mark visited + if ($prevManagedCopy !== null) { + $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy); + } + + return $managedCopy; + } $class = $this->em->getClassMetadata(get_class($entity)); @@ -1855,6 +1859,8 @@ class UnitOfWork implements PropertyChangedListener } } + $visited[$oid] = $managedCopy; // mark visited + // Merge state of $entity into existing (managed) entity foreach ($class->reflClass->getProperties() as $prop) { $name = $prop->name; @@ -1898,9 +1904,9 @@ class UnitOfWork implements PropertyChangedListener $managedCol = $prop->getValue($managedCopy); if (!$managedCol) { $managedCol = new PersistentCollection($this->em, - $this->em->getClassMetadata($assoc2['targetEntity']), - new ArrayCollection - ); + $this->em->getClassMetadata($assoc2['targetEntity']), + new ArrayCollection + ); $managedCol->setOwner($managedCopy, $assoc2); $prop->setValue($managedCopy, $managedCol); $this->originalEntityData[$oid][$name] = $managedCol; @@ -1933,28 +1939,48 @@ class UnitOfWork implements PropertyChangedListener } if ($prevManagedCopy !== null) { - $assocField = $assoc['fieldName']; - $prevClass = $this->em->getClassMetadata(get_class($prevManagedCopy)); - - if ($assoc['type'] & ClassMetadata::TO_ONE) { - $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy); - } else { - $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy); - - if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) { - $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy); - } - } + $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy); } // Mark the managed copy visited as well - $visited[spl_object_hash($managedCopy)] = true; + $visited[spl_object_hash($managedCopy)] = $managedCopy; $this->cascadeMerge($entity, $managedCopy, $visited); return $managedCopy; } + /** + * Sets/adds associated managed copies into the previous entity's association field + * + * @param object $entity + * @param array $association + * @param object $previousManagedCopy + * @param object $managedCopy + * + * @return void + */ + private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy) + { + $assocField = $association['fieldName']; + $prevClass = $this->em->getClassMetadata(get_class($previousManagedCopy)); + + if ($association['type'] & ClassMetadata::TO_ONE) { + $prevClass->reflFields[$assocField]->setValue($previousManagedCopy, $managedCopy); + + return; + } + + $value = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy); + $value[] = $managedCopy; + + if ($association['type'] == ClassMetadata::ONE_TO_MANY) { + $class = $this->em->getClassMetadata(get_class($entity)); + + $class->reflFields[$association['mappedBy']]->setValue($managedCopy, $previousManagedCopy); + } + } + /** * Detaches an entity from the persistence management. It's persistence will * no longer be managed by Doctrine. diff --git a/tests/Doctrine/Tests/ORM/Functional/MergeSharedEntitiesTest.php b/tests/Doctrine/Tests/ORM/Functional/MergeSharedEntitiesTest.php new file mode 100644 index 000000000..3dae34b67 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/MergeSharedEntitiesTest.php @@ -0,0 +1,113 @@ +. + */ + +namespace Doctrine\Tests\ORM\Functional; + +use Doctrine\ORM\Tools\ToolsException; +use Doctrine\Tests\OrmFunctionalTestCase; + +class MergeSharedEntitiesTest extends OrmFunctionalTestCase +{ + /** + * {@inheritDoc} + */ + protected function setUp() + { + parent::setUp(); + + try { + $this->_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\MSEFile'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\MSEPicture'), + )); + } catch (ToolsException $ignored) { + } + } + + public function testMergeSharedNewEntities() + { + $file = new MSEFile; + $picture = new MSEPicture; + + $picture->file = $file; + $picture->otherFile = $file; + + $picture = $this->_em->merge($picture); + + $this->assertEquals($picture->file, $picture->otherFile, 'Identical entities must remain identical'); + } + + public function testMergeSharedManagedEntities() + { + $file = new MSEFile; + $picture = new MSEPicture; + + $picture->file = $file; + $picture->otherFile = $file; + + $this->_em->persist($file); + $this->_em->persist($picture); + $this->_em->flush(); + $this->_em->clear(); + + $picture = $this->_em->merge($picture); + + $this->assertEquals($picture->file, $picture->otherFile, 'Identical entities must remain identical'); + } + + public function testMergeSharedDetachedSerializedEntities() + { + $file = new MSEFile; + $picture = new MSEPicture; + + $picture->file = $file; + $picture->otherFile = $file; + + $serializedPicture = serialize($picture); + + $this->_em->persist($file); + $this->_em->persist($picture); + $this->_em->flush(); + $this->_em->clear(); + + $picture = $this->_em->merge(unserialize($serializedPicture)); + + $this->assertEquals($picture->file, $picture->otherFile, 'Identical entities must remain identical'); + } +} + +/** @Entity */ +class MSEPicture +{ + /** @Column(type="integer") @Id @GeneratedValue */ + public $id; + + /** @ManyToOne(targetEntity="MSEFile", cascade={"merge"}) */ + public $file; + + /** @ManyToOne(targetEntity="MSEFile", cascade={"merge"}) */ + public $otherFile; +} + +/** @Entity */ +class MSEFile +{ + /** @Column(type="integer") @Id @GeneratedValue(strategy="AUTO") */ + public $id; +}