From 70f1f3e7f62e1fff1c67361bed3406a4ec64c267 Mon Sep 17 00:00:00 2001 From: Tim Roediger Date: Fri, 13 Dec 2013 10:37:00 +1000 Subject: [PATCH 1/9] Make it possible to re-persist removed embeded documents Not sure why the restriction was there in the first place, but it seems like it isn't needed. If it is kept, I can't find any way to re-persist an embedded document marked for deletion. --- lib/Doctrine/ODM/MongoDB/UnitOfWork.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index e00aa537e3..dbdebb6ff7 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -1914,16 +1914,14 @@ private function doPersist($document, array &$visited) "Behavior of persist() for a detached document is not yet defined."); break; case self::STATE_REMOVED: - if ( ! $class->isEmbeddedDocument) { - // Document becomes managed again - if ($this->isScheduledForDelete($document)) { - unset($this->documentDeletions[$oid]); - } else { - //FIXME: There's more to think of here... - $this->scheduleForInsert($class, $document); - } - break; + // Document becomes managed again + if ($this->isScheduledForDelete($document)) { + unset($this->documentDeletions[$oid]); + } else { + //FIXME: There's more to think of here... + $this->scheduleForInsert($class, $document); } + break; default: throw MongoDBException::invalidDocumentState($documentState); } From f5bddadea1e07278193c9936210cdd8fce05f356 Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Sat, 25 Jan 2014 13:30:09 -0500 Subject: [PATCH 2/9] UnitOfWork cleanup. --- .../ODM/MongoDB/Mapping/ClassMetadataInfo.php | 10 + .../MongoDB/Persisters/PersistenceBuilder.php | 24 +- lib/Doctrine/ODM/MongoDB/UnitOfWork.php | 793 ++++++++---------- .../Tests/Events/LifecycleCallbacksTest.php | 8 + .../Tests/Events/LifecycleListenersTest.php | 2 +- .../Tests/Functional/EmbeddedIdTest.php | 17 +- .../ODM/MongoDB/Tests/UnitOfWorkTest.php | 128 +++ 7 files changed, 535 insertions(+), 447 deletions(-) diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php index aeb4e6ed7b..9ac255ed35 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php @@ -1625,6 +1625,16 @@ public function isIdGeneratorNone() return $this->generatorType == self::GENERATOR_TYPE_NONE; } + /** + * Checks whether the mapped class uses an Id generator. + * + * @return boolean TRUE if the mapped class uses an Id generator, FALSE otherwise. + */ + public function usesIdGenerator() + { + return $this->generatorType != self::GENERATOR_TYPE_NONE; + } + /** * Sets the version field mapping used for versioning. Sets the default * value to use depending on the column type. diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php b/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php index 6dbe427556..33c3130072 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php @@ -222,6 +222,7 @@ public function prepareUpdateData($document) } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_MANY) { // Do nothing right now } + // @ReferenceMany is handled by CollectionPersister } return $updateData; } @@ -276,29 +277,14 @@ public function prepareUpsertData($document) } } - // @EmbedMany - } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_MANY && $new) { - foreach ($new as $key => $embeddedDoc) { - if ( ! $this->uow->isScheduledForInsert($embeddedDoc)) { - $update = $this->prepareUpsertData($embeddedDoc); - foreach ($update as $cmd => $values) { - foreach ($values as $name => $value) { - $updateData[$cmd][$mapping['name'] . '.' . $key . '.' . $name] = $value; - } - } - } - } - // @ReferenceOne } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) { if (isset($new) || $mapping['nullable'] === true) { $updateData['$set'][$mapping['name']] = (is_null($new) ? null : $this->prepareReferencedDocumentValue($mapping, $new)); } - // @ReferenceMany - } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_MANY) { - // Do nothing right now } + // @EmbedMany and @ReferenceMany are handled by CollectionPersister } // add discriminator if the class has one @@ -366,10 +352,8 @@ public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDo case ClassMetadata::EMBED_MANY: case ClassMetadata::REFERENCE_MANY: - // Skip PersistentCollections already scheduled for deletion/update - if ($rawValue instanceof PersistentCollection && - ($this->uow->isCollectionScheduledForDeletion($rawValue) || - $this->uow->isCollectionScheduledForUpdate($rawValue))) { + // Skip PersistentCollections already scheduled for deletion + if ($rawValue instanceof PersistentCollection && $this->uow->isCollectionScheduledForDeletion($rawValue)) { break; } diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index dbdebb6ff7..453245fc83 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -369,10 +369,16 @@ public function setDocumentPersister($documentName, Persisters\DocumentPersister * * 1) All document insertions * 2) All document updates - * 3) All document deletions + * 3) All collection deletions + * 4) All collection updates + * 5) All document deletions * - * @param object $document + * @param null|object|array $document * @param array $options Array of options to be used with batchInsert(), update() and remove() + * + * @return void + * + * @throws \Exception */ public function commit($document = null, array $options = array()) { @@ -406,6 +412,9 @@ public function commit($document = null, array $options = array()) $this->collectionDeletions || $this->orphanRemovals) ) { + $this->dispatchOnFlushEvent(); + $this->dispatchPostFlushEvent(); + return; // Nothing to do. } @@ -415,19 +424,13 @@ public function commit($document = null, array $options = array()) } } - // Raise onFlush - if ($this->evm->hasListeners(Events::onFlush)) { - $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm)); - } + $this->dispatchOnFlushEvent(); // Now we need a commit order to maintain referential integrity $commitOrder = $this->getCommitOrder(); if ($this->documentUpserts) { foreach ($commitOrder as $class) { - if ($class->isEmbeddedDocument) { - continue; - } $this->executeUpserts($class, $options); } } @@ -435,9 +438,10 @@ public function commit($document = null, array $options = array()) if ($this->documentInsertions) { foreach ($commitOrder as $class) { if ($class->isEmbeddedDocument) { - continue; + $this->executeEmbeddedInserts($class, $options); + } else { + $this->executeInserts($class, $options); } - $this->executeInserts($class, $options); } } @@ -473,10 +477,7 @@ public function commit($document = null, array $options = array()) $coll->takeSnapshot(); } - // Raise postFlush - if ($this->evm->hasListeners(Events::postFlush)) { - $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm)); - } + $this->dispatchPostFlushEvent(); // Clear up $this->documentInsertions = @@ -496,16 +497,14 @@ public function commit($document = null, array $options = array()) * Compute changesets of all documents scheduled for insertion. * * Embedded documents will not be processed. + * + * @return void */ private function computeScheduleInsertsChangeSets() { foreach ($this->documentInsertions as $document) { $class = $this->dm->getClassMetadata(get_class($document)); - if ($class->isEmbeddedDocument) { - continue; - } - $this->computeChangeSet($class, $document); } } @@ -514,16 +513,14 @@ private function computeScheduleInsertsChangeSets() * Compute changesets of all documents scheduled for upsert. * * Embedded documents will not be processed. + * + * @return void */ private function computeScheduleUpsertsChangeSets() { foreach ($this->documentUpserts as $document) { $class = $this->dm->getClassMetadata(get_class($document)); - if ($class->isEmbeddedDocument) { - continue; - } - $this->computeChangeSet($class, $document); } } @@ -536,7 +533,9 @@ private function computeScheduleUpsertsChangeSets() * 3. Only if document is properly managed. * * @param object $document + * * @throws \InvalidArgumentException If the document is not STATE_MANAGED + * * @return void */ private function computeSingleDocumentChangeSet($document) @@ -575,12 +574,13 @@ private function computeSingleDocumentChangeSet($document) } /** - * Executes reference updates + * Executes any extra updates that have been scheduled. */ private function executeExtraUpdates(array $options) { foreach ($this->extraUpdates as $oid => $update) { list ($document, $changeset) = $update; + $this->documentChangeSets[$oid] = $changeset; $this->getDocumentPersister(get_class($document))->update($document, $options); } @@ -590,14 +590,17 @@ private function executeExtraUpdates(array $options) * Gets the changeset for a document. * * @param object $document + * * @return array */ public function getDocumentChangeSet($document) { $oid = spl_object_hash($document); + if (isset($this->documentChangeSets[$oid])) { return $this->documentChangeSets[$oid]; } + return array(); } @@ -610,20 +613,28 @@ public function getDocumentChangeSet($document) public function getDocumentActualData($document) { $class = $this->dm->getClassMetadata(get_class($document)); + $actualData = array(); foreach ($class->reflFields as $name => $refProp) { $mapping = $class->fieldMappings[$name]; + // skip not saved fields if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) { continue; } + $value = $refProp->getValue($document); + if (isset($mapping['file']) && ! $value instanceof GridFSFile) { $value = new GridFSFile($value); $class->reflFields[$name]->setValue($document, $value); $actualData[$name] = $value; - } elseif ((isset($mapping['association']) && $mapping['type'] === 'many') - && $value !== null && ! ($value instanceof PersistentCollection)) { + } elseif (isset($mapping['association']) + && $mapping['type'] === 'many' + && $value !== null + && ! $value instanceof PersistentCollection + ) { + // If $actualData[$name] is not a Collection then use an ArrayCollection. if ( ! $value instanceof Collection) { $value = new ArrayCollection($value); @@ -639,6 +650,7 @@ public function getDocumentActualData($document) $actualData[$name] = $value; } } + return $actualData; } @@ -690,7 +702,9 @@ public function computeChangeSet(ClassMetadata $class, $document) private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false) { $oid = spl_object_hash($document); + $actualData = $this->getDocumentActualData($document); + $isNewDocument = ! isset($this->originalDocumentData[$oid]); if ($isNewDocument) { // Document is either NEW or MANAGED but not yet fully persisted (only has an id). @@ -809,35 +823,37 @@ private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $r } // Look for changes in associations of the document - foreach ($class->fieldMappings as $mapping) { - // skip not saved fields - if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) { + $associationMappings = array_filter( + $class->associationMappings, + function ($assoc) { return empty($assoc['notSaved']); } + ); + + foreach ($associationMappings as $mapping) { + $value = $class->reflFields[$mapping['fieldName']]->getValue($document); + + if ($value === null) { continue; } - if (isset($mapping['reference']) || isset($mapping['embedded'])) { - $value = $class->reflFields[$mapping['fieldName']]->getValue($document); - if ($value !== null) { - $this->computeAssociationChanges($document, $mapping, $value); - if (isset($mapping['reference'])) { - continue; - } - $values = $value; - if (isset($mapping['type']) && $mapping['type'] === 'one') { - $values = array($values); - } elseif ($values instanceof PersistentCollection) { - $values = $values->unwrap(); - } - foreach ($values as $obj) { - $oid2 = spl_object_hash($obj); - if (isset($this->documentChangeSets[$oid2])) { - $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value); - if ( ! $isNewDocument) { - $this->documentUpdates[$oid] = $document; - } - break; - } + $this->computeAssociationChanges($document, $mapping, $value); + + if (isset($mapping['reference'])) { + continue; + } + + $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap(); + + foreach ($values as $obj) { + $oid2 = spl_object_hash($obj); + + if (isset($this->documentChangeSets[$oid2])) { + $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value); + + if ( ! $isNewDocument) { + $this->documentUpdates[$oid] = $document; } + + break; } } } @@ -856,28 +872,32 @@ public function computeChangeSets() // Compute changes for other MANAGED documents. Change tracking policies take effect here. foreach ($this->identityMap as $className => $documents) { $class = $this->dm->getClassMetadata($className); - if ($class->isEmbeddedDocument) { - // Embedded documents should only compute by the document itself which include the embedded document. - // This is done separately later. - // @see computeChangeSet() - // @see computeAssociationChanges() - continue; - } // If change tracking is explicit or happens through notification, then only compute - // changes on documents of that type that are explicitly marked for synchronization. - $documentsToProcess = ! $class->isChangeTrackingDeferredImplicit() ? - (isset($this->scheduledForDirtyCheck[$className]) ? - $this->scheduledForDirtyCheck[$className] : array()) - : $documents; + // changes on document of that type that are explicitly marked for synchronization. + switch (true) { + case ($class->isChangeTrackingDeferredImplicit()): + $documentsToProcess = $documents; + break; + + case (isset($this->scheduledForDirtyCheck[$className])): + $documentsToProcess = $this->scheduledForDirtyCheck[$className]; + break; + + default: + $documentsToProcess = array(); + + } foreach ($documentsToProcess as $document) { // Ignore uninitialized proxy objects if (/* $document is readOnly || */ $document instanceof Proxy && ! $document->__isInitialized__) { continue; } + // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here. $oid = spl_object_hash($document); + if ( ! isset($this->documentInsertions[$oid]) && ! isset($this->documentUpserts[$oid]) && ! isset($this->documentDeletions[$oid]) @@ -889,72 +909,100 @@ public function computeChangeSets() } } - /** - * Computes the changes of an embedded document. + /** + * Computes the changes of an association. * * @param object $parentDocument - * @param array $mapping - * @param mixed $value The value of the association. + * @param array $assoc The association mapping. + * @param mixed $value The value of the association. + * * @throws \InvalidArgumentException + * + * @return void */ - private function computeAssociationChanges($parentDocument, $mapping, $value) + private function computeAssociationChanges($parentDocument, array $assoc, $value) { - $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]); - $class = $this->dm->getClassMetadata(get_class($parentDocument)); - $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument); + if ($value instanceof Proxy && ! $value->__isInitialized__) { + return; + } - if ($value instanceof PersistentCollection && $value->isDirty() && $mapping['isOwningSide']) { - if ($topOrExistingDocument || strncmp($mapping['strategy'], 'set', 3) === 0) { - if ( ! in_array($value, $this->collectionUpdates, true)) { - $this->collectionUpdates[] = $value; + if ($value instanceof PersistentCollection && $value->isDirty()) { + $coid = spl_object_hash($value); + + if ($assoc['isOwningSide']) { + $class = $this->dm->getClassMetadata(get_class($parentDocument)); + $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]); + $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument); + + if ($topOrExistingDocument || strncmp($assoc['strategy'], 'set', 3) === 0) { + $this->collectionUpdates[$coid] = $value; } } - $this->visitedCollections[] = $value; - } - if ( ! $mapping['isCascadePersist']) { - return; // "Persistence by reachability" only if persist cascade specified + $this->visitedCollections[$coid] = $value; } - if ($mapping['type'] === 'one') { - if ($value instanceof Proxy && ! $value->__isInitialized__) { - return; // Ignore uninitialized proxy objects - } - $value = array($value); - } elseif ($value instanceof PersistentCollection) { - $value = $value->unwrap(); - } + + // Look through the documents, and in any of their associations, + // for transient (new) documents, recursively. ("Persistence by reachability") + // Unwrap. Uninitialized collections will simply be empty. + $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap(); + $count = 0; - foreach ($value as $key => $entry) { + foreach ($unwrappedValue as $key => $entry) { $targetClass = $this->dm->getClassMetadata(get_class($entry)); + + if ($targetClass->isReadOnly) { + continue; + } + $state = $this->getDocumentState($entry, self::STATE_NEW); // Handle "set" strategy for multi-level hierarchy - $pathKey = $mapping['strategy'] !== 'set' ? $count : $key; - $path = $mapping['type'] === 'many' ? $mapping['name'] . '.' . $pathKey : $mapping['name']; + $pathKey = $assoc['strategy'] !== 'set' ? $count : $key; + $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name']; $count++; - if ($state == self::STATE_NEW) { - if ( ! $mapping['isCascadePersist']) { - throw new \InvalidArgumentException("A new document was found through a relationship that was not" - . " configured to cascade persist operations: " . self::objToStr($entry) . "." - . " Explicitly persist the new document or configure cascading persist operations" - . " on the relationship."); - } - $this->persistNew($targetClass, $entry); - $this->setParentAssociation($entry, $mapping, $parentDocument, $path); - $this->computeChangeSet($targetClass, $entry); - } elseif ($state == self::STATE_MANAGED && $targetClass->isEmbeddedDocument) { - $this->setParentAssociation($entry, $mapping, $parentDocument, $path); - $this->computeChangeSet($targetClass, $entry); - } elseif ($state == self::STATE_REMOVED) { - throw new \InvalidArgumentException("Removed document detected during flush: " - . self::objToStr($entry) . ". Remove deleted documents from associations."); - } elseif ($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 document was found through a " - . "relationship during cascading a persist operation."); + + switch ($state) { + case self::STATE_NEW: + if ( ! $assoc['isCascadePersist']) { + throw new \InvalidArgumentException("A new document was found through a relationship that was not" + . " configured to cascade persist operations: " . self::objToStr($entry) . "." + . " Explicitly persist the new document or configure cascading persist operations" + . " on the relationship."); + } + + $this->persistNew($targetClass, $entry); + $this->setParentAssociation($entry, $assoc, $parentDocument, $path); + $this->computeChangeSet($targetClass, $entry); + break; + + case self::STATE_MANAGED: + if ($targetClass->isEmbeddedDocument) { + $this->setParentAssociation($entry, $assoc, $parentDocument, $path); + $this->computeChangeSet($targetClass, $entry); + } + break; + + case self::STATE_REMOVED: + // Consume the $value as array (it's either an array or an ArrayAccess) + // and remove the element from Collection. + if ($assoc['type'] === ClassMetadata::MANY) { + unset($value[$key]); + } + break; + + case self::STATE_DETACHED: + // Can actually not happen right now as we assume STATE_NEW. + throw new \InvalidArgumentException("A detached document was found through a " + . "relationship during cascading a persist operation."); + break; + + default: + // MANAGED associated documents are already taken into account + // during changeset calculation anyway, since they are in the identity map. + } } } @@ -1014,6 +1062,9 @@ private function persistNew($class, $document) } $this->documentIdentifiers[$oid] = $idValue; + } else { + // this is for embedded documents without identifiers + $this->documentIdentifiers[$oid] = $oid; } $this->documentStates[$oid] = self::STATE_MANAGED; @@ -1025,6 +1076,36 @@ private function persistNew($class, $document) } } + /** + * Executes all embedded document insertions for documents of the specified type. + * + * @param ClassMetadata $class + * @param array $options Array of options to be used with batchInsert() + */ + private function executeEmbeddedInserts(ClassMetadata $class, array $options = array()) + { + $hasPostPersistLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postPersist]); + $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist); + + // no callbacks? then we have nothing to do here. + if ( ! ($hasPostPersistLifecycleCallbacks || $hasPostPersistListeners)) { + return; + } + + $className = $class->name; + + foreach ($this->documentInsertions as $oid => $document) { + if (get_class($document) === $className) { + if ($hasPostPersistLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::postPersist, $document); + } + if ($hasPostPersistListeners) { + $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm)); + } + } + } + } + /** * Executes all document insertions for documents of the specified type. * @@ -1072,7 +1153,6 @@ private function executeInserts(ClassMetadata $class, array $options = array()) if ($hasPostPersistListeners) { $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm)); } - $this->cascadePostPersist($class, $document); } } @@ -1110,52 +1190,6 @@ private function executeUpserts(ClassMetadata $class, array $options = array()) if ($hasListeners) { $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm)); } - $this->cascadePostPersist($class, $document); - } - } - - /** - * Cascades the postPersist events to embedded documents. - * - * @param ClassMetadata $class - * @param object $document - */ - private function cascadePostPersist(ClassMetadata $class, $document) - { - $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist); - - foreach ($class->fieldMappings as $mapping) { - if (empty($mapping['embedded'])) { - continue; - } - - $value = $class->reflFields[$mapping['fieldName']]->getValue($document); - - if ($value === null) { - continue; - } - - if ($mapping['type'] === 'one') { - $value = array($value); - } - - if (isset($mapping['targetDocument'])) { - $embeddedClass = $this->dm->getClassMetadata($mapping['targetDocument']); - } - - foreach ($value as $embeddedDocument) { - if ( ! isset($mapping['targetDocument'])) { - $embeddedClass = $this->dm->getClassMetadata(get_class($embeddedDocument)); - } - - if ( ! empty($embeddedClass->lifecycleCallbacks[Events::postPersist])) { - $embeddedClass->invokeLifecycleCallbacks(Events::postPersist, $embeddedDocument); - } - if ($hasPostPersistListeners) { - $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($embeddedDocument, $this->dm)); - } - $this->cascadePostPersist($embeddedClass, $embeddedDocument); - } } } @@ -1194,6 +1228,7 @@ private function executeUpdates(ClassMetadata $class, array $options = array()) if ( ! $class->isEmbeddedDocument && isset($this->documentChangeSets[$oid]) && $this->documentChangeSets[$oid]) { $persister->update($document, $options); } + unset($this->documentUpdates[$oid]); if ( ! $class->isEmbeddedDocument) { @@ -1203,7 +1238,7 @@ private function executeUpdates(ClassMetadata $class, array $options = array()) if ($hasPostUpdateListeners) { $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($document, $this->dm)); } - $this->cascadePostUpdateAndPostPersist($class, $document); + $this->cascadePostUpdate($class, $document); } } } @@ -1219,34 +1254,43 @@ private function cascadePreUpdate(ClassMetadata $class, $document) { $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate); - foreach ($class->fieldMappings as $mapping) { - if (isset($mapping['embedded'])) { - $value = $class->reflFields[$mapping['fieldName']]->getValue($document); - if ($value === null) { + $embeddedMappings = array_filter( + $class->associationMappings, + function ($assoc) { return ! empty($assoc['embedded']); } + ); + + foreach ($embeddedMappings as $mapping) { + $value = $class->reflFields[$mapping['fieldName']]->getValue($document); + + if ($value === null) { + continue; + } + + $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value; + + foreach ($values as $entry) { + $entryOid = spl_object_hash($entry); + $entryClass = $this->dm->getClassMetadata(get_class($entry)); + + if ( ! isset($this->documentChangeSets[$entryOid])) { continue; } - if ($mapping['type'] === 'one') { - $value = array($value); + + if (isset($this->documentInsertions[$entryOid])) { + continue; } - foreach ($value as $entry) { - $entryOid = spl_object_hash($entry); - $entryClass = $this->dm->getClassMetadata(get_class($entry)); - if ( ! isset($this->documentChangeSets[$entryOid])) { - continue; - } - if ( ! isset($this->documentInsertions[$entryOid])) { - if ( ! empty($entryClass->lifecycleCallbacks[Events::preUpdate])) { - $entryClass->invokeLifecycleCallbacks(Events::preUpdate, $entry); - $this->recomputeSingleDocumentChangeSet($entryClass, $entry); - } - if ($hasPreUpdateListeners) { - $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs( - $entry, $this->dm, $this->documentChangeSets[$entryOid]) - ); - } - } - $this->cascadePreUpdate($entryClass, $entry); + + if ( ! empty($entryClass->lifecycleCallbacks[Events::preUpdate])) { + $entryClass->invokeLifecycleCallbacks(Events::preUpdate, $entry); + $this->recomputeSingleDocumentChangeSet($entryClass, $entry); } + if ($hasPreUpdateListeners) { + $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs( + $entry, $this->dm, $this->documentChangeSets[$entryOid]) + ); + } + + $this->cascadePreUpdate($entryClass, $entry); } } } @@ -1257,44 +1301,46 @@ private function cascadePreUpdate(ClassMetadata $class, $document) * @param ClassMetadata $class * @param object $document */ - private function cascadePostUpdateAndPostPersist(ClassMetadata $class, $document) + private function cascadePostUpdate(ClassMetadata $class, $document) { $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist); $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate); - foreach ($class->fieldMappings as $mapping) { - if (isset($mapping['embedded'])) { - $value = $class->reflFields[$mapping['fieldName']]->getValue($document); - if ($value === null) { + $embeddedMappings = array_filter( + $class->associationMappings, + function($assoc) { return ! empty($assoc['embedded']); } + ); + + foreach ($embeddedMappings as $mapping) { + $value = $class->reflFields[$mapping['fieldName']]->getValue($document); + + if ($value === null) { + continue; + } + + $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value; + + foreach ($values as $entry) { + $entryOid = spl_object_hash($entry); + $entryClass = $this->dm->getClassMetadata(get_class($entry)); + + if ( ! isset($this->documentChangeSets[$entryOid])) { continue; } - if ($mapping['type'] === 'one') { - $value = array($value); + + if (isset($this->documentInsertions[$entryOid])) { + continue; } - foreach ($value as $entry) { - $entryOid = spl_object_hash($entry); - $entryClass = $this->dm->getClassMetadata(get_class($entry)); - if ( ! isset($this->documentChangeSets[$entryOid])) { - continue; - } - if (isset($this->documentInsertions[$entryOid])) { - if ( ! empty($entryClass->lifecycleCallbacks[Events::postPersist])) { - $entryClass->invokeLifecycleCallbacks(Events::postPersist, $entry); - } - if ($hasPostPersistListeners) { - $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entry, $this->dm)); - } - } else { - if ( ! empty($entryClass->lifecycleCallbacks[Events::postUpdate])) { - $entryClass->invokeLifecycleCallbacks(Events::postUpdate, $entry); - $this->recomputeSingleDocumentChangeSet($entryClass, $entry); - } - if ($hasPostUpdateListeners) { - $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entry, $this->dm)); - } - } - $this->cascadePostUpdateAndPostPersist($entryClass, $entry); + + if ( ! empty($entryClass->lifecycleCallbacks[Events::postUpdate])) { + $entryClass->invokeLifecycleCallbacks(Events::postUpdate, $entry); + $this->recomputeSingleDocumentChangeSet($entryClass, $entry); } + if ($hasPostUpdateListeners) { + $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entry, $this->dm)); + } + + $this->cascadePostUpdate($entryClass, $entry); } } } @@ -1318,6 +1364,7 @@ private function executeDeletions(ClassMetadata $class, array $options = array() if ( ! $class->isEmbeddedDocument) { $persister->delete($document, $options); } + unset( $this->documentDeletions[$oid], $this->documentIdentifiers[$oid], @@ -1326,8 +1373,8 @@ private function executeDeletions(ClassMetadata $class, array $options = array() // Clear snapshot information for any referenced PersistentCollection // http://www.doctrine-project.org/jira/browse/MODM-95 - foreach ($class->fieldMappings as $fieldMapping) { - if (isset($fieldMapping['type']) && $fieldMapping['type'] === 'many') { + foreach ($class->associationMappings as $fieldMapping) { + if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) { $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document); if ($value instanceof PersistentCollection) { $value->clearSnapshot(); @@ -1345,7 +1392,6 @@ private function executeDeletions(ClassMetadata $class, array $options = array() if ($hasPostRemoveListeners) { $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($document, $this->dm)); } - $this->cascadePostRemove($class, $document); } } } @@ -1573,6 +1619,8 @@ public function isScheduledForDirtyCheck($document) * Schedules a document for deletion. * * @param object $document + * + * @return void */ public function scheduleForDelete($document) { @@ -1582,7 +1630,9 @@ public function scheduleForDelete($document) if ($this->isInIdentityMap($document)) { $this->removeFromIdentityMap($document); } - unset($this->documentInsertions[$oid]); + + unset($this->documentInsertions[$oid], $this->documentStates[$oid]); + return; // document has not been persisted yet, so nothing more to do. } @@ -1591,13 +1641,14 @@ public function scheduleForDelete($document) } $this->removeFromIdentityMap($document); - $this->documentStates[$oid] = self::STATE_REMOVED; if (isset($this->documentUpdates[$oid])) { unset($this->documentUpdates[$oid]); } + if ( ! isset($this->documentDeletions[$oid])) { $this->documentDeletions[$oid] = $document; + $this->documentStates[$oid] = self::STATE_REMOVED; } } @@ -1890,6 +1941,7 @@ public function persist($document) private function doPersist($document, array &$visited) { $oid = spl_object_hash($document); + if (isset($visited[$oid])) { return; // Prevent infinite recursion } @@ -1899,6 +1951,7 @@ private function doPersist($document, array &$visited) $class = $this->dm->getClassMetadata(get_class($document)); $documentState = $this->getDocumentState($document, self::STATE_NEW); + switch ($documentState) { case self::STATE_MANAGED: // Nothing to do, except if policy is "deferred explicit" @@ -1906,22 +1959,22 @@ private function doPersist($document, array &$visited) $this->scheduleForDirtyCheck($document); } break; + case self::STATE_NEW: $this->persistNew($class, $document); break; - case self::STATE_DETACHED: - throw new \InvalidArgumentException( - "Behavior of persist() for a detached document is not yet defined."); - break; + case self::STATE_REMOVED: // Document becomes managed again - if ($this->isScheduledForDelete($document)) { - unset($this->documentDeletions[$oid]); - } else { - //FIXME: There's more to think of here... - $this->scheduleForInsert($class, $document); - } + unset($this->documentDeletions[$oid]); + + $this->documentStates[$oid] = self::STATE_MANAGED; break; + + case self::STATE_DETACHED: + throw new \InvalidArgumentException("Behavior of persist() for a detached document is not yet defined."); + break; + default: throw MongoDBException::invalidDocumentState($documentState); } @@ -1959,6 +2012,10 @@ private function doRemove($document, array &$visited) $visited[$oid] = $document; // mark visited + // Cascade first, because scheduleForDelete() removes the document from the identity map, which + // can cause problems when a lazy proxy has to be initialized for the cascade operation. + $this->cascadeRemove($document, $visited); + $class = $this->dm->getClassMetadata(get_class($document)); $documentState = $this->getDocumentState($document); switch ($documentState) { @@ -1966,89 +2023,24 @@ private function doRemove($document, array &$visited) case self::STATE_REMOVED: // nothing to do break; + case self::STATE_MANAGED: if ( ! empty($class->lifecycleCallbacks[Events::preRemove])) { $class->invokeLifecycleCallbacks(Events::preRemove, $document); } + if ($this->evm->hasListeners(Events::preRemove)) { $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($document, $this->dm)); } + $this->scheduleForDelete($document); - $this->cascadePreRemove($class, $document); break; + case self::STATE_DETACHED: throw MongoDBException::detachedDocumentCannotBeRemoved(); default: throw MongoDBException::invalidDocumentState($documentState); } - - $this->cascadeRemove($document, $visited); - } - - /** - * Cascades the preRemove event to embedded documents. - * - * @param ClassMetadata $class - * @param object $document - */ - private function cascadePreRemove(ClassMetadata $class, $document) - { - $hasPreRemoveListeners = $this->evm->hasListeners(Events::preRemove); - - foreach ($class->fieldMappings as $mapping) { - if (isset($mapping['embedded'])) { - $value = $class->reflFields[$mapping['fieldName']]->getValue($document); - if ($value === null) { - continue; - } - if ($mapping['type'] === 'one') { - $value = array($value); - } - foreach ($value as $entry) { - $entryClass = $this->dm->getClassMetadata(get_class($entry)); - if ( ! empty($entryClass->lifecycleCallbacks[Events::preRemove])) { - $entryClass->invokeLifecycleCallbacks(Events::preRemove, $entry); - } - if ($hasPreRemoveListeners) { - $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($entry, $this->dm)); - } - $this->cascadePreRemove($entryClass, $entry); - } - } - } - } - - /** - * Cascades the postRemove event to embedded documents. - * - * @param ClassMetadata $class - * @param object $document - */ - private function cascadePostRemove(ClassMetadata $class, $document) - { - $hasPostRemoveListeners = $this->evm->hasListeners(Events::postRemove); - - foreach ($class->fieldMappings as $mapping) { - if (isset($mapping['embedded'])) { - $value = $class->reflFields[$mapping['fieldName']]->getValue($document); - if ($value === null) { - continue; - } - if ($mapping['type'] === 'one') { - $value = array($value); - } - foreach ($value as $entry) { - $entryClass = $this->dm->getClassMetadata(get_class($entry)); - if ( ! empty($entryClass->lifecycleCallbacks[Events::postRemove])) { - $entryClass->invokeLifecycleCallbacks(Events::postRemove, $entry); - } - if ($hasPostRemoveListeners) { - $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($entry, $this->dm)); - } - $this->cascadePostRemove($entryClass, $entry); - } - } - } } /** @@ -2075,9 +2067,9 @@ public function merge($document) * @return object The managed copy of the document. * * @throws InvalidArgumentException If the document instance is NEW. - * @throws LockException If the entity uses optimistic locking through a - * version attribute and the version check against the - * managed copy fails. + * @throws LockException If the document uses optimistic locking through a + * version attribute and the version check against the + * managed copy fails. */ private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null) { @@ -2116,7 +2108,7 @@ private function doMerge($document, array &$visited, $prevManagedCopy = null, $a if ($managedCopy) { // We have the document in memory already, just make sure it is not removed. if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) { - throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.'); + throw new \InvalidArgumentException('Removed document detected during merge. Cannot merge with a removed entity.'); } } else { // We need to fetch the managed copy in order to merge. @@ -2321,6 +2313,7 @@ public function refresh($document) private function doRefresh($document, array &$visited) { $oid = spl_object_hash($document); + if (isset($visited[$oid])) { return; // Prevent infinite recursion } @@ -2328,11 +2321,14 @@ private function doRefresh($document, array &$visited) $visited[$oid] = $document; // mark visited $class = $this->dm->getClassMetadata(get_class($document)); - if ($this->getDocumentState($document) == self::STATE_MANAGED) { - $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]); - $this->getDocumentPersister($class->name)->refresh($id, $document); - } else { - throw new \InvalidArgumentException("Document is not MANAGED."); + + if ( ! $class->isEmbeddedDocument) { + if ($this->getDocumentState($document) == self::STATE_MANAGED) { + $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]); + $this->getDocumentPersister($class->name)->refresh($id, $document); + } else { + throw new \InvalidArgumentException("Document is not MANAGED."); + } } $this->cascadeRefresh($document, $visited); @@ -2347,36 +2343,24 @@ private function doRefresh($document, array &$visited) private function cascadeRefresh($document, array &$visited) { $class = $this->dm->getClassMetadata(get_class($document)); - foreach ($class->fieldMappings as $mapping) { - if ( ! $mapping['isCascadeRefresh']) { - continue; - } - if (isset($mapping['embedded'])) { - $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); - if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) { - if ($relatedDocuments instanceof PersistentCollection) { - // Unwrap so that foreach() does not initialize - $relatedDocuments = $relatedDocuments->unwrap(); - } - foreach ($relatedDocuments as $relatedDocument) { - $this->cascadeRefresh($relatedDocument, $visited); - } - } elseif ($relatedDocuments !== null) { - $this->cascadeRefresh($relatedDocuments, $visited); + + $associationMappings = array_filter( + $class->associationMappings, + function ($assoc) { return $assoc['isCascadeRefresh']; } + ); + + foreach ($associationMappings as $mapping) { + $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); + if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) { + if ($relatedDocuments instanceof PersistentCollection) { + // Unwrap so that foreach() does not initialize + $relatedDocuments = $relatedDocuments->unwrap(); } - } elseif (isset($mapping['reference'])) { - $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); - if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) { - if ($relatedDocuments instanceof PersistentCollection) { - // Unwrap so that foreach() does not initialize - $relatedDocuments = $relatedDocuments->unwrap(); - } - foreach ($relatedDocuments as $relatedDocument) { - $this->doRefresh($relatedDocument, $visited); - } - } elseif ($relatedDocuments !== null) { - $this->doRefresh($relatedDocuments, $visited); + foreach ($relatedDocuments as $relatedDocument) { + $this->doRefresh($relatedDocument, $visited); } + } elseif ($relatedDocuments !== null) { + $this->doRefresh($relatedDocuments, $visited); } } } @@ -2390,36 +2374,24 @@ private function cascadeRefresh($document, array &$visited) private function cascadeDetach($document, array &$visited) { $class = $this->dm->getClassMetadata(get_class($document)); - foreach ($class->fieldMappings as $mapping) { - if ( ! $mapping['isCascadeDetach']) { - continue; - } - if (isset($mapping['embedded'])) { - $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); - if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) { - if ($relatedDocuments instanceof PersistentCollection) { - // Unwrap so that foreach() does not initialize - $relatedDocuments = $relatedDocuments->unwrap(); - } - foreach ($relatedDocuments as $relatedDocument) { - $this->cascadeDetach($relatedDocument, $visited); - } - } elseif ($relatedDocuments !== null) { - $this->cascadeDetach($relatedDocuments, $visited); + + $associationMappings = array_filter( + $class->associationMappings, + function ($assoc) { return $assoc['isCascadeDetach']; } + ); + + foreach ($associationMappings as $mapping) { + $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); + if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) { + if ($relatedDocuments instanceof PersistentCollection) { + // Unwrap so that foreach() does not initialize + $relatedDocuments = $relatedDocuments->unwrap(); } - } elseif (isset($mapping['reference'])) { - $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); - if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) { - if ($relatedDocuments instanceof PersistentCollection) { - // Unwrap so that foreach() does not initialize - $relatedDocuments = $relatedDocuments->unwrap(); - } - foreach ($relatedDocuments as $relatedDocument) { - $this->doDetach($relatedDocument, $visited); - } - } elseif ($relatedDocuments !== null) { - $this->doDetach($relatedDocuments, $visited); + foreach ($relatedDocuments as $relatedDocument) { + $this->doDetach($relatedDocument, $visited); } + } elseif ($relatedDocuments !== null) { + $this->doDetach($relatedDocuments, $visited); } } } @@ -2474,11 +2446,12 @@ private function cascadePersist($document, array &$visited) { $class = $this->dm->getClassMetadata(get_class($document)); - foreach ($class->associationMappings as $fieldName => $mapping) { - if ( ! $mapping['isCascadePersist']) { - continue; - } + $associationMappings = array_filter( + $class->associationMappings, + function ($assoc) { return $assoc['isCascadePersist']; } + ); + foreach ($associationMappings as $fieldName => $mapping) { $relatedDocuments = $class->reflFields[$fieldName]->getValue($document); if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) { @@ -2505,30 +2478,22 @@ private function cascadePersist($document, array &$visited) private function cascadeRemove($document, array &$visited) { $class = $this->dm->getClassMetadata(get_class($document)); - foreach ($class->fieldMappings as $mapping) { - if ( ! $mapping['isCascadeRemove']) { - continue; - } - if (isset($mapping['embedded'])) { - $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); - if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) { - // If its a PersistentCollection initialization is intended! No unwrap! - foreach ($relatedDocuments as $relatedDocument) { - $this->cascadeRemove($relatedDocument, $visited); - } - } elseif ($relatedDocuments !== null) { - $this->cascadeRemove($relatedDocuments, $visited); - } - } elseif (isset($mapping['reference'])) { - $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); - if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) { - // If its a PersistentCollection initialization is intended! No unwrap! - foreach ($relatedDocuments as $relatedDocument) { - $this->doRemove($relatedDocument, $visited); - } - } elseif ($relatedDocuments !== null) { - $this->doRemove($relatedDocuments, $visited); + + $associationMappings = array_filter( + $class->associationMappings, + function ($assoc) { return $assoc['isCascadeRemove']; } + ); + + foreach ($associationMappings as $mapping) { + $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); + + if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) { + // If its a PersistentCollection initialization is intended! No unwrap! + foreach ($relatedDocuments as $relatedDocument) { + $this->doRemove($relatedDocument, $visited); } + } elseif ($relatedDocuments !== null) { + $this->doRemove($relatedDocuments, $visited); } } } @@ -2775,42 +2740,8 @@ public function getOrCreateDocument($className, $data, &$hints = array()) $data = $this->hydratorFactory->hydrate($document, $data, $hints); $this->originalDocumentData[$oid] = $data; } - return $document; - } - - /** - * Cascades the preLoad event to embedded documents. - * - * @param ClassMetadata $class - * @param object $document - * @param array $data - */ - private function cascadePreLoad(ClassMetadata $class, $document, $data) - { - $hasPreLoadListeners = $this->evm->hasListeners(Events::preLoad); - foreach ($class->fieldMappings as $mapping) { - if (isset($mapping['embedded'])) { - $value = $class->reflFields[$mapping['fieldName']]->getValue($document); - if ($value === null) { - continue; - } - if ($mapping['type'] === 'one') { - $value = array($value); - } - foreach ($value as $entry) { - $entryClass = $this->dm->getClassMetadata(get_class($entry)); - if ( ! empty($entryClass->lifecycleCallbacks[Events::preLoad])) { - $args = array(&$data); - $entryClass->invokeLifecycleCallbacks(Events::preLoad, $entry, $args); - } - if ($hasPreLoadListeners) { - $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($entry, $this->dm, $data[$mapping['name']])); - } - $this->cascadePreLoad($entryClass, $entry, $data[$mapping['name']]); - } - } - } + return $document; } /** @@ -3055,4 +2986,18 @@ private static function objToStr($obj) { return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj); } + + private function dispatchOnFlushEvent() + { + if ($this->evm->hasListeners(Events::onFlush)) { + $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm)); + } + } + + private function dispatchPostFlushEvent() + { + if ($this->evm->hasListeners(Events::postFlush)) { + $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm)); + } + } } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleCallbacksTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleCallbacksTest.php index fe1acf1340..9c6211e00f 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleCallbacksTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleCallbacksTest.php @@ -3,6 +3,7 @@ namespace Doctrine\ODM\MongoDB\Tests\Events; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\UnitOfWork; class LifecycleCallbacksTest extends \Doctrine\ODM\MongoDB\Tests\BaseTest { @@ -87,6 +88,10 @@ public function testPreLoadAndPostLoad() public function testPreAndPostRemove() { $user = $this->createUser(); + + $this->assertTrue($this->uow->isInIdentityMap($user)); + $this->assertTrue($this->uow->isInIdentityMap($user->profile)); + $this->dm->remove($user); $this->dm->flush(); @@ -151,6 +156,9 @@ public function testMultipleLevelsOfEmbedded() $user->profile->profile = $profile; $this->dm->flush(); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->uow->getDocumentState($user->profile->profile)); + $this->assertTrue($this->uow->isInIdentityMap($user->profile->profile)); + $this->assertTrue($profile->prePersist); $this->assertTrue($profile->postPersist); $this->assertFalse($profile->preUpdate); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleListenersTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleListenersTest.php index 51a84f6f2d..b65f9daef9 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleListenersTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleListenersTest.php @@ -78,7 +78,7 @@ public function testLifecycleListeners() $dm->flush(); $called = array( - Events::preRemove => array('Doctrine\ODM\MongoDB\Tests\Events\TestDocument', 'Doctrine\ODM\MongoDB\Tests\Events\TestEmbeddedDocument'), + Events::preRemove => array('Doctrine\ODM\MongoDB\Tests\Events\TestEmbeddedDocument', 'Doctrine\ODM\MongoDB\Tests\Events\TestDocument'), Events::postRemove => array('Doctrine\ODM\MongoDB\Tests\Events\TestDocument', 'Doctrine\ODM\MongoDB\Tests\Events\TestEmbeddedDocument') ); $this->assertEquals($called, $this->listener->called); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedIdTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedIdTest.php index 53c36828e9..82941931a7 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedIdTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedIdTest.php @@ -29,7 +29,7 @@ public function testEmbeddedIdsAreNotOverwritten() public function testEmbedOneDocumentWithMissingIdentifier() { - $user = new EmbeddedIdTestUser(); + $user = new EmbeddedStrategyNoneIdTestUser(); $user->embedOne = new DefaultIdStrategyNoneEmbeddedDocument(); $this->dm->persist($user); @@ -41,7 +41,7 @@ public function testEmbedOneDocumentWithMissingIdentifier() public function testEmbedManyDocumentWithMissingIdentifier() { - $user = new EmbeddedIdTestUser(); + $user = new EmbeddedStrategyNoneIdTestUser(); $user->embedMany[] = new DefaultIdStrategyNoneEmbeddedDocument(); $this->dm->persist($user); @@ -68,6 +68,19 @@ class EmbeddedIdTestUser public $embedMany = array(); } +/** @ODM\Document */ +class EmbeddedStrategyNoneIdTestUser +{ + /** @ODM\Id */ + public $id; + + /** @ODM\EmbedOne(targetDocument="DefaultIdStrategyNoneEmbeddedDocument") */ + public $embedOne; + + /** @ODM\EmbedMany(targetDocument="DefaultIdStrategyNoneEmbeddedDocument") */ + public $embedMany = array(); +} + /** @ODM\EmbeddedDocument */ class DefaultIdEmbeddedDocument { diff --git a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php index eaf21bb4bd..1eba08172d 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php @@ -438,6 +438,116 @@ public function testRegisterManagedEmbeddedDocumentWithMappedIdStrategyNoneAndNu $this->assertEquals($oid, $this->uow->getDocumentIdentifier($document)); } + public function testPersistRemovedDocument() + { + $user = new ForumUser(); + $user->username = 'jwage'; + + $this->uow->persist($user); + $this->uow->commit(); + + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->uow->getDocumentState($user)); + + $this->uow->remove($user); + + $this->assertEquals(UnitOfWork::STATE_REMOVED, $this->uow->getDocumentState($user)); + + $this->uow->persist($user); + + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->uow->getDocumentState($user)); + + $this->uow->commit(); + + $this->assertNotNull($this->dm->getRepository(get_class($user))->find($user->id)); + } + + public function testRemovePersistedButNotFlushedDocument() + { + $user = new ForumUser(); + $user->username = 'jwage'; + + $this->uow->persist($user); + $this->uow->remove($user); + $this->uow->commit(); + + $this->assertNull($this->dm->getRepository(get_class($user))->find($user->id)); + } + + public function testPersistRemovedEmbeddedDocument() + { + $test = new PersistRemovedEmbeddedDocument(); + $test->embedded = new EmbeddedDocumentWithId(); + $this->uow->persist($test); + $this->uow->commit(); + $this->uow->clear(); + + $test = $this->dm->getRepository(get_class($test))->find($test->id); + + $this->uow->remove($test); + + $this->assertEquals(UnitOfWork::STATE_REMOVED, $this->uow->getDocumentState($test)); + $this->assertTrue($this->uow->isScheduledForDelete($test)); + + // removing a top level document should cascade to embedded documents + $this->assertEquals(UnitOfWork::STATE_REMOVED, $this->uow->getDocumentState($test->embedded)); + $this->assertTrue($this->uow->isScheduledForDelete($test->embedded)); + + $this->uow->persist($test); + $this->uow->commit(); + + $this->assertFalse($test->embedded->preRemove); + + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->uow->getDocumentState($test)); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->uow->getDocumentState($test->embedded)); + } + + public function testPersistingEmbeddedDocumentWithoutIdentifier() + { + $address = new \Documents\Address(); + $user = new \Documents\User(); + $user->setAddress($address); + + $this->assertEquals(UnitOfWork::STATE_NEW, $this->uow->getDocumentState($address)); + $this->assertFalse($this->uow->isInIdentityMap($address)); + $this->assertNull($this->uow->getDocumentIdentifier($address)); + + $this->uow->persist($user); + + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->uow->getDocumentState($user->getAddress())); + $this->assertTrue($this->uow->isInIdentityMap($address)); + $this->assertTrue($this->uow->isScheduledForInsert($address)); + $this->assertEquals(spl_object_hash($address), $this->uow->getDocumentIdentifier($address)); + + $this->uow->commit(); + + $this->assertTrue($this->uow->isInIdentityMap($address)); + $this->assertFalse($this->uow->isScheduledForInsert($address)); + } + + public function testEmbeddedDocumentChangeSets() + { + $address = new \Documents\Address(); + $user = new \Documents\User(); + $user->setAddress($address); + + $this->uow->persist($user); + + $this->uow->computeChangeSets(); + + $changeSet = $this->uow->getDocumentChangeSet($address); + $this->assertNotEmpty($changeSet); + + $this->uow->commit(); + + $address->setCity('Nashville'); + + $this->uow->computeChangeSets(); + $changeSet = $this->uow->getDocumentChangeSet($address); + + $this->assertTrue(isset($changeSet['city'])); + $this->assertEquals('Nashville', $changeSet['city'][1]); + } + protected function getDocumentManager() { return new \Stubs\DocumentManager(); @@ -652,8 +762,16 @@ class EmbeddedDocumentWithoutId /** @ODM\EmbeddedDocument */ class EmbeddedDocumentWithId { + public $preRemove = false; + /** @ODM\Id */ public $id; + + /** @ODM\PreRemove */ + public function preRemove() + { + $this->preRemove = true; + } } /** @ODM\EmbeddedDocument */ @@ -662,3 +780,13 @@ class EmbeddedDocumentWithIdStrategyNone /** @ODM\Id(strategy="none") */ public $id; } + +/** @ODM\Document */ +class PersistRemovedEmbeddedDocument +{ + /** @ODM\Id */ + public $id; + + /** @ODM\EmbedOne(targetDocument="EmbeddedDocumentWithId") */ + public $embedded; +} From e67b9c4b39a413a805f9ea84bd87185245c685ae Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Sun, 26 Jan 2014 20:57:45 -0500 Subject: [PATCH 3/9] Fix issue with ClassMetadataFactory not inheriting association mappings properly. --- .../MongoDB/Mapping/ClassMetadataFactory.php | 29 +++++++++++++++++++ .../ODM/MongoDB/Mapping/ClassMetadataInfo.php | 18 ++++++++++++ .../Tests/Functional/FunctionalTest.php | 6 ++++ 3 files changed, 53 insertions(+) diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php index 5c402d07e9..d11d4506bf 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php @@ -135,6 +135,7 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS $class->setDiscriminatorMap($parent->discriminatorMap); $class->setIdGeneratorType($parent->generatorType); $this->addInheritedFields($class, $parent); + $this->addInheritedRelations($class, $parent); $this->addInheritedIndexes($class, $parent); $class->setIdentifier($parent->identifier); $class->setVersioned($parent->isVersioned); @@ -294,6 +295,34 @@ private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $pare } } + + /** + * Adds inherited association mappings to the subclass mapping. + * + * @param \Doctrine\ORM\Mapping\ClassMetadata $subClass + * @param \Doctrine\ORM\Mapping\ClassMetadata $parentClass + * + * @return void + * + * @throws MappingException + */ + private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $parentClass) + { + foreach ($parentClass->associationMappings as $field => $mapping) { + if ($parentClass->isMappedSuperclass) { + $mapping['sourceDocument'] = $subClass->name; + } + + if ( ! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) { + $mapping['inherited'] = $parentClass->name; + } + if ( ! isset($mapping['declared'])) { + $mapping['declared'] = $parentClass->name; + } + $subClass->addInheritedAssociationMapping($mapping); + } + } + /** * Adds inherited indexes to the subclass mapping. * diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php index 9ac255ed35..fdea7fc2f9 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php @@ -1158,7 +1158,9 @@ public function mapField(array $mapping) } } + // @TODO: Stop storing associations in field mappings. $this->fieldMappings[$mapping['fieldName']] = $mapping; + if (isset($mapping['association'])) { $this->associationMappings[$mapping['fieldName']] = $mapping; } @@ -1238,6 +1240,22 @@ public function addInheritedFieldMapping(array $fieldMapping) $this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping; } + /** + * INTERNAL: + * Adds an association mapping without completing/validating it. + * This is mainly used to add inherited association mappings to derived classes. + * + * @param array $mapping + * + * @return void + * + * @throws MappingException + */ + public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/) + { + $this->associationMappings[$mapping['fieldName']] = $mapping; + } + /** * Checks whether the class has a mapped association with the given field name. * diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/FunctionalTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/FunctionalTest.php index 9a517db139..3b72d56293 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/FunctionalTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/FunctionalTest.php @@ -101,6 +101,12 @@ public function testUpsertObject($className, $id, $discriminator) $this->assertEquals('test', $check['username']); } + public function testInheritedAssociationMappings() + { + $class = $this->dm->getClassMetadata('Documents\UserUpsertChild'); + $this->assertTrue(isset($class->associationMappings['groups'])); + } + public function testFlushSingleDocument() { $user1 = new \Documents\ForumUser(); From fcfc9ff2bb71326ff5f0e5151f56fcdf3c72e3e1 Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Sun, 26 Jan 2014 21:28:15 -0500 Subject: [PATCH 4/9] Add test to ensure embed one and embed many identifiers can be modified. --- .../MongoDB/Tests/Functional/EmbeddedTest.php | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php index bdae08ac5e..138e4c57c3 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php @@ -16,6 +16,7 @@ use Documents\Functional\NotSavedEmbedded; use Documents\Functional\VirtualHost; use Documents\Functional\VirtualHostDirective; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\PersistentCollection; class EmbeddedTest extends \Doctrine\ODM\MongoDB\Tests\BaseTest @@ -458,4 +459,80 @@ public function testEmbeddedDocumentNotSavedFields() $this->assertEquals('foo', $document->embedded->name); $this->assertNull($document->embedded->notSaved); } + + public function testChangeEmbedOneDocumentId() + { + $originalId = (string) new \MongoId(); + + $test = new ChangeEmbeddedIdTest(); + $test->embed = new EmbeddedDocumentWithId(); + $test->embed->id = $originalId; + $this->dm->persist($test); + + $this->dm->flush(); + + $newId = (string) new \MongoId(); + + $test->embed->id = $newId; + + $this->dm->flush(); + $this->dm->clear(); + + $test = $this->dm->find(get_class($test), $test->id); + + $this->assertEquals($newId, $test->embed->id); + } + + public function testChangeEmbedManyDocumentId() + { + $originalId = (string) new \MongoId(); + + $test = new ChangeEmbeddedIdTest(); + $test->embedMany[] = new EmbeddedDocumentWithId(); + $test->embedMany[0]->id = $originalId; + $this->dm->persist($test); + + $this->dm->flush(); + + $newId = (string) new \MongoId(); + + $test->embedMany[0]->id = $newId; + + $this->dm->flush(); + $this->dm->clear(); + + $test = $this->dm->find(get_class($test), $test->id); + + $this->assertEquals($newId, $test->embedMany[0]->id); + } +} + +/** + * @ODM\Document + */ +class ChangeEmbeddedIdTest +{ + /** + * @ODM\Id + */ + public $id; + + /** + * @ODM\EmbedOne(targetDocument="EmbeddedDocumentWithId") + */ + public $embed; + + /** + * @ODM\EmbedMany(targetDocument="EmbeddedDocumentWithId") + */ + public $embedMany = array(); +} + +/** + * @ODM\EmbeddedDocument + */ +class EmbeddedDocumentWithId +{ + /** @ODM\Id */ + public $id; } From 14b3e613899e11e6c0d4e1a38596d13958d2df83 Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Sun, 26 Jan 2014 22:44:01 -0500 Subject: [PATCH 5/9] Test for embedded documents with same id should not be the same instance. --- .../MongoDB/Tests/Functional/EmbeddedTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php index 138e4c57c3..62fd3ec291 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php @@ -505,6 +505,25 @@ public function testChangeEmbedManyDocumentId() $this->assertEquals($newId, $test->embedMany[0]->id); } + + public function testEmbeddedDocumentsWithSameIdAreNotSameInstance() + { + $originalId = (string) new \MongoId(); + + $test = new ChangeEmbeddedIdTest(); + $test->embed = new EmbeddedDocumentWithId(); + $test->embedMany[] = $test->embed; + $test->embedMany[] = $test->embed; + + $this->dm->persist($test); + $this->dm->flush(); + $this->dm->clear(); + + $test = $this->dm->find(get_class($test), $test->id); + + $this->assertNotSame($test->embed, $test->embedMany[0]); + $this->assertNotSame($test->embed, $test->embedMany[1]); + } } /** From 15f93e548360ed7e2924682d3c13cb93382c1e2c Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Sun, 26 Jan 2014 22:48:33 -0500 Subject: [PATCH 6/9] Remove code in PersistenceBuilder::prepareInsertData() that is never called. --- .../ODM/MongoDB/Persisters/PersistenceBuilder.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php b/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php index 33c3130072..7bca2b0121 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php @@ -104,19 +104,6 @@ public function prepareInsertData($document) } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) { $value = $this->prepareEmbeddedDocumentValue($mapping, $new); - // @ReferenceMany - } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_MANY) { - $value = array(); - foreach ($new as $reference) { - $value[] = $this->prepareReferencedDocumentValue($mapping, $reference); - } - - // @EmbedMany - } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_MANY) { - $value = array(); - foreach ($new as $reference) { - $value[] = $this->prepareEmbeddedDocumentValue($mapping, $reference); - } } } From 40f0207349f71af20bbfc473ae745062f810b6d0 Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Sun, 26 Jan 2014 23:00:25 -0500 Subject: [PATCH 7/9] Remove commit order calculator. We don't need it. --- .../Internal/CommitOrderCalculator.php | 130 ------------------ lib/Doctrine/ODM/MongoDB/UnitOfWork.php | 104 ++------------ .../Tests/CommitOrderCalculatorTest.php | 54 -------- .../Tests/Events/LifecycleListenersTest.php | 4 +- 4 files changed, 15 insertions(+), 277 deletions(-) delete mode 100644 lib/Doctrine/ODM/MongoDB/Internal/CommitOrderCalculator.php delete mode 100644 tests/Doctrine/ODM/MongoDB/Tests/CommitOrderCalculatorTest.php diff --git a/lib/Doctrine/ODM/MongoDB/Internal/CommitOrderCalculator.php b/lib/Doctrine/ODM/MongoDB/Internal/CommitOrderCalculator.php deleted file mode 100644 index 130814877f..0000000000 --- a/lib/Doctrine/ODM/MongoDB/Internal/CommitOrderCalculator.php +++ /dev/null @@ -1,130 +0,0 @@ -. - */ - -namespace Doctrine\ODM\MongoDB\Internal; - -/** - * The CommitOrderCalculator is used by the UnitOfWork to sort out the - * correct order in which changes to documents need to be persisted. - * - * @since 1.0 - * @author Jonathan H. Wage - * @author Roman Borschel - */ -class CommitOrderCalculator -{ - const NOT_VISITED = 1; - const IN_PROGRESS = 2; - const VISITED = 3; - - private $nodeStates = array(); - private $classes = array(); // The nodes to sort - private $relatedClasses = array(); - private $sorted = array(); - - /** - * Clears the current graph. - * - * @return void - */ - public function clear() - { - $this->classes = - $this->relatedClasses = array(); - } - - /** - * Gets a valid commit order for all current nodes. - * - * Uses a depth-first search (DFS) to traverse the graph. - * The desired topological sorting is the reverse postorder of these searches. - * - * @return array The list of ordered classes. - */ - public function getCommitOrder() - { - // Check whether we need to do anything. 0 or 1 node is easy. - $nodeCount = count($this->classes); - if ($nodeCount === 0) { - return array(); - } - - if ($nodeCount === 1) { - return array_values($this->classes); - } - - // Init - foreach ($this->classes as $node) { - $this->nodeStates[$node->name] = self::NOT_VISITED; - } - - // Go - foreach ($this->classes as $node) { - if ($this->nodeStates[$node->name] == self::NOT_VISITED) { - $this->visitNode($node); - } - } - - $sorted = array_reverse($this->sorted); - - $this->sorted = $this->nodeStates = array(); - - return $sorted; - } - - private function visitNode($node) - { - $this->nodeStates[$node->name] = self::IN_PROGRESS; - - if (isset($this->relatedClasses[$node->name])) { - foreach ($this->relatedClasses[$node->name] as $relatedNode) { - if ($this->nodeStates[$relatedNode->name] == self::NOT_VISITED) { - $this->visitNode($relatedNode); - } - } - } - - $this->nodeStates[$node->name] = self::VISITED; - $this->sorted[] = $node; - } - - public function addDependency($fromClass, $toClass) - { - $this->relatedClasses[$fromClass->name][] = $toClass; - } - - public function hasDependency($fromClass, $toClass) - { - if ( ! isset($this->relatedClasses[$fromClass->name])) { - return false; - } - - return in_array($toClass, $this->relatedClasses[$fromClass->name]); - } - - public function hasClass($className) - { - return isset($this->classes[$className]); - } - - public function addClass($class) - { - $this->classes[$class->name] = $class; - } -} diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index 453245fc83..0e20b221f8 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -28,7 +28,6 @@ use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs; use Doctrine\ODM\MongoDB\Event\PreLoadEventArgs; use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory; -use Doctrine\ODM\MongoDB\Internal\CommitOrderCalculator; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\PersistentCollection; use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder; @@ -200,14 +199,6 @@ class UnitOfWork implements PropertyChangedListener */ private $dm; - /** - * The calculator used to calculate the order in which changes to - * documents need to be written to the database. - * - * @var Internal\CommitOrderCalculator - */ - private $commitOrderCalculator; - /** * The EventManager used for dispatching events. * @@ -426,7 +417,6 @@ public function commit($document = null, array $options = array()) $this->dispatchOnFlushEvent(); - // Now we need a commit order to maintain referential integrity $commitOrder = $this->getCommitOrder(); if ($this->documentUpserts) { @@ -465,10 +455,10 @@ public function commit($document = null, array $options = array()) $this->getCollectionPersister()->update($collectionToUpdate, $options); } - // Document deletions come last and need to be in reverse commit order + // Document deletions come last if ($this->documentDeletions) { - for ($count = count($commitOrder), $i = $count - 1; $i >= 0; --$i) { - $this->executeDeletions($commitOrder[$i], $options); + foreach ($commitOrder as $class) { + $this->executeDeletions($class, $options); } } @@ -1401,76 +1391,25 @@ private function executeDeletions(ClassMetadata $class, array $options = array() * * @return array */ - private function getCommitOrder(array $documentChangeSet = null) + private function getCommitOrder() { - if ($documentChangeSet === null) { - $documentChangeSet = array_merge( - $this->documentInsertions, - $this->documentUpserts, - $this->documentUpdates, - $this->documentDeletions - ); - } - - $calc = $this->getCommitOrderCalculator(); + $documentChangeSet = array_merge( + $this->documentInsertions, + $this->documentUpserts, + $this->documentUpdates, + $this->documentDeletions + ); - // See if there are any new classes in the changeset, that are not in the - // commit order graph yet (don't have a node). - // We have to inspect changeSet to be able to correctly build dependencies. - // It is not possible to use IdentityMap here because post inserted ids - // are not yet available. - $newNodes = array(); + $classes = array(); foreach ($documentChangeSet as $document) { $className = get_class($document); - - if ($calc->hasClass($className)) { - continue; - } - $class = $this->dm->getClassMetadata($className); - $calc->addClass($class); - - $newNodes[] = $class; - } - - // Calculate dependencies for new nodes - while ($class = array_pop($newNodes)) { - foreach ($class->associationMappings as $assoc) { - if ( ! ($assoc['isOwningSide'] && isset($assoc['targetDocument']))) { - continue; - } - - $targetClass = $this->dm->getClassMetadata($assoc['targetDocument']); - - if ( ! $calc->hasClass($targetClass->name)) { - $calc->addClass($targetClass); - - $newNodes[] = $targetClass; - } - - $calc->addDependency($targetClass, $class); - - // If the target class has mapped subclasses, these share the same dependency. - if ( ! $targetClass->subClasses) { - continue; - } - foreach ($targetClass->subClasses as $subClassName) { - $targetSubClass = $this->dm->getClassMetadata($subClassName); - - if ( ! $calc->hasClass($subClassName)) { - $calc->addClass($targetSubClass); - - $newNodes[] = $targetSubClass; - } - - $calc->addDependency($targetSubClass, $class); - } - } + $classes[$className] = $class; } - return $calc->getCommitOrder(); + return array_values($classes); } /** @@ -2547,19 +2486,6 @@ public function unlock($document) $this->getDocumentPersister($documentName)->unlock($document); } - /** - * Gets the CommitOrderCalculator used by the UnitOfWork to order commits. - * - * @return \Doctrine\ODM\MongoDB\Internal\CommitOrderCalculator - */ - public function getCommitOrderCalculator() - { - if ($this->commitOrderCalculator === null) { - $this->commitOrderCalculator = new CommitOrderCalculator; - } - return $this->commitOrderCalculator; - } - /** * Clears the UnitOfWork. * @@ -2583,10 +2509,6 @@ public function clear($documentName = null) $this->collectionDeletions = $this->parentAssociations = $this->orphanRemovals = array(); - - if ($this->commitOrderCalculator !== null) { - $this->commitOrderCalculator->clear(); - } } else { $visited = array(); foreach ($this->identityMap as $className => $documents) { diff --git a/tests/Doctrine/ODM/MongoDB/Tests/CommitOrderCalculatorTest.php b/tests/Doctrine/ODM/MongoDB/Tests/CommitOrderCalculatorTest.php deleted file mode 100644 index 0173cd39e5..0000000000 --- a/tests/Doctrine/ODM/MongoDB/Tests/CommitOrderCalculatorTest.php +++ /dev/null @@ -1,54 +0,0 @@ -calc = new \Doctrine\ODM\MongoDB\Internal\CommitOrderCalculator(); - } - - public function testCommitOrdering1() - { - $class1 = new ClassMetadata(__NAMESPACE__ . '\NodeClass1'); - $class2 = new ClassMetadata(__NAMESPACE__ . '\NodeClass2'); - $class3 = new ClassMetadata(__NAMESPACE__ . '\NodeClass3'); - $class4 = new ClassMetadata(__NAMESPACE__ . '\NodeClass4'); - $class5 = new ClassMetadata(__NAMESPACE__ . '\NodeClass5'); - - $this->calc->addClass($class1); - $this->calc->addClass($class2); - $this->calc->addClass($class3); - $this->calc->addClass($class4); - $this->calc->addClass($class5); - - $this->calc->addDependency($class1, $class2); - $this->calc->addDependency($class2, $class3); - $this->calc->addDependency($class3, $class4); - $this->calc->addDependency($class5, $class1); - - $sorted = $this->calc->getCommitOrder(); - - // There is only 1 valid ordering for this constellation - $correctOrder = array($class5, $class1, $class2, $class3, $class4); - $this->assertSame($correctOrder, $sorted); - } -} - -class NodeClass1 {} -class NodeClass2 {} -class NodeClass3 {} -class NodeClass4 {} -class NodeClass5 {} \ No newline at end of file diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleListenersTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleListenersTest.php index b65f9daef9..6f0512080e 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleListenersTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Events/LifecycleListenersTest.php @@ -79,7 +79,7 @@ public function testLifecycleListeners() $called = array( Events::preRemove => array('Doctrine\ODM\MongoDB\Tests\Events\TestEmbeddedDocument', 'Doctrine\ODM\MongoDB\Tests\Events\TestDocument'), - Events::postRemove => array('Doctrine\ODM\MongoDB\Tests\Events\TestDocument', 'Doctrine\ODM\MongoDB\Tests\Events\TestEmbeddedDocument') + Events::postRemove => array('Doctrine\ODM\MongoDB\Tests\Events\TestEmbeddedDocument', 'Doctrine\ODM\MongoDB\Tests\Events\TestDocument') ); $this->assertEquals($called, $this->listener->called); $this->listener->called = array(); @@ -220,4 +220,4 @@ public function __construct($name) { $this->name = $name; } -} \ No newline at end of file +} From b16b3c7ce2b66c0aab0106d182e7fee6f564d09e Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Mon, 27 Jan 2014 00:34:21 -0500 Subject: [PATCH 8/9] Implement support for read only objects. --- doctrine-mongo-mapping.xsd | 2 + .../MongoDB/Mapping/Annotations/Document.php | 1 + .../Mapping/Annotations/EmbeddedDocument.php | 1 + .../ODM/MongoDB/Mapping/ClassMetadata.php | 4 + .../ODM/MongoDB/Mapping/ClassMetadataInfo.php | 20 +++ .../Mapping/Driver/AnnotationDriver.php | 3 + .../ODM/MongoDB/Mapping/Driver/XmlDriver.php | 3 + .../ODM/MongoDB/Mapping/Driver/YamlDriver.php | 3 + lib/Doctrine/ODM/MongoDB/UnitOfWork.php | 11 +- .../MongoDB/Tests/Functional/ReadOnlyTest.php | 133 ++++++++++++++++++ 10 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 tests/Doctrine/ODM/MongoDB/Tests/Functional/ReadOnlyTest.php diff --git a/doctrine-mongo-mapping.xsd b/doctrine-mongo-mapping.xsd index a727d33dbc..3cbb430a96 100644 --- a/doctrine-mongo-mapping.xsd +++ b/doctrine-mongo-mapping.xsd @@ -24,6 +24,7 @@ + @@ -50,6 +51,7 @@ + diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Document.php b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Document.php index cf536475cc..423db922dc 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Document.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Document.php @@ -28,4 +28,5 @@ final class Document extends AbstractDocument public $indexes = array(); public $requireIndexes = false; public $slaveOkay; + public $readOnly = false; } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/EmbeddedDocument.php b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/EmbeddedDocument.php index 8a625d173c..db80b3a10e 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/EmbeddedDocument.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/EmbeddedDocument.php @@ -23,4 +23,5 @@ final class EmbeddedDocument extends AbstractDocument { public $indexes = array(); + public $readOnly = false; } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php index 67bedb4483..f9d9482671 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php @@ -151,6 +151,10 @@ public function __sleep() $serialized[] = 'lifecycleCallbacks'; } + if ($this->isReadOnly) { + $serialized[] = 'isReadOnly'; + } + if ($this->file) { $serialized[] = 'file'; } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php index fdea7fc2f9..b2ee869f93 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php @@ -404,6 +404,16 @@ class ClassMetadataInfo implements \Doctrine\Common\Persistence\Mapping\ClassMet */ public $reflClass; + /** + * Is this document marked as "read-only"? + * + * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance + * optimization for documents that are immutable. + * + * @var bool + */ + public $isReadOnly = false; + /** * Initializes a new ClassMetadata instance that will hold the object-document mapping * metadata of the class with the given name. @@ -1690,6 +1700,16 @@ public function setVersionField($versionField) $this->versionField = $versionField; } + /** + * Marks this class as read only, no change tracking is applied to it. + * + * @return void + */ + public function markReadOnly() + { + $this->isReadOnly = true; + } + /** * Sets the version field mapping used for versioning. Sets the default * value to use depending on the column type. diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php index b0c847a59d..c00f71d305 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php @@ -117,6 +117,9 @@ public function loadMetadataForClass($className, ClassMetadata $class) } elseif ($documentAnnot instanceof ODM\EmbeddedDocument) { $class->isEmbeddedDocument = true; } + if (isset($documentAnnot->readOnly) && $documentAnnot->readOnly) { + $class->markReadOnly(); + } if (isset($documentAnnot->db)) { $class->setDatabase($documentAnnot->db); } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php index df5974b209..b6cf3968e6 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php @@ -63,6 +63,9 @@ public function loadMetadataForClass($className, ClassMetadata $class) } elseif ($xmlRoot->getName() == 'embedded-document') { $class->isEmbeddedDocument = true; } + if (isset($xmlRoot['read-only'])) { + $class->markReadOnly(); + } if (isset($xmlRoot['db'])) { $class->setDatabase((string) $xmlRoot['db']); } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php index a50ae45b91..b32b525327 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php @@ -70,6 +70,9 @@ public function loadMetadataForClass($className, ClassMetadata $class) } elseif ($element['type'] === 'embeddedDocument') { $class->isEmbeddedDocument = true; } + if (isset($element['readOnly']) && $element['readOnly'] == true) { + $class->markReadOnly(); + } if (isset($element['indexes'])) { foreach($element['indexes'] as $index) { $class->addIndex($index['keys'], isset($index['options']) ? $index['options'] : array()); diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index 0e20b221f8..b36882545c 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -546,6 +546,10 @@ private function computeSingleDocumentChangeSet($document) $this->computeScheduleInsertsChangeSets(); $this->computeScheduleUpsertsChangeSets(); + if ($class->isReadOnly) { + return; + } + // Ignore uninitialized proxy objects if ($document instanceof Proxy && ! $document->__isInitialized__) { return; @@ -863,6 +867,11 @@ public function computeChangeSets() foreach ($this->identityMap as $className => $documents) { $class = $this->dm->getClassMetadata($className); + // Skip class if instances are read-only + if ($class->isReadOnly) { + continue; + } + // If change tracking is explicit or happens through notification, then only compute // changes on document of that type that are explicitly marked for synchronization. switch (true) { @@ -881,7 +890,7 @@ public function computeChangeSets() foreach ($documentsToProcess as $document) { // Ignore uninitialized proxy objects - if (/* $document is readOnly || */ $document instanceof Proxy && ! $document->__isInitialized__) { + if ($document instanceof Proxy && ! $document->__isInitialized__) { continue; } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReadOnlyTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReadOnlyTest.php new file mode 100644 index 0000000000..3735ea2d79 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReadOnlyTest.php @@ -0,0 +1,133 @@ +dm->persist($readOnly); + $this->dm->flush(); + + $readOnly->name = "Test2"; + $readOnly->numericValue = 4321; + + $this->dm->flush(); + $this->dm->clear(); + + $dbReadOnly = $this->dm->find('Doctrine\Tests\ODM\MongoDB\Functional\ReadOnlyDocument', $readOnly->id); + $this->assertEquals("Test1", $dbReadOnly->name); + $this->assertEquals(1234, $dbReadOnly->numericValue); + } + + public function testReadOnlyEmbedOneNeverChangeTracked() + { + $write = new WriteDocument(); + $write->readOnlyEmbedOne = new ReadOnlyEmbeddedDocument("Test1", 1234); + $this->dm->persist($write); + $this->dm->flush(); + + $write->readOnlyEmbedOne->name = "Test2"; + $write->readOnlyEmbedOne->numericValue = 4321; + + $this->dm->flush(); + $this->dm->clear(); + + $write = $this->dm->find('Doctrine\Tests\ODM\MongoDB\Functional\WriteDocument', $write->id); + + $this->assertEquals("Test1", $write->readOnlyEmbedOne->name); + $this->assertEquals(1234, $write->readOnlyEmbedOne->numericValue); + } + + public function testReadOnlyEmbedManyNeverChangeTracked() + { + $write = new WriteDocument(); + $write->readOnlyEmbedMany[0] = new ReadOnlyEmbeddedDocument("Test1", 1234); + $this->dm->persist($write); + $this->dm->flush(); + + $write->readOnlyEmbedMany[0]->name = "Test2"; + $write->readOnlyEmbedMany[0]->numericValue = 4321; + + $this->dm->flush(); + $this->dm->clear(); + + $write = $this->dm->find('Doctrine\Tests\ODM\MongoDB\Functional\WriteDocument', $write->id); + + $this->assertEquals("Test1", $write->readOnlyEmbedMany[0]->name); + $this->assertEquals(1234, $write->readOnlyEmbedMany[0]->numericValue); + } +} + +/** + * @ODM\Document(readOnly=true) + */ +class ReadOnlyDocument +{ + /** + * @ODM\Id + */ + public $id; + + /** @ODM\String */ + public $name; + + /** @ODM\Int */ + public $numericValue; + + public function __construct($name, $number) + { + $this->name = $name; + $this->numericValue = $number; + } +} + +/** @ODM\Document */ +class WriteDocument +{ + /** + * @ODM\Id + */ + public $id; + + /** + * @ODM\EmbedOne(targetDocument="EmbeddedDocument") + */ + public $embedOne; + + /** + * @ODM\EmbedMany(targetDocument="EmbeddedDocument") + */ + public $embedMany; + + /** + * @ODM\EmbedOne(targetDocument="ReadOnlyEmbeddedDocument") + */ + public $readOnlyEmbedOne; + + /** + * @ODM\EmbedMany(targetDocument="ReadOnlyEmbeddedDocument") + */ + public $readOnlyEmbedMany = array(); +} + +/** + * @ODM\EmbeddedDocument(readOnly=true) + */ +class ReadOnlyEmbeddedDocument +{ + /** @ODM\String */ + public $name; + + /** @ODM\Int */ + public $numericValue; + + public function __construct($name, $number) + { + $this->name = $name; + $this->numericValue = $number; + } +} From d1398d539261833644d4e0f473f344d766b2ca36 Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Mon, 27 Jan 2014 12:34:55 -0500 Subject: [PATCH 9/9] Cleanup exceptions. --- lib/Doctrine/ODM/MongoDB/DocumentManager.php | 61 +++--- .../ODM/MongoDB/DocumentRepository.php | 4 +- lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php | 4 +- lib/Doctrine/ODM/MongoDB/LockException.php | 42 ---- .../MongoDB/Mapping/ClassMetadataFactory.php | 2 +- .../ODM/MongoDB/Mapping/ClassMetadataInfo.php | 8 +- .../ODM/MongoDB/Mapping/MappingException.php | 15 ++ .../MongoDBInvalidArgumentException.php | 203 ++++++++++++++++++ .../ODM/MongoDB/OptimisticLockException.php | 90 ++++++++ .../MongoDB/Persisters/DocumentPersister.php | 15 +- .../ODM/MongoDB/PessimisticLockException.php | 38 ++++ lib/Doctrine/ODM/MongoDB/UnitOfWork.php | 93 ++++---- .../ODM/MongoDB/Tests/Functional/LockTest.php | 8 +- 13 files changed, 453 insertions(+), 130 deletions(-) create mode 100644 lib/Doctrine/ODM/MongoDB/MongoDBInvalidArgumentException.php create mode 100644 lib/Doctrine/ODM/MongoDB/OptimisticLockException.php create mode 100644 lib/Doctrine/ODM/MongoDB/PessimisticLockException.php diff --git a/lib/Doctrine/ODM/MongoDB/DocumentManager.php b/lib/Doctrine/ODM/MongoDB/DocumentManager.php index 7b7841b6d1..f31a2b011c 100644 --- a/lib/Doctrine/ODM/MongoDB/DocumentManager.php +++ b/lib/Doctrine/ODM/MongoDB/DocumentManager.php @@ -384,14 +384,16 @@ public function createQueryBuilder($documentName = null) * this DocumentManager as NEW. Do not pass detached documents to the persist operation. * * @param object $document The instance to make managed and persistent. - * @throws \InvalidArgumentException When the given $document param is not an object + * @throws MongoDBInvalidArgumentException When the given $document param is not an object */ public function persist($document) { if ( ! is_object($document)) { - throw new \InvalidArgumentException(gettype($document)); + throw MongoDBInvalidArgumentException::invalidObject('DocumentManager#persist()' , $document); } + $this->errorIfClosed(); + $this->unitOfWork->persist($document); } @@ -402,14 +404,16 @@ public function persist($document) * or as a result of the flush operation. * * @param object $document The document instance to remove. - * @throws \InvalidArgumentException when the $document param is not an object + * @throws MongoDBInvalidArgumentException when the $document param is not an object */ public function remove($document) { if ( ! is_object($document)) { - throw new \InvalidArgumentException(gettype($document)); + throw MongoDBInvalidArgumentException::invalidObject('DocumentManager#remove()' , $document); } + $this->errorIfClosed(); + $this->unitOfWork->remove($document); } @@ -418,14 +422,16 @@ public function remove($document) * overriding any local changes that have not yet been persisted. * * @param object $document The document to refresh. - * @throws \InvalidArgumentException When the given $document param is not an object + * @throws MongoDBInvalidArgumentException When the given $document param is not an object */ public function refresh($document) { if ( ! is_object($document)) { - throw new \InvalidArgumentException(gettype($document)); + throw MongoDBInvalidArgumentException::invalidObject('DocumentManager#refresh()' , $document); } + $this->errorIfClosed(); + $this->unitOfWork->refresh($document); } @@ -437,13 +443,14 @@ public function refresh($document) * reference it. * * @param object $document The document to detach. - * @throws \InvalidArgumentException when the $document param is not an object + * @throws MongoDBInvalidArgumentException when the $document param is not an object */ public function detach($document) { if ( ! is_object($document)) { - throw new \InvalidArgumentException(gettype($document)); + throw MongoDBInvalidArgumentException::invalidObject('DocumentManager#detach()' , $document); } + $this->unitOfWork->detach($document); } @@ -454,15 +461,17 @@ public function detach($document) * * @param object $document The detached document to merge into the persistence context. * @throws LockException - * @throws \InvalidArgumentException if the $document param is not an object + * @throws MongoDBInvalidArgumentException if the $document param is not an object * @return object The managed copy of the document. */ public function merge($document) { if ( ! is_object($document)) { - throw new \InvalidArgumentException(gettype($document)); + throw MongoDBInvalidArgumentException::invalidObject('DocumentManager#merge()' , $document); } + $this->errorIfClosed(); + return $this->unitOfWork->merge($document); } @@ -472,13 +481,14 @@ public function merge($document) * @param object $document * @param int $lockMode * @param int $lockVersion - * @throws \InvalidArgumentException + * @throws MongoDBInvalidArgumentException */ public function lock($document, $lockMode, $lockVersion = null) { if ( ! is_object($document)) { - throw new \InvalidArgumentException(gettype($document)); + throw MongoDBInvalidArgumentException::invalidObject('DocumentManager#lock()' , $document); } + $this->unitOfWork->lock($document, $lockMode, $lockVersion); } @@ -486,13 +496,14 @@ public function lock($document, $lockMode, $lockVersion = null) * Releases a lock on the given document. * * @param object $document - * @throws \InvalidArgumentException if the $document param is not an object + * @throws MongoDBInvalidArgumentException if the $document param is not an object */ public function unlock($document) { if ( ! is_object($document)) { - throw new \InvalidArgumentException(gettype($document)); + throw MongoDBInvalidArgumentException::invalidObject('DocumentManager#unlock()' , $document); } + $this->unitOfWork->unlock($document); } @@ -531,14 +542,13 @@ public function getRepository($documentName) * * @param object $document * @param array $options Array of options to be used with batchInsert(), update() and remove() - * @throws \InvalidArgumentException + * @throws \Doctrine\ODM\MongoDB\OptimisticLockException If a version check on a document that + * makes use of optimistic locking fails. */ public function flush($document = null, array $options = array()) { - if (null !== $document && ! is_object($document) && ! is_array($document)) { - throw new \InvalidArgumentException(gettype($document)); - } $this->errorIfClosed(); + $this->unitOfWork->commit($document, $options); } @@ -647,14 +657,15 @@ public function close() * Determines whether a document instance is managed in this DocumentManager. * * @param object $document - * @throws \InvalidArgumentException When the $document param is not an object + * @throws MongoDBInvalidArgumentException When the $document param is not an object * @return boolean TRUE if this DocumentManager currently manages the given document, FALSE otherwise. */ public function contains($document) { if ( ! is_object($document)) { - throw new \InvalidArgumentException(gettype($document)); + throw MongoDBInvalidArgumentException::invalidObject('DocumentManager#contains()' , $document); } + return $this->unitOfWork->isScheduledForInsert($document) || $this->unitOfWork->isInIdentityMap($document) && ! $this->unitOfWork->isScheduledForDelete($document); @@ -676,22 +687,20 @@ public function getConfiguration() * @param mixed $document A document object * @param array $referenceMapping Mapping for the field the references the document * - * @throws \InvalidArgumentException + * @throws MongoDBInvalidArgumentException * @return array A DBRef array */ public function createDBRef($document, array $referenceMapping = null) { if ( ! is_object($document)) { - throw new \InvalidArgumentException('Cannot create a DBRef, the document is not an object'); + throw MongoDBInvalidArgumentException::invalidObject('DocumentManager#createDBRef()' , $document); } $class = $this->getClassMetadata(get_class($document)); $id = $this->unitOfWork->getDocumentIdentifier($document); - if (!$id) { - throw new \RuntimeException( - sprintf('Cannot create a DBRef without an identifier. UnitOfWork::getDocumentIdentifier() did not return an identifier for class %s', $class->name) - ); + if ( ! $id) { + throw MongoDBInvalidArgumentException::cannotCreateDocumentDBRef($document); } if ( ! empty($referenceMapping['simple'])) { diff --git a/lib/Doctrine/ODM/MongoDB/DocumentRepository.php b/lib/Doctrine/ODM/MongoDB/DocumentRepository.php index 979c4a02b3..4098b513c2 100644 --- a/lib/Doctrine/ODM/MongoDB/DocumentRepository.php +++ b/lib/Doctrine/ODM/MongoDB/DocumentRepository.php @@ -99,7 +99,7 @@ public function clear() * @param int $lockMode * @param int $lockVersion * @throws Mapping\MappingException - * @throws LockException + * @throws OptimisticLockException * @return object The document. */ public function find($id, $lockMode = LockMode::NONE, $lockVersion = null) @@ -136,7 +136,7 @@ public function find($id, $lockMode = LockMode::NONE, $lockVersion = null) if ($lockMode == LockMode::OPTIMISTIC) { if (!$this->class->isVersioned) { - throw LockException::notVersioned($this->documentName); + throw OptimisticLockException::notVersioned($this->documentName); } $document = $this->getDocumentPersister()->load($criteria); diff --git a/lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php b/lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php index 410fcfecfc..ffbb64d470 100644 --- a/lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php +++ b/lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php @@ -112,13 +112,13 @@ public function generateV4() * * @param string $namespace The GUID to seed with * @param string $salt The string to salt this new UUID with - * @throws \Exception when the provided namespace is invalid + * @throws \InvalidArgumentException when the provided namespace is invalid * @return string */ public function generateV5($namespace, $salt) { if ( ! $this->isValid($namespace)) { - throw new \Exception('Provided $namespace is invalid: ' . $namespace); + throw new \InvalidArgumentException('Provided $namespace is invalid: ' . $namespace); } // Get hexadecimal components of namespace diff --git a/lib/Doctrine/ODM/MongoDB/LockException.php b/lib/Doctrine/ODM/MongoDB/LockException.php index 0034c32ab0..01c1fd9884 100644 --- a/lib/Doctrine/ODM/MongoDB/LockException.php +++ b/lib/Doctrine/ODM/MongoDB/LockException.php @@ -29,46 +29,4 @@ */ class LockException extends MongoDBException { - private $document; - - public function __construct($msg, $document = null) - { - parent::__construct($msg); - $this->document = $document; - } - - /** - * Gets the document that caused the exception. - * - * @return object - */ - public function getDocument() - { - return $this->document; - } - - public static function lockFailed($document) - { - return new self('A lock failed on a document.', $document); - } - - public static function lockFailedVersionMissmatch($document, $expectedLockVersion, $actualLockVersion) - { - return new self('The optimistic lock failed, version ' . $expectedLockVersion . ' was expected, but is actually '.$actualLockVersion, $document); - } - - public static function notVersioned($documentName) - { - return new self('Document ' . $documentName . ' is not versioned.'); - } - - public static function invalidLockFieldType($type) - { - return new self('Invalid lock field type '.$type.'. Lock field must be int.'); - } - - public static function invalidVersionFieldType($type) - { - return new self('Invalid version field type '.$type.'. Version field must be int or date.'); - } } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php index d11d4506bf..b867e28d64 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php @@ -269,7 +269,7 @@ private function completeIdGeneratorMapping(ClassMetadataInfo $class) case ClassMetadata::GENERATOR_TYPE_NONE; break; default: - throw new MappingException("Unknown generator type: " . $class->generatorType); + throw MappingException::unknownIdGeneratorType($class->generatorType); } } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php index b2ee869f93..396775a5af 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php @@ -19,7 +19,6 @@ namespace Doctrine\ODM\MongoDB\Mapping; -use Doctrine\ODM\MongoDB\LockException; use Doctrine\ODM\MongoDB\Mapping\MappingException; use Doctrine\ODM\MongoDB\Proxy\Proxy; use Doctrine\ODM\MongoDB\Types\Type; @@ -1668,12 +1667,12 @@ public function usesIdGenerator() * value to use depending on the column type. * * @param array $mapping The version field mapping array - * @throws \Doctrine\ODM\MongoDB\LockException + * @throws \Doctrine\ODM\MongoDB\MappingException */ public function setVersionMapping(array &$mapping) { if ($mapping['type'] !== 'int' && $mapping['type'] !== 'date') { - throw LockException::invalidVersionFieldType($mapping['type']); + throw MappingException::invalidVersionFieldType($mapping['type']); } $this->isVersioned = true; $this->versionField = $mapping['fieldName']; @@ -1715,11 +1714,12 @@ public function markReadOnly() * value to use depending on the column type. * * @param array $mapping The version field mapping array + * @throws \Doctrine\ODM\MongoDB\MappingException */ public function setLockMapping(array &$mapping) { if ($mapping['type'] !== 'int') { - throw LockException::invalidLockFieldType($mapping['type']); + throw MappingException::invalidLockFieldType($mapping['type']); } $this->isLockable = true; $this->lockField = $mapping['fieldName']; diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php b/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php index d7901e6af3..5b8ddb0e65 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php @@ -151,4 +151,19 @@ public static function cascadeOnEmbeddedNotAllowed($className, $fieldName) { return new self("Cascade on $className::$fieldName is not allowed."); } + + public static function invalidLockFieldType($type) + { + return new self('Invalid lock field type '.$type.'. Lock field must be int.'); + } + + public static function invalidVersionFieldType($type) + { + return new self('Invalid version field type '.$type.'. Version field must be int or date.'); + } + + public static function unknownIdGeneratorType($type) + { + return new self("Unknown generator type: " . $type); + } } diff --git a/lib/Doctrine/ODM/MongoDB/MongoDBInvalidArgumentException.php b/lib/Doctrine/ODM/MongoDB/MongoDBInvalidArgumentException.php new file mode 100644 index 0000000000..5881964e16 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/MongoDBInvalidArgumentException.php @@ -0,0 +1,203 @@ +. + */ + +namespace Doctrine\ODM\MongoDB; + +/** + * Contains exception messages for all invalid lifecycle state exceptions inside UnitOfWork + * + * @author Benjamin Eberlei + * @author Jonathan H. Wage + */ +class MongoDBInvalidArgumentException extends \InvalidArgumentException +{ + /** + * @param object $document + * + * @return MongoDBInvalidArgumentException + */ + static public function scheduleInsertForManagedDocument($document) + { + return new self("A managed+dirty document " . self::objToStr($document) . " can not be scheduled for insertion."); + } + + /** + * @param object $document + * + * @return MongoDBInvalidArgumentException + */ + static public function scheduleInsertForRemovedDocument($document) + { + return new self("Removed document " . self::objToStr($document) . " can not be scheduled for insertion."); + } + + /** + * @param object $document + * + * @return MongoDBInvalidArgumentException + */ + static public function scheduleInsertTwice($document) + { + return new self("Document " . self::objToStr($document) . " can not be scheduled for insertion twice."); + } + + /** + * @param array $assoc + * @param object $entry + * + * @return MongoDBInvalidArgumentException + */ + static public function newDocumentFoundThroughRelationship(array $assoc, $entry) + { + return new self("A new document was found through the relationship '" + . $assoc['sourceDocument'] . "#" . $assoc['fieldName'] . "' that was not" + . " configured to cascade persist operations for document: " . self::objToStr($entry) . "." + . " To solve this issue: Either explicitly call DocumentManager#persist()" + . " on this unknown document or configure cascade persist " + . " this association in the mapping for example @ReferenceOne(..,cascade={\"persist\"})." + . (method_exists($entry, '__toString') ? + "": + " If you cannot find out which document causes the problem" + ." implement '" . $assoc['targetDocument'] . "#__toString()' to get a clue.")); + } + + /** + * @param array $assoc + * @param object $entry + * + * @return MongoDBInvalidArgumentException + */ + static public function detachedDocumentFoundThroughRelationship(array $assoc, $entry) + { + return new self("A detached document of type " . $assoc['targetDocument'] . " (" . self::objToStr($entry) . ") " + . " was found through the relationship '" . $assoc['sourceDocument'] . "#" . $assoc['fieldName'] . "' " + . "during cascading a persist operation."); + } + + /** + * @param object $document + * + * @return MongoDBInvalidArgumentException + */ + static public function documentNotManaged($document) + { + return new self("Document " . self::objToStr($document) . " is not managed. A document is managed if its fetched " . + "from the database or registered as new through DocumentManager#persist"); + } + + /** + * @param object $document + * @param string $operation + * + * @return MongoDBInvalidArgumentException + */ + static public function documentHasNoIdentity($document, $operation) + { + return new self("Document has no identity, therefore " . $operation ." cannot be performed. " . self::objToStr($document)); + } + + /** + * @param object $document + * @param string $operation + * + * @return MongoDBInvalidArgumentException + */ + static public function documentIsRemoved($document, $operation) + { + return new self("Document is removed, therefore " . $operation ." cannot be performed. " . self::objToStr($document)); + } + + /** + * @param object $document + * @param string $operation + * + * @return MongoDBInvalidArgumentException + */ + static public function detachedDocumentCannot($document, $operation) + { + return new self("A detached document was found during " . $operation . " " . self::objToStr($document)); + } + + /** + * @param string $context + * @param mixed $given + * @param int $parameterIndex + * + * @return MongoDBInvalidArgumentException + */ + public static function invalidObject($context, $given, $parameterIndex = 1) + { + return new self($context . ' expects parameter ' . $parameterIndex . + ' to be a document object, '. gettype($given) . ' given.'); + } + + /** + * @param object $document + * + * @return MongoDBInvalidArgumentException + */ + public static function invalidSingleDocumentFlush($document) + { + return new self("Document has to be managed or scheduled for removal for single computation " . self::objToStr($document)); + } + + /** + * @param object $document + * + * @return MongoDBInvalidArgumentException + */ + public static function dirtyDocumentScheduledForInsert($document) + { + return new self("Dirty document cannot be scheduled for insertion."); + } + + /** + * @param object $document + * + * @return MongoDBInvalidArgumentException + */ + public static function noIdentifier($className) + { + return new self(sprintf('Class "%s" does not have an identifier.', $className)); + } + + /** + * @param object $document + * + * @return MongoDBInvalidArgumentException + */ + public static function cannotCreateDocumentDBRef($document) + { + return new self( + sprintf('Cannot create a DBRef without an identifier. UnitOfWork::getDocumentIdentifier() did not return an identifier for %s', self::objToStr($document)) + ); + } + + /** + * 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); + } +} diff --git a/lib/Doctrine/ODM/MongoDB/OptimisticLockException.php b/lib/Doctrine/ODM/MongoDB/OptimisticLockException.php new file mode 100644 index 0000000000..f9dedacd95 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/OptimisticLockException.php @@ -0,0 +1,90 @@ +. + */ + +namespace Doctrine\ODM\MongoDB; + +/** + * An OptimisticLockException is thrown when a version check on an object + * that uses optimistic locking through a version field fails. + * + * @author Roman Borschel + * @author Benjamin Eberlei + * @author Jonathan H. Wage + */ +class OptimisticLockException extends LockException +{ + /** + * @var object|null + */ + private $document; + + /** + * @param string $msg + * @param object $document + */ + public function __construct($msg, $document) + { + parent::__construct($msg); + $this->document = $document; + } + + /** + * Gets the document that caused the exception. + * + * @return object|null + */ + public function getDocument() + { + return $this->document; + } + + /** + * @param object $document + * + * @return OptimisticLockException + */ + public static function lockFailed($document) + { + return new self("The optimistic lock on a document failed.", $document); + } + + /** + * @param object $document + * @param int $expectedLockVersion + * @param int $actualLockVersion + * + * @return OptimisticLockException + */ + public static function lockFailedVersionMismatch($document, $expectedLockVersion, $actualLockVersion) + { + $expectedLockVersion = ($expectedLockVersion instanceof \DateTime) ? $expectedLockVersion->getTimestamp() : $expectedLockVersion; + $actualLockVersion = ($actualLockVersion instanceof \DateTime) ? $actualLockVersion->getTimestamp() : $actualLockVersion; + return new self("The optimistic lock failed, version " . $expectedLockVersion . " was expected, but is actually ".$actualLockVersion, $document); + } + + /** + * @param string $documentName + * + * @return OptimisticLockException + */ + public static function notVersioned($documentName) + { + return new self("Cannot obtain optimistic lock on unversioned document " . $documentName, null); + } +} diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php index b5c33e8273..6795dcf8c1 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php @@ -24,7 +24,8 @@ use Doctrine\ODM\MongoDB\Cursor; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory; -use Doctrine\ODM\MongoDB\LockException; +use Doctrine\ODM\MongoDB\OptimisticLockException; +use Doctrine\ODM\MongoDB\PessimisticLockException; use Doctrine\ODM\MongoDB\LockMode; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\PersistentCollection; @@ -301,7 +302,7 @@ private function executeUpsert(array $data, array $options) * * @param object $document * @param array $options Array of options to be used with update() - * @throws \Doctrine\ODM\MongoDB\LockException + * @throws \Doctrine\ODM\MongoDB\OptimisticLockException */ public function update($document, array $options = array()) { @@ -349,7 +350,7 @@ public function update($document, array $options = array()) $result = $this->collection->update($query, $update, $options); if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) { - throw LockException::lockFailed($document); + throw OptimisticLockException::lockFailed($document); } } } @@ -359,7 +360,7 @@ public function update($document, array $options = array()) * * @param mixed $document * @param array $options Array of options to be used with remove() - * @throws \Doctrine\ODM\MongoDB\LockException + * @throws \Doctrine\ODM\MongoDB\OptimisticLockException */ public function delete($document, array $options = array()) { @@ -374,7 +375,7 @@ public function delete($document, array $options = array()) $result = $this->collection->remove($query, $options); if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) { - throw LockException::lockFailed($document); + throw OptimisticLockException::lockFailed($document); } } @@ -403,7 +404,7 @@ public function refresh($id, $document) * @param array $hints Hints for document creation * @param integer $lockMode * @param array $sort Sort array for Cursor::sort() - * @throws \Doctrine\ODM\MongoDB\LockException + * @throws \Doctrine\ODM\MongoDB\PessimisticLockException * @return object|null The loaded and managed document instance or null if no document was found * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? */ @@ -429,7 +430,7 @@ public function load($criteria, $document = null, array $hints = array(), $lockM if ($this->class->isLockable) { $lockMapping = $this->class->fieldMappings[$this->class->lockField]; if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) { - throw LockException::lockFailed($result); + throw PessimisticLockException::lockFailed(); } } diff --git a/lib/Doctrine/ODM/MongoDB/PessimisticLockException.php b/lib/Doctrine/ODM/MongoDB/PessimisticLockException.php new file mode 100644 index 0000000000..50daa09ada --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/PessimisticLockException.php @@ -0,0 +1,38 @@ +. + */ + +namespace Doctrine\ODM\MongoDB; + +/** + * Pessimistic Lock Exception + * + * @author Benjamin Eberlei + * @author Roman Borschel + * @author Jonathan H. Wage + */ +class PessimisticLockException extends LockException +{ + /** + * @return PessimisticLockException + */ + public static function lockFailed() + { + return new self("The pessimistic lock failed."); + } +} diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index b36882545c..6f4e801428 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -524,7 +524,7 @@ private function computeScheduleUpsertsChangeSets() * * @param object $document * - * @throws \InvalidArgumentException If the document is not STATE_MANAGED + * @throws MongoDBInvalidArgumentException If the document is not STATE_MANAGED * * @return void */ @@ -533,7 +533,7 @@ private function computeSingleDocumentChangeSet($document) $state = $this->getDocumentState($document); if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) { - throw new \InvalidArgumentException("Document has to be managed or scheduled for removal for single computation " . self::objToStr($document)); + throw MongoDBInvalidArgumentException::invalidSingleDocumentFlush($document); } $class = $this->dm->getClassMetadata(get_class($document)); @@ -915,7 +915,7 @@ public function computeChangeSets() * @param array $assoc The association mapping. * @param mixed $value The value of the association. * - * @throws \InvalidArgumentException + * @throws MongoDBInvalidArgumentException * * @return void */ @@ -966,10 +966,7 @@ private function computeAssociationChanges($parentDocument, array $assoc, $value switch ($state) { case self::STATE_NEW: if ( ! $assoc['isCascadePersist']) { - throw new \InvalidArgumentException("A new document was found through a relationship that was not" - . " configured to cascade persist operations: " . self::objToStr($entry) . "." - . " Explicitly persist the new document or configure cascading persist operations" - . " on the relationship."); + throw MongoDBInvalidArgumentException::newDocumentFoundThroughRelationship($assoc, $entry); } $this->persistNew($targetClass, $entry); @@ -994,8 +991,7 @@ private function computeAssociationChanges($parentDocument, array $assoc, $value case self::STATE_DETACHED: // Can actually not happen right now as we assume STATE_NEW. - throw new \InvalidArgumentException("A detached document was found through a " - . "relationship during cascading a persist operation."); + throw MongoDBInvalidArgumentException::detachedDocumentFoundThroughRelationship($assoc, $entry); break; default: @@ -1018,14 +1014,14 @@ private function computeAssociationChanges($parentDocument, array $assoc, $value * @ignore * @param ClassMetadata $class The class descriptor of the document. * @param object $document The document for which to (re)calculate the change set. - * @throws \InvalidArgumentException If the passed document is not MANAGED. + * @throws MongoDBInvalidArgumentException If the passed document is not MANAGED. */ public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document) { $oid = spl_object_hash($document); if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) { - throw new \InvalidArgumentException('Document must be managed.'); + throw MongoDBInvalidArgumentException::documentNotManaged($document); } if ( ! $class->isInheritanceTypeNone()) { @@ -1428,20 +1424,25 @@ private function getCommitOrder() * * @param ClassMetadata $class * @param object $document The document to schedule for insertion. - * @throws \InvalidArgumentException + * @throws MongoDBInvalidArgumentException */ public function scheduleForInsert(ClassMetadata $class, $document) { $oid = spl_object_hash($document); if (isset($this->documentUpdates[$oid])) { - throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion."); + throw MongoDBInvalidArgumentException::dirtyDocumentScheduledForInsert($document); } + if (isset($this->documentDeletions[$oid])) { - throw new \InvalidArgumentException("Removed document can not be scheduled for insertion."); + throw MongoDBInvalidArgumentException::scheduleInsertForRemovedDocument($document); } - if (isset($this->documentInsertions[$oid])) { - throw new \InvalidArgumentException("Document can not be scheduled for insertion twice."); + if (isset($this->originalDocumentData[$oid]) && ! isset($this->documentInsertions[$oid])) { + throw MongoDBInvalidArgumentException::scheduleInsertForManagedDocument($document); + } + + if (isset($this->entityInsertions[$oid])) { + throw MongoDBInvalidArgumentException::scheduleInsertTwice($document); } $this->documentInsertions[$oid] = $document; @@ -1457,20 +1458,25 @@ public function scheduleForInsert(ClassMetadata $class, $document) * * @param ClassMetadata $class * @param object $document The document to schedule for upsert. - * @throws \InvalidArgumentException + * @throws MongoDBInvalidArgumentException */ public function scheduleForUpsert(ClassMetadata $class, $document) { $oid = spl_object_hash($document); if (isset($this->documentUpdates[$oid])) { - throw new \InvalidArgumentException("Dirty document can not be scheduled for upsert."); + throw MongoDBInvalidArgumentException::dirtyDocumentScheduledForInsert($document); } + if (isset($this->documentDeletions[$oid])) { - throw new \InvalidArgumentException("Removed document can not be scheduled for upsert."); + throw MongoDBInvalidArgumentException::scheduleInsertForRemovedDocument($document); + } + if (isset($this->originalDocumentData[$oid]) && ! isset($this->documentInsertions[$oid])) { + throw MongoDBInvalidArgumentException::scheduleInsertForManagedDocument($document); } + if (isset($this->documentUpserts[$oid])) { - throw new \InvalidArgumentException("Document can not be scheduled for upsert twice."); + throw MongoDBInvalidArgumentException::scheduleUpsertTwice($document); } $this->documentUpserts[$oid] = $document; @@ -1504,16 +1510,18 @@ public function isScheduledForUpsert($document) * Schedules a document for being updated. * * @param object $document The document to schedule for being updated. - * @throws \InvalidArgumentException + * @throws MongoDBInvalidArgumentException */ public function scheduleForUpdate($document) { $oid = spl_object_hash($document); + if ( ! isset($this->documentIdentifiers[$oid])) { - throw new \InvalidArgumentException("Document has no identity."); + throw MongoDBInvalidArgumentException::documentHasNoIdentity($document, "scheduling for update"); } + if (isset($this->documentDeletions[$oid])) { - throw new \InvalidArgumentException("Document is removed."); + throw MongoDBInvalidArgumentException::documentIsRemoved($document, "schedule for update"); } if ( ! isset($this->documentUpdates[$oid]) && ! isset($this->documentInsertions[$oid]) && ! isset($this->documentUpserts[$oid])) { @@ -1732,7 +1740,7 @@ public function getDocumentState($document, $assume = null) * * @ignore * @param object $document - * @throws \InvalidArgumentException + * @throws MongoDBInvalidArgumentException * @return boolean */ public function removeFromIdentityMap($document) @@ -1775,7 +1783,7 @@ public function removeFromIdentityMap($document) public function getById($id, ClassMetadata $class) { if ( ! $class->identifier) { - throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name)); + throw MongoDBInvalidArgumentException::noIdentifier($class->name); } $serializedId = serialize($class->getDatabaseIdentifierValue($id)); @@ -1797,7 +1805,7 @@ public function getById($id, ClassMetadata $class) public function tryGetById($id, ClassMetadata $class) { if ( ! $class->identifier) { - throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name)); + throw MongoDBInvalidArgumentException::noIdentifier($class->name); } $serializedId = serialize($class->getDatabaseIdentifierValue($id)); @@ -1883,7 +1891,7 @@ public function persist($document) * * @param object $document The document to persist. * @param array $visited The already visited documents. - * @throws \InvalidArgumentException + * @throws MongoDBInvalidArgumentException * @throws MongoDBException */ private function doPersist($document, array &$visited) @@ -1920,11 +1928,11 @@ private function doPersist($document, array &$visited) break; case self::STATE_DETACHED: - throw new \InvalidArgumentException("Behavior of persist() for a detached document is not yet defined."); + throw ORMInvalidArgumentException::detachedDocumentCannot($document, "persisted"); break; default: - throw MongoDBException::invalidDocumentState($documentState); + throw new \UnexpectedValueException("Unexpected document state: $documentState." . self::objToStr($document)); } $this->cascadePersist($document, $visited); @@ -2015,7 +2023,7 @@ public function merge($document) * @return object The managed copy of the document. * * @throws InvalidArgumentException If the document instance is NEW. - * @throws LockException If the document uses optimistic locking through a + * @throws OptimisticLockException If the document uses optimistic locking through a * version attribute and the version check against the * managed copy fails. */ @@ -2056,7 +2064,7 @@ private function doMerge($document, array &$visited, $prevManagedCopy = null, $a if ($managedCopy) { // We have the document in memory already, just make sure it is not removed. if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) { - throw new \InvalidArgumentException('Removed document detected during merge. Cannot merge with a removed entity.'); + throw MongoDBInvalidArgumentException::documentIsRemoved($managedCopy, "merge"); } } else { // We need to fetch the managed copy in order to merge. @@ -2081,7 +2089,7 @@ private function doMerge($document, array &$visited, $prevManagedCopy = null, $a // Throw exception if versions don't match if ($managedCopyVersion != $documentVersion) { - throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion); + throw OptimisticLockException::lockFailedVersionMismatch($document, $documentVersion, $managedCopyVersion); } } @@ -2243,7 +2251,7 @@ private function doDetach($document, array &$visited) * any local, unpersisted changes. * * @param object $document The document to refresh. - * @throws \InvalidArgumentException If the document is not MANAGED. + * @throws MongoDBInvalidArgumentException If the document is not MANAGED. */ public function refresh($document) { @@ -2256,7 +2264,7 @@ public function refresh($document) * * @param object $document The document to refresh. * @param array $visited The already visited documents during cascades. - * @throws \InvalidArgumentException If the document is not MANAGED. + * @throws MongoDBInvalidArgumentException If the document is not MANAGED. */ private function doRefresh($document, array &$visited) { @@ -2275,7 +2283,7 @@ private function doRefresh($document, array &$visited) $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]); $this->getDocumentPersister($class->name)->refresh($id, $document); } else { - throw new \InvalidArgumentException("Document is not MANAGED."); + throw MongoDBInvalidArgumentException::documentNotManaged($document); } } @@ -2452,13 +2460,13 @@ function ($assoc) { return $assoc['isCascadeRemove']; } * @param object $document * @param int $lockMode * @param int $lockVersion - * @throws LockException - * @throws \InvalidArgumentException + * @throws OptimisticLockException + * @throws MongoDBInvalidArgumentException */ public function lock($document, $lockMode, $lockVersion = null) { if ($this->getDocumentState($document) != self::STATE_MANAGED) { - throw new \InvalidArgumentException("Document is not MANAGED."); + throw MongoDBInvalidArgumentException::documentNotManaged($document); } $documentName = get_class($document); @@ -2466,13 +2474,13 @@ public function lock($document, $lockMode, $lockVersion = null) if ($lockMode == LockMode::OPTIMISTIC) { if ( ! $class->isVersioned) { - throw LockException::notVersioned($documentName); + throw OptimisticLockException::notVersioned($documentName); } if ($lockVersion != null) { $documentVersion = $class->reflFields[$class->versionField]->getValue($document); if ($documentVersion != $lockVersion) { - throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion); + throw OptimisticLockException::lockFailedVersionMismatch($document, $lockVersion, $documentVersion); } } } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) { @@ -2484,13 +2492,14 @@ public function lock($document, $lockMode, $lockVersion = null) * Releases a lock on the given document. * * @param object $document - * @throws \InvalidArgumentException + * @throws MongoDBInvalidArgumentException */ public function unlock($document) { if ($this->getDocumentState($document) != self::STATE_MANAGED) { - throw new \InvalidArgumentException("Document is not MANAGED."); + throw MongoDBInvalidArgumentException::documentNotManaged($document); } + $documentName = get_class($document); $this->getDocumentPersister($documentName)->unlock($document); } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/LockTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/LockTest.php index 563cace06e..549e8aaa64 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/LockTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/LockTest.php @@ -123,7 +123,7 @@ public function testLockUnversionedDocumentThrowsException() $this->dm->persist($user); $this->dm->flush(); - $this->setExpectedException('Doctrine\ODM\MongoDB\LockException', 'Document Documents\User is not versioned.'); + $this->setExpectedException('Doctrine\ODM\MongoDB\OptimisticLockException', 'Cannot obtain optimistic lock on unversioned document Documents\User'); $this->dm->lock($user, LockMode::OPTIMISTIC); } @@ -132,7 +132,7 @@ public function testLockUnmanagedDocumentThrowsException() { $article = new LockInt(); - $this->setExpectedException('InvalidArgumentException', 'Document is not MANAGED.'); + $this->setExpectedException('Doctrine\ODM\MongoDB\MongoDBInvalidArgumentException'); $this->dm->lock($article, LockMode::OPTIMISTIC, $article->version + 1); } @@ -311,13 +311,13 @@ public function testPessimisticWriteLockFunctional() public function testInvalidLockDocument() { - $this->setExpectedException('Doctrine\ODM\MongoDB\MongoDBException', 'Invalid lock field type string. Lock field must be int.'); + $this->setExpectedException('Doctrine\ODM\MongoDB\Mapping\MappingException', 'Invalid lock field type string. Lock field must be int.'); $this->dm->getClassMetadata(__NAMESPACE__.'\InvalidLockDocument'); } public function testInvalidVersionDocument() { - $this->setExpectedException('Doctrine\ODM\MongoDB\MongoDBException', 'Invalid version field type string. Version field must be int or date.'); + $this->setExpectedException('Doctrine\ODM\MongoDB\Mapping\MappingException', 'Invalid version field type string. Version field must be int or date.'); $this->dm->getClassMetadata(__NAMESPACE__.'\InvalidVersionDocument'); } }