From 6c185a2891111dfbd83d381bad8c5a2b16536cad Mon Sep 17 00:00:00 2001 From: Christian Heinrich Date: Wed, 12 May 2010 16:18:25 +0200 Subject: [PATCH] Fixed #DDC-501 PersistenCollections had a problem with serializing; they need to be initialized beforehands. --- lib/Doctrine/ORM/PersistentCollection.php | 47 +++---- tests/Doctrine/Tests/Models/CMS/CmsUser.php | 8 +- .../ORM/Functional/Ticket/DDC501Test.php | 127 ++++++++++++++++++ 3 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC501Test.php diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index afbbf361f05..603d28f1e4c 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -94,21 +94,21 @@ final class PersistentCollection implements Collection /** * Whether the collection has already been initialized. - * + * * @var boolean */ private $_initialized = true; - + /** * The wrapped Collection instance. - * + * * @var Collection */ private $_coll; /** * Creates a new persistent collection. - * + * * @param EntityManager $em The EntityManager the collection will be associated with. * @param ClassMetadata $class The class descriptor of the entity type of this collection. * @param array The collection elements. @@ -145,7 +145,7 @@ public function getOwner() { return $this->_owner; } - + public function getTypeClass() { return $this->_typeClass; @@ -155,7 +155,7 @@ public function getTypeClass() * INTERNAL: * Adds an element to a collection during hydration. This will automatically * complete bidirectional associations in the case of a one-to-many association. - * + * * @param mixed $element The element to add. */ public function hydrateAdd($element) @@ -173,7 +173,7 @@ public function hydrateAdd($element) $this->_owner); } } - + /** * INTERNAL: * Sets a keyed element in the collection during hydration. @@ -272,7 +272,7 @@ public function getMapping() { return $this->_association; } - + /** * Marks this collection as changed/dirty. */ @@ -306,17 +306,17 @@ public function setDirty($dirty) { $this->_isDirty = $dirty; } - + /** * Sets the initialized flag of the collection, forcing it into that state. - * + * * @param boolean $bool */ public function setInitialized($bool) { $this->_initialized = $bool; } - + /** * Checks whether this collection has been initialized. * @@ -377,7 +377,7 @@ public function removeElement($element) $this->_em->getUnitOfWork()->getCollectionPersister($this->_association) ->deleteRows($this, $element); }*/ - + $this->_initialize(); $result = $this->_coll->removeElement($element); $this->_changed(); @@ -414,7 +414,7 @@ public function contains($element) } return false; }*/ - + $this->_initialize(); return $this->_coll->contains($element); } @@ -501,7 +501,7 @@ public function isEmpty() $this->_initialize(); return $this->_coll->isEmpty(); } - + /** * {@inheritdoc} */ @@ -528,7 +528,7 @@ public function filter(Closure $p) $this->_initialize(); return $this->_coll->filter($p); } - + /** * {@inheritdoc} */ @@ -546,7 +546,7 @@ public function partition(Closure $p) $this->_initialize(); return $this->_coll->partition($p); } - + /** * {@inheritdoc} */ @@ -567,10 +567,10 @@ public function clear() $this->_changed(); $this->_em->getUnitOfWork()->scheduleCollectionDeletion($this); } - + return $result; } - + /** * Called by PHP when this collection is serialized. Ensures that only the * elements are properly serialized. @@ -580,9 +580,10 @@ public function clear() */ public function __sleep() { + $this->_initialize(); return array('_coll'); } - + /* ArrayAccess implementation */ /** @@ -620,12 +621,12 @@ public function offsetUnset($offset) { return $this->remove($offset); } - + public function key() { return $this->_coll->key(); } - + /** * Gets the element of the collection at the current iterator position. */ @@ -633,7 +634,7 @@ public function current() { return $this->_coll->current(); } - + /** * Moves the internal iterator position to the next element. */ @@ -641,7 +642,7 @@ public function next() { return $this->_coll->next(); } - + /** * Retrieves the wrapped Collection instance. */ diff --git a/tests/Doctrine/Tests/Models/CMS/CmsUser.php b/tests/Doctrine/Tests/Models/CMS/CmsUser.php index d7f0727c026..e1017f87892 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsUser.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsUser.php @@ -40,14 +40,14 @@ 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")} * ) */ public $groups; - + public function __construct() { $this->phonenumbers = new ArrayCollection; $this->articles = new ArrayCollection; @@ -107,9 +107,9 @@ public function removePhonenumber($index) { } return false; } - + public function getAddress() { return $this->address; } - + public function setAddress(CmsAddress $address) { if ($this->address !== $address) { $this->address = $address; 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 00000000000..11e3c49305b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC501Test.php @@ -0,0 +1,127 @@ +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 + $string = serialize($userReloaded); + unset($userReloaded); + $userClone = unserialize($string); + $this->assertType('Doctrine\Tests\Models\CMS\CmsUser', $userClone); + + $this->assertEquals(4, count($userClone->getPhonenumbers())); + $this->assertEquals(2, 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