diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index afbbf361f..6cb697c75 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -199,7 +199,7 @@ final class PersistentCollection implements Collection */ private function _initialize() { - if ( ! $this->_initialized) { + if ( ! $this->_initialized && $this->_association) { if ($this->_isDirty) { // Has NEW objects added through add(). Remember them. $newObjects = $this->_coll->toArray(); @@ -580,7 +580,7 @@ final class PersistentCollection implements Collection */ public function __sleep() { - return array('_coll'); + return array('_coll', '_initialized'); } /* ArrayAccess implementation */ diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 8287d1fa7..3aefcc6c5 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1366,6 +1366,7 @@ class UnitOfWork implements PropertyChangedListener if ( ! isset($class->associationMappings[$name])) { $prop->setValue($managedCopy, $prop->getValue($entity)); } else { + // why $assoc2? See the method signature, there is $assoc already! $assoc2 = $class->associationMappings[$name]; if ($assoc2->isOneToOne()) { if ( ! $assoc2->isCascadeMerge) { @@ -1379,6 +1380,12 @@ class UnitOfWork implements PropertyChangedListener } } } else { + $mergeCol = $prop->getValue($entity); + if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized()) { + // keep the lazy persistent collection of the managed copy. + continue; + } + $coll = new PersistentCollection($this->_em, $this->_em->getClassMetadata($assoc2->targetEntityName), new ArrayCollection diff --git a/tests/Doctrine/Tests/Models/CMS/CmsUser.php b/tests/Doctrine/Tests/Models/CMS/CmsUser.php index d7f0727c0..0b900bf60 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsUser.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsUser.php @@ -40,7 +40,7 @@ class CmsUser */ public $address; /** - * @ManyToMany(targetEntity="CmsGroup", inversedBy="users", cascade={"persist"}) + * @ManyToMany(targetEntity="CmsGroup", inversedBy="users", cascade={"persist", "merge"}) * @JoinTable(name="cms_users_groups", * joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")}, * inverseJoinColumns={@JoinColumn(name="group_id", referencedColumnName="id")} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC501Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC501Test.php new file mode 100644 index 000000000..a40c5c7cd --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC501Test.php @@ -0,0 +1,128 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testCreateUser() + { + ### Create User + $user = $this->createAndPersistUser(); + $this->_em->flush(); + + $this->assertTrue($this->_em->contains($user)); + $this->_em->clear(); + $this->assertFalse($this->_em->contains($user)); + + unset($user); + + ### Reload User from DB *without* any associations + $userReloaded = $this->loadUserFromEntityManager(); + + $this->assertTrue($this->_em->contains($userReloaded)); + $this->_em->clear(); + $this->assertFalse($this->_em->contains($userReloaded)); + + // freeze and unfreeze + $userClone = unserialize(serialize($userReloaded)); + $this->assertType('Doctrine\Tests\Models\CMS\CmsUser', $userClone); + + // detached user can't know about his phonenumbers + $this->assertEquals(0, count($userClone->getPhonenumbers())); + + // detached user can't know about his groups either + $this->assertEquals(0, count($userClone->getGroups())); + + ### Merge back and flush + $userClone = $this->_em->merge($userClone); + + /* + * Back in managed world I would expect to have my phonenumbers back but they aren't! + * Remember I didn't touch (and propably didn't need) them at all while in detached mode. + */ + $this->assertEquals(4, count($userClone->getPhonenumbers()), 'Phonenumbers are not available anymore'); + + /* + * This works fine as long as cmUser::groups doesn't cascade "merge" + */ + $this->assertEquals(2, count($userClone->getGroups())); + + $this->_em->flush(); + $this->_em->clear(); + + $this->assertFalse($this->_em->contains($userClone)); + + ### Reload user from DB + $userFromEntityManager = $this->loadUserFromEntityManager(); + + /* + * Strange: Now the phonenumbers are back again + */ + $this->assertEquals(4, count($userFromEntityManager->getPhonenumbers())); + + /* + * This works fine as long as cmUser::groups doesn't cascade "merge" + * Otherwise group memberships are physically deleted now! + */ + $this->assertEquals(2, count($userClone->getGroups())); + } + + protected function createAndPersistUser() + { + $user = new CmsUser(); + $user->name = 'Luka'; + $user->username = 'lukacho'; + $user->status = 'developer'; + + foreach(array(1111,2222,3333,4444) as $number) { + $phone = new CmsPhonenumber; + $phone->phonenumber = $number; + $user->addPhonenumber($phone); + } + + foreach(array('Moshers', 'Headbangers') as $groupName) { + $group = new CmsGroup; + $group->setName($groupName); + $user->addGroup($group); + } + + $this->_em->persist($user); + + return $user; + } + + /** + * @return Doctrine\Tests\Models\CMS\CmsUser + */ + protected function loadUserFromEntityManager() + { + return $this->_em + ->createQuery('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name like :name') + ->setParameter('name', 'Luka') + ->getSingleResult(); + } + +} \ No newline at end of file