/**
     * 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);
                }
            }

            if ($class->isVersioned) {
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
                // Throw exception if versions dont match.
                if ($managedCopyVersion != $documentVersion) {
                    throw LockException::lockFailedVersionMissmatch($documentVersion, $managedCopyVersion);
                }
            }

            // 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, $this->dm->getConfiguration());
                            $mergeCol->setInitialized(true);
                        }
                        $mergeCol->setOwner($managedCopy, $assoc2);
                        $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;
    }
Example #2
0
 /**
  * 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 object $document
  * @param array $options Array of options to be used with batchInsert(), update() and remove()
  */
 public function commit($document = null, array $options = array())
 {
     // Raise preFlush
     if ($this->evm->hasListeners(Events::preFlush)) {
         $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
     }
     $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
     if ($options) {
         $options = array_merge($defaultOptions, $options);
     } else {
         $options = $defaultOptions;
     }
     // Compute changes done since last commit.
     if ($document === null) {
         $this->computeChangeSets();
     } else {
         if (is_object($document)) {
             $this->computeSingleDocumentChangeSet($document);
         } else {
             if (is_array($document)) {
                 foreach ($document as $object) {
                     $this->computeSingleDocumentChangeSet($object);
                 }
             }
         }
     }
     if (!($this->documentInsertions || $this->documentUpserts || $this->documentDeletions || $this->documentUpdates || $this->collectionUpdates || $this->collectionDeletions || $this->orphanRemovals)) {
         return;
         // Nothing to do.
     }
     if ($this->orphanRemovals) {
         foreach ($this->orphanRemovals as $removal) {
             $this->remove($removal);
         }
     }
     // Raise onFlush
     if ($this->evm->hasListeners(Events::onFlush)) {
         $this->evm->dispatchEvent(Events::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);
         }
     }
     if ($this->documentUpdates) {
         foreach ($commitOrder as $class) {
             $this->executeUpdates($class, $options);
         }
     }
     // Extra updates that were requested by persisters.
     if ($this->extraUpdates) {
         $this->executeExtraUpdates($options);
     }
     // Collection deletions (deletions of complete collections)
     foreach ($this->collectionDeletions as $collectionToDelete) {
         $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete, $options);
     }
     // Collection updates (deleteRows, updateRows, insertRows)
     foreach ($this->collectionUpdates as $collectionToUpdate) {
         $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate, $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();
     }
     // Raise postFlush
     if ($this->evm->hasListeners(Events::postFlush)) {
         $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
     }
     // Clear up
     $this->documentInsertions = $this->documentUpserts = $this->documentUpdates = $this->documentDeletions = $this->extraUpdates = $this->documentChangeSets = $this->collectionUpdates = $this->collectionDeletions = $this->visitedCollections = $this->scheduledForDirtyCheck = $this->orphanRemovals = array();
 }
    /**
     * Hydrate array of MongoDB document data into the given document object.
     *
     * @param object $document  The document object to hydrate the data into.
     * @param array $data The array of document data.
     * @return array $values The array of hydrated values.
     */
    public function hydrate($document, &$data)
    {
        $metadata = $this->dm->getClassMetadata(get_class($document));

        if (isset($metadata->lifecycleCallbacks[Events::preLoad])) {
            $args = array(&$data);
            $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args);
        }
        if ($this->evm->hasListeners(Events::preLoad)) {
            $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data));
        }

        if (isset($metadata->alsoLoadMethods)) {
            foreach ($metadata->alsoLoadMethods as $fieldName => $method) {
                if (isset($data[$fieldName])) {
                    $document->$method($data[$fieldName]);
                }
            }
        }
        foreach ($metadata->fieldMappings as $mapping) {
            if (isset($mapping['alsoLoadFields'])) {
                $rawValue = null;
                $names = isset($mapping['alsoLoadFields']) ? $mapping['alsoLoadFields'] : array();
                array_unshift($names, $mapping['name']);
                foreach ($names as $name) {
                    if (isset($data[$name])) {
                        $rawValue = $data[$name];
                        break;
                    }
                }
            } else {
                $rawValue = isset($data[$mapping['name']]) ? $data[$mapping['name']] : null;
            }
            $value = null;

            if (isset($mapping['embedded'])) {
                $uow = $this->dm->getUnitOfWork();
                if ($mapping['type'] === 'one') {
                    if ($rawValue === null) {
                        continue;
                    }
                    $embeddedDocument = $rawValue;
                    $className = $this->dm->getClassNameFromDiscriminatorValue($mapping, $embeddedDocument);
                    $embeddedMetadata = $this->dm->getClassMetadata($className);
                    $value = $embeddedMetadata->newInstance();

                    // unset a potential discriminator map field (unless it's a persisted property)
                    $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : '_doctrine_class_name';
                    if (!isset($embeddedMetadata->fieldMappings[$discriminatorField])) {
                        unset($embeddedDocument[$discriminatorField]);
                    }

                    $this->hydrate($value, $embeddedDocument);
                    $uow->registerManaged($value, null, $embeddedDocument);
                    $uow->setParentAssociation($value, $mapping, $document, $mapping['name']);
                } elseif ($mapping['type'] === 'many') {
                    $embeddedDocuments = $rawValue;
                    $coll = new PersistentCollection(new ArrayCollection(), $this->dm, $this->dm->getConfiguration());
                    if ($embeddedDocuments) {
                        foreach ($embeddedDocuments as $key => $embeddedDocument) {
                            $className = $this->dm->getClassNameFromDiscriminatorValue($mapping, $embeddedDocument);
                            $embeddedMetadata = $this->dm->getClassMetadata($className);
                            $embeddedDocumentObject = $embeddedMetadata->newInstance();

                            // unset a potential discriminator map field (unless it's a persisted property)
                            $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : '_doctrine_class_name';
                            if (!isset($embeddedMetadata->fieldMappings[$discriminatorField])) {
                                unset($embeddedDocument[$discriminatorField]);
                            }

                            $this->hydrate($embeddedDocumentObject, $embeddedDocument);
                            $uow->registerManaged($embeddedDocumentObject, null, $embeddedDocument);
                            $uow->setParentAssociation($embeddedDocumentObject, $mapping, $document, $mapping['name'].'.'.$key);
                            $coll->add($embeddedDocumentObject);
                        }
                    }
                    $coll->setOwner($document, $mapping);
                    $coll->takeSnapshot();
                    $value = $coll;
                }
            // Hydrate reference
            } elseif (isset($mapping['reference'])) {
                $reference = $rawValue;
                if ($mapping['type'] === 'one' && isset($reference[$this->cmd . 'id'])) {
                    if ($reference === null) {
                        continue;
                    }
                    $className = $this->dm->getClassNameFromDiscriminatorValue($mapping, $reference);
                    $targetMetadata = $this->dm->getClassMetadata($className);
                    $id = $targetMetadata->getPHPIdentifierValue($reference[$this->cmd . 'id']);
                    $value = $this->dm->getReference($className, $id);
                } elseif ($mapping['type'] === 'many' && (is_array($reference) || $reference instanceof Collection)) {
                    $references = $reference;
                    $value = new PersistentCollection(new ArrayCollection(), $this->dm, $this->dm->getConfiguration());
                    $value->setInitialized(false);
                    $value->setOwner($document, $mapping);

                    // Delay any hydration of reference objects until the collection is
                    // accessed and initialized for the first ime
                    $value->setReferences($references);
                }
            // Hydrate regular field
            } else {
                $value = Type::getType($mapping['type'])->convertToPHPValue($rawValue);
            }

            unset($data[$mapping['name']]);
            // Set hydrated field value to document
            if ($value !== null) {
                $metadata->setFieldValue($document, $mapping['fieldName'], $value);
                $data[$mapping['fieldName']] = $value;
            }
        }
        // Set the document identifier
        if (isset($data['_id'])) {
            $metadata->setIdentifierValue($document, $data['_id']);
            $data[$metadata->identifier] = Type::getType($metadata->fieldMappings[$metadata->identifier]['type'])->convertToPHPValue($data['_id']);
            unset($data['_id']);
        }

        if (isset($metadata->lifecycleCallbacks[Events::postLoad])) {
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document);
        }
        if ($this->evm->hasListeners(Events::postLoad)) {
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
        }

        return $document;
    }