Location: PHPKode > projects > MongoDB Object Document Mapper > lib/Doctrine/ODM/MongoDB/DocumentManager.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\Mapping\ClassMetadata,
    Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory,
    Doctrine\ODM\MongoDB\Mapping\Driver\PHPDriver,
    Doctrine\ODM\MongoDB\Query,
    Doctrine\ODM\MongoDB\Mongo,
    Doctrine\ODM\MongoDB\PersistentCollection,
    Doctrine\ODM\MongoDB\Proxy\ProxyFactory,
    Doctrine\ODM\MongoDB\Query\Parser,
    Doctrine\Common\Collections\ArrayCollection,
    Doctrine\Common\EventManager;

/**
 * The DocumentManager class is the central access point for managing the
 * persistence of documents.
 *
 *     <?php
 *
 *     $config = new Configuration();
 *     $dm = DocumentManager::create(new Mongo(), $config);
 *
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link        www.doctrine-project.com
 * @since       1.0
 * @author      Jonathan H. Wage <hide@address.com>
 * @author      Roman Borschel <hide@address.com>
 */
class DocumentManager
{
    /**
     * The Doctrine Mongo wrapper instance
     *
     * @var Doctrine\ODM\MongoDB\Mongo
     */
    private $mongo;

    /**
     * The used Configuration.
     *
     * @var Doctrine\ODM\MongoDB\Configuration
     */
    private $config;

    /**
     * The metadata factory, used to retrieve the ODM metadata of document classes.
     *
     * @var Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory
     */
    private $metadataFactory;

    /**
     * The DocumentRepository instances.
     *
     * @var array
     */
    private $repositories = array();

    /**
     * The UnitOfWork used to coordinate object-level transactions.
     *
     * @var Doctrine\ODM\MongoDB\UnitOfWork
     */
    private $unitOfWork;

    /**
     * The event manager that is the central point of the event system.
     *
     * @var Doctrine\Common\EventManager
     */
    private $eventManager;

    /**
     * The Document hydrator instance.
     *
     * @var Doctrine\ODM\MongoDB\Hydrator
     */
    private $hydrator;

    /**
     * SchemaManager instance
     *
     * @var Doctrine\ODM\MongoDB\SchemaManager
     */
    private $schemaManager;

    /**
     * Array of cached MongoDB instances that are lazily loaded.
     *
     * @var array
     */
    private $documentDBs = array();

    /**
     * Array of cached MongoCollection instances that are lazily loaded.
     *
     * @var array
     */
    private $documentCollections = array();

    /**
     * The Query\Parser instance for parsing string based queries.
     *
     * @var Query\Parser $parser
     */
    private $queryParser;

    /**
     * Whether the DocumentManager is closed or not.
     */
    private $closed = false;

    /**
     * Creates a new Document that operates on the given Mongo connection
     * and uses the given Configuration.
     *
     * @param Doctrine\ODM\MongoDB\Mongo $mongo
     * @param Doctrine\ODM\MongoDB\Configuration $config
     * @param Doctrine\Common\EventManager $eventManager
     */
    protected function __construct(Mongo $mongo = null, Configuration $config = null, EventManager $eventManager = null)
    {
        if (is_string($mongo) || $mongo instanceof \Mongo) {
            $mongo = new Mongo($mongo);
        }
        $this->mongo = $mongo ? $mongo : new Mongo();
        $this->config = $config ? $config : new Configuration();
        $this->eventManager = $eventManager ? $eventManager : new EventManager();
        $this->hydrator = new Hydrator($this);
        $this->metadataFactory = new ClassMetadataFactory($this);
        if ($cacheDriver = $this->config->getMetadataCacheImpl()) {
            $this->metadataFactory->setCacheDriver($cacheDriver);
        }
        $this->queryParser = new Parser($this);
        $this->unitOfWork = new UnitOfWork($this);
        $this->schemaManager = new SchemaManager($this);
        $this->proxyFactory = new ProxyFactory($this,
                $this->config->getProxyDir(),
                $this->config->getProxyNamespace(),
                $this->config->getAutoGenerateProxyClasses());
    }

