/** * Executes all document updates * * @param array $documents array of all to be updated documents * @param boolean $dispatchEvents if to dispatch events */ private function executeUpdates($documents, $dispatchEvents = true) { foreach ($documents as $oid => $document) { if (!$this->contains($oid)) { continue; } $class = $this->dm->getClassMetadata(get_class($document)); $node = $this->session->getNode($this->getDocumentId($document)); if ($this->writeMetadata) { $this->documentClassMapper->writeMetadata($this->dm, $node, $class->name); } if ($dispatchEvents) { if ($invoke = $this->eventListenersInvoker->getSubscribedSystems($class, Event::preUpdate)) { $this->eventListenersInvoker->invoke($class, Event::preUpdate, $document, new PreUpdateEventArgs($document, $this->dm, $this->documentChangesets[$oid]), $invoke); $this->changesetComputed = array_diff($this->changesetComputed, array($oid)); $this->computeChangeSet($class, $document); } } $fields = isset($this->documentChangesets[$oid]['fields']) ? $this->documentChangesets[$oid]['fields'] : array(); foreach ($fields as $fieldName => $data) { $fieldValue = $data[1]; // PHPCR does not validate nullable unless we would start to // generate custom node types, which we at the moment don't. // the ORM can delegate this validation to the relational database // that is using a strict schema. // do this after the preUpdate events to give listener a last // chance to provide values if (null === $fieldValue && in_array($fieldName, $class->fieldMappings) && !$class->isNullable($fieldName) && !$this->isAutocreatedProperty($class, $fieldName)) { throw new PHPCRException(sprintf('Field "%s" of class "%s" is not nullable', $fieldName, $class->name)); } // Ignore translatable fields (they will be persisted by the translation strategy) if (in_array($fieldName, $class->translatableFields)) { continue; } $mapping = $class->mappings[$fieldName]; if (in_array($fieldName, $class->fieldMappings)) { $type = PropertyType::valueFromName($mapping['type']); if ($mapping['multivalue']) { $value = empty($fieldValue) ? null : ($fieldValue instanceof Collection ? $fieldValue->toArray() : $fieldValue); if ($value && isset($mapping['assoc'])) { $value = $this->processAssoc($node, $mapping, $value); } } else { $value = $fieldValue; } $node->setProperty($mapping['property'], $value, $type); } elseif ($mapping['type'] === $class::MANY_TO_ONE || $mapping['type'] === $class::MANY_TO_MANY) { if (!$this->writeMetadata) { continue; } if ($node->hasProperty($mapping['property']) && is_null($fieldValue)) { $node->getProperty($mapping['property'])->remove(); if (isset($mapping['assoc'])) { $this->removeAssoc($node, $mapping); } continue; } switch ($mapping['strategy']) { case 'hard': $strategy = PropertyType::REFERENCE; break; case 'path': $strategy = PropertyType::PATH; break; default: $strategy = PropertyType::WEAKREFERENCE; break; } if ($mapping['type'] === $class::MANY_TO_MANY) { if (isset($fieldValue)) { $refNodesIds = array(); foreach ($fieldValue as $fv) { if ($fv === null) { continue; } $associatedNode = $this->session->getNode($this->getDocumentId($fv)); if ($strategy === PropertyType::PATH) { $refNodesIds[] = $associatedNode->getPath(); } else { $refClass = $this->dm->getClassMetadata(get_class($fv)); $this->setMixins($refClass, $associatedNode, $fv); if (!$associatedNode->isNodeType('mix:referenceable')) { throw new PHPCRException(sprintf('Referenced document %s is not referenceable. Use referenceable=true in Document annotation: ' . self::objToStr($document, $this->dm), ClassUtils::getClass($fv))); } $refNodesIds[] = $associatedNode->getIdentifier(); } } $refNodesIds = empty($refNodesIds) ? null : $refNodesIds; $node->setProperty($mapping['property'], $refNodesIds, $strategy); } } elseif ($mapping['type'] === $class::MANY_TO_ONE) { if (isset($fieldValue)) { $associatedNode = $this->session->getNode($this->getDocumentId($fieldValue)); if ($strategy === PropertyType::PATH) { $node->setProperty($fieldName, $associatedNode->getPath(), $strategy); } else { $refClass = $this->dm->getClassMetadata(get_class($fieldValue)); $this->setMixins($refClass, $associatedNode, $document); if (!$associatedNode->isNodeType('mix:referenceable')) { throw new PHPCRException(sprintf('Referenced document %s is not referenceable. Use referenceable=true in Document annotation: ' . self::objToStr($document, $this->dm), ClassUtils::getClass($fieldValue))); } $node->setProperty($mapping['property'], $associatedNode->getIdentifier(), $strategy); } } } } elseif ('referrers' === $mapping['type']) { if (isset($fieldValue)) { /* * each document in referrers field is supposed to * reference this document, so we have to update its * referencing property to contain the uuid of this * document */ foreach ($fieldValue as $fv) { if ($fv === null) { continue; } if (!$fv instanceof $mapping['referringDocument']) { throw new PHPCRException(sprintf("%s is not an instance of %s for document %s field %s", self::objToStr($fv, $this->dm), $mapping['referencedBy'], self::objToStr($document, $this->dm), $mapping['fieldName'])); } $referencingNode = $this->session->getNode($this->getDocumentId($fv)); $referencingMeta = $this->dm->getClassMetadata($mapping['referringDocument']); $referencingField = $referencingMeta->getAssociation($mapping['referencedBy']); $uuid = $node->getIdentifier(); $strategy = $referencingField['strategy'] == 'weak' ? PropertyType::WEAKREFERENCE : PropertyType::REFERENCE; switch ($referencingField['type']) { case ClassMetadata::MANY_TO_ONE: $ref = $referencingMeta->getFieldValue($fv, $referencingField['fieldName']); if ($ref !== null && $ref !== $document) { throw new PHPCRException(sprintf('Conflicting settings for referrer and reference: Document %s field %s points to %s but document %s has set first document as referrer on field %s', self::objToStr($fv, $this->dm), $referencingField['fieldName'], self::objToStr($ref, $this->dm), self::objToStr($document, $this->dm), $mapping['fieldName'])); } // update the referencing document field to point to this document $referencingMeta->setFieldValue($fv, $referencingField['fieldName'], $document); // and make sure the reference is not deleted in this change because the field could be null unset($this->documentChangesets[spl_object_hash($fv)]['fields'][$referencingField['fieldName']]); // store the change in PHPCR $referencingNode->setProperty($referencingField['property'], $uuid, $strategy); break; case ClassMetadata::MANY_TO_MANY: /** @var $collection ReferenceManyCollection */ $collection = $referencingMeta->getFieldValue($fv, $referencingField['fieldName']); if ($collection instanceof PersistentCollection && $collection->isDirty()) { throw new PHPCRException(sprintf('You may not modify the reference and referrer collections of interlinked documents as this is ambiguous. Reference %s on document %s and referrers %s on document %s are both modified', self::objToStr($fv, $this->dm), $referencingField['fieldName'], self::objToStr($document, $this->dm), $mapping['fieldName'])); } if ($collection) { // make sure the reference is not deleted in this change because the field could be null unset($this->documentChangesets[spl_object_hash($fv)]['fields'][$referencingField['fieldName']]); } else { $collection = new ReferenceManyCollection($this->dm, $fv, $referencingField['property'], array($node), $class->name); $referencingMeta->setFieldValue($fv, $referencingField['fieldName'], $collection); } if ($referencingNode->hasProperty($referencingField['property'])) { if (!in_array($uuid, $referencingNode->getProperty($referencingField['property'])->getString())) { if (!$collection instanceof PersistentCollection || !$collection->isDirty()) { // update the reference collection: add us to it $collection->add($document); } // store the change in PHPCR $referencingNode->getProperty($referencingField['property'])->addValue($uuid); // property should be correct type already } } else { // store the change in PHPCR $referencingNode->setProperty($referencingField['property'], array($uuid), $strategy); } // avoid confusion later, this change to the reference collection is already saved $collection->setDirty(false); break; default: // in class metadata we only did a santiy check but not look at the actual mapping throw new MappingException(sprintf('Field "%s" of document "%s" is not a reference field. Error in referrer annotation: ' . self::objToStr($document, $this->dm), $mapping['referencedBy'], ClassUtils::getClass($fv))); } } } } elseif ('child' === $mapping['type']) { if ($fieldValue === null && $node->hasNode($mapping['nodeName'])) { $child = $node->getNode($mapping['nodeName']); $childDocument = $this->getOrCreateDocument(null, $child); $this->purgeChildren($childDocument); $child->remove(); } } } if (!empty($this->documentChangesets[$oid]['reorderings'])) { foreach ($this->documentChangesets[$oid]['reorderings'] as $reorderings) { foreach ($reorderings as $srcChildRelPath => $destChildRelPath) { $node->orderBefore($srcChildRelPath, $destChildRelPath); } } } $this->doSaveTranslation($document, $node, $class); if ($dispatchEvents) { if ($invoke = $this->eventListenersInvoker->getSubscribedSystems($class, Event::postUpdate)) { $this->eventListenersInvoker->invoke($class, Event::postUpdate, $document, new LifecycleEventArgs($document, $this->dm), $invoke); } } } }
/** * Executes all document updates * * @param array $documents array of all to be updated documents * @param boolean $dispatchEvents if to dispatch events */ private function executeUpdates($documents, $dispatchEvents = true) { foreach ($documents as $oid => $document) { if (!$this->contains($oid)) { continue; } $class = $this->dm->getClassMetadata(get_class($document)); $node = $this->session->getNode($this->getDocumentId($document)); if ($this->writeMetadata) { $this->documentClassMapper->writeMetadata($this->dm, $node, $class->name); } if ($dispatchEvents) { if (isset($class->lifecycleCallbacks[Event::preUpdate])) { $class->invokeLifecycleCallbacks(Event::preUpdate, $document); $this->computeChangeSet($class, $document); } if ($this->evm->hasListeners(Event::preUpdate)) { $this->evm->dispatchEvent(Event::preUpdate, new LifecycleEventArgs($document, $this->dm)); $this->computeChangeSet($class, $document); } } foreach ($this->documentChangesets[$oid] as $fieldName => $fieldValue) { // Ignore translatable fields (they will be persisted by the translation strategy) if (in_array($fieldName, $class->translatableFields)) { continue; } if (isset($class->fieldMappings[$fieldName])) { $type = PropertyType::valueFromName($class->fieldMappings[$fieldName]['type']); if ($class->fieldMappings[$fieldName]['multivalue']) { $value = $fieldValue === null ? null : $fieldValue->toArray(); $node->setProperty($class->fieldMappings[$fieldName]['name'], $value, $type); } else { $node->setProperty($class->fieldMappings[$fieldName]['name'], $fieldValue, $type); } } elseif (isset($class->associationsMappings[$fieldName]) && $this->writeMetadata) { if ($node->hasProperty($class->associationsMappings[$fieldName]['fieldName']) && is_null($fieldValue)) { $node->getProperty($class->associationsMappings[$fieldName]['fieldName'])->remove(); continue; } $type = $class->associationsMappings[$fieldName]['weak'] ? PropertyType::WEAKREFERENCE : PropertyType::REFERENCE; if ($class->associationsMappings[$fieldName]['type'] === $class::MANY_TO_MANY) { if (isset($fieldValue)) { $refNodesIds = array(); foreach ($fieldValue as $fv) { if ($fv === null) { continue; } $associatedNode = $this->session->getNode($this->getDocumentId($fv)); $refClass = $this->dm->getClassMetadata(get_class($fv)); $this->setMixins($refClass, $associatedNode); if (!$associatedNode->isNodeType('mix:referenceable')) { throw new PHPCRException(sprintf('Referenced document %s is not referenceable. Use referenceable=true in Document annotation: ' . self::objToStr($document, $this->dm), get_class($fv))); } $refNodesIds[] = $associatedNode->getIdentifier(); } if (!empty($refNodesIds)) { $node->setProperty($class->associationsMappings[$fieldName]['fieldName'], $refNodesIds, $type); } } } elseif ($class->associationsMappings[$fieldName]['type'] === $class::MANY_TO_ONE) { if (isset($fieldValue)) { $associatedNode = $this->session->getNode($this->getDocumentId($fieldValue)); $refClass = $this->dm->getClassMetadata(get_class($fieldValue)); $this->setMixins($refClass, $associatedNode); if (!$associatedNode->isNodeType('mix:referenceable')) { throw new PHPCRException(sprintf('Referenced document %s is not referenceable. Use referenceable=true in Document annotation: ' . self::objToStr($document, $this->dm), get_class($fieldValue))); } $node->setProperty($class->associationsMappings[$fieldName]['fieldName'], $associatedNode->getIdentifier(), $type); } } } elseif (isset($class->childMappings[$fieldName])) { if ($fieldValue === null) { if ($node->hasNode($class->childMappings[$fieldName]['name'])) { $child = $node->getNode($class->childMappings[$fieldName]['name']); $childDocument = $this->createDocument(null, $child); $this->purgeChildren($childDocument); $child->remove(); } } elseif ($this->originalData[$oid][$fieldName] && $this->originalData[$oid][$fieldName] !== $fieldValue) { throw new PHPCRException('Cannot move/copy children by assignment as it would be ambiguous. Please use the DocumentManager::move() or PHPCR\\Session::copy() operations for this.'); } } } $this->doSaveTranslation($document, $node, $class); if ($dispatchEvents) { if (isset($class->lifecycleCallbacks[Event::postUpdate])) { $class->invokeLifecycleCallbacks(Event::postUpdate, $document); } if ($this->evm->hasListeners(Event::postUpdate)) { $this->evm->dispatchEvent(Event::postUpdate, new LifecycleEventArgs($document, $this->dm)); } } } }