Location: PHPKode > projects > MongoDB Object Document Mapper > lib/Doctrine/ODM/MongoDB/Persisters/BasicDocumentPersister.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\Persisters;

use Doctrine\ODM\MongoDB\DocumentManager,
    Doctrine\ODM\MongoDB\UnitOfWork,
    Doctrine\ODM\MongoDB\Mapping\ClassMetadata,
    Doctrine\ODM\MongoDB\MongoCursor,
    Doctrine\ODM\MongoDB\Mapping\Types\Type,
    Doctrine\Common\Collections\Collection,
    Doctrine\ODM\MongoDB\ODMEvents,
    Doctrine\ODM\MongoDB\Event\OnUpdatePreparedArgs,
    Doctrine\ODM\MongoDB\MongoDBException,
    Doctrine\ODM\MongoDB\PersistentCollection;

/**
 * The BasicDocumentPersister is responsible for actual persisting the calculated
 * changesets performed by the UnitOfWork.
 *
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @since       1.0
 * @author      Jonathan H. Wage <hide@address.com>
 * @author      Bulat Shakirzyanov <hide@address.com>
 */
class BasicDocumentPersister
{
    /**
     * The DocumentManager instance.
     *
     * @var Doctrine\ODM\MongoDB\DocumentManager
     */
    private $dm;

    /**
     * The UnitOfWork instance.
     *
     * @var Doctrine\ODM\MongoDB\UnitOfWork
     */
    private $uow;

    /**
     * The ClassMetadata instance for the document type being persisted.
     *
     * @var Doctrine\ODM\MongoDB\Mapping\ClassMetadata
     */
    protected $class;

    /**
     * The MongoCollection instance for this document.
     *
     * @var Doctrine\ODM\MongoDB\MongoCollection
     */
    private $collection;

    /**
     * The string document name being persisted.
     *
     * @var string
     */
    private $documentName;

    /**
     * Array of quered inserts for the persister to insert.
     *
     * @var array
     */
    private $queuedInserts = array();

    /**
     * Documents to be updated, used in executeReferenceUpdates() method
     * @var array
     */
    private $documentsToUpdate = array();

    /**
     * Fields to update, used in executeReferenceUpdates() method
     * @var array
     */
    private $fieldsToUpdate = array();

    /**
     * Mongo command prefix
     * @var string
     */
    private $cmd;

    /**
     * Initializes a new BasicDocumentPersister instance.
     *
     * @param Doctrine\ODM\MongoDB\DocumentManager $dm
     * @param Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
     */
    public function __construct(DocumentManager $dm, ClassMetadata $class)
    {
        $this->dm = $dm;
        $this->uow = $dm->getUnitOfWork();
        $this->class = $class;
        $this->documentName = $class->getName();
        $this->collection = $dm->getDocumentCollection($class->name);
        $this->cmd = $this->dm->getConfiguration()->getMongoCmd();
    }

    /**
     * Adds a document to the queued insertions.
     * The document remains queued until {@link executeInserts} is invoked.
     *
     * @param object $document The document to queue for insertion.
     */
    public function addInsert($document)
    {
        $this->queuedInserts[spl_object_hash($document)] = $document;
    }

    /**
     * Executes all queued document insertions and returns any generated post-insert
     * identifiers that were created as a result of the insertions.
     *
     * If no inserts are queued, invoking this method is a NOOP.
     *
     * @param array $options Array of options to be used with batchInsert()
     * @return array An array of any generated post-insert IDs. This will be an empty array
     *               if the document class does not use the IDENTITY generation strategy.
     */
    public function executeInserts(array $options = array())
    {
        if ( ! $this->queuedInserts) {
            return;
        }

        $postInsertIds = array();
        $inserts = array();
        foreach ($this->queuedInserts as $oid => $document) {
            $data = $this->prepareInsertData($document);
            if ( ! $data) {
                continue;
            }
            $inserts[$oid] = $data;
        }
        if (empty($inserts)) {
            return;
        }
        $this->collection->batchInsert($inserts, $options);

        foreach ($inserts as $oid => $data) {
            $document = $this->queuedInserts[$oid];
            $postInsertIds[] = array($data['_id'], $document);
            if ($this->class->isFile()) {
                $this->dm->getHydrator()->hydrate($document, $data);
            }
        }
        $this->queuedInserts = array();

        return $postInsertIds;
    }