    /**
     * Gets the proxy factory used by the DocumentManager to create document proxies.
     *
     * @return ProxyFactory
     */
    public function getProxyFactory()
    {
        return $this->proxyFactory;
    }

    /**
     * Creates a new Document that operates on the given Mongo connection
     * and uses the given Configuration.
     *
     * @param Doctrine\ODM\MongoDB\Mongo $mongo
     * @param Doctrine\ODM\MongoDB\Configuration $config
     * @param Doctrine\Common\EventManager $eventManager
     */
    public static function create(Mongo $mongo, Configuration $config = null, EventManager $eventManager = null)
    {
        return new DocumentManager($mongo, $config, $eventManager);
    }

    /**
     * Determines whether an document instance is managed in this DocumentManager.
     *
     * @param object $document
     * @return boolean TRUE if this DocumentManager currently manages the given document, FALSE otherwise.
     */
    public function contains($document)
    {
        return $this->unitOfWork->isScheduledForInsert($document) ||
               $this->unitOfWork->isInIdentityMap($document) &&
               ! $this->unitOfWork->isScheduledForDelete($document);
    }

    /**
     * Gets the EventManager used by the DocumentManager.
     *
     * @return Doctrine\Common\EventManager
     */
    public function getEventManager()
    {
        return $this->eventManager;
    }

    public function getConfiguration()
    {
        return $this->config;
    }

    public function getMongo()
    {
        return $this->mongo;
    }

    /**
     * Gets the metadata factory used to gather the metadata of classes.
     *
     * @return Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory
     */
    public function getMetadataFactory()
    {
        return $this->metadataFactory;
    }

    /**
     * Gets the UnitOfWork used by the DocumentManager to coordinate operations.
     *
     * @return Doctrine\ODM\MongoDB\UnitOfWork
     */
    public function getUnitOfWork()
    {
        return $this->unitOfWork;
    }

    /**
     * Gets the Hydrator used by the DocumentManager to hydrate document arrays
     * to document objects.
     *
     * @return Doctrine\ODM\MongoDB\Hydrator
     */
    public function getHydrator()
    {
        return $this->hydrator;
    }

    /**
     * Retuns SchemaManager, used to create/drop indexes/collections/databases
     *
     * @return Doctrine\ODM\MongoDB\SchemaManager
     */
    public function getSchemaManager()
    {
        return $this->schemaManager;
    }

    /**
     * Returns the metadata for a class.
     *
     * @param string $className The class name.
     * @return Doctrine\ODM\MongoDB\Mapping\ClassMetadata
     * @internal Performance-sensitive method.
     */
    public function getClassMetadata($className)
    {
        return $this->metadataFactory->getMetadataFor($className);
    }

    /**
     * Returns the MongoDB instance for a class.
     *
     * @param string $className The class name.
     * @return Doctrine\ODM\MongoDB\MongoDB
     */
    public function getDocumentDB($className)
    {
        $db = $this->metadataFactory->getMetadataFor($className)->getDB();
        $db = $db ? $db : $this->config->getDefaultDB();
        $db = $db ? $db : 'doctrine';
        $db = sprintf('%s%s', $this->config->getEnvironmentPrefix(), $db);
        if ($db && ! isset($this->documentDBs[$db])) {
            $database = $this->mongo->selectDB($db);
            $this->documentDBs[$db] = new MongoDB($database);
        }
        if ( ! isset($this->documentDBs[$db])) {
            throw MongoDBException::documentNotMappedToDB($className);
        }
        return $this->documentDBs[$db];
    }

