Location: PHPKode > projects > MongoDB Object Document Mapper > lib/Doctrine/ODM/MongoDB/UnitOfWork.php
<?php
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.doctrine-project.org>.
 */

namespace Doctrine\ODM\MongoDB;

use Doctrine\ODM\MongoDB\DocumentManager,
    Doctrine\ODM\MongoDB\Internal\CommitOrderCalculator,
    Doctrine\ODM\MongoDB\Mapping\ClassMetadata,
    Doctrine\ODM\MongoDB\Proxy\Proxy,
    Doctrine\ODM\MongoDB\Mapping\Types\Type,
    Doctrine\ODM\MongoDB\Event\LifecycleEventArgs,
    Doctrine\ODM\MongoDB\Event\PreLoadEventArgs,
    Doctrine\ODM\MongoDB\PersistentCollection,
    Doctrine\Common\Collections\Collection,
    Doctrine\Common\NotifyPropertyChanged,
    Doctrine\Common\PropertyChangedListener,
    Doctrine\Common\Collections\ArrayCollection;

/**
 * The UnitOfWork is responsible for tracking changes to objects during an
 * "object-level" transaction and for writing out changes to the database
 * in the correct order.
 *
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @since       1.0
 * @author      Jonathan H. Wage <hide@address.com>
 * @author      Roman Borschel <hide@address.com>
 */
class UnitOfWork implements PropertyChangedListener
{
    /**
     * An document is in MANAGED state when its persistence is managed by an DocumentManager.
     */
    const STATE_MANAGED = 1;

    /**
     * An document is new if it has just been instantiated (i.e. using the "new" operator)
     * and is not (yet) managed by an DocumentManager.
     */
    const STATE_NEW = 2;

    /**
     * A detached document is an instance with a persistent identity that is not
     * (or no longer) associated with an DocumentManager (and a UnitOfWork).
     */
    const STATE_DETACHED = 3;

    /**
     * A removed document instance is an instance with a persistent identity,
     * associated with an DocumentManager, whose persistent state has been
     * deleted (or is scheduled for deletion).
     */
    const STATE_REMOVED = 4;

    /**
     * The identity map that holds references to all managed documents that have
     * an identity. The documents are grouped by their class name.
     * Since all classes in a hierarchy must share the same identifier set,
     * we always take the root class name of the hierarchy.
     *
     * @var array
     */
    private $identityMap = array();

    /**
     * Map of all identifiers of managed documents.
     * Keys are object ids (spl_object_hash).
     *
     * @var array
     */
    private $documentIdentifiers = array();

    /**
     * Map of the original document data of managed documents.
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
     * at commit time.
     *
     * @var array
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
     *           A value will only really be copied if the value in the document is modified
     *           by the user.
     */
    private $originalDocumentData = array();

    /**
     * Map of document changes. Keys are object ids (spl_object_hash).
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
     *
     * @var array
     */
    private $documentChangeSets = array();

    /**
     * The (cached) states of any known documents.
     * Keys are object ids (spl_object_hash).
     *
     * @var array
     */
    private $documentStates = array();

    /**
     * Map of documents that are scheduled for dirty checking at commit time.
     * This is only used for documents with a change tracking policy of DEFERRED_EXPLICIT.
     * Keys are object ids (spl_object_hash).
     *
     * @var array
     * @todo rename: scheduledForSynchronization
     */
    private $scheduledForDirtyCheck = array();

    /**
     * A list of all pending document insertions.
     *
     * @var array
     */
    private $documentInsertions = array();

    /**
     * A list of all pending document updates.
     *
     * @var array
     */
    private $documentUpdates = array();

    /**
     * A list of all pending document deletions.
     *
     * @var array
     */
    private $documentDeletions = array();

    /**
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
     * At the end of the UnitOfWork all these collections will make new snapshots
     * of their data.
     *
     * @var array
     */
    private $visitedCollections = array();

    /**
     * The DocumentManager that "owns" this UnitOfWork instance.
     *
     * @var Doctrine\ODM\MongoDB\DocumentManager
     */
    private $dm;

    /**
     * The calculator used to calculate the order in which changes to
     * documents need to be written to the database.
     *
     * @var Doctrine\ODM\MongoDB\Internal\CommitOrderCalculator
     */
    private $commitOrderCalculator;

    /**
     * The EventManager used for dispatching events.
     *
     * @var EventManager
     */
    private $evm;

    /**
     * Embedded documents that are scheduled for removal.
     *
     * @var array
     */
    private $embeddedRemovals = array();

    /**
     * The Hydrator used for hydrating array Mongo documents to Doctrine object documents.
     *
     * @var string
     */
    private $hydrator;

    /**
     * The document persister instances used to persist document instances.
     *
     * @var array
     */
    private $persisters = array();

    /**
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
     *
     * @param Doctrine\ODM\MongoDB\DocumentManager $dm
     */
    public function __construct(DocumentManager $dm)
    {
        $this->dm = $dm;
        $this->evm = $dm->getEventManager();
        $this->hydrator = $dm->getHydrator();
    }

    /**
     * Get the document persister instance for the given document name
     *
     * @param string $documentName
     * @return BasicDocumentPersister
     */
    public function getDocumentPersister($documentName)
    {
        if ( ! isset($this->persisters[$documentName])) {
            $class = $this->dm->getClassMetadata($documentName);
            $this->persisters[$documentName] = new Persisters\BasicDocumentPersister($this->dm, $class);
        }
        return $this->persisters[$documentName];
    }

    /**
     * Set the document persister instance to use for the given document name
     *
     * @param string $documentName
     * @param BasicDocumentPersister $persister
     */
    public function setDocumentPersister($documentName, Persisters\BasicDocumentPersister $persister)
    {
        $this->persisters[$documentName] = $persister;
    }

    /**
     * Commits the UnitOfWork, executing all operations that have been postponed
     * up to this point. The state of all managed documents will be synchronized with
     * the database.
     *
     * The operations are executed in the following order:
     *
     * 1) All document insertions
     * 2) All document updates
     * 3) All document deletions
     *
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
     */
    public function commit(array $options = array())
    {
        // Compute changes done since last commit.
        $this->computeChangeSets();

        if ( ! ($this->documentInsertions ||
                $this->documentDeletions ||
                $this->documentUpdates ||
                $this->embeddedRemovals)) {
            return; // Nothing to do.
        }

        if ($this->embeddedRemovals) {
            foreach ($this->embeddedRemovals as $removal) {
                $this->remove($removal);
            }
        }

        // Raise onFlush
        if ($this->evm->hasListeners(ODMEvents::onFlush)) {
            $this->evm->dispatchEvent(ODMEvents::onFlush, new Event\OnFlushEventArgs($this->dm));
        }

        // Now we need a commit order to maintain referential integrity
        $commitOrder = $this->getCommitOrder();

        if ($this->documentInsertions) {
            foreach ($commitOrder as $class) {
                if ($class->isEmbeddedDocument) {
                    continue;
                }
                $this->executeInserts($class, $options);
            }
            foreach ($commitOrder as $class) {
                if ($class->isEmbeddedDocument) {
                    continue;
                }
                $this->executeReferenceUpdates($class, $options);
            }
        }

        if ($this->documentUpdates) {
            foreach ($commitOrder as $class) {
                $this->executeUpdates($class, $options);
            }
        }

        // Document deletions come last and need to be in reverse commit order
        if ($this->documentDeletions) {
            for ($count = count($commitOrder), $i = $count - 1; $i >= 0; --$i) {
                $this->executeDeletions($commitOrder[$i], $options);
            }
        }

        // Take new snapshots from visited collections
        foreach ($this->visitedCollections as $coll) {
            $coll->takeSnapshot();
        }

        // Clear up
        $this->documentInsertions =
        $this->documentUpdates =
        $this->documentDeletions =
        $this->documentChangeSets =
        $this->visitedCollections =
        $this->scheduledForDirtyCheck =
        $this->embeddedRemovals = array();
    }