    /**
     * Executes reference updates in case document had references to new documents,
     * without identifier value.
     *
     * @param array $options Array of options to be used with update()
     */
    public function executeReferenceUpdates(array $options = array())
    {
        foreach ($this->documentsToUpdate as $oid => $document) {
            $update = array();
            foreach ($this->fieldsToUpdate[$oid] as $fieldName => $fieldData) {
                list($mapping, $value) = $fieldData;
                $update[$fieldName] = $this->prepareValue($mapping, $value);
            }
            $classMetadata = $this->dm->getClassMetadata(get_class($document));
            $id = $this->uow->getDocumentIdentifier($document);
            $id = $classMetadata->getDatabaseIdentifierValue($id);
            $this->collection->update(array(
                '_id' => $id
            ), array(
                $this->cmd . 'set' => $update
            ), $options);
        }
        $this->documentsToUpdate = array();
        $this->fieldsToUpdate = array();
    }

    /**
     * Updates the already persisted document if it has any new changesets.
     *
     * @param object $document
     * @param array $options Array of options to be used with update()
     */
    public function update($document, array $options = array())
    {
        $id = $this->uow->getDocumentIdentifier($document);
        $update = $this->prepareUpdateData($document);

        if ( ! empty($update)) {
            if ($this->dm->getEventManager()->hasListeners(ODMEvents::onUpdatePrepared)) {
                $this->dm->getEventManager()->dispatchEvent(
                    ODMEvents::onUpdatePrepared, new OnUpdatePreparedArgs($this->dm, $document, $update)
                );
            }
            $id = $this->class->getDatabaseIdentifierValue($id);

            if ((isset($update[$this->cmd . 'pushAll']) || isset($update[$this->cmd . 'pullAll'])) && isset($update[$this->cmd . 'set'])) {
                $tempUpdate = array($this->cmd . 'set' => $update[$this->cmd . 'set']);
                unset($update[$this->cmd . 'set']);
                $this->collection->update(array('_id' => $id), $tempUpdate, $options);
            }

            /**
             * temporary fix for @link http://jira.mongodb.org/browse/SERVER-1050
             * atomic modifiers $pushAll and $pullAll, $push, $pop and $pull
             * are not allowed on the same field in one update
             */
            if (isset($update[$this->cmd . 'pushAll']) && isset($update[$this->cmd . 'pullAll'])) {
                $fields = array_intersect(
                    array_keys($update[$this->cmd . 'pushAll']),
                    array_keys($update[$this->cmd . 'pullAll'])
                );
                if ( ! empty($fields)) {
                    $tempUpdate = array();
                    foreach ($fields as $field) {
                        $tempUpdate[$field] = $update[$this->cmd . 'pullAll'][$field];
                        unset($update[$this->cmd . 'pullAll'][$field]);
                    }
                    if (empty($update[$this->cmd . 'pullAll'])) {
                        unset($update[$this->cmd . 'pullAll']);
                    }
                    $tempUpdate = array(
                        $this->cmd . 'pullAll' => $tempUpdate
                    );
                    $this->collection->update(array('_id' => $id), $tempUpdate, $options);
                }
            }

            $this->collection->update(array('_id' => $id), $update, $options);
        }
    }

    /**
     * Removes document from mongo
     *
     * @param mixed $document
     * @param array $options Array of options to be used with remove()
     */
    public function delete($document, array $options = array())
    {
        $id = $this->uow->getDocumentIdentifier($document);

        $this->collection->remove(array(
            '_id' => $this->class->getDatabaseIdentifierValue($id)
        ), $options);
    }

    /**
     * Gets the ClassMetadata instance of the document class this persister is used for.
     *
     * @return Doctrine\ODM\MongoDB\Mapping\ClassMetadata
     */
    public function getClassMetadata()
    {
        return $this->class;
    }

    /**
     * Refreshes a managed document.
     *
     * @param object $document The document to refresh.
     */
    public function refresh($document)
    {
        $id = $this->uow->getDocumentIdentifier($document);
        if ($this->dm->loadByID($this->class->name, $id) === null) {
            throw new \InvalidArgumentException(sprintf('Could not loadByID because ' . $this->class->name . ' '.$id . ' does not exist anymore.'));
        }
    }