    /**
     * Returns the MongoCollection instance for a class.
     *
     * @param string $className The class name.
     * @return Doctrine\ODM\MongoDB\MongoCollection
     */
    public function getDocumentCollection($className)
    {
        $metadata = $this->metadataFactory->getMetadataFor($className);
        $db = $metadata->getDB();
        $collection = $metadata->getCollection();
        $key = $db . '.' . $collection . '.' . $className;
        if ($collection && ! isset($this->documentCollections[$key])) {
            if ($metadata->isFile()) {
                $collection = $this->getDocumentDB($className)->getGridFS($collection);
            } else {
                $collection = $this->getDocumentDB($className)->selectCollection($collection);
            }
            $mongoCollection = new MongoCollection($collection, $metadata, $this);
            $this->documentCollections[$key] = $mongoCollection;
        }
        if ( ! isset($this->documentCollections[$key])) {
            throw MongoDBException::documentNotMappedToCollection($className);
        }
        return $this->documentCollections[$key];
    }

    public function query($queryString, $parameters = array())
    {
        if ( ! is_array($parameters)) {
            $parameters = array($parameters);
        }
        return $this->queryParser->parse($queryString, $parameters);
    }

    /**
     * Create a new Query instance for a class.
     *
     * @param string $documentName The document class name.
     * @return Document\ODM\MongoDB\Query
     */
    public function createQuery($documentName = null)
    {
        return new Query($this, $documentName);
    }

    /**
     * Tells the DocumentManager to make an instance managed and persistent.
     *
     * The document will be entered into the database at or before transaction
     * commit or as a result of the flush operation.
     *
     * NOTE: The persist operation always considers documents that are not yet known to
     * this DocumentManager as NEW. Do not pass detached documents to the persist operation.
     *
     * @param object $document The instance to make managed and persistent.
     */
    public function persist($document)
    {
        if ( ! is_object($document)) {
            throw new \InvalidArgumentException(gettype($document));
        }
        $this->errorIfClosed();
        $this->unitOfWork->persist($document);
    }

    /**
     * Removes a document instance.
     *
     * A removed document will be removed from the database at or before transaction commit
     * or as a result of the flush operation.
     *
     * @param object $document The document instance to remove.
     */
    public function remove($document)
    {
        if ( ! is_object($document)) {
            throw new \InvalidArgumentException(gettype($document));
        }
        $this->errorIfClosed();
        $this->unitOfWork->remove($document);
    }

    /**
     * Refreshes the persistent state of a document from the database,
     * overriding any local changes that have not yet been persisted.
     *
     * @param object $document The document to refresh.
     */
    public function refresh($document)
    {
        if ( ! is_object($document)) {
            throw new \InvalidArgumentException(gettype($document));
        }
        $this->errorIfClosed();
        $this->unitOfWork->refresh($document);
    }

    /**
     * Detaches a document from the DocumentManager, causing a managed document to
     * become detached.  Unflushed changes made to the document if any
     * (including removal of the document), will not be synchronized to the database.
     * Documents which previously referenced the detached document will continue to
     * reference it.
     *
     * @param object $document The document to detach.
     */
    public function detach($document)
    {
        if ( ! is_object($document)) {
            throw new \InvalidArgumentException(gettype($document));
        }
        $this->unitOfWork->detach($document);
    }

    /**
     * Merges the state of a detached document into the persistence context
     * of this DocumentManager and returns the managed copy of the document.
     * The document passed to merge will not become associated/managed with this DocumentManager.
     *
     * @param object $document The detached document to merge into the persistence context.
     * @return object The managed copy of the document.
     */
    public function merge($document)
    {
        if ( ! is_object($document)) {
            throw new \InvalidArgumentException(gettype($document));
        }
        $this->errorIfClosed();
        return $this->unitOfWork->merge($document);
    }

    /**
     * Gets the repository for a document class.
     *
     * @param string $documentName  The name of the Document.
     * @return DocumentRepository  The repository.
     */
    public function getRepository($documentName)
    {
        if (isset($this->repositories[$documentName])) {
            return $this->repositories[$documentName];
        }

        $metadata = $this->getClassMetadata($documentName);
        $customRepositoryClassName = $metadata->customRepositoryClassName;

        if ($customRepositoryClassName !== null) {
            $repository = new $customRepositoryClassName($this, $metadata);
        } else {
            $repository = new DocumentRepository($this, $metadata);
        }

        $this->repositories[$documentName] = $repository;

        return $repository;
    }