    /**
     * Executes reference updates
     *
     * @param Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
     * @param array $options Array of options to be used for update()
     */
    private function executeReferenceUpdates(ClassMetadata $class, array $options = array())
    {
        $className = $class->name;
        $persister = $this->getDocumentPersister($className);
        $persister->executeReferenceUpdates($options);
    }

    /**
     * Gets the changeset for an document.
     *
     * @return array
     */
    public function getDocumentChangeSet($document)
    {
        $oid = spl_object_hash($document);
        if (isset($this->documentChangeSets[$oid])) {
            return $this->documentChangeSets[$oid];
        }
        return array();
    }

    /**
     * Get a documents actual data, flattening all the objects to arrays.
     *
     * @param object $document
     * @return array
     */
    public function getDocumentActualData($document)
    {
        $class = $this->dm->getClassMetadata(get_class($document));
        $actualData = array();
        foreach ($class->reflFields as $name => $refProp) {
            $mapping = $class->fieldMappings[$name];

            // Skip identifiers if custom ones are not allowed
            if ($class->isIdentifier($name) && ! $class->getAllowCustomID()) {
                continue;
            }

            $value = $class->getFieldValue($document, $mapping['fieldName']);

            // Skip MongoGridFSFile instances as we never store these objects
            if ($value instanceof \MongoGridFSFile) {
                continue;
            }

            if (($class->isCollectionValuedReference($name) || $class->isCollectionValuedEmbed($name))
                    && $value !== null && ! ($value instanceof PersistentCollection)) {
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
                if ( ! $value instanceof Collection) {
                    $value = new ArrayCollection($value);
                }

                // Inject PersistentCollection
                $value = new PersistentCollection($value, $this->dm);
                $value->setOwner($document, $mapping);
                $value->setDirty( ! $value->isEmpty());
                $class->reflFields[$name]->setValue($document, $value);
            }

            // We need to flatten the embedded documents so they are just arrays of
            // data instead of the actual objects. This is necessary to maintain all the old
            // values.
            if ($class->isCollectionValuedEmbed($name) && $value) {
                $embeddedDocuments = $value;
                $actualData[$name] = array();
                foreach ($embeddedDocuments as $key => $embeddedDocument) {
                    $actualData[$name][$key] = $this->getDocumentActualData($embeddedDocument);
                }
            } elseif ($class->isSingleValuedEmbed($name) && is_object($value)) {
                $embeddedDocument = $value;
                $actualData[$name] = $this->getDocumentActualData($embeddedDocument);
            } else {
                $actualData[$name] = $value;
            }
        }
        return $actualData;
    }

