/** * Executes a merge operation on a document. * * @param object $document * @param array $visited * @param object|null $prevManagedCopy * @param array|null $assoc * * @return object The managed copy of the document. * * @throws InvalidArgumentException If the entity instance is NEW. * @throws LockException If the document uses optimistic locking through a * version attribute and the version check against the * managed copy fails. */ private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null) { $oid = spl_object_hash($document); if (isset($visited[$oid])) { return $visited[$oid]; // 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 round trip 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. */ $managedCopy = $document; if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) { if ($document instanceof Proxy && !$document->__isInitialized()) { $document->__load(); } // Try to look the document up in the identity map. $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document); if ($id === null) { // If there is no identifier, it is actually NEW. $managedCopy = $class->newInstance(); $this->persistNew($class, $managedCopy); } else { $managedCopy = $this->tryGetById($id, $class); if ($managedCopy) { // We have the document in memory already, just make sure it is not removed. if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) { throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.'); } } 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 $managedCopy = $class->newInstance(); $class->setIdentifierValue($managedCopy, $id); $this->persistNew($class, $managedCopy); } else { if ($managedCopy instanceof Proxy && !$managedCopy->__isInitialized__) { $managedCopy->__load(); } } } if ($class->isVersioned) { $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy); $documentVersion = $class->reflFields[$class->versionField]->getValue($document); // Throw exception if versions don't match if ($managedCopyVersion != $documentVersion) { throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion); } } // Merge state of $document into existing (managed) document foreach ($class->reflClass->getProperties() as $prop) { $name = $prop->name; $prop->setAccessible(true); if (!isset($class->associationMappings[$name])) { if (!$class->isIdentifier($name)) { $prop->setValue($managedCopy, $prop->getValue($document)); } } else { $assoc2 = $class->associationMappings[$name]; if ($assoc2['type'] === 'one') { $other = $prop->getValue($document); if ($other === null) { $prop->setValue($managedCopy, null); } elseif ($other instanceof Proxy && !$other->__isInitialized__) { // Do not merge fields marked lazy that have not been fetched continue; } elseif (!$assoc2['isCascadeMerge']) { if ($this->getDocumentState($other) === self::STATE_DETACHED) { $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other); /* @var $targetClass \CosmoW\ODM\Riak\Mapping\ClassMetadataInfo */ $targetClass = $this->dm->getClassMetadata($targetDocument); $relatedId = $targetClass->getIdentifierObject($other); if ($targetClass->subClasses) { $other = $this->dm->find($targetClass->name, $relatedId); } else { $other = $this->dm->getProxyFactory()->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId)); $this->registerManaged($other, $relatedId, array()); } } $prop->setValue($managedCopy, $other); } } 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; } $managedCol = $prop->getValue($managedCopy); if (!$managedCol) { $managedCol = new PersistentCollection(new ArrayCollection(), $this->dm, $this); $managedCol->setOwner($managedCopy, $assoc2); $prop->setValue($managedCopy, $managedCol); $this->originalDocumentData[$oid][$name] = $managedCol; } /* Note: do not process association's target documents. * They will be handled during the cascade. Initialize * and, if necessary, clear $managedCol for now. */ if ($assoc2['isCascadeMerge']) { $managedCol->initialize(); // If $managedCol differs from the merged collection, clear and set dirty if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) { $managedCol->unwrap()->clear(); $managedCol->setDirty(true); if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) { $this->scheduleForDirtyCheck($managedCopy); } } } } } 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['fieldName']; $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy)); if ($assoc['type'] === 'one') { $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy); } else { $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy); if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) { $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; }