    /**
     * Loads a given document by its ID refreshing the values with the data from
     * the database if the document already exists in the identity map.
     *
     * @param string $documentName The document name to load.
     * @param string $id  The id the document to load.
     * @return object $document  The loaded document.
     * @todo this function seems to be doing to much, should we move parts of it
     * to BasicDocumentPersister maybe?
     */
    public function loadByID($documentName, $id)
    {
        $class = $this->getClassMetadata($documentName);
        $collection = $this->getDocumentCollection($documentName);

        $result = $collection->findOne(array('_id' => $class->getDatabaseIdentifierValue($id)));

        if ( ! $result) {
            return null;
        }
        return $this->load($documentName, $id, $result);
    }

    /**
     * Loads data for a document id refreshing and overriding any local values
     * if the document already exists in the identity map.
     *
     * @param string $documentName  The document name to load.
     * @param string $id  The id of the document being loaded.
     * @param string $data  The data to load into the document.
     * @return object $document The loaded document.
     */
    public function load($documentName, $id, $data)
    {
        if ($data !== null) {
            $hints = array(Query::HINT_REFRESH => Query::HINT_REFRESH);
            $document = $this->unitOfWork->getOrCreateDocument($documentName, $data, $hints);
            return $document;
        }
        return false;
    }

    /**
     * Flushes all changes to objects that have been queued up to now to the database.
     * This effectively synchronizes the in-memory state of managed objects with the
     * database.
     *
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
     */
    public function flush(array $options = array())
    {
        $this->errorIfClosed();
        $this->unitOfWork->commit($options);
    }

    /**
     * Execute a map reduce operation.
     *
     * @param string $documentName The document name to run the operation on.
     * @param string $map The javascript map function.
     * @param string $reduce The javascript reduce function.
     * @param array $query The mongo query.
     * @param array $options Array of options.
     * @return MongoCursor $cursor
     */
    public function mapReduce($documentName, $map, $reduce, array $query = array(), array $options = array())
    {
        $class = $this->getClassMetadata($documentName);
        $db = $this->getDocumentDB($documentName);
        if (is_string($map)) {
            $map = new \MongoCode($map);
        }
        if (is_string($reduce)) {
            $reduce = new \MongoCode($reduce);
        }
        $command = array(
            'mapreduce' => $class->getCollection(),
            'map' => $map,
            'reduce' => $reduce,
            'query' => $query
        );
        $command = array_merge($command, $options);
        $result = $db->command($command);
        if ( ! $result['ok']) {
            throw new \RuntimeException($result['errmsg']);
        }
        $cursor = $db->selectCollection($result['result'])->find();
        $cursor = new MongoCursor($this, $this->hydrator, $class, $cursor);
        $cursor->hydrate(false);
        return $cursor;
    }

    /**
     * Gets a reference to the document identified by the given type and identifier
     * without actually loading it.
     *
     * If partial objects are allowed, this method will return a partial object that only
     * has its identifier populated. Otherwise a proxy is returned that automatically
     * loads itself on first access.
     *
     * @return object The document reference.
     */
    public function getReference($documentName, $identifier)
    {
        $class = $this->metadataFactory->getMetadataFor($documentName);

        // Check identity map first, if its already in there just return it.
        if ($document = $this->unitOfWork->tryGetById($identifier, $class->rootDocumentName)) {
            return $document;
        }
        $document = $this->proxyFactory->getProxy($class->name, $identifier);
        $this->unitOfWork->registerManaged($document, $identifier, array());

        return $document;
    }