    /**
     * Computes the changes that happened to a single document.
     *
     * Modifies/populates the following properties:
     *
     * {@link originalDocumentData}
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
     * then it was not fetched from the database and therefore we have no original
     * document data yet. All of the current document data is stored as the original document data.
     *
     * {@link documentChangeSets}
     * The changes detected on all properties of the document are stored there.
     * A change is a tuple array where the first entry is the old value and the second
     * entry is the new value of the property. Changesets are used by persisters
     * to INSERT/UPDATE the persistent document state.
     *
     * {@link documentUpdates}
     * If the document is already fully MANAGED (has been fetched from the database before)
     * and any changes to its properties are detected, then a reference to the document is stored
     * there to mark it for an update.
     *
     * @param object $parentDocument The top most parent document of the document we are computing.
     * @param ClassMetadata $class The class descriptor of the document.
     * @param object $document The document for which to compute the changes.
     */
    public function computeChangeSet($parentDocument, Mapping\ClassMetadata $class, $document)
    {
        if ( ! $class->isInheritanceTypeNone()) {
            $class = $this->dm->getClassMetadata(get_class($document));
        }

        $oid = spl_object_hash($document);
        $actualData = $this->getDocumentActualData($document);
        if ( ! isset($this->originalDocumentData[$oid])) {
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
            // These result in an INSERT.
            $this->originalDocumentData[$oid] = $actualData;
            $changeSet = array();
            foreach ($actualData as $propName => $actualValue) {
                $changeSet[$propName] = array(null, $actualValue);
            }
            $this->documentChangeSets[$oid] = $changeSet;
        } else {
            // Document is "fully" MANAGED: it was already fully persisted before
            // and we have a copy of the original data
            $originalData = $this->originalDocumentData[$oid];
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
            $changeSet = $isChangeTrackingNotify ? $this->documentChangeSets[$oid] : array();

            foreach ($actualData as $propName => $actualValue) {
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
                if ($actualValue instanceof PersistentCollection || $actualValue instanceof ArrayCollection) {
                    if ($actualValue instanceof PersistentCollection && $actualValue->isDirty()) {
                        $this->visitedCollections[] = $actualValue;
                    }
                    $actualValue = $actualValue->toArray();
                }
                if ($orgValue instanceof PersistentCollection) {
                    if ($orgValue !== $actualValue && $orgValue->isDirty()) {
                        $this->visitedCollections[] = $orgValue;
                    }
                    $orgValue = $orgValue->getSnapshot();
                }
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one' && $orgValue !== $actualValue) {
                    if (is_object($orgValue)) {
                        $embeddedOid = spl_object_hash($orgValue);
                        $orgValue = isset($this->originalDocumentData[$embeddedOid]) ? $this->originalDocumentData[$embeddedOid] : $orgValue;
                        if ($orgValue !== null) {
                            $this->scheduleEmbeddedRemoval($orgValue);
                        }
                    }
                    $changeSet[$propName] = array($orgValue, $actualValue);
                } else if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $orgValue !== $actualValue) {
                    if (is_object($orgValue)) {
                        $referenceOid = spl_object_hash($orgValue);
                        $orgValue = isset($this->originalDocumentData[$referenceOid]) ? $this->originalDocumentData[$referenceOid] : $orgValue;
                    }
                    $changeSet[$propName] = array($orgValue, $actualValue);
                } else if ($isChangeTrackingNotify) {
                    continue;
                } else if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
                    if ($orgValue !== $actualValue) {
                        $changeSet[$propName] = array($orgValue, $actualValue);
                    }
                } else if (is_object($orgValue) && $orgValue !== $actualValue) {
                    $changeSet[$propName] = array($orgValue, $actualValue);
                } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) {
                    $changeSet[$propName] = array($orgValue, $actualValue);
                }
            }
            if ($changeSet) {
                $this->documentChangeSets[$oid] = $changeSet;
                $this->originalDocumentData[$oid] = $actualData;
                if ( ! $class->isEmbeddedDocument) {
                    $this->documentUpdates[$oid] = $document;
                }
            }
        }

        // Look for changes in references of the document
        foreach ($class->fieldMappings as $mapping) {
            if (isset($mapping['reference']) || isset($mapping['embedded'])) {
                $val = $class->reflFields[$mapping['fieldName']]->getValue($document);
                if ($val !== null) {
                    $this->computeAssociationChanges($parentDocument, $mapping, $val);
                }
            }
        }
    }

    /**
     * Computes all the changes that have been done to documents and collections
     * since the last commit and stores these changes in the _documentChangeSet map
     * temporarily for access by the persisters, until the UoW commit is finished.
     */
    public function computeChangeSets()
    {
        // Compute changes for INSERTed documents first. This must always happen.
        foreach ($this->documentInsertions as $document) {
            $class = $this->dm->getClassMetadata(get_class($document));
            $this->computeChangeSet($document, $class, $document);
        }

        // 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) {
                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;

            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 are processed here.
                $oid = spl_object_hash($document);
                if ( ! isset($this->documentInsertions[$oid]) && isset($this->documentStates[$oid])) {
                    $this->computeChangeSet($document, $class, $document);
                }
            }
        }
    }

    /**
     * Computes the changes of an embedded document.
     *
     * @param object $parentDocument The top most parent document of the document we are computing.
     * @param array $mapping
     * @param mixed $value The value of the association.
     */
    private function computeAssociationChanges($parentDocument, $mapping, $value)
    {
        if ($value instanceof PersistentCollection && $value->isDirty()) {
            $this->visitedCollections[] = $value;
        }

        if ( ! isset($mapping['embedded']) && ! $mapping['isCascadePersist']) {
            return; // "Persistence by reachability" only if persist cascade specified
        }

        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();
        }
        foreach ($value as $entry) {
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
            $state = $this->getDocumentState($entry, self::STATE_NEW);
            if ($targetClass->isEmbeddedDocument) {
                if ($state == self::STATE_NEW) {
                    $this->persistNew($targetClass, $entry);
                }
                $this->computeChangeSet($parentDocument, $targetClass, $entry);
            } else {
                $oid = spl_object_hash($entry);
                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->computeChangeSet($parentDocument, $targetClass, $entry);
                } else if ($state == self::STATE_REMOVED) {
                    return new \InvalidArgumentException("Removed document detected during flush: "
                            . self::objToStr($removedDocument).". Remove deleted documents from associations.");
                } else if ($state == self::STATE_DETACHED) {
                    // Can actually not happen right now as we assume STATE_NEW,
                    // so the exception will be raised from the DBAL layer (constraint violation).
                    throw new \InvalidArgumentException("A detached document was found through a "
                            . "relationship during cascading a persist operation.");
                }
                // MANAGED associated documents are already taken into account
                // during changeset calculation anyway, since they are in the identity map.
            }
        }
    }

    /**
     * INTERNAL:
     * Computes the changeset of an individual document, independently of the
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
     *
     * The passed document must be a managed document. If the document already has a change set
     * because this method is invoked during a commit cycle then the change sets are added.
     * whereby changes detected in this method prevail.
     *
     * @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.
     */
    public function recomputeSingleDocumentChangeSet($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.');
        }

        if ( ! $class->isInheritanceTypeNone()) {
            $class = $this->dm->getClassMetadata(get_class($document));
        }

        $actualData = $this->getDocumentActualData($document);

        $originalData = isset($this->originalDocumentData[$oid]) ? $this->originalDocumentData[$oid] : array();
        $changeSet = array();
        foreach ($actualData as $propName => $actualValue) {
            $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
            if ($actualValue instanceof PersistentCollection) {
                if ($actualValue->isDirty()) {
                    $this->visitedCollections[] = $actualValue;
                }
                $actualValue = $actualValue->toArray();
            }
            if ($orgValue instanceof PersistentCollection) {
                if ($orgValue !== $actualValue && $orgValue->isDirty()) {
                    $this->visitedCollections[] = $orgValue;
                }
                $orgValue = $orgValue->getSnapshot();
            }
            if ((isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one')
                    || (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one')) {
                if ($orgValue !== $actualValue) {
                    $changeSet[$propName] = array($orgValue, $actualValue);
                }
            } else if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
                if ($orgValue !== $actualValue) {
                    $changeSet[$propName] = array($orgValue, $actualValue);
                }
            } else if (is_object($orgValue) && $orgValue !== $actualValue) {
                $changeSet[$propName] = array($orgValue, $actualValue);
            } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) {
                $changeSet[$propName] = array($orgValue, $actualValue);
            }
        }
        if ($changeSet) {
            if (isset($this->documentChangeSets[$oid])) {
                $this->documentChangeSets[$oid] = $changeSet + $this->documentChangeSets[$oid];
            }
            $this->originalDocumentData[$oid] = $actualData;
        }
    }

    private function persistNew($class, $document)
    {
        $oid = spl_object_hash($document);
        if (isset($class->lifecycleCallbacks[ODMEvents::prePersist])) {
            $class->invokeLifecycleCallbacks(ODMEvents::prePersist, $document);
        }
        if ($this->evm->hasListeners(ODMEvents::prePersist)) {
            $this->evm->dispatchEvent(ODMEvents::prePersist, new LifecycleEventArgs($document, $this->dm));
        }

        $this->documentStates[$oid] = self::STATE_MANAGED;

        $this->scheduleForInsert($document);
    }

    /**
     * Executes all document insertions for documents of the specified type.
     *
     * @param Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
     * @param array $options Array of options to be used with batchInsert()
     */
    private function executeInserts($class, array $options = array())
    {
        $className = $class->name;
        $persister = $this->getDocumentPersister($className);
        $collection = $this->dm->getDocumentCollection($className);

        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[ODMEvents::postPersist]);
        $hasListeners = $this->evm->hasListeners(ODMEvents::postPersist);
        if ($hasLifecycleCallbacks || $hasListeners) {
            $documents = array();
        }

        $inserts = array();
        foreach ($this->documentInsertions as $oid => $document) {
            if (get_class($document) === $className) {
                $persister->addInsert($document);
                unset($this->documentInsertions[$oid]);
                if ($hasLifecycleCallbacks || $hasListeners) {
                    $documents[] = $document;
                }
            }
        }

        $postInsertIds = $persister->executeInserts($options);

        if ($postInsertIds) {
            foreach ($postInsertIds as $pair) {
                list($id, $document) = $pair;
                $oid = spl_object_hash($document);
                $class->setIdentifierValue($document, $id);
                $this->documentIdentifiers[$oid] = $id;
                $this->documentStates[$oid] = self::STATE_MANAGED;
                $this->originalDocumentData[$oid][$class->identifier] = $id;
                $this->addToIdentityMap($document);

                if ($hasLifecycleCallbacks || $hasListeners) {
                    if ($hasLifecycleCallbacks) {
                        $class->invokeLifecycleCallbacks(ODMEvents::postPersist, $document);
                    }
                    if ($hasListeners) {
                        $this->evm->dispatchEvent(ODMEvents::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)
    {
        foreach ($class->fieldMappings as $mapping) {
            if (isset($mapping['embedded'])) {
                $value = $class->getFieldValue($document, $mapping['fieldName']);
                if ($value === null) {
                    continue;
                }
                if ($mapping['type'] === 'one') {
                    $value = array($value);
                }
                foreach ($value as $entry) {
                    $entryClass = $this->dm->getClassMetadata(get_class($entry));
                    $hasLifecycleCallbacks = isset($entryClass->lifecycleCallbacks[ODMEvents::postPersist]);
                    $hasListeners = $this->evm->hasListeners(ODMEvents::postPersist);
                    if ($hasLifecycleCallbacks || $hasListeners) {
                        if ($hasLifecycleCallbacks) {
                            $entryClass->invokeLifecycleCallbacks(ODMEvents::postPersist, $entry);
                        }
                        if ($hasListeners) {
                            $this->evm->dispatchEvent(ODMEvents::postPersist, new LifecycleEventArgs($entry, $this->dm));
                        }
                    }
                    $this->cascadePostPersist($entryClass, $entry);
                }
            }
        }
    }

    /**
     * Executes all document updates for documents of the specified type.
     *
     * @param Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
     * @param array $options Array of options to be used with update()
     */
    private function executeUpdates($class, array $options = array())
    {
        $className = $class->name;
        $persister = $this->getDocumentPersister($className);

        $hasPreUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[ODMEvents::preUpdate]);
        $hasPreUpdateListeners = $this->evm->hasListeners(ODMEvents::preUpdate);
        $hasPostUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[ODMEvents::postUpdate]);
        $hasPostUpdateListeners = $this->evm->hasListeners(ODMEvents::postUpdate);

        foreach ($this->documentUpdates as $oid => $document) {
            if (get_class($document) == $className || $document instanceof Proxy && $document instanceof $className) {
                if ($hasPreUpdateLifecycleCallbacks) {
                    $class->invokeLifecycleCallbacks(ODMEvents::preUpdate, $document);
                    $this->recomputeSingleDocumentChangeSet($class, $document);
                }

                if ($hasPreUpdateListeners) {
                    $this->evm->dispatchEvent(ODMEvents::preUpdate, new Event\PreUpdateEventArgs(
                        $document, $this->dm, $this->documentChangeSets[$oid])
                    );
                }
                $this->cascadePreUpdate($class, $document);

                $persister->update($document, $options);
                unset($this->documentUpdates[$oid]);

                if ($hasPostUpdateLifecycleCallbacks) {
                    $class->invokeLifecycleCallbacks(ODMEvents::postUpdate, $document);
                }
                if ($hasPostUpdateListeners) {
                    $this->evm->dispatchEvent(ODMEvents::postUpdate, new LifecycleEventArgs($document, $this->dm));
                }
                $this->cascadePostUpdateAndPostPersist($class, $document);
            }
        }
    }

    /**
     * Cascades the preUpdate event to embedded documents.
     *
     * @param ClassMetadata $class
     * @param object $document
     */
    private function cascadePreUpdate(ClassMetadata $class, $document)
    {
        foreach ($class->fieldMappings as $mapping) {
            if (isset($mapping['embedded'])) {
                $value = $class->getFieldValue($document, $mapping['fieldName']);
                if ($value === null) {
                    continue;
                }
                if ($mapping['type'] === 'one') {
                    $value = array($value);
                }
                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 (isset($entryClass->lifecycleCallbacks[ODMEvents::preUpdate])) {
                            $entryClass->invokeLifecycleCallbacks(ODMEvents::preUpdate, $entry);
                            $this->recomputeSingleDocumentChangeSet($entryClass, $entry);
                        }
                        if ($this->evm->hasListeners(ODMEvents::preUpdate)) {
                            $this->evm->dispatchEvent(ODMEvents::preUpdate, new Event\PreUpdateEventArgs(
                                $entry, $this->dm, $this->documentChangeSets[$entryOid])
                            );
                        }
                    }
                    $this->cascadePreUpdate($entryClass, $entry);
                }
            }
        }
    }

    /**
     * Cascades the postUpdate and postPersist events to embedded documents.
     *
     * @param ClassMetadata $class
     * @param object $document
     */
    private function cascadePostUpdateAndPostPersist(ClassMetadata $class, $document)
    {
        foreach ($class->fieldMappings as $mapping) {
            if (isset($mapping['embedded'])) {
                $value = $class->getFieldValue($document, $mapping['fieldName']);
                if ($value === null) {
                    continue;
                }
                if ($mapping['type'] === 'one') {
                    $value = array($value);
                }
                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 (isset($entryClass->lifecycleCallbacks[ODMEvents::postPersist])) {
                            $entryClass->invokeLifecycleCallbacks(ODMEvents::postPersist, $entry);
                        }
                        if ($this->evm->hasListeners(ODMEvents::postPersist)) {
                            $this->evm->dispatchEvent(ODMEvents::postPersist, new LifecycleEventArgs($entry, $this->dm));
                        }
                    } else {
                        if (isset($entryClass->lifecycleCallbacks[ODMEvents::postUpdate])) {
                            $entryClass->invokeLifecycleCallbacks(ODMEvents::postUpdate, $entry);
                            $this->recomputeSingleDocumentChangeSet($entryClass, $entry);
                        }
                        if ($this->evm->hasListeners(ODMEvents::postUpdate)) {
                            $this->evm->dispatchEvent(ODMEvents::postUpdate, new Event\PreUpdateEventArgs(
                                $entry, $this->dm, $this->documentChangeSets[$entryOid])
                            );
                        }
                    }
                    $this->cascadePostUpdateAndPostPersist($entryClass, $entry);
                }
            }
        }
    }

    /**
     * Executes all document deletions for documents of the specified type.
     *
     * @param Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
     * @param array $options Array of options to be used with remove()
     */
    private function executeDeletions($class, array $options = array())
    {
        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[ODMEvents::postRemove]);
        $hasListeners = $this->evm->hasListeners(ODMEvents::postRemove);

        $className = $class->name;
        $persister = $this->getDocumentPersister($className);
        $collection = $this->dm->getDocumentCollection($className);
        foreach ($this->documentDeletions as $oid => $document) {
            if (get_class($document) == $className || $document instanceof Proxy && $document instanceof $className) {
                if ( ! $class->isEmbeddedDocument) {
                    $persister->delete($document, $options);
                }
                unset(
                    $this->documentDeletions[$oid],
                    $this->documentIdentifiers[$oid],
                    $this->originalDocumentData[$oid]
                );
                // Document with this $oid after deletion treated as NEW, even if the $oid
                // is obtained by a new document because the old one went out of scope.
                $this->documentStates[$oid] = self::STATE_NEW;

                if ($hasLifecycleCallbacks) {
                    $class->invokeLifecycleCallbacks(ODMEvents::postRemove, $document);
                }
                if ($hasListeners) {
                    $this->evm->dispatchEvent(ODMEvents::postRemove, new LifecycleEventArgs($document, $this->dm));
                }
                $this->cascadePostRemove($class, $document);
            }
        }
    }

    /**
     * Gets the commit order.
     *
     * @return array
     */
    private function getCommitOrder(array $documentChangeSet = null)
    {
        if ($documentChangeSet === null) {
            $documentChangeSet = array_merge(
                $this->documentInsertions,
                $this->documentUpdates,
                $this->documentDeletions
            );
        }

        $calc = $this->getCommitOrderCalculator();

        // See if there are any new classes in the changeset, that are not in the
        // commit order graph yet (dont have a node).
        $newNodes = array();
        foreach ($documentChangeSet as $oid => $document) {
            $className = get_class($document);
            if ( ! $calc->hasClass($className)) {
                $class = $this->dm->getClassMetadata($className);
                $calc->addClass($class);
                $newNodes[] = $class;
            }
        }

        // Calculate dependencies for new nodes
        foreach ($newNodes as $class) {
            $this->addDependencies($class, $calc);
        }
        return $calc->getCommitOrder();
        $classes = $calc->getCommitOrder();
        foreach ($classes as $key => $class) {
            if ($class->isEmbeddedDocument) {
                unset($classes[$key]);
            }
        }
        return array_values($classes);
    }

    /**
     * Add dependencies recursively through embedded documents. Embedded documents
     * may have references to other documents so those need to be saved first.
     *
     * @param ClassMetadata $class
     * @param CommitOrderCalculator $calc
     */
    private function addDependencies($class, $calc)
    {
        foreach ($class->fieldMappings as $mapping) {
            if (isset($mapping['reference']) && isset($mapping['targetDocument'])) {
                $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
                if ( ! $calc->hasClass($targetClass->name)) {
                    $calc->addClass($targetClass);
                }
                if ( ! $calc->hasDependency($targetClass, $class)) {
                    $calc->addDependency($targetClass, $class);
                }
            }
            if (isset($mapping['embedded']) && isset($mapping['targetDocument'])) {
                $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
                if ( ! $calc->hasClass($targetClass->name)) {
                    $calc->addClass($targetClass);
                }
                if ( ! $calc->hasDependency($targetClass, $class)) {
                    $calc->addDependency($targetClass, $class);
                }

                // avoid infinite recursion
                if ($class != $targetClass) {
                    $this->addDependencies($targetClass, $calc);
                }
            }
        }
    }

    /**
     * Schedules an document for insertion into the database.
     * If the document already has an identifier, it will be added to the identity map.
     *
     * @param object $document The document to schedule for insertion.
     */
    public function scheduleForInsert($document)
    {
        $oid = spl_object_hash($document);

        if (isset($this->documentUpdates[$oid])) {
            throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion.");
        }
        if (isset($this->documentDeletions[$oid])) {
            throw new \InvalidArgumentException("Removed document can not be scheduled for insertion.");
        }
        if (isset($this->documentInsertions[$oid])) {
            throw new \InvalidArgumentException("Document can not be scheduled for insertion twice.");
        }

        $this->documentInsertions[$oid] = $document;

        if (isset($this->documentIdentifiers[$oid])) {
            $this->addToIdentityMap($document);
        }
    }

    /**
     * Checks whether an document is scheduled for insertion.
     *
     * @param object $document
     * @return boolean
     */
    public function isScheduledForInsert($document)
    {
        return isset($this->documentInsertions[spl_object_hash($document)]);
    }

    /**
     * Schedules an document for being updated.
     *
     * @param object $document The document to schedule for being updated.
     */
    public function scheduleForUpdate($document)
    {
        $oid = spl_object_hash($document);
        if ( ! isset($this->documentIdentifiers[$oid])) {
            throw new \InvalidArgumentException("Document has no identity.");
        }
        if (isset($this->documentDeletions[$oid])) {
            throw new \InvalidArgumentException("Document is removed.");
        }

        if ( ! isset($this->documentUpdates[$oid]) && ! isset($this->documentInsertions[$oid])) {
            $this->documentUpdates[$oid] = $document;
        }
    }

    /**
     * Checks whether an document is registered as dirty in the unit of work.
     * Note: Is not very useful currently as dirty documents are only registered
     * at commit time.
     *
     * @param object $document
     * @return boolean
     */
    public function isScheduledForUpdate($document)
    {
        return isset($this->documentUpdates[spl_object_hash($document)]);
    }

    public function isScheduledForDirtyCheck($document)
    {
        $rootDocumentName = $this->dm->getClassMetadata(get_class($document))->rootDocumentName;
        return isset($this->scheduledForDirtyCheck[$rootDocumentName][spl_object_hash($document)]);
    }

    /**
     * INTERNAL:
     * Schedules an document for deletion.
     *
     * @param object $document
     */
    public function scheduleForDelete($document)
    {
        $oid = spl_object_hash($document);

        if (isset($this->documentInsertions[$oid])) {
            if ($this->isInIdentityMap($document)) {
                $this->removeFromIdentityMap($document);
            }
            unset($this->documentInsertions[$oid]);
            return; // document has not been persisted yet, so nothing more to do.
        }

        if ( ! $this->isInIdentityMap($document)) {
            return; // ignore
        }

        $this->removeFromIdentityMap($document);

        if (isset($this->documentUpdates[$oid])) {
            unset($this->documentUpdates[$oid]);
        }
        if ( ! isset($this->documentDeletions[$oid])) {
            $this->documentDeletions[$oid] = $document;
        }
    }

    /**
     * Checks whether an document is registered as removed/deleted with the unit
     * of work.
     *
     * @param object $document
     * @return boolean
     */
    public function isScheduledForDelete($document)
    {
        return isset($this->documentDeletions[spl_object_hash($document)]);
    }

    /**
     * Checks whether an document is scheduled for insertion, update or deletion.
     *
     * @param $document
     * @return boolean
     */
    public function isDocumentScheduled($document)
    {
        $oid = spl_object_hash($document);
        return isset($this->documentInsertions[$oid]) ||
                isset($this->documentUpdates[$oid]) ||
                isset($this->documentDeletions[$oid]);
    }

    /**
     * INTERNAL:
     * Registers an document in the identity map.
     * Note that documents in a hierarchy are registered with the class name of
     * the root document.
     *
     * @ignore
     * @param object $document  The document to register.
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
     *                  the document in question is already managed.
     */
    public function addToIdentityMap($document)
    {
        $classMetadata = $this->dm->getClassMetadata(get_class($document));
        if ($classMetadata->isEmbeddedDocument) {
            $id = spl_object_hash($document);
        } else {
            $id = $this->documentIdentifiers[spl_object_hash($document)];
            $id = $classMetadata->getPHPIdentifierValue($id);
        }
        if ($id === '') {
            throw new \InvalidArgumentException("The given document has no identity.");
        }
        $className = $classMetadata->rootDocumentName;
        if (isset($this->identityMap[$className][$id])) {
            return false;
        }
        $this->identityMap[$className][$id] = $document;
        if ($document instanceof NotifyPropertyChanged) {
            $document->addPropertyChangedListener($this);
        }
        return true;
    }

    /**
     * Gets the state of an document within the current unit of work.
     *
     * NOTE: This method sees documents that are not MANAGED or REMOVED and have a
     *       populated identifier, whether it is generated or manually assigned, as
     *       DETACHED. This can be incorrect for manually assigned identifiers.
     *
     * @param object $document
     * @param integer $assume The state to assume if the state is not yet known. This is usually
     *                        used to avoid costly state lookups, in the worst case with a database
     *                        lookup.
     * @return int The document state.
     */
    public function getDocumentState($document, $assume = null)
    {
        $oid = spl_object_hash($document);
        if ( ! isset($this->documentStates[$oid])) {
            $class = $this->dm->getClassMetadata(get_class($document));
            if ($class->isEmbeddedDocument) {
                return self::STATE_NEW;
            }
            // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
            // Note that you can not remember the NEW or DETACHED state in _documentStates since
            // the UoW does not hold references to such objects and the object hash can be reused.
            // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
            if ($assume === null) {
                $id = $class->getIdentifierValue($document);
                if ( ! $id) {
                    return self::STATE_NEW;
                } else {
                    // Last try before db lookup: check the identity map.
                    if ($this->tryGetById($id, $class->rootDocumentName)) {
                        return self::STATE_DETACHED;
                    } else {
                        // db lookup
                        if ($this->getDocumentPersister(get_class($document))->exists($document)) {
                            return self::STATE_DETACHED;
                        } else {
                            return self::STATE_NEW;
                        }
                    }
                }
            } else {
                return $assume;
            }
        }
        return $this->documentStates[$oid];
    }

    /**
     * INTERNAL:
     * Removes an document from the identity map. This effectively detaches the
     * document from the persistence management of Doctrine.
     *
     * @ignore
     * @param object $document
     * @return boolean
     */
    public function removeFromIdentityMap($document)
    {
        $oid = spl_object_hash($document);
        $classMetadata = $this->dm->getClassMetadata(get_class($document));
        $id = $this->documentIdentifiers[$oid];
        if ( ! $classMetadata->isEmbeddedDocument) {
            $id = $classMetadata->getPHPIdentifierValue($id);
        }
        if ($id === '') {
            throw new \InvalidArgumentException("The given document has no identity.");
        }
        $className = $classMetadata->rootDocumentName;
        if (isset($this->identityMap[$className][$id])) {
            unset($this->identityMap[$className][$id]);
            $this->documentStates[$oid] = self::STATE_DETACHED;
            return true;
        }

        return false;
    }

    /**
     * INTERNAL:
     * Gets an document in the identity map by its identifier hash.
     *
     * @ignore
     * @param string $id
     * @param string $rootClassName
     * @return object
     */
    public function getById($id, $rootClassName)
    {
        return $this->identityMap[$rootClassName][$id];
    }

    /**
     * INTERNAL:
     * Tries to get an document by its identifier hash. If no document is found for
     * the given hash, FALSE is returned.
     *
     * @ignore
     * @param string $id
     * @param string $rootClassName
     * @return mixed The found document or FALSE.
     */
    public function tryGetById($id, $rootClassName)
    {
        return isset($this->identityMap[$rootClassName][$id]) ?
                $this->identityMap[$rootClassName][$id] : false;
    }

    /**
     * Schedules a document for dirty-checking at commit-time.
     *
     * @param object $document The document to schedule for dirty-checking.
     * @todo Rename: scheduleForSynchronization
     */
    public function scheduleForDirtyCheck($document)
    {
        $rootClassName = $this->dm->getClassMetadata(get_class($document))->rootDocumentName;
        $this->scheduledForDirtyCheck[$rootClassName][spl_object_hash($document)] = $document;
    }

    /**
     * Checks whether an document is registered in the identity map of this UnitOfWork.
     *
     * @param object $document
     * @return boolean
     */
    public function isInIdentityMap($document)
    {
        $oid = spl_object_hash($document);
        if ( ! isset($this->documentIdentifiers[$oid])) {
            return false;
        }
        $classMetadata = $this->dm->getClassMetadata(get_class($document));
        $id = $this->documentIdentifiers[$oid];
        if ( ! $classMetadata->isEmbeddedDocument) {
            $id = $classMetadata->getPHPIdentifierValue($id);
        }
        if ($id === '') {
            return false;
        }

        return isset($this->identityMap[$classMetadata->rootDocumentName][$id]);
    }

    /**
     * INTERNAL:
     * Checks whether an identifier hash exists in the identity map.
     *
     * @ignore
     * @param string $id
     * @param string $rootClassName
     * @return boolean
     */
    public function containsId($id, $rootClassName)
    {
        return isset($this->identityMap[$rootClassName][$id]);
    }

    /**
     * Persists an document as part of the current unit of work.
     *
     * @param object $document The document to persist.
     */
    public function persist($document)
    {
        $class = $this->dm->getClassMetadata(get_class($document));
        if ($class->isMappedSuperclass) {
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
        }
        $visited = array();
        $this->doPersist($document, $visited);
    }

    /**
     * Saves an document as part of the current unit of work.
     * This method is internally called during save() cascades as it tracks
     * the already visited documents to prevent infinite recursions.
     *
     * NOTE: This method always considers documents that are not yet known to
     * this UnitOfWork as NEW.
     *
     * @param object $document The document to persist.
     * @param array $visited The already visited documents.
     */
    private function doPersist($document, array &$visited)
    {
        $oid = spl_object_hash($document);
        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $document; // Mark 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"
                if ($class->isChangeTrackingDeferredExplicit()) {
                    $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.");
            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($document);
                    }
                    break;
                }
            default:
                throw MongoDBException::invalidDocumentState($documentState);
        }

        $this->cascadePersist($document, $visited);
    }

    /**
     * Deletes an document as part of the current unit of work.
     *
     * @param object $document The document to remove.
     */
    public function remove($document)
    {
        $visited = array();
        $this->doRemove($document, $visited);
    }

    /**
     * Deletes an document as part of the current unit of work.
     *
     * This method is internally called during delete() cascades as it tracks
     * the already visited documents to prevent infinite recursions.
     *
     * @param object $document The document to delete.
     * @param array $visited The map of the already visited documents.
     * @throws InvalidArgumentException If the instance is a detached document.
     */
    private function doRemove($document, array &$visited)
    {
        $oid = spl_object_hash($document);
        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $document; // mark visited

        $class = $this->dm->getClassMetadata(get_class($document));
        $documentState = $this->getDocumentState($document);
        switch ($documentState) {
            case self::STATE_NEW:
            case self::STATE_REMOVED:
                // nothing to do
                break;
            case self::STATE_MANAGED:
                if (isset($class->lifecycleCallbacks[ODMEvents::preRemove])) {
                    $class->invokeLifecycleCallbacks(ODMEvents::preRemove, $document);
                }
                if ($this->evm->hasListeners(ODMEvents::preRemove)) {
                    $this->evm->dispatchEvent(ODMEvents::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)
    {
        foreach ($class->fieldMappings as $mapping) {
            if (isset($mapping['embedded'])) {
                $value = $class->getFieldValue($document, $mapping['fieldName']);
                if ($value === null) {
                    continue;
                }
                if ($mapping['type'] === 'one') {
                    $value = array($value);
                }
                foreach ($value as $entry) {
                    $entryClass = $this->dm->getClassMetadata(get_class($entry));
                    if (isset($entryClass->lifecycleCallbacks[ODMEvents::preRemove])) {
                        $entryClass->invokeLifecycleCallbacks(ODMEvents::preRemove, $entry);
                    }
                    if ($this->evm->hasListeners(ODMEvents::preRemove)) {
                        $this->evm->dispatchEvent(ODMEvents::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)
    {
        foreach ($class->fieldMappings as $mapping) {
            if (isset($mapping['embedded'])) {
                $value = $class->getFieldValue($document, $mapping['fieldName']);
                if ($value === null) {
                    continue;
                }
                if ($mapping['type'] === 'one') {
                    $value = array($value);
                }
                foreach ($value as $entry) {
                    $entryClass = $this->dm->getClassMetadata(get_class($entry));
                    if (isset($entryClass->lifecycleCallbacks[ODMEvents::postRemove])) {
                        $entryClass->invokeLifecycleCallbacks(ODMEvents::postRemove, $entry);
                    }
                    if ($this->evm->hasListeners(ODMEvents::postRemove)) {
                        $this->evm->dispatchEvent(ODMEvents::postRemove, new LifecycleEventArgs($entry, $this->dm));
                    }
                    $this->cascadePostRemove($entryClass, $entry);
                }
            }
        }
    }

    /**
     * Merges the state of the given detached document into this UnitOfWork.
     *
     * @param object $document
     * @return object The managed copy of the document.
     */
    public function merge($document)
    {
        $visited = array();
        return $this->doMerge($document, $visited);
    }

    /**
     * Executes a merge operation on an document.
     *
     * @param object $document
     * @param array $visited
     * @return object The managed copy of the document.
     * @throws InvalidArgumentException If the document instance is NEW.
     */
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
    {
        $oid = spl_object_hash($document);
        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $document; // mark visited

        $class = $this->dm->getClassMetadata(get_class($document));

        // First we assume DETACHED, although it can still be NEW but we can avoid
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
        // we need to fetch it from the db anyway in order to merge.
        // MANAGED documents are ignored by the merge operation.
        if ($this->getDocumentState($document, self::STATE_DETACHED) == self::STATE_MANAGED) {
            $managedCopy = $document;
        } else {
            // Try to look the entity up in the identity map.
            $id = $class->getIdentifierValue($document);

            // If there is no ID, it is actually NEW.
            if ( ! $id) {
                $managedCopy = $class->newInstance();
                $this->persistNew($class, $managedCopy);
            } else {
                $managedCopy = $this->tryGetById($id, $class->rootDocumentName);
                if ($managedCopy) {
                    // We have the entity in-memory already, just make sure its not removed.
                    if ($this->getDocumentState($managedCopy) == self::STATE_REMOVED) {
                        throw new InvalidArgumentException('Removed entity detected during merge.'
                                . ' Can not merge with a removed entity.');
                    }
                } else {
                    // We need to fetch the managed copy in order to merge.
                    $managedCopy = $this->dm->find($class->name, $id);
                }

                if ($managedCopy === null) {
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
                    // since the managed entity was not found.
                    $managedCopy = $class->newInstance();
                    $class->setIdentifierValue($managedCopy, $id);
                    $this->persistNew($class, $managedCopy);
                }
            }

            // Merge state of $document into existing (managed) entity
            foreach ($class->reflFields as $name => $prop) {
                if ( ! isset($class->fieldMappings[$name]['embedded']) &&  ! isset($class->fieldMappings[$name]['reference'])) {
                    $prop->setValue($managedCopy, $prop->getValue($document));
                } else {
                    $assoc2 = $class->fieldMappings[$name];
                    if ($assoc2['type'] === 'one') {
                        $other = $prop->getValue($document);
                        if ($other === null) {
                            $prop->setValue($managedCopy, null);
                        } else if ($other instanceof Proxy && !$other->__isInitialized__) {
                            // do not merge fields marked lazy that have not been fetched.
                            continue;
                        } else if ( ! isset($assoc2['embedded']) && ! $assoc2['isCascadeMerge']) {
                            if ($this->getDocumentState($other, self::STATE_DETACHED) == self::STATE_MANAGED) {
                                $prop->setValue($managedCopy, $other);
                            } else {
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
                                $id = $targetClass->getIdentifierValue($other);
                                $proxy = $this->dm->getProxyFactory()->getProxy($targetDocument, $id);
                                $prop->setValue($managedCopy, $proxy);
                                $this->registerManaged($proxy, $id, array());
                            }
                        }
                    } else {
                        $mergeCol = $prop->getValue($document);
                        if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
                            // do not merge fields marked lazy that have not been fetched.
                            // keep the lazy persistent collection of the managed copy.
                            continue;
                        }

                        foreach ($mergeCol as $entry) {
                            $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($entry);
                            $targetClass = $this->dm->getClassMetadata($targetDocument);
                            if ($targetClass->isEmbeddedDocument) {
                                $this->registerManaged($entry, null, array());
                            } else {
                                $id = $targetClass->getIdentifierValue($entry);
                                $this->registerManaged($entry, $id, array());
                            }
                        }

                        if ( ! $mergeCol instanceof PersistentCollection) {
                            $mergeCol = new PersistentCollection($mergeCol, $this->dm);
                            $mergeCol->setOwner($managedCopy, $assoc2);
                            $mergeCol->setInitialized(true);
                        }
                        $prop->setValue($managedCopy, $mergeCol);
                    }
                }
                if ($class->isChangeTrackingNotify()) {
                    // Just treat all properties as changed, there is no other choice.
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
                }
            }
            if ($class->isChangeTrackingDeferredExplicit()) {
                $this->scheduleForDirtyCheck($document);
            }
        }

        if ($prevManagedCopy !== null) {
            $assocField = $assoc->sourceFieldName;
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
            if ($assoc->isOneToOne()) {
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
            } else {
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->unwrap()->add($managedCopy);
                if ($assoc->isOneToMany()) {
                    $class->reflFields[$assoc->mappedBy]->setValue($managedCopy, $prevManagedCopy);
                }
            }
        }

        // Mark the managed copy visited as well
        $visited[spl_object_hash($managedCopy)] = true;

        $this->cascadeMerge($document, $managedCopy, $visited);

        return $managedCopy;
    }

    /**
     * Detaches an document from the persistence management. It's persistence will
     * no longer be managed by Doctrine.
     *
     * @param object $document The document to detach.
     */
    public function detach($document)
    {
        $visited = array();
        $this->doDetach($document, $visited);
    }

    /**
     * Executes a detach operation on the given document.
     *
     * @param object $document
     * @param array $visited
     * @internal This method always considers documents with an assigned identifier as DETACHED.
     */
    private function doDetach($document, array &$visited)
    {
        $oid = spl_object_hash($document);
        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $document; // mark visited

        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
            case self::STATE_MANAGED:
                $this->removeFromIdentityMap($document);
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
                        $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
                        $this->documentStates[$oid], $this->originalDocumentData[$oid]);
                break;
            case self::STATE_NEW:
            case self::STATE_DETACHED:
                return;
        }

        $this->cascadeDetach($document, $visited);
    }

    /**
     * Refreshes the state of the given document from the database, overwriting
     * any local, unpersisted changes.
     *
     * @param object $document The document to refresh.
     * @throws InvalidArgumentException If the document is not MANAGED.
     */
    public function refresh($document)
    {
        $visited = array();
        $this->doRefresh($document, $visited);
    }

    /**
     * Executes a refresh operation on an 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.
     */
    private function doRefresh($document, array &$visited)
    {
        $oid = spl_object_hash($document);
        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $document; // mark visited

        $class = $this->dm->getClassMetadata(get_class($document));
        if ($this->getDocumentState($document) == self::STATE_MANAGED) {
            $this->getDocumentPersister($class->name)->refresh($document);
        } else {
            throw new \InvalidArgumentException("Document is not MANAGED.");
        }

        $this->cascadeRefresh($document, $visited);
    }

    /**
     * Cascades a refresh operation to associated documents.
     *
     * @param object $document
     * @param array $visited
     */
    private function cascadeRefresh($document, array &$visited)
    {
        $class = $this->dm->getClassMetadata(get_class($document));
        foreach ($class->fieldMappings as $mapping) {
            if (isset($mapping['reference']) && ! $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);
                }
            } 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);
                }
            }
        }
    }

    /**
     * Cascades a detach operation to associated documents.
     *
     * @param object $document
     * @param array $visited
     */
    private function cascadeDetach($document, array &$visited)
    {
        $class = $this->dm->getClassMetadata(get_class($document));
        foreach ($class->fieldMappings as $mapping) {
            if ( ! isset($mapping['embedded']) && ! $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);
                }
            } 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);
                }
            }
        }
    }

    /**
     * Cascades a merge operation to associated documents.
     *
     * @param object $document
     * @param object $managedCopy
     * @param array $visited
     */
    private function cascadeMerge($document, $managedCopy, array &$visited)
    {
        $class = $this->dm->getClassMetadata(get_class($document));
        foreach ($class->fieldMappings as $mapping) {
            if ( ! isset($mapping['embedded']) && ! $mapping['isCascadeMerge']) {
                continue;
            }
            if (isset($mapping['embedded']) || 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->doMerge($relatedDocument, $visited);
                    }
                } elseif ($relatedDocuments !== null) {
                    $this->doMerge($relatedDocuments, $visited);
                }
            }
        }
    }

    /**
     * Cascades the save operation to associated documents.
     *
     * @param object $document
     * @param array $visited
     * @param array $insertNow
     */
    private function cascadePersist($document, array &$visited)
    {
        $class = $this->dm->getClassMetadata(get_class($document));
        foreach ($class->fieldMappings as $mapping) {
            if ( ! isset($mapping['embedded']) && ! $mapping['isCascadePersist']) {
                continue;
            }
            if (isset($mapping['embedded']) || 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->doPersist($relatedDocument, $visited);
                    }
                } elseif ($relatedDocuments !== null) {
                    $this->doPersist($relatedDocuments, $visited);
                }
            }
        }
    }

    /**
     * Cascades the delete operation to associated documents.
     *
     * @param object $document
     * @param array $visited
     */
    private function cascadeRemove($document, array &$visited)
    {
        $class = $this->dm->getClassMetadata(get_class($document));
        foreach ($class->fieldMappings as $mapping) {
            if ( ! isset($mapping['embedded']) && ! $mapping['isCascadeRemove']) {
                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->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 ($relatedDocuments instanceof PersistentCollection) {
                        // Unwrap so that foreach() does not initialize
                        $relatedDocuments = $relatedDocuments->unwrap();
                    }
                    foreach ($relatedDocuments as $relatedDocument) {
                        $this->doRemove($relatedDocument, $visited);
                    }
                } elseif ($relatedDocuments !== null) {
                    $this->doRemove($relatedDocuments, $visited);
                }
            }
        }
    }

    /**
     * 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 Internal\CommitOrderCalculator;
        }
        return $this->commitOrderCalculator;
    }

    /**
     * Clears the UnitOfWork.
     */
    public function clear()
    {
        $this->identityMap =
        $this->documentIdentifiers =
        $this->originalDocumentData =
        $this->documentChangeSets =
        $this->documentStates =
        $this->scheduledForDirtyCheck =
        $this->documentInsertions =
        $this->documentUpdates =
        $this->documentDeletions =
        $this->embeddedRemovals = array();
        if ($this->commitOrderCalculator !== null) {
            $this->commitOrderCalculator->clear();
        }
    }

    /**
     * INTERNAL:
     * Schedules an embedded document for removal. The remove() operation will be
     * invoked on that document at the beginning of the next commit of this
     * UnitOfWork.
     *
     * @ignore
     * @param object $document
     */
    public function scheduleEmbeddedRemoval($document)
    {
        $this->embeddedRemovals[spl_object_hash($document)] = $document;
    }

    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
    {
        return in_array($coll, $this->collectionsDeletions, true);
    }

    /**
     * INTERNAL:
     * Creates an document. Used for reconstitution of documents during hydration.
     *
     * @ignore
     * @param string $className The name of the document class.
     * @param array $data The data for the document.
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
     * @return object The document instance.
     * @internal Highly performance-sensitive method.
     */
    public function getOrCreateDocument($className, $data, &$hints = array())
    {
        $class = $this->dm->getClassMetadata($className);
        if ($data instanceof \MongoGridFSFile) {
            $file = $data;
            $data = $file->file;
            $data[$class->file] = $file;
        }

        if ($class->hasDiscriminator()) {
            if (isset($data[$class->discriminatorField['name']])) {
                $type = $data[$class->discriminatorField['name']];
                $class = $this->dm->getClassMetadata($class->discriminatorMap[$data[$class->discriminatorField['name']]]);
                unset($data[$class->discriminatorField['name']]);
            }
        }

        $id = $class->getPHPIdentifierValue($data['_id']);
        if (isset($this->identityMap[$class->rootDocumentName][$id])) {
            $document = $this->identityMap[$class->rootDocumentName][$id];
        } else {
            $document = $class->newInstance();
        }

        if (isset($this->identityMap[$class->rootDocumentName][$id])) {
            $oid = spl_object_hash($document);
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
                $document->__isInitialized__ = true;
                $overrideLocalValues = true;
                if ($document instanceof NotifyPropertyChanged) {
                    $document->addPropertyChangedListener($this);
                }
            } else {
                $overrideLocalValues = isset($hints[Query::HINT_REFRESH]);
            }
            if ($overrideLocalValues) {
                $this->hydrator->hydrate($document, $data);
                $this->originalDocumentData[$oid] = $data;
            }
        } else {
            $this->hydrator->hydrate($document, $data);
            $this->registerManaged($document, $id, $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)
    {
        foreach ($class->fieldMappings as $mapping) {
            if (isset($mapping['embedded'])) {
                $value = $class->getFieldValue($document, $mapping['fieldName']);
                if ($value === null) {
                    continue;
                }
                if ($mapping['type'] === 'one') {
                    $value = array($value);
                }
                foreach ($value as $entry) {
                    $entryClass = $this->dm->getClassMetadata(get_class($entry));
                    if (isset($entryClass->lifecycleCallbacks[ODMEvents::preLoad])) {
                        $args = array(&$data);
                        $entryClass->invokeLifecycleCallbacks(ODMEvents::preLoad, $entry, $args);
                    }
                    if ($this->evm->hasListeners(ODMEvents::preLoad)) {
                        $this->evm->dispatchEvent(ODMEvents::preLoad, new PreLoadEventArgs($entry, $this->dm, $data[$mapping['name']]));
                    }
                    $this->cascadePreLoad($entryClass, $entry, $data[$mapping['name']]);
                }
            }
        }
    }

    /**
     * Gets the identity map of the UnitOfWork.
     *
     * @return array
     */
    public function getIdentityMap()
    {
        return $this->identityMap;
    }

    /**
     * Gets the original data of an document. The original data is the data that was
     * present at the time the document was reconstituted from the database.
     *
     * @param object $document
     * @return array
     */
    public function getOriginalDocumentData($document)
    {
        $oid = spl_object_hash($document);
        if (isset($this->originalDocumentData[$oid])) {
            return $this->originalDocumentData[$oid];
        }
        return array();
    }

    /**
     * @ignore
     */
    public function setOriginalDocumentData($document, array $data)
    {
        $this->originalDocumentData[spl_object_hash($document)] = $data;
    }

    /**
     * INTERNAL:
     * Sets a property value of the original data array of an document.
     *
     * @ignore
     * @param string $oid
     * @param string $property
     * @param mixed $value
     */
    public function setOriginalDocumentProperty($oid, $property, $value)
    {
        $this->originalDocumentData[$oid][$property] = $value;
    }

    /**
     * Gets the identifier of an document.
     * The returned value is always an array of identifier values. If the document
     * has a composite identifier then the identifier values are in the same
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
     *
     * @param object $document
     * @return array The identifier values.
     */
    public function getDocumentIdentifier($document)
    {
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
            $this->documentIdentifiers[spl_object_hash($document)] : null;
    }

    /**
     * Checks whether the UnitOfWork has any pending insertions.
     *
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
     */
    public function hasPendingInsertions()
    {
        return ! empty($this->documentInsertions);
    }

    /**
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
     * number of documents in the identity map.
     *
     * @return integer
     */
    public function size()
    {
        $count = 0;
        foreach ($this->identityMap as $documentSet) {
            $count += count($documentSet);
        }
        return $count;
    }

    /**
     * INTERNAL:
     * Registers a document as managed.
     *
     * @param object $document The document.
     * @param array $id The identifier values.
     * @param array $data The original document data.
     */
    public function registerManaged($document, $id, array $data)
    {
        $oid = spl_object_hash($document);
        if ($id === null) {
            $this->documentIdentifiers[$oid] = $oid;
        } else {
            $this->documentIdentifiers[$oid] = $id;
        }
        $this->documentStates[$oid] = self::STATE_MANAGED;
        $this->originalDocumentData[$oid] = $data;
        $this->addToIdentityMap($document);
    }

    /**
     * INTERNAL:
     * Clears the property changeset of the document with the given OID.
     *
     * @param string $oid The document's OID.
     */
    public function clearDocumentChangeSet($oid)
    {
        unset($this->documentChangeSets[$oid]);
    }

    /* PropertyChangedListener implementation */

    /**
     * Notifies this UnitOfWork of a property change in an document.
     *
     * @param object $document The document that owns the property.
     * @param string $propertyName The name of the property that changed.
     * @param mixed $oldValue The old value of the property.
     * @param mixed $newValue The new value of the property.
     */
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
    {
        $oid = spl_object_hash($document);
        $class = $this->dm->getClassMetadata(get_class($document));

        if ( ! isset($class->fieldMappings[$propertyName])) {
            return; // ignore non-persistent fields
        }

        // Update changeset and mark document for synchronization
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
        if ( ! isset($this->scheduledForDirtyCheck[$class->rootDocumentName][$oid])) {
            $this->scheduleForDirtyCheck($document);
        }
    }

    /**
     * Gets the currently scheduled document insertions in this UnitOfWork.
     *
     * @return array
     */
    public function getScheduledDocumentInsertions()
    {
        return $this->documentInsertions;
    }

    /**
     * Gets the currently scheduled document updates in this UnitOfWork.
     *
     * @return array
     */
    public function getScheduledDocumentUpdates()
    {
        return $this->documentUpdates;
    }

    /**
     * Gets the currently scheduled document deletions in this UnitOfWork.
     *
     * @return array
     */
    public function getScheduledDocumentDeletions()
    {
        return $this->documentDeletions;
    }

    private static function objToStr($obj)
    {
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj).'@'.spl_object_hash($obj);
    }
}
Return current item: MongoDB Object Document Mapper