    /**
     * Loads an document by a list of field criteria.
     *
     * @param array $query The criteria by which to load the document.
     * @param object $document The document to load the data into. If not specified,
     *        a new document is created.
     * @param $assoc The association that connects the document to load to another document, if any.
     * @param array $hints Hints for document creation.
     * @return object The loaded and managed document instance or NULL if the document can not be found.
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
     * @todo Modify DocumentManager to use this method instead of its own hard coded
     */
    public function load(array $query = array(), array $select = array())
    {
        $result = $this->collection->findOne($query, $select);
        if ($result !== null) {
            return $this->uow->getOrCreateDocument($this->documentName, $result);
        }
        return null;
    }

    /**
     * Lood document by its identifier.
     *
     * @param string $id
     * @return object|null
     */
    public function loadById($id)
    {
        $result = $this->collection->findOne(array(
            '_id' => $this->class->getDatabaseIdentifierValue($id)
        ));
        if ($result !== null) {
            return $this->uow->getOrCreateDocument($this->documentName, $result);
        }
        return null;
    }

    /**
     * Loads a list of documents by a list of field criteria.
     *
     * @param array $criteria
     * @return array
     */
    public function loadAll(array $query = array(), array $select = array())
    {
        $cursor = $this->collection->find($query, $select);
        return new MongoCursor($this->dm, $this->dm->getHydrator(), $this->class, $cursor);
    }

    /**
     * Checks whether the given managed document exists in the database.
     *
     * @param object $document
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
     */
    public function exists($document)
    {
        $id = $this->class->getIdentifierObject($document);
        return (bool) $this->collection->findOne(array(array('_id' => $id)), array('_id'));
    }