    /**
     * Gets a partial reference to the document identified by the given type and identifier
     * without actually loading it, if the document is not yet loaded.
     *
     * The returned reference may be a partial object if the document is not yet loaded/managed.
     * If it is a partial object it will not initialize the rest of the document state on access.
     * Thus you can only ever safely access the identifier of an document obtained through
     * this method.
     *
     * The use-cases for partial references involve maintaining bidirectional associations
     * without loading one side of the association or to update an document without loading it.
     * Note, however, that in the latter case the original (persistent) document data will
     * never be visible to the application (especially not event listeners) as it will
     * never be loaded in the first place.
     *
     * @param string $documentName The name of the document type.
     * @param mixed $identifier The document identifier.
     * @return object The (partial) document reference.
     */
    public function getPartialReference($documentName, $identifier)
    {
        $class = $this->metadataFactory->getMetadataFor($documentName);

        // Check identity map first, if its already in there just return it.
        if ($entity = $this->unitOfWork->tryGetById($identifier, $class->rootDocumentName)) {
            return $entity;
        }
        $document = $class->newInstance();
        $class->setIdentifierValue($document, $identifier);
        $this->unitOfWork->registerManaged($document, $identifier, array());

        return $document;
    }

    /**
     * Find a single document by its identifier or multiple by a given criteria.
     *
     * @param string $documentName The document to find.
     * @param mixed $query A single identifier or an array of criteria.
     * @param array $select The fields to select.
     * @return Doctrine\ODM\MongoDB\MongoCursor $cursor
     * @return object $document
     */
    public function find($documentName, $query = array(), array $select = array())
    {
        if (is_array($documentName)) {
            $classNames = $documentName;
            $documentName = $classNames[0];

            $discriminatorField = $this->getClassMetadata($documentName)->discriminatorField['name'];
            $discriminatorValues = $this->getDiscriminatorValues($classNames);
            $query[$discriminatorField] = array('$in' => $discriminatorValues);
        }
        return $this->getRepository($documentName)->find($query, $select);
    }

    /**
     * Find a single document with the given query and select fields.
     *
     * @param string $documentName The document to find.
     * @param array $query The query criteria.
     * @param array $select The fields to select
     * @return object $document
     */
    public function findOne($documentName, array $query = array(), array $select = array())
    {
        return $this->getRepository($documentName)->findOne($query, $select);
    }

    /**
     * Clears the DocumentManager. All documents that are currently managed
     * by this DocumentManager become detached.
     */
    public function clear()
    {
        $this->unitOfWork->clear();
    }

    /**
     * Closes the DocumentManager. All documents that are currently managed
     * by this DocumentManager become detached. The DocumentManager may no longer
     * be used after it is closed.
     */
    public function close()
    {
        $this->clear();
        $this->closed = true;
    }

    public function formatDBName($dbName)
    {
        return sprintf('%s%s%s',
            $this->config->getDBPrefix(),
            $dbName,
            $this->config->getDBSuffix()
        );
    }

    public function getDiscriminatorValues($classNames)
    {
        $discriminatorValues = array();
        $collections = array();
        foreach ($classNames as $className) {
            $class = $this->getClassMetadata($className);
            $discriminatorValues[] = $class->discriminatorValue;
            $key = $class->getDB() . '.' . $class->getCollection();
            $collections[$key] = $key;
        }
        if (count($collections) > 1) {
            throw new \InvalidArgumentException('Documents involved are not all mapped to the same database collection.');
        }
        return $discriminatorValues;
    }

    public function getClassNameFromDiscriminatorValue(array $mapping, $value)
    {
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : '_doctrine_class_name';
        if (isset($value[$discriminatorField])) {
            $discriminatorValue = $value[$discriminatorField];
            return isset($mapping['discriminatorMap'][$discriminatorValue]) ? $mapping['discriminatorMap'][$discriminatorValue] : $discriminatorValue;
        } else {
            return $mapping['targetDocument'];
        }
    }

    /**
     * Throws an exception if the DocumentManager is closed or currently not active.
     *
     * @throws MongoDBException If the DocumentManager is closed.
     */
    private function errorIfClosed()
    {
        if ($this->closed) {
            throw MongoDBException::documentManagerClosed();
        }
    }
}
Return current item: MongoDB Object Document Mapper