diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index b951e355e..b3583de8e 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -325,13 +325,17 @@ class EntityManager implements ObjectManager * This effectively synchronizes the in-memory state of managed objects with the * database. * + * If an entity is explicitly passed to this method only this entity and + * the cascade-persist semantics + scheduled inserts/removals are synchronized. + * + * @param object $entity * @throws Doctrine\ORM\OptimisticLockException If a version check on an entity that * makes use of optimistic locking fails. */ - public function flush() + public function flush($entity = null) { $this->errorIfClosed(); - $this->unitOfWork->commit(); + $this->unitOfWork->commit($entity); } /** diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 8f5977e5d..8c7273856 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -253,12 +253,18 @@ class UnitOfWork implements PropertyChangedListener * 3) All collection deletions * 4) All collection updates * 5) All entity deletions - * + * + * @param object $entity + * @return void */ - public function commit() + public function commit($entity = null) { // Compute changes done since last commit. - $this->computeChangeSets(); + if ($entity === null) { + $this->computeChangeSets(); + } else { + $this->computeSingleEntityChangeSet($entity); + } if ( ! ($this->entityInsertions || $this->entityDeletions || @@ -346,6 +352,61 @@ class UnitOfWork implements PropertyChangedListener $this->scheduledForDirtyCheck = $this->orphanRemovals = array(); } + + /** + * Compute the changesets of all entities scheduled for insertion + * + * @return void + */ + private function computeScheduleInsertsChangeSets() + { + foreach ($this->entityInsertions as $entity) { + $class = $this->em->getClassMetadata(get_class($entity)); + $this->computeChangeSet($class, $entity); + } + } + + /** + * Only flush the given entity according to a ruleset that keeps the UoW consistent. + * + * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well! + * 2. Read Only entities are skipped. + * 3. Proxies are skipped. + * 4. Only if entity is properly managed. + * + * @param object $entity + * @return void + */ + private function computeSingleEntityChangeSet($entity) + { + if ( ! $this->isInIdentityMap($entity) ) { + throw new \InvalidArgumentException("Entity has to be managed for single computation " . self::objToStr($entity)); + } + + $class = $this->em->getClassMetadata(get_class($entity)); + + if ($class->isChangeTrackingDeferredImplicit()) { + $this->persist($entity); + } + + // Compute changes for INSERTed entities first. This must always happen even in this case. + $this->computeScheduleInsertsChangeSets(); + + if ( $class->isReadOnly ) { + return; + } + + // Ignore uninitialized proxy objects + if ($entity instanceof Proxy && ! $entity->__isInitialized__) { + return; + } + + // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here. + $oid = spl_object_hash($entity); + if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) { + $this->computeChangeSet($class, $entity); + } + } /** * Executes any extra updates that have been scheduled. @@ -526,10 +587,7 @@ class UnitOfWork implements PropertyChangedListener public function computeChangeSets() { // Compute changes for INSERTed entities first. This must always happen. - foreach ($this->entityInsertions as $entity) { - $class = $this->em->getClassMetadata(get_class($entity)); - $this->computeChangeSet($class, $entity); - } + $this->computeScheduleInsertsChangeSets(); // Compute changes for other MANAGED entities. Change tracking policies take effect here. foreach ($this->identityMap as $className => $entities) { diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index a2e2d05dd..feef41c76 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -1030,4 +1030,173 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(\Doctrine\ORM\UnitOfWork::STATE_DETACHED, $unitOfWork->getEntityState($address)); } + + /** + * @group DDC-720 + */ + public function testFlushSingleManagedEntity() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->_em->persist($user); + $this->_em->flush(); + + $user->status = 'administrator'; + $this->_em->flush($user); + $this->_em->clear(); + + $user = $this->_em->find(get_class($user), $user->id); + $this->assertEquals('administrator', $user->status); + } + + /** + * @group DDC-720 + */ + public function testFlushSingleUnmanagedEntity() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->setExpectedException('InvalidArgumentException', 'Entity has to be managed for single computation'); + $this->_em->flush($user); + } + + /** + * @group DDC-720 + */ + public function testFlushSingleAndNewEntity() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->_em->persist($user); + $this->_em->flush(); + + $otherUser = new CmsUser; + $otherUser->name = 'Dominik2'; + $otherUser->username = 'domnikl2'; + $otherUser->status = 'developer'; + + $user->status = 'administrator'; + + $this->_em->persist($otherUser); + $this->_em->flush($user); + + $this->assertTrue($this->_em->contains($otherUser), "Other user is contained in EntityManager"); + $this->assertTrue($otherUser->id > 0, "other user has an id"); + } + + /** + * @group DDC-720 + */ + public function testFlushAndCascadePersist() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->_em->persist($user); + $this->_em->flush(); + + $address = new CmsAddress(); + $address->city = "Springfield"; + $address->zip = "12354"; + $address->country = "Germany"; + $address->street = "Foo Street"; + $address->user = $user; + $user->address = $address; + + $this->_em->flush($user); + + $this->assertTrue($this->_em->contains($address), "Other user is contained in EntityManager"); + $this->assertTrue($address->id > 0, "other user has an id"); + } + + /** + * @group DDC-720 + */ + public function testFlushSingleAndNoCascade() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->_em->persist($user); + $this->_em->flush(); + + $article1 = new CmsArticle(); + $article1->topic = 'Foo'; + $article1->text = 'Foo Text'; + $article1->author = $user; + $user->articles[] = $article1; + + $this->setExpectedException('InvalidArgumentException', "A new entity was found through the relationship 'Doctrine\Tests\Models\CMS\CmsUser#articles'"); + $this->_em->flush($user); + } + + /** + * @group DDC-720 + */ + public function testProxyIsIgnored() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + + $this->_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + + $user = $this->_em->getReference(get_class($user), $user->id); + + $otherUser = new CmsUser; + $otherUser->name = 'Dominik2'; + $otherUser->username = 'domnikl2'; + $otherUser->status = 'developer'; + + $this->_em->persist($otherUser); + $this->_em->flush($user); + + $this->assertTrue($this->_em->contains($otherUser), "Other user is contained in EntityManager"); + $this->assertTrue($otherUser->id > 0, "other user has an id"); + } + + /** + * @group DDC-720 + */ + public function testFlushSingleSaveOnlySingle() + { + $user = new CmsUser; + $user->name = 'Dominik'; + $user->username = 'domnikl'; + $user->status = 'developer'; + $this->_em->persist($user); + + $user2 = new CmsUser; + $user2->name = 'Dominik'; + $user2->username = 'domnikl2'; + $user2->status = 'developer'; + $this->_em->persist($user2); + + $this->_em->flush(); + + $user->status = 'admin'; + $user2->status = 'admin'; + + $this->_em->flush($user); + $this->_em->clear(); + + $user2 = $this->_em->find(get_class($user2), $user2->id); + $this->assertEquals('developer', $user2->status); + } }