diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 6c09e9697..bf7c6da1e 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -196,7 +196,7 @@ final class PersistentCollection implements Collection * Initializes the collection by loading its contents from the database * if the collection is not yet initialized. */ - private function initialize() + public function initialize() { if ( ! $this->initialized && $this->association) { if ($this->isDirty) { diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 5b5782df8..1261a4cd5 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1437,7 +1437,17 @@ class UnitOfWork implements PropertyChangedListener $prop->setValue($managedCopy, $managedCol); $this->originalEntityData[$oid][$name] = $managedCol; } - $managedCol->setInitialized($assoc2['isCascadeMerge']); + if ($assoc2['isCascadeMerge']) { + $managedCol->initialize(); + $managedCol->takeSnapshot(); + if (!$managedCol->isEmpty()) { + $managedCol->unwrap()->clear(); + $managedCol->setDirty(true); + if ($assoc2['isOwningSide'] && $assoc2['type'] == ClassMetadata::MANY_TO_MANY && $class->isChangeTrackingNotify()) { + $this->scheduleForDirtyCheck($managedCol); + } + } + } } } if ($class->isChangeTrackingNotify()) { @@ -1456,7 +1466,7 @@ class UnitOfWork implements PropertyChangedListener if ($assoc['type'] & ClassMetadata::TO_ONE) { $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy); } else { - $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->unwrap()->add($managedCopy); + $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy); if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) { $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy); } diff --git a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php index 16f3ce602..a993606cf 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php @@ -54,7 +54,7 @@ class DetachedEntityTest extends \Doctrine\Tests\OrmFunctionalTestCase $user->status = 'developer'; $ph1 = new CmsPhonenumber; - $ph1->phonenumber = 1234; + $ph1->phonenumber = "1234"; $user->addPhonenumber($ph1); $this->_em->persist($user); @@ -68,24 +68,36 @@ class DetachedEntityTest extends \Doctrine\Tests\OrmFunctionalTestCase unset($user); $user = unserialize($serialized); + + $this->assertEquals(1, count($user->getPhonenumbers()), "Pre-Condition: 1 Phonenumber"); $ph2 = new CmsPhonenumber; - $ph2->phonenumber = 56789; + $ph2->phonenumber = "56789"; $user->addPhonenumber($ph2); - $this->assertEquals(2, count($user->getPhonenumbers())); + $oldPhonenumbers = $user->getPhonenumbers(); + $this->assertEquals(2, count($oldPhonenumbers), "Pre-Condition: 2 Phonenumbers"); $this->assertFalse($this->_em->contains($user)); $this->_em->persist($ph2); - + // Merge back in $user = $this->_em->merge($user); // merge cascaded to phonenumbers + $this->assertType('Doctrine\Tests\Models\CMS\CmsUser', $user->phonenumbers[0]->user); + $this->assertType('Doctrine\Tests\Models\CMS\CmsUser', $user->phonenumbers[1]->user); + $im = $this->_em->getUnitOfWork()->getIdentityMap(); $this->_em->flush(); - $this->assertTrue($this->_em->contains($user)); - $this->assertEquals(2, count($user->getPhonenumbers())); + $this->assertTrue($this->_em->contains($user), "Failed to assert that merged user is contained inside EntityManager persistence context."); $phonenumbers = $user->getPhonenumbers(); - $this->assertTrue($this->_em->contains($phonenumbers[0])); - $this->assertTrue($this->_em->contains($phonenumbers[1])); + $this->assertNotSame($oldPhonenumbers, $phonenumbers, "Merge should replace the Detached Collection with a new PersistentCollection."); + $this->assertEquals(2, count($phonenumbers), "Failed to assert that two phonenumbers are contained in the merged users phonenumber collection."); + + $this->assertType('Doctrine\Tests\Models\CMS\CmsPhonenumber', $phonenumbers[1]); + $this->assertTrue($this->_em->contains($phonenumbers[1]), "Failed to assert that second phonenumber in collection is contained inside EntityManager persistence context."); + + $this->assertType('Doctrine\Tests\Models\CMS\CmsPhonenumber', $phonenumbers[0]); + $this->assertTrue($this->_em->getUnitOfWork()->isInIdentityMap($phonenumbers[0])); + $this->assertTrue($this->_em->contains($phonenumbers[0]), "Failed to assert that first phonenumber in collection is contained inside EntityManager persistence context."); } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC758Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC758Test.php new file mode 100644 index 000000000..60a0ff89c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC758Test.php @@ -0,0 +1,184 @@ +useModelSet("cms"); + + parent::setUp(); + } + + /** + * Helper method to set cascade to merge only + */ + private function setCascadeMergeFor($class) + { + $metadata = $this->_em->getMetadataFactory()->getMetaDataFor($class); + foreach ($metadata->associationMappings as $key => $associationMapping) { + $metadata->associationMappings[$key]["isCascadePersist"] = false; + $metadata->associationMappings[$key]["isCascadeMerge"] = true; + $metadata->associationMappings[$key]["isCascadeRemove"] = false; + $metadata->associationMappings[$key]["isCascadeDetach"] = false; + } + } + + /** + * Test that changing associations on detached entities and then cascade merging them + * causes the database to be updated with the new associations. + * This specifically tests adding new associations. + */ + public function testManyToManyMergeAssociationAdds() + { + $this->setCascadeMergeFor('Doctrine\Tests\Models\CMS\CmsUser'); + $this->setCascadeMergeFor('Doctrine\Tests\Models\CMS\CmsGroup'); + + // Put entities in the database + $cmsUser = new CmsUser(); + $cmsUser->username = "dave"; + $cmsUser->name = "Dave Keen"; + $cmsUser->status = "testing"; + + $group1 = new CmsGroup(); + $group1->name = "Group 1"; + + $group2 = new CmsGroup(); + $group2->name = "Group 2"; + + $this->_em->persist($cmsUser); + $this->_em->persist($group1); + $this->_em->persist($group2); + $this->_em->flush(); + + $cmsUserId = $cmsUser->id; + $group1Id = $group1->id; + $group2Id = $group2->id; + + $this->_em->clear(); + + // Now create detached versions of the entities with some new associations. + $cmsUser = new CmsUser(); + $cmsUser->id = $cmsUserId; + $cmsUser->username = "dave"; + $cmsUser->name = "Dave Keen"; + $cmsUser->status = "testing"; + $cmsUser->groups = new ArrayCollection(); + + $group1 = new CmsGroup(); + $group1->id = $group1Id; + $group1->name = "Group 1"; + $group1->users = new ArrayCollection(); + + $group2 = new CmsGroup(); + $group2->id = $group2Id; + $group2->name = "Group 2"; + $group2->users = new ArrayCollection(); + + $cmsUser->addGroup($group1); + $cmsUser->addGroup($group2); + + // Cascade merge of cmsUser followed by a flush should add in the birectional new many-to-many associations between the user and the groups + $this->_em->merge($cmsUser); + $this->_em->flush(); + + $this->_em->clear(); + + $cmsUsers = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + $cmsGroups = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsGroup')->findAll(); + + // Check the entities are in the database + $this->assertEquals(1, sizeof($cmsUsers)); + $this->assertEquals(2, sizeof($cmsGroups)); + + // Check the associations between the entities are now in the database + $this->assertEquals(2, sizeof($cmsUsers[0]->groups)); + $this->assertEquals(1, sizeof($cmsGroups[0]->users)); + $this->assertEquals(1, sizeof($cmsGroups[1]->users)); + + $this->assertSame($cmsUsers[0]->groups[0], $cmsGroups[0]); + $this->assertSame($cmsUsers[0]->groups[1], $cmsGroups[1]); + $this->assertSame($cmsGroups[0]->users[0], $cmsUsers[0]); + $this->assertSame($cmsGroups[1]->users[0], $cmsUsers[0]); + } + + /** + * Test that changing associations on detached entities and then cascade merging them causes the + * database to be updated with the new associations. + */ + public function testManyToManyMergeAssociationRemoves() + { + $this->setCascadeMergeFor('Doctrine\Tests\Models\CMS\CmsUser'); + $this->setCascadeMergeFor('Doctrine\Tests\Models\CMS\CmsGroup'); + + $cmsUser = new CmsUser(); + $cmsUser->username = "dave"; + $cmsUser->name = "Dave Keen"; + $cmsUser->status = "testing"; + + $group1 = new CmsGroup(); + $group1->name = "Group 1"; + + $group2 = new CmsGroup(); + $group2->name = "Group 2"; + + $cmsUser->addGroup($group1); + $cmsUser->addGroup($group2); + + $this->_em->persist($cmsUser); + $this->_em->persist($group1); + $this->_em->persist($group2); + $this->_em->flush(); + + $cmsUserId = $cmsUser->id; + $group1Id = $group1->id; + $group2Id = $group2->id; + + $this->_em->clear(); + + // Now create detached versions of the entities with NO associations. + $cmsUser = new CmsUser(); + $cmsUser->id = $cmsUserId; + $cmsUser->username = "dave"; + $cmsUser->name = "Dave Keen"; + $cmsUser->status = "testing"; + $cmsUser->groups = new ArrayCollection(); + + $group1 = new CmsGroup(); + $group1->id = $group1Id; + $group1->name = "Group 1"; + $group1->users = new ArrayCollection(); + + $group2 = new CmsGroup(); + $group2->id = $group2Id; + $group2->name = "Group 2"; + $group2->users = new ArrayCollection(); + + // Cascade merge of cmsUser followed by a flush should result in the association array collection being empty + $this->_em->merge($cmsUser); + $this->_em->flush(); + + $this->_em->clear(); + + $cmsUsers = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + $cmsGroups = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsGroup')->findAll(); + + // Check the entities are in the database + $this->assertEquals(1, sizeof($cmsUsers)); + $this->assertEquals(2, sizeof($cmsGroups)); + + // Check the associations between the entities are now in the database + $this->assertEquals(0, sizeof($cmsUsers[0]->groups)); + $this->assertEquals(0, sizeof($cmsGroups[0]->users)); + $this->assertEquals(0, sizeof($cmsGroups[1]->users)); + } +} \ No newline at end of file