    /**
     * Prepares insert data for document
     *
     * @param mixed $document
     * @return array
     */
    public function prepareInsertData($document)
    {
        $oid = spl_object_hash($document);
        $changeset = $this->uow->getDocumentChangeSet($document);
        $insertData = array();
        foreach ($this->class->fieldMappings as $mapping) {
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
                continue;
            }
            $new = isset($changeset[$mapping['fieldName']][1]) ? $changeset[$mapping['fieldName']][1] : null;
            if ($new === null && $mapping['nullable'] === false) {
                continue;
            }
            if ($this->class->isIdentifier($mapping['fieldName'])) {
                $insertData['_id'] = $this->prepareValue($mapping, $new);
                continue;
            }
            $current = $this->class->getFieldValue($document, $mapping['fieldName']);
            $value = $this->prepareValue($mapping, $current);
            if ($value === null && $mapping['nullable'] === false) {
                continue;
            }

            $insertData[$mapping['name']] = $value;
            if (isset($mapping['reference'])) {
                $scheduleForUpdate = false;
                if ($mapping['type'] === 'one') {
                    if ( ! isset($insertData[$mapping['name']][$this->cmd . 'id'])) {
                        $scheduleForUpdate = true;
                    }
                } elseif ($mapping['type'] === 'many') {
                    foreach ($insertData[$mapping['name']] as $ref) {
                        if ( ! isset($ref[$this->cmd . 'id'])) {
                            $scheduleForUpdate = true;
                            break;
                        }
                    }
                }
                if ($scheduleForUpdate) {
                    unset($insertData[$mapping['name']]);
                    $id = spl_object_hash($document);
                    $this->documentsToUpdate[$id] = $document;
                    $this->fieldsToUpdate[$id][$mapping['fieldName']] = array($mapping, $new);
                }
            }
        }
        // add discriminator if the class has one
        if ($this->class->hasDiscriminator()) {
            $insertData[$this->class->discriminatorField['name']] = $this->class->discriminatorValue;
        }
        return $insertData;
    }

    /**
     * Prepares update array for document, using atomic operators
     *
     * @param mixed $document
     * @return array
     */
    public function prepareUpdateData($document)
    {
        $oid = spl_object_hash($document);
        $class = $this->dm->getClassMetadata(get_class($document));
        $changeset = $this->uow->getDocumentChangeSet($document);
        $result = array();
        foreach ($class->fieldMappings as $mapping) {
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
                continue;
            }
            $old = isset($changeset[$mapping['fieldName']][0]) ? $changeset[$mapping['fieldName']][0] : null;
            $new = isset($changeset[$mapping['fieldName']][1]) ? $changeset[$mapping['fieldName']][1] : null;
            $current = $class->getFieldValue($document, $mapping['fieldName']);

            if ($mapping['type'] === 'many' || $mapping['type'] === 'collection') {
                $mapping['strategy'] = isset($mapping['strategy']) ? $mapping['strategy'] : 'pushPull';
                if ($mapping['strategy'] === 'pushPull') {
                    if (isset($mapping['embedded']) && $new) {
                        foreach ($new as $k => $v) {
                            if ( ! isset($old[$k])) {
                                continue;
                            }
                            $update = $this->prepareUpdateData($current[$k]);
                            foreach ($update as $cmd => $values) {
                                foreach ($values as $key => $value) {
                                    $result[$cmd][$mapping['name'] . '.' . $k . '.' . $key] = $value;
                                }
                            }
                        }
                    }
                    if ($old !== $new) {
                        if ($mapping['type'] === 'collection' || isset($mapping['reference'])) {
                            $old = $old ? $old : array();
                            $new = $new ? $new : array();
                            $compare = function($a, $b) {
                                return $a === $b ? 0 : 1;
                            };
                            $deleteDiff = array_udiff_assoc($old, $new, $compare);
                            $insertDiff = array_udiff_assoc($new, $old, $compare);
                        } elseif (isset($mapping['embedded'])) {
                            $deleteDiff = $current->getDeleteDiff();
                            $insertDiff = $current->getInsertDiff();
                        }

                        // insert diff
                        if ($insertDiff) {
                            $result[$this->cmd . 'pushAll'][$mapping['name']] = $this->prepareValue($mapping, $insertDiff);
                        }
                        // delete diff
                        if ($deleteDiff) {
                            $result[$this->cmd . 'pullAll'][$mapping['name']] = $this->prepareValue($mapping, $deleteDiff);
                        }
                    }
                } elseif ($mapping['strategy'] === 'set') {
                    if ($old !== $new) {
                        $new = $this->prepareValue($mapping, $current);
                        $result[$this->cmd . 'set'][$mapping['name']] = $new;
                    }
                }
            } else {
                if ($old !== $new) {
                    if ($mapping['type'] === 'increment') {
                        $new = $this->prepareValue($mapping, $new);
                        $old = $this->prepareValue($mapping, $old);
                        if ($new >= $old) {
                            $result[$this->cmd . 'inc'][$mapping['name']] = $new - $old;
                        } else {
                            $result[$this->cmd . 'inc'][$mapping['name']] = ($old - $new) * -1;
                        }
                    } else {
                        // Single embedded
                        if (isset($mapping['embedded']) && $mapping['type'] === 'one') {
                            // If we didn't have a value before and now we do
                            if ( ! $old && $new) {
                                $new = $this->prepareValue($mapping, $current);
                                if (isset($new) || $mapping['nullable'] === true) {
                                    $result[$this->cmd . 'set'][$mapping['name']] = $new;
                                }
                            // If we had an old value before and it has changed
                            } elseif ($old && $new) {
                                $update = $this->prepareUpdateData($current);
                                foreach ($update as $cmd => $values) {
                                    foreach ($values as $key => $value) {
                                        $result[$cmd][$mapping['name'] . '.' . $key] = $value;
                                    }
                                }
                            // If we had an old value before and now we don't
                            } elseif ($old && !$new) {
                                if ($mapping['nullable'] === true) {
                                    $result[$this->cmd . 'set'][$mapping['name']] = null;
                                }
                            }
                        // $set all other fields
                        } else {
                            $new = $this->prepareValue($mapping, $current);
                            if (isset($new) || $mapping['nullable'] === true) {
                                $result[$this->cmd . 'set'][$mapping['name']] = $new;
                            } else {
                                $result[$this->cmd . 'unset'][$mapping['name']] = true;
                            }
                        }
                    }
                }
            }
        }
        return $result;
    }

    /**
     *
     * @param array $mapping
     * @param mixed $value
     */
    private function prepareValue(array $mapping, $value)
    {
        if ($value === null) {
            return null;
        }
        if ($mapping['type'] === 'many') {
            $prepared = array();
            $oneMapping = $mapping;
            $oneMapping['type'] = 'one';
            foreach ($value as $rawValue) {
                $prepared[] = $this->prepareValue($oneMapping, $rawValue);
            }
            if (empty($prepared)) {
                $prepared = null;
            }
        } elseif (isset($mapping['reference']) || isset($mapping['embedded'])) {
            if (isset($mapping['embedded'])) {
                $prepared = $this->prepareEmbeddedDocValue($mapping, $value);
            } elseif (isset($mapping['reference'])) {
                $prepared = $this->prepareReferencedDocValue($mapping, $value);
            }
        } else {
            $prepared = Type::getType($mapping['type'])->convertToDatabaseValue($value);
        }
        return $prepared;
    }

    /**
     * Returns the reference representation to be stored in mongodb or null if not applicable.
     *
     * @param array $referenceMapping
     * @param Document $document
     * @return array|null
     */
    private function prepareReferencedDocValue(array $referenceMapping, $document)
    {
        $id = null;
        if (is_array($document)) {
            $className = $referenceMapping['targetDocument'];
        } else {
            $className = get_class($document);
            $id = $this->uow->getDocumentIdentifier($document);
        }
        $class = $this->dm->getClassMetadata($className);
        if (null !== $id) {
            $id = $class->getDatabaseIdentifierValue($id);
        }
        $ref = array(
            $this->cmd . 'ref' => $class->getCollection(),
            $this->cmd . 'id' => $id,
            $this->cmd . 'db' => $class->getDB()
        );
        if ( ! isset($referenceMapping['targetDocument'])) {
            $discriminatorField = isset($referenceMapping['discriminatorField']) ? $referenceMapping['discriminatorField'] : '_doctrine_class_name';
            $discriminatorValue = isset($referenceMapping['discriminatorMap']) ? array_search($class->getName(), $referenceMapping['discriminatorMap']) : $class->getName();
            $ref[$discriminatorField] = $discriminatorValue;
        }
        return $ref;
    }

    /**
     * Prepares array of values to be stored in mongo to represent embedded object.
     *
     * @param array $embeddedMapping
     * @param Document $embeddedDocument
     * @return array
     */
    private function prepareEmbeddedDocValue(array $embeddedMapping, $embeddedDocument)
    {
        $className = get_class($embeddedDocument);
        $class = $this->dm->getClassMetadata($className);
        $embeddedDocumentValue = array();
        foreach ($class->fieldMappings as $mapping) {
            // Skip not saved fields
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
                continue;
            }

            $rawValue = $class->getFieldValue($embeddedDocument, $mapping['fieldName']);

            // Don't store null values unless nullable is specified
            if ($rawValue === null && $mapping['nullable'] === false) {
                continue;
            }
            if (isset($mapping['embedded']) || isset($mapping['reference'])) {
                if (isset($mapping['embedded'])) {
                    if ($mapping['type'] == 'many') {
                        $value = array();
                        foreach ($rawValue as $embeddedDoc) {
                            $value[] = $this->prepareEmbeddedDocValue($mapping, $embeddedDoc);
                        }
                        if (empty($value)) {
                            $value = null;
                        }
                    } elseif ($mapping['type'] == 'one') {
                        $value = $this->prepareEmbeddedDocValue($mapping, $rawValue);
                    }
                } elseif (isset($mapping['reference'])) {
                    if ($mapping['type'] == 'many') {
                        $value = array();
                        foreach ($rawValue as $referencedDoc) {
                            $value[] = $this->prepareReferencedDocValue($mapping, $referencedDoc);
                        }
                        if (empty($value)) {
                            $value = null;
                        }
                    } else {
                        $value = $this->prepareReferencedDocValue($mapping, $rawValue);
                    }
                }
            } else {
                $value = Type::getType($mapping['type'])->convertToDatabaseValue($rawValue);
            }
            if ($value === null && $mapping['nullable'] === false) {
                continue;
            }
            $embeddedDocumentValue[$mapping['name']] = $value;
        }
        if ( ! isset($embeddedMapping['targetDocument'])) {
            $discriminatorField = isset($embeddedMapping['discriminatorField']) ? $embeddedMapping['discriminatorField'] : '_doctrine_class_name';
            $discriminatorValue = isset($embeddedMapping['discriminatorMap']) ? array_search($class->getName(), $embeddedMapping['discriminatorMap']) : $class->getName();
            $embeddedDocumentValue[$discriminatorField] = $discriminatorValue;
        }
        return $embeddedDocumentValue;
    }
}
Return current item: MongoDB Object Document Mapper