diff --git a/lib/Doctrine/ORM/ORMInvalidArgumentException.php b/lib/Doctrine/ORM/ORMInvalidArgumentException.php new file mode 100644 index 000000000..b4edc5dd2 --- /dev/null +++ b/lib/Doctrine/ORM/ORMInvalidArgumentException.php @@ -0,0 +1,81 @@ +. + */ + +namespace Doctrine\ORM; + +class ORMInvalidArgumentException extends \InvalidArgumentException +{ + static public function scheduleInsertForManagedEntity($entity) + { + return new self("A managed+dirty entity " . self::objToStr($entity) . " can not be scheduled for insertion."); + } + + static public function scheduleInsertForRemovedEntity($entity) + { + return new self("Removed entity " . self::objToStr($entity) . " can not be scheduled for insertion."); + } + + static public function scheduleInsertTwice($entity) + { + return new self("Entity " . self::objToStr($entity) . " can not be scheduled for insertion twice."); + } + + static public function entityWithoutIdentity($className, $entity) + { + throw new self( + "The given entity of type '" . $className . "' (".self::objToStr($entity).") has no identity/no " . + "id values set. It cannot be added to the identity map." + ); + } + + static public function readOnlyRequiresManagedEntity($entity) + { + return new self("Only managed entities can be marked or checked as read only. But " . self::objToStr($entity) . " is not"); + } + + static public function newEntityFoundThroughRelationship(array $assoc, $entry) + { + return new self("A new entity was found through the relationship '" + . $assoc['sourceEntity'] . "#" . $assoc['fieldName'] . "' that was not" + . " configured to cascade persist operations for entity: " . self::objToStr($entry) . "." + . " To solve this issue: Either explicitly call EntityManager#persist()" + . " on this unknown entity or configure cascade persist " + . " this association in the mapping for example @ManyToOne(..,cascade={\"persist\"}). " + . " If you cannot find out which entity causes the problem" + . " implement '" . $assoc['targetEntity'] . "#__toString()' to get a clue."); + } + + static public function detachedEntityFoundThroughRelationship(array $assoc, $entry) + { + throw new self("A detached entity of type " . $assoc['targetEntity'] . " (" . self::objToStr($entry) . ") " + . " was found through the relationship '" . $assoc['sourceEntity'] . "#" . $assoc['fieldName'] . "' " + . "during cascading a persist operation."); + } + + /** + * Helper method to show an object as string. + * + * @param object $obj + * @return string + */ + private static function objToStr($obj) + { + return method_exists($obj, '__toString') ? (string)$obj : get_class($obj).'@'.spl_object_hash($obj); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 1e6d13fb5..2b696063f 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -595,25 +595,16 @@ class UnitOfWork implements PropertyChangedListener $oid = spl_object_hash($entry); if ($state == self::STATE_NEW) { if ( ! $assoc['isCascadePersist']) { - throw new InvalidArgumentException("A new entity was found through the relationship '" - . $assoc['sourceEntity'] . "#" . $assoc['fieldName'] . "' that was not" - . " configured to cascade persist operations for entity: " . self::objToStr($entry) . "." - . " Explicitly call EntityManager#persist() on this entity or configure to cascade persist " - . " the association. If you cannot find out which entity causes the problem" - . " implement '" . $assoc['targetEntity'] . "#__toString()' to get a clue."); + throw ORMInvalidArgumentException::newEntityFoundThroughRelationship($assoc, $entry); } $this->persistNew($targetClass, $entry); $this->computeChangeSet($targetClass, $entry); - } else if ($state == self::STATE_REMOVED) { - return new InvalidArgumentException("Removed entity detected during flush: " - . self::objToStr($entry).". Remove deleted entities from associations."); } else if ($state == self::STATE_DETACHED) { // Can actually not happen right now as we assume STATE_NEW, // so the exception will be raised from the DBAL layer (constraint violation). - throw new InvalidArgumentException("A detached entity was found through a " - . "relationship during cascading a persist operation."); + throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry); } - // MANAGED associated entities are already taken into account + // MANAGED and REMOVED associated entities are already taken into account // during changeset calculation anyway, since they are in the identity map. } } @@ -662,7 +653,7 @@ class UnitOfWork implements PropertyChangedListener $oid = spl_object_hash($entity); if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) { - throw new InvalidArgumentException('Entity must be managed.'); + throw new InvalidArgumentException("Entity of type '" . $class->name . "' must be managed."); } /* TODO: Just return if changetracking policy is NOTIFY? @@ -907,14 +898,14 @@ class UnitOfWork implements PropertyChangedListener { $oid = spl_object_hash($entity); - if (isset($this->entityUpdates[$oid])) { - throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion."); - } if (isset($this->entityDeletions[$oid])) { - throw new InvalidArgumentException("Removed entity can not be scheduled for insertion."); + throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity); + } + if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) { + throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity); } if (isset($this->entityInsertions[$oid])) { - throw new InvalidArgumentException("Entity can not be scheduled for insertion twice."); + throw ORMInvalidArgumentException::scheduleInsertTwice($entity); } $this->entityInsertions[$oid] = $entity; @@ -1071,7 +1062,7 @@ class UnitOfWork implements PropertyChangedListener $classMetadata = $this->em->getClassMetadata(get_class($entity)); $idHash = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]); if ($idHash === '') { - throw new InvalidArgumentException("The given entity has no identity."); + throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity); } $className = $classMetadata->rootEntityName; if (isset($this->identityMap[$className][$idHash])) { @@ -2466,7 +2457,7 @@ class UnitOfWork implements PropertyChangedListener public function markReadOnly($object) { if ( ! is_object($object) || ! $this->isInIdentityMap($object)) { - throw new InvalidArgumentException("Managed entity required"); + throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object); } $this->readOnlyObjects[spl_object_hash($object)] = true; } @@ -2481,7 +2472,7 @@ class UnitOfWork implements PropertyChangedListener public function isReadOnly($object) { if ( ! is_object($object) ) { - throw new InvalidArgumentException("Managed entity required"); + throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object); } return isset($this->readOnlyObjects[spl_object_hash($object)]); } diff --git a/tests/Doctrine/Tests/ORM/Functional/UnitOfWorkLifecycleTest.php b/tests/Doctrine/Tests/ORM/Functional/UnitOfWorkLifecycleTest.php new file mode 100644 index 000000000..08e3720ca --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/UnitOfWorkLifecycleTest.php @@ -0,0 +1,71 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testScheduleInsertManaged() + { + $user = new CmsUser(); + $user->username = "beberlei"; + $user->name = "Benjamin"; + $user->status = "active"; + $this->_em->persist($user); + $this->_em->flush(); + + $this->setExpectedException("Doctrine\ORM\ORMInvalidArgumentException", "A managed+dirty entity Doctrine\Tests\Models\CMS\CmsUser"); + $this->_em->getUnitOfWork()->scheduleForInsert($user); + } + + public function testScheduleInsertDeleted() + { + $user = new CmsUser(); + $user->username = "beberlei"; + $user->name = "Benjamin"; + $user->status = "active"; + $this->_em->persist($user); + $this->_em->flush(); + + $this->_em->remove($user); + + $this->setExpectedException("Doctrine\ORM\ORMInvalidArgumentException", "Removed entity Doctrine\Tests\Models\CMS\CmsUser"); + $this->_em->getUnitOfWork()->scheduleForInsert($user); + } + + public function testScheduleInsertTwice() + { + $user = new CmsUser(); + $user->username = "beberlei"; + $user->name = "Benjamin"; + $user->status = "active"; + + $this->_em->getUnitOfWork()->scheduleForInsert($user); + + $this->setExpectedException("Doctrine\ORM\ORMInvalidArgumentException", "Entity Doctrine\Tests\Models\CMS\CmsUser"); + $this->_em->getUnitOfWork()->scheduleForInsert($user); + } + + public function testAddToIdentityMapWithoutIdentity() + { + $user = new CmsUser(); + + $this->setExpectedException("Doctrine\ORM\ORMInvalidArgumentException", "The given entity of type 'Doctrine\Tests\Models\CMS\CmsUser' (Doctrine\Tests\Models\CMS\CmsUser@"); + $this->_em->getUnitOfWork()->registerManaged($user, array(), array()); + } + + public function testMarkReadOnlyNonManaged() + { + $user = new CmsUser(); + + $this->setExpectedException("Doctrine\ORM\ORMInvalidArgumentException", "Only managed entities can be marked or checked as read only. But Doctrine\Tests\Models\CMS\CmsUser@"); + $this->_em->getUnitOfWork()->markReadOnly($user); + } +} \ No newline at end of file