From 12923c255dff50b6ae5c1309c994567dca55b61e Mon Sep 17 00:00:00 2001 From: dantleech Date: Sat, 7 Dec 2013 18:07:21 +0100 Subject: [PATCH 1/4] POC for operations queue --- lib/Doctrine/ODM/PHPCR/OperationQueue.php | 38 + lib/Doctrine/ODM/PHPCR/UnitOfWork.php | 1063 ++++++++++----------- 2 files changed, 530 insertions(+), 571 deletions(-) create mode 100644 lib/Doctrine/ODM/PHPCR/OperationQueue.php diff --git a/lib/Doctrine/ODM/PHPCR/OperationQueue.php b/lib/Doctrine/ODM/PHPCR/OperationQueue.php new file mode 100644 index 000000000..437cba665 --- /dev/null +++ b/lib/Doctrine/ODM/PHPCR/OperationQueue.php @@ -0,0 +1,38 @@ + document - * @var array - */ - private $scheduledUpdates = array(); - - /** - * List of documents that will be inserted on next flush - * oid => document - * @var array - */ - private $scheduledInserts = array(); - - /** - * List of documents that will be moved on next flush - * oid => array(document, target path) - * @var array - */ - private $scheduledMoves = array(); - - /** - * List of parent documents that have children that will be reordered on next flush - * parent oid => list of array with records array(parent document, srcName, targetName, before) with - * - parent document the document of the child to be reordered - * - srcName the Nodename of the document to be moved, - * - targetName the Nodename of the document to move srcName to - * - before a boolean telling whether to move srcName before or after targetName - * - * @var array - */ - private $scheduledReorders = array(); - - /** - * List of documents that will be removed on next flush - * oid => document - * @var array + * @var OperationStack */ - private $scheduledRemovals = array(); + private $operationQueue; /** * @var array @@ -241,6 +215,8 @@ public function __construct(DocumentManager $dm) if ($this->session instanceof JackalopeSession) { $this->useFetchDepth = 'jackalope.fetch_depth'; } + + $this->operationQueue = new OperationQueue(); } /** @@ -616,7 +592,7 @@ private function doScheduleInsert($document, &$visited, $overrideIdGenerator = n // TODO: Change Tracking Deferred Explicit break; case self::STATE_REMOVED: - unset($this->scheduledRemovals[$oid]); + $this->operationQueue->push(self::OP_UNREMOVE, $document); $this->setDocumentState($oid, self::STATE_MANAGED); break; case self::STATE_DETACHED: @@ -796,25 +772,16 @@ public function scheduleMove($document, $targetPath) $state = $this->getDocumentState($document); - switch ($state) { - case self::STATE_NEW: - unset($this->scheduledInserts[$oid]); - break; - case self::STATE_REMOVED: - unset($this->scheduledRemovals[$oid]); - break; - case self::STATE_DETACHED: - throw new InvalidArgumentException('Detached document passed to move(): '.self::objToStr($document, $this->dm)); + if ($state === self::STATE_DETACHED) { + throw new InvalidArgumentException('Detached document passed to move(): '.self::objToStr($document, $this->dm)); } - $this->scheduledMoves[$oid] = array($document, $targetPath); + $this->operationQueue->push(self::OP_MOVE, $document); $this->setDocumentState($oid, self::STATE_MANAGED); } public function scheduleReorder($document, $srcName, $targetName, $before) { - $oid = spl_object_hash($document); - $state = $this->getDocumentState($document); switch ($state) { case self::STATE_REMOVED: @@ -823,10 +790,7 @@ public function scheduleReorder($document, $srcName, $targetName, $before) throw new InvalidArgumentException('Detached document passed to reorder(): '.self::objToStr($document, $this->dm)); } - if (! isset($this->scheduledReorders[$oid])) { - $this->scheduledReorders[$oid] = array(); - } - $this->scheduledReorders[$oid][] = array($document, $srcName, $targetName, $before); + $this->operationQueue->push(self::OP_REORDER, $document, array($srcName, $targetName, $before)); } public function scheduleRemove($document) @@ -841,22 +805,16 @@ private function doRemove($document, &$visited) if (isset($visited[$oid])) { return; } + $visited[$oid] = true; $state = $this->getDocumentState($document); - switch ($state) { - case self::STATE_NEW: - unset($this->scheduledInserts[$oid]); - break; - case self::STATE_MANAGED: - unset($this->scheduledMoves[$oid]); - unset($this->scheduledReorders[$oid]); - break; - case self::STATE_DETACHED: - throw new InvalidArgumentException('Detached document passed to remove(): '.self::objToStr($document, $this->dm)); + + if ($state === self::STATE_DETACHED) { + throw new InvalidArgumentException('Detached document passed to remove(): '.self::objToStr($document, $this->dm)); } - $this->scheduledRemovals[$oid] = $document; + $this->operationQueue->push(self::OP_REMOVE, $document); $this->setDocumentState($oid, self::STATE_REMOVED); $class = $this->dm->getClassMetadata(get_class($document)); @@ -968,7 +926,7 @@ public function getDocumentState($document) */ public function isScheduledForInsert($document) { - return isset($this->scheduledInserts[spl_object_hash($document)]); + return $this->operationQueue->hasDocumentForOperation($document, self::OP_INSERT); } /** @@ -983,7 +941,7 @@ public function computeSingleDocumentChangeSet($document) throw new InvalidArgumentException('Document has to be managed for single computation '.self::objToStr($document, $this->dm)); } - foreach ($this->scheduledInserts as $insertedDocument) { + foreach ($this->getScheduledInserts() as $insertedDocument) { $class = $this->dm->getClassMetadata(get_class($insertedDocument)); $this->computeChangeSet($class, $insertedDocument); } @@ -993,8 +951,7 @@ public function computeSingleDocumentChangeSet($document) return; } - $oid = spl_object_hash($document); - if (!isset($this->scheduledInserts[$oid])) { + if (!$this->operationQueue->hasDocumentForOperation($document, self::OP_INSERT)) { $class = $this->dm->getClassMetadata(get_class($document)); $this->computeChangeSet($class, $document); } @@ -1062,16 +1019,16 @@ private function getChildNodename($id, $nodename, $child, $parent) private function computeAssociationChanges($class, $oid, $isNew, $actualData, $assocType) { switch ($assocType) { - case 'reference': - $mappings = $class->referenceMappings; - $computeMethod = 'computeReferenceChanges'; - break; - case 'referrer': - $mappings = $class->referrersMappings; - $computeMethod = 'computeReferrerChanges'; - break; - default: - throw new InvalidArgumentException('Unsupported association type used: '.$assocType); + case 'reference': + $mappings = $class->referenceMappings; + $computeMethod = 'computeReferenceChanges'; + break; + case 'referrer': + $mappings = $class->referrersMappings; + $computeMethod = 'computeReferrerChanges'; + break; + default: + throw new InvalidArgumentException('Unsupported association type used: '.$assocType); } foreach ($mappings as $fieldName) { @@ -1095,8 +1052,8 @@ private function computeAssociationChanges($class, $oid, $isNew, $actualData, $a if (!$isNew && $mapping['cascade'] & ClassMetadata::CASCADE_REMOVE && (is_array($this->originalData[$oid][$fieldName]) - || $this->originalData[$oid][$fieldName] instanceof Collection - ) + || $this->originalData[$oid][$fieldName] instanceof Collection + ) ) { $associations = $this->originalData[$oid][$fieldName]->getOriginalPaths(); foreach ($associations as $association) { @@ -1141,7 +1098,7 @@ public function computeChangeSet(ClassMetadata $class, $document) // Document is New and should be inserted $this->originalData[$oid] = $actualData; $this->documentChangesets[$oid] = array('fields' => $actualData, 'reorderings' => array()); - $this->scheduledInserts[$oid] = $document; + $this->operationQueue->push(self::OP_INSERT, $document); foreach ($class->childrenMappings as $fieldName) { if ($actualData[$fieldName]) { @@ -1272,7 +1229,7 @@ public function computeChangeSet(ClassMetadata $class, $document) $this->documentChangesets[$oid]['reorderings'][] = $reordering; } - $this->scheduledUpdates[$oid] = $document; + $this->operationQueue->push(self::OP_UPDATE, $document); } elseif (isset($this->documentChangesets[$oid])) { // make sure we don't keep an old changeset if an event changed // the document and no reoderings changeset remain. @@ -1291,9 +1248,9 @@ public function computeChangeSet(ClassMetadata $class, $document) continue; } if (($fieldValue instanceof ReferenceManyCollection - || $fieldValue instanceof ReferrersCollection - ) - && $fieldValue->changed() + || $fieldValue instanceof ReferrersCollection + ) + && $fieldValue->changed() ) { continue; } @@ -1310,7 +1267,7 @@ public function computeChangeSet(ClassMetadata $class, $document) $this->documentChangesets[$oid]['fields'] = $actualData; } - $this->scheduledUpdates[$oid] = $document; + $this->operationQueue->push(self::OP_UPDATE, $document); } elseif (isset($this->documentChangesets[$oid])) { // make sure we don't keep an old changeset if an event changed // the document and no field changeset remains. @@ -1336,28 +1293,28 @@ private function computeChildChanges($mapping, $child, $parentId, $nodename, $pa $state = $this->getDocumentState($child); switch ($state) { - case self::STATE_NEW: - if (!($mapping['cascade'] & ClassMetadata::CASCADE_PERSIST) ) { - throw CascadeException::newDocumentFound(self::objToStr($child)); - } + case self::STATE_NEW: + if (!($mapping['cascade'] & ClassMetadata::CASCADE_PERSIST) ) { + throw CascadeException::newDocumentFound(self::objToStr($child)); + } - $childId = $parentId.'/'.$nodename; - $targetClass->setIdentifierValue($child, $childId); + $childId = $parentId.'/'.$nodename; + $targetClass->setIdentifierValue($child, $childId); - if ($this->getDocumentById($childId)) { - $child = $this->merge($child); - } else { - $this->persistNew($targetClass, $child, ClassMetadata::GENERATOR_TYPE_ASSIGNED, $parent); - } + if ($this->getDocumentById($childId)) { + $child = $this->merge($child); + } else { + $this->persistNew($targetClass, $child, ClassMetadata::GENERATOR_TYPE_ASSIGNED, $parent); + } - $this->computeChangeSet($targetClass, $child); - break; - case self::STATE_DETACHED: - throw new InvalidArgumentException('A detached document was found through a child relationship during cascading a persist operation: '.self::objToStr($child, $this->dm)); - default: - if (PathHelper::getParentPath($this->getDocumentId($child)) !== $parentId) { - throw PHPCRException::cannotMoveByAssignment(self::objToStr($child, $this->dm)); - } + $this->computeChangeSet($targetClass, $child); + break; + case self::STATE_DETACHED: + throw new InvalidArgumentException('A detached document was found through a child relationship during cascading a persist operation: '.self::objToStr($child, $this->dm)); + default: + if (PathHelper::getParentPath($this->getDocumentId($child)) !== $parentId) { + throw PHPCRException::cannotMoveByAssignment(self::objToStr($child, $this->dm)); + } } return $child; @@ -1375,15 +1332,15 @@ private function computeReferenceChanges($mapping, $reference) $state = $this->getDocumentState($reference); switch ($state) { - case self::STATE_NEW: - if (!($mapping['cascade'] & ClassMetadata::CASCADE_PERSIST) ) { - throw CascadeException::newDocumentFound(self::objToStr($reference)); - } - $this->persistNew($targetClass, $reference); - $this->computeChangeSet($targetClass, $reference); - break; - case self::STATE_DETACHED: - throw new InvalidArgumentException('A detached document was found through a reference during cascading a persist operation: '.self::objToStr($reference, $this->dm)); + case self::STATE_NEW: + if (!($mapping['cascade'] & ClassMetadata::CASCADE_PERSIST) ) { + throw CascadeException::newDocumentFound(self::objToStr($reference)); + } + $this->persistNew($targetClass, $reference); + $this->computeChangeSet($targetClass, $reference); + break; + case self::STATE_DETACHED: + throw new InvalidArgumentException('A detached document was found through a reference during cascading a persist operation: '.self::objToStr($reference, $this->dm)); } } @@ -1399,15 +1356,15 @@ private function computeReferrerChanges($mapping, $referrer) $state = $this->getDocumentState($referrer); switch ($state) { - case self::STATE_NEW: - if (!($mapping['cascade'] & ClassMetadata::CASCADE_PERSIST) ) { - throw CascadeException::newDocumentFound(self::objToStr($referrer)); - } - $this->persistNew($targetClass, $referrer); - $this->computeChangeSet($targetClass, $referrer); - break; - case self::STATE_DETACHED: - throw new InvalidArgumentException('A detached document was found through a referrer during cascading a persist operation: '.self::objToStr($referrer, $this->dm)); + case self::STATE_NEW: + if (!($mapping['cascade'] & ClassMetadata::CASCADE_PERSIST) ) { + throw CascadeException::newDocumentFound(self::objToStr($referrer)); + } + $this->persistNew($targetClass, $referrer); + $this->computeChangeSet($targetClass, $referrer); + break; + case self::STATE_DETACHED: + throw new InvalidArgumentException('A detached document was found through a referrer during cascading a persist operation: '.self::objToStr($referrer, $this->dm)); } } @@ -1738,12 +1695,12 @@ private function doDetach($document, array &$visited) $state = $this->getDocumentState($document); switch ($state) { - case self::STATE_MANAGED: - $this->unregisterDocument($document); - break; - case self::STATE_NEW: - case self::STATE_DETACHED: - return; + case self::STATE_MANAGED: + $this->unregisterDocument($document); + break; + case self::STATE_NEW: + case self::STATE_DETACHED: + return; } } @@ -1850,15 +1807,37 @@ public function commit($document = null) } try { - $this->executeInserts($this->scheduledInserts); + while ($this->operationQueue->valid() && $operation = $this->operationQueue->dequeue()) { + list($operationType, $document, $data) = $operation; - $this->executeUpdates($this->scheduledUpdates); - - $this->executeRemovals($this->scheduledRemovals); + if (!$this->contains($document)) { + continue; + } - $this->executeReorders($this->scheduledReorders); - $this->executeMoves($this->scheduledMoves); + switch ($operationType) { + case self::OP_INSERT: + $this->executeInsert($document); + break; + case self::OP_UPDATE: + $this->executeUpdate($document); + break; + case self::OP_REMOVE: + $this->executeRemoval($document); + break; + case self::OP_UNREMOVE: + $this->executeInsert($document); + break; + case self::OP_REORDER: + $this->executeReorder($document, $data); + break; + case self::OP_MOVE: + $this->executeMove($document, $data); + break; + default: + throw new RuntimeException('Unknown operation'); + } + } $this->session->save(); @@ -1875,6 +1854,7 @@ public function commit($document = null) } catch (\Exception $innerException) { //TODO: log error while closing dm after error: $innerException->getMessage } + throw $e; } @@ -1903,14 +1883,9 @@ public function commit($document = null) } } - $this->scheduledUpdates = - $this->scheduledRemovals = - $this->scheduledMoves = - $this->scheduledReorders = - $this->scheduledInserts = $this->visitedCollections = - $this->documentChangesets = - $this->changesetComputed = array(); + $this->documentChangesets = + $this->changesetComputed = array(); } /** @@ -1918,146 +1893,122 @@ public function commit($document = null) * * @param array $documents array of all to be inserted documents */ - private function executeInserts($documents) + private function executeInsert($document) { - // sort the documents to insert parents first but maintain child order - $oids = array(); - foreach ($documents as $oid => $document) { - if (!$this->contains($oid)) { - continue; - } - - $oids[$oid] = $this->getDocumentId($document); - } - - $order = array_flip(array_values($oids)); - uasort($oids, function ($a, $b) use ($order) { - // compute the node depths - $aCount = substr_count($a, '/'); - $bCount = substr_count($b, '/'); - - // ensure that the original order is maintained for nodes with the same depth - if ($aCount === $bCount) { - return ($order[$a] < $order[$b]) ? -1 : 1; - } - - return ($aCount < $bCount) ? -1 : 1; - } - ); - + $oid = spl_object_hash($document); $associationChangesets = $associationUpdates = array(); - foreach ($oids as $oid => $id) { - $document = $documents[$oid]; - $class = $this->dm->getClassMetadata(get_class($document)); + $class = $this->dm->getClassMetadata(get_class($document)); - // 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 - foreach ($class->fieldMappings as $fieldName) { - if (!isset($this->documentChangesets[$oid]['fields'][$fieldName]) // empty string is ok - && !$class->isNullable($fieldName) - && !$this->isAutocreatedProperty($class, $fieldName) - ) { - throw new PHPCRException(sprintf('Field "%s" of class "%s" is not nullable', $fieldName, $class->name)); - } + // 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 + foreach ($class->fieldMappings as $fieldName) { + if (!isset($this->documentChangesets[$oid]['fields'][$fieldName]) // empty string is ok + && !$class->isNullable($fieldName) + && !$this->isAutocreatedProperty($class, $fieldName) + ) { + throw new PHPCRException(sprintf('Field "%s" of class "%s" is not nullable', $fieldName, $class->name)); } + } - $parentNode = $this->session->getNode(PathHelper::getParentPath($id)); - $nodename = PathHelper::getNodeName($id); - $node = $parentNode->addNode($nodename, $class->nodeType); - if ($class->node) { - $this->originalData[$oid][$class->node] = $node; - } - if ($class->nodename) { - $this->originalData[$oid][$class->nodename] = $nodename; - } + $id = $this->getDocumentId($document); + $parentNode = $this->session->getNode(PathHelper::getParentPath($id)); + $nodename = PathHelper::getNodeName($id); + $node = $parentNode->addNode($nodename, $class->nodeType); + if ($class->node) { + $this->originalData[$oid][$class->node] = $node; + } + if ($class->nodename) { + $this->originalData[$oid][$class->nodename] = $nodename; + } - try { - $node->addMixin('phpcr:managed'); - } catch (NoSuchNodeTypeException $e) { - throw new PHPCRException('Register phpcr:managed node type first. See https://github.com/doctrine/phpcr-odm/wiki/Custom-node-type-phpcr:managed'); - } + try { + $node->addMixin('phpcr:managed'); + } catch (NoSuchNodeTypeException $e) { + throw new PHPCRException('Register phpcr:managed node type first. See https://github.com/doctrine/phpcr-odm/wiki/Custom-node-type-phpcr:managed'); + } - foreach ($class->mixins as $mixin) { - $node->addMixin($mixin); - } + foreach ($class->mixins as $mixin) { + $node->addMixin($mixin); + } - if ($class->identifier) { - $class->setIdentifierValue($document, $id); - } - if ($class->node) { - $class->reflFields[$class->node]->setValue($document, $node); - } - if ($class->nodename) { - // make sure this reflects the id generator strategy generated id - $class->reflFields[$class->nodename]->setValue($document, $node->getName()); - } + if ($class->identifier) { + $class->setIdentifierValue($document, $id); + } + if ($class->node) { + $class->reflFields[$class->node]->setValue($document, $node); + } + if ($class->nodename) { // make sure this reflects the id generator strategy generated id - if ($class->parentMapping && !$class->reflFields[$class->parentMapping]->getValue($document)) { - $class->reflFields[$class->parentMapping]->setValue($document, $this->getOrCreateProxyFromNode($parentNode, $this->getCurrentLocale($document, $class))); - } + $class->reflFields[$class->nodename]->setValue($document, $node->getName()); + } + // make sure this reflects the id generator strategy generated id + if ($class->parentMapping && !$class->reflFields[$class->parentMapping]->getValue($document)) { + $class->reflFields[$class->parentMapping]->setValue($document, $this->getOrCreateProxyFromNode($parentNode, $this->getCurrentLocale($document, $class))); + } - if ($this->writeMetadata) { - $this->documentClassMapper->writeMetadata($this->dm, $node, $class->name); - } + if ($this->writeMetadata) { + $this->documentClassMapper->writeMetadata($this->dm, $node, $class->name); + } + + $this->setMixins($class, $node); - $this->setMixins($class, $node); + // set the uuid value if it needs to be set + $uuidFieldName = $class->getUuidFieldName(); + if ($uuidFieldName && $node->hasProperty('jcr:uuid')) { + $uuidValue = $node->getProperty('jcr:uuid')->getValue(); + $class->setFieldValue($document, $uuidFieldName, $uuidValue); + } - // set the uuid value if it needs to be set - $uuidFieldName = $class->getUuidFieldName(); - if ($uuidFieldName && $node->hasProperty('jcr:uuid')) { - $uuidValue = $node->getProperty('jcr:uuid')->getValue(); - $class->setFieldValue($document, $uuidFieldName, $uuidValue); + foreach ($this->documentChangesets[$oid]['fields'] as $fieldName => $fieldValue) { + // Ignore translatable fields (they will be persisted by the translation strategy) + if (in_array($fieldName, $class->translatableFields)) { + continue; } - foreach ($this->documentChangesets[$oid]['fields'] as $fieldName => $fieldValue) { - // Ignore translatable fields (they will be persisted by the translation strategy) - if (in_array($fieldName, $class->translatableFields)) { + if (in_array($fieldName, $class->fieldMappings)) { + $mapping = $class->mappings[$fieldName]; + $type = PropertyType::valueFromName($mapping['type']); + if (null === $fieldValue) { continue; } - if (in_array($fieldName, $class->fieldMappings)) { - $mapping = $class->mappings[$fieldName]; - $type = PropertyType::valueFromName($mapping['type']); - if (null === $fieldValue) { - continue; - } - - if ($mapping['multivalue'] && $fieldValue) { - $fieldValue = (array) $fieldValue; - if (isset($mapping['assoc'])) { - $node->setProperty($mapping['assoc'], array_keys($fieldValue), PropertyType::STRING); - $fieldValue = array_values($fieldValue); - } + if ($mapping['multivalue'] && $fieldValue) { + $fieldValue = (array) $fieldValue; + if (isset($mapping['assoc'])) { + $node->setProperty($mapping['assoc'], array_keys($fieldValue), PropertyType::STRING); + $fieldValue = array_values($fieldValue); } + } - $node->setProperty($mapping['property'], $fieldValue, $type); - } elseif (in_array($fieldName, $class->referenceMappings) || in_array($fieldName, $class->referrersMappings)) { - $associationUpdates[$oid] = $document; + $node->setProperty($mapping['property'], $fieldValue, $type); + } elseif (in_array($fieldName, $class->referenceMappings) || in_array($fieldName, $class->referrersMappings)) { + $associationUpdates[$oid] = $document; - //populate $associationChangesets to force executeUpdates($associationUpdates) - //to only update association fields - $data = isset($associationChangesets[$oid]['fields']) ? $associationChangesets[$oid]['fields'] : array(); - $data[$fieldName] = $fieldValue; - $associationChangesets[$oid] = array('fields' => $data, 'reorderings' => array()); - } + //populate $associationChangesets to force executeUpdates($associationUpdates) + //to only update association fields + $data = isset($associationChangesets[$oid]['fields']) ? $associationChangesets[$oid]['fields'] : array(); + $data[$fieldName] = $fieldValue; + $associationChangesets[$oid] = array('fields' => $data, 'reorderings' => array()); } + } - $this->doSaveTranslation($document, $node, $class); + $this->doSaveTranslation($document, $node, $class); - if (isset($class->lifecycleCallbacks[Event::postPersist])) { - $class->invokeLifecycleCallbacks(Event::postPersist, $document); - } - if ($this->evm->hasListeners(Event::postPersist)) { - $this->evm->dispatchEvent(Event::postPersist, new LifecycleEventArgs($document, $this->dm)); - } + if (isset($class->lifecycleCallbacks[Event::postPersist])) { + $class->invokeLifecycleCallbacks(Event::postPersist, $document); + } + if ($this->evm->hasListeners(Event::postPersist)) { + $this->evm->dispatchEvent(Event::postPersist, new LifecycleEventArgs($document, $this->dm)); } $this->documentChangesets = array_merge($this->documentChangesets, $associationChangesets); - $this->executeUpdates($associationUpdates, false); + foreach ($associationUpdates as $associationUpdate) { + $this->executeUpdate($associationUpdate, false); + } } /** @@ -2096,233 +2047,228 @@ private function isAutocreatedProperty(ClassMetadata $class, $fieldName) } /** - * Executes all document updates + * Executes a document update * * @param array $documents array of all to be updated documents * @param boolean $dispatchEvents if to dispatch events */ - private function executeUpdates($documents, $dispatchEvents = true) + private function executeUpdate($document, $dispatchEvents = true) { - foreach ($documents as $oid => $document) { - if (!$this->contains($oid)) { - continue; - } + $oid = spl_object_hash($document); + $class = $this->dm->getClassMetadata(get_class($document)); + $node = $this->session->getNode($this->getDocumentId($document)); - $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 ($this->writeMetadata) { - $this->documentClassMapper->writeMetadata($this->dm, $node, $class->name); + if ($dispatchEvents) { + if (isset($class->lifecycleCallbacks[Event::preUpdate])) { + $class->invokeLifecycleCallbacks(Event::preUpdate, $document); + $this->changesetComputed = array_diff($this->changesetComputed, array($oid)); + $this->computeChangeSet($class, $document); + } + if ($this->evm->hasListeners(Event::preUpdate)) { + $this->evm->dispatchEvent(Event::preUpdate, new LifecycleEventArgs($document, $this->dm)); + $this->changesetComputed = array_diff($this->changesetComputed, array($oid)); + $this->computeChangeSet($class, $document); } + } - if ($dispatchEvents) { - if (isset($class->lifecycleCallbacks[Event::preUpdate])) { - $class->invokeLifecycleCallbacks(Event::preUpdate, $document); - $this->changesetComputed = array_diff($this->changesetComputed, array($oid)); - $this->computeChangeSet($class, $document); - } - if ($this->evm->hasListeners(Event::preUpdate)) { - $this->evm->dispatchEvent(Event::preUpdate, new LifecycleEventArgs($document, $this->dm)); - $this->changesetComputed = array_diff($this->changesetComputed, array($oid)); - $this->computeChangeSet($class, $document); - } + foreach ($this->documentChangesets[$oid]['fields'] as $fieldName => $fieldValue) { + // 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) // only care about non-virtual fields + && !$class->isNullable($fieldName) + && !$this->isAutocreatedProperty($class, $fieldName) + ) { + throw new PHPCRException(sprintf('Field "%s" of class "%s" is not nullable', $fieldName, $class->name)); } - foreach ($this->documentChangesets[$oid]['fields'] as $fieldName => $fieldValue) { - // 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) // only care about non-virtual fields - && !$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; + } - // Ignore translatable fields (they will be persisted by the translation strategy) - if (in_array($fieldName, $class->translatableFields)) { + $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'])) { + $node->setProperty($mapping['assoc'], array_keys($value), PropertyType::STRING); + $value = array_values($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(); 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'])) { - $node->setProperty($mapping['assoc'], array_keys($value), PropertyType::STRING); - $value = array_values($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(); - 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; - } + switch ($mapping['strategy']) { + case 'hard': + $strategy = PropertyType::REFERENCE; + break; + case 'path': + $strategy = PropertyType::PATH; + break; + default: + $strategy = PropertyType::WEAKREFERENCE; + break; + } - $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); - 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 ($mapping['type'] === $class::MANY_TO_MANY) { + if (isset($fieldValue)) { + $refNodesIds = array(); + foreach ($fieldValue as $fv) { + if ($fv === null) { + continue; } - $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)); - + $associatedNode = $this->session->getNode($this->getDocumentId($fv)); if ($strategy === PropertyType::PATH) { - $node->setProperty($fieldName, $associatedNode->getPath(), $strategy); + $refNodesIds[] = $associatedNode->getPath(); } else { - $refClass = $this->dm->getClassMetadata(get_class($fieldValue)); + $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($fieldValue))); + throw new PHPCRException(sprintf('Referenced document %s is not referenceable. Use referenceable=true in Document annotation: '.self::objToStr($document, $this->dm), get_class($fv))); } - $node->setProperty($mapping['property'], $associatedNode->getIdentifier(), $strategy); + $refNodesIds[] = $associatedNode->getIdentifier(); } } + + $refNodesIds = empty($refNodesIds) ? null : $refNodesIds; + $node->setProperty($mapping['property'], $refNodesIds, $strategy); } - } elseif ('referrers' === $mapping['type']) { + } elseif ($mapping['type'] === $class::MANY_TO_ONE) { if (isset($fieldValue)) { + $associatedNode = $this->session->getNode($this->getDocumentId($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 ($strategy === PropertyType::PATH) { + $node->setProperty($fieldName, $associatedNode->getPath(), $strategy); + } else { + $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($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'])); + } - 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 && $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, array($node), $class->name); + $referencingMeta->setFieldValue($fv, $referencingField['fieldName'], $collection); } - $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'])); + if ($referencingNode->hasProperty($referencingField['property'])) { + if (! in_array($uuid, $referencingNode->getPropertyValue($referencingField['property']), PropertyType::STRING)) { + if (! $collection->isDirty()) { + // update the reference collection: add us to it + $collection->add($document); } - // 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 && $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, array($node), $class->name); - $referencingMeta->setFieldValue($fv, $referencingField['fieldName'], $collection); - } - - if ($referencingNode->hasProperty($referencingField['property'])) { - if (! in_array($uuid, $referencingNode->getPropertyValue($referencingField['property']), PropertyType::STRING)) { - if (! $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'], get_class($fv))); + $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'], get_class($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(); - } + } + } 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(); } } + } - foreach ($this->documentChangesets[$oid]['reorderings'] as $reorderings) { - foreach ($reorderings as $srcChildRelPath => $destChildRelPath) { - $node->orderBefore($srcChildRelPath, $destChildRelPath); - } + foreach ($this->documentChangesets[$oid]['reorderings'] as $reorderings) { + foreach ($reorderings as $srcChildRelPath => $destChildRelPath) { + $node->orderBefore($srcChildRelPath, $destChildRelPath); } + } - $this->doSaveTranslation($document, $node, $class); + $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)); - } + 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)); } } } @@ -2332,74 +2278,68 @@ private function executeUpdates($documents, $dispatchEvents = true) * * @param array $documents array of all to be moved documents */ - private function executeMoves($documents) + private function executeMove($document, $data) { - foreach ($documents as $oid => $value) { - if (!$this->contains($oid)) { - continue; - } + list($targetPath) = $data; - list($document, $targetPath) = $value; + $sourcePath = $this->getDocumentId($document); + if ($sourcePath === $targetPath) { + continue; + } - $sourcePath = $this->getDocumentId($document); - if ($sourcePath === $targetPath) { - continue; - } + $class = $this->dm->getClassMetadata(get_class($document)); + if (isset($class->lifecycleCallbacks[Event::preMove])) { + $class->invokeLifecycleCallbacks(Event::preMove, $document); + } - $class = $this->dm->getClassMetadata(get_class($document)); - if (isset($class->lifecycleCallbacks[Event::preMove])) { - $class->invokeLifecycleCallbacks(Event::preMove, $document); - } + if ($this->evm->hasListeners(Event::preMove)) { + $this->evm->dispatchEvent(Event::preMove, new MoveEventArgs($document, $this->dm, $sourcePath, $targetPath)); + } - if ($this->evm->hasListeners(Event::preMove)) { - $this->evm->dispatchEvent(Event::preMove, new MoveEventArgs($document, $this->dm, $sourcePath, $targetPath)); - } + $this->session->move($sourcePath, $targetPath); - $this->session->move($sourcePath, $targetPath); + // update fields nodename and parentMapping if they exist in this type + $node = $this->session->getNode($targetPath); // get node from session, document class might not map it + if ($class->nodename) { + $class->setFieldValue($document, $class->nodename, $node->getName()); + } - // update fields nodename and parentMapping if they exist in this type - $node = $this->session->getNode($targetPath); // get node from session, document class might not map it - if ($class->nodename) { - $class->setFieldValue($document, $class->nodename, $node->getName()); - } + if ($class->parentMapping) { + $class->setFieldValue($document, $class->parentMapping, $this->getOrCreateProxyFromNode($node->getParent(), $this->getCurrentLocale($document, $class))); + } - if ($class->parentMapping) { - $class->setFieldValue($document, $class->parentMapping, $this->getOrCreateProxyFromNode($node->getParent(), $this->getCurrentLocale($document, $class))); + // update all cached children of the document to reflect the move (path id changes) + foreach ($this->documentIds as $childOid => $id) { + if (0 !== strpos($id, $sourcePath)) { + continue; } - // update all cached children of the document to reflect the move (path id changes) - foreach ($this->documentIds as $childOid => $id) { - if (0 !== strpos($id, $sourcePath)) { - continue; - } - - $newId = $targetPath.substr($id, strlen($sourcePath)); - $this->documentIds[$childOid] = $newId; + $newId = $targetPath.substr($id, strlen($sourcePath)); + $this->documentIds[$childOid] = $newId; - $child = $this->getDocumentById($id); - if (!$child) { - continue; - } + $child = $this->getDocumentById($id); + if (!$child) { + continue; + } - unset($this->identityMap[$id]); - $this->identityMap[$newId] = $child; + unset($this->identityMap[$id]); + $this->identityMap[$newId] = $child; - $childClass = $this->dm->getClassMetadata(get_class($child)); - if ($childClass->identifier) { - $childClass->setIdentifierValue($child, $newId); - if (! $child instanceof Proxy || $child->__isInitialized()) { - $this->originalData[$oid][$childClass->identifier] = $newId; - } + $childClass = $this->dm->getClassMetadata(get_class($child)); + if ($childClass->identifier) { + $childClass->setIdentifierValue($child, $newId); + if (! $child instanceof Proxy || $child->__isInitialized()) { + $this->originalData[$oid][$childClass->identifier] = $newId; } } + } - if (isset($class->lifecycleCallbacks[Event::postMove])) { - $class->invokeLifecycleCallbacks(Event::postMove, $document); - } + if (isset($class->lifecycleCallbacks[Event::postMove])) { + $class->invokeLifecycleCallbacks(Event::postMove, $document); + } - if ($this->evm->hasListeners(Event::postMove)) { - $this->evm->dispatchEvent(Event::postMove, new MoveEventArgs($document, $this->dm, $sourcePath, $targetPath)); - } + if ($this->evm->hasListeners(Event::postMove)) { + $this->evm->dispatchEvent(Event::postMove, new MoveEventArgs($document, $this->dm, $sourcePath, $targetPath)); } } @@ -2408,40 +2348,36 @@ private function executeMoves($documents) * * @param $documents */ - private function executeReorders($documents) - { - foreach ($documents as $oid => $list) { - if (!$this->contains($oid)) { - continue; - } - foreach ($list as $value) { - list($parent, $src, $target, $before) = $value; - $parentNode = $this->session->getNode($this->getDocumentId($parent)); - - // check for src and target ... - $dest = $target; - if ($parentNode->hasNode($src) && $parentNode->hasNode($target)) { - // there is no orderAfter, so we need to find the child after target to use it in orderBefore - if (!$before) { - $dest = null; - $found = false; - foreach ($parentNode->getNodes() as $name => $child) { - if ($name === $target) { - $found = true; - } elseif ($found) { - $dest = $name; - break; - } + private function executeReorder($data) + { + list($document, $list) = $data; + foreach ($list as $value) { + list($parent, $src, $target, $before) = $value; + $parentNode = $this->session->getNode($this->getDocumentId($parent)); + + // check for src and target ... + $dest = $target; + if ($parentNode->hasNode($src) && $parentNode->hasNode($target)) { + // there is no orderAfter, so we need to find the child after target to use it in orderBefore + if (!$before) { + $dest = null; + $found = false; + foreach ($parentNode->getNodes() as $name => $child) { + if ($name === $target) { + $found = true; + } elseif ($found) { + $dest = $name; + break; } } + } - $parentNode->orderBefore($src, $dest); - // set all children collection to initialized = false to force reload after reordering - $class = $this->dm->getClassMetadata(get_class($parent)); - foreach ($class->childrenMappings as $fieldName) { - $children = $class->reflFields[$fieldName]->getValue($parent); - $children->setInitialized(false); - } + $parentNode->orderBefore($src, $dest); + // set all children collection to initialized = false to force reload after reordering + $class = $this->dm->getClassMetadata(get_class($parent)); + foreach ($class->childrenMappings as $fieldName) { + $children = $class->reflFields[$fieldName]->getValue($parent); + $children->setInitialized(false); } } } @@ -2452,32 +2388,26 @@ private function executeReorders($documents) * * @param array $documents array of all to be removed documents */ - private function executeRemovals($documents) + private function executeRemoval($document) { - foreach ($documents as $oid => $document) { - if (empty($this->documentIds[$oid])) { - continue; - } - - $class = $this->dm->getClassMetadata(get_class($document)); - $id = $this->getDocumentId($document); + $class = $this->dm->getClassMetadata(get_class($document)); + $id = $this->getDocumentId($document); - try { - $node = $this->session->getNode($id); - $this->doRemoveAllTranslations($document, $class); - $node->remove(); - } catch (PathNotFoundException $e) { - } + try { + $node = $this->session->getNode($id); + $this->doRemoveAllTranslations($document, $class); + $node->remove(); + } catch (PathNotFoundException $e) { + } - $this->unregisterDocument($document); - $this->purgeChildren($document); + $this->unregisterDocument($document); + $this->purgeChildren($document); - if (isset($class->lifecycleCallbacks[Event::postRemove])) { - $class->invokeLifecycleCallbacks(Event::postRemove, $document); - } - if ($this->evm->hasListeners(Event::postRemove)) { - $this->evm->dispatchEvent(Event::postRemove, new LifecycleEventArgs($document, $this->dm)); - } + if (isset($class->lifecycleCallbacks[Event::postRemove])) { + $class->invokeLifecycleCallbacks(Event::postRemove, $document); + } + if ($this->evm->hasListeners(Event::postRemove)) { + $this->evm->dispatchEvent(Event::postRemove, new LifecycleEventArgs($document, $this->dm)); } } @@ -2649,11 +2579,7 @@ private function unregisterDocument($document) unset($this->identityMap[$this->documentIds[$oid]]); } - unset($this->scheduledRemovals[$oid], - $this->scheduledUpdates[$oid], - $this->scheduledMoves[$oid], - $this->scheduledReorders[$oid], - $this->scheduledInserts[$oid], + unset( $this->originalData[$oid], $this->documentIds[$oid], $this->documentState[$oid], @@ -2693,7 +2619,7 @@ public function contains($document) { $oid = is_object($document) ? spl_object_hash($document) : $document; - return isset($this->documentIds[$oid]) && !isset($this->scheduledRemovals[$oid]); + return isset($this->documentIds[$oid]) && !$this->operationQueue->hasDocumentForOperation($document, self::OP_REMOVE); } /** @@ -2761,22 +2687,17 @@ public function initializeObject($obj) public function clear() { $this->identityMap = - $this->documentIds = - $this->documentState = - $this->documentTranslations = - $this->documentLocales = - $this->nonMappedData = - $this->originalData = - $this->documentChangesets = - $this->changesetComputed = - $this->scheduledUpdates = - $this->scheduledInserts = - $this->scheduledMoves = - $this->scheduledReorders = - $this->scheduledRemovals = - $this->visitedCollections = - $this->documentHistory = - $this->documentVersion = array(); + $this->documentIds = + $this->documentState = + $this->documentTranslations = + $this->documentLocales = + $this->nonMappedData = + $this->originalData = + $this->documentChangesets = + $this->changesetComputed = + $this->visitedCollections = + $this->documentHistory = + $this->documentVersion = array(); if ($this->evm->hasListeners(Event::onClear)) { $this->evm->dispatchEvent(Event::onClear, new ManagerEventArgs($this->dm)); @@ -2839,7 +2760,7 @@ private function doSaveTranslation($document, NodeInterface $node, $metadata) // handle case for initial persisting if (empty($this->documentTranslations[$oid])) { $this->bindTranslation($document, $locale); - // handle case when locale in the mapped property changed + // handle case when locale in the mapped property changed } elseif (isset($this->documentLocales[$oid]['current']) && $locale !== $this->documentLocales[$oid]['current'] ) { @@ -3189,7 +3110,7 @@ public function setFetchDepth($fetchDepth = null) */ public function getScheduledUpdates() { - return $this->scheduledUpdates; + return $this->operationQueue->filterByOperationType(self::OP_UPDATE); } /** @@ -3199,7 +3120,7 @@ public function getScheduledUpdates() */ public function getScheduledInserts() { - return $this->scheduledInserts; + return $this->operationQueue->filterByOperationType(self::OP_INSERT); } /** @@ -3209,7 +3130,7 @@ public function getScheduledInserts() */ public function getScheduledMoves() { - return $this->scheduledMoves; + return $this->operationQueue->filterByOperationType(self::OP_REMOVE);; } /** @@ -3219,7 +3140,7 @@ public function getScheduledMoves() */ public function getScheduledReorders() { - return $this->scheduledReorders; + return $this->operationQueue->filterByOperationType(self::OP_REORDER); } /** @@ -3229,6 +3150,6 @@ public function getScheduledReorders() */ public function getScheduledRemovals() { - return $this->scheduledRemovals; + return $this->operationQueue->filterByOperationType(self::OP_REMOVAL); } } From 555ac97b05a68f8d560df6b2e3e5e6b8e7580805 Mon Sep 17 00:00:00 2001 From: dantleech Date: Sun, 22 Dec 2013 14:57:05 +0100 Subject: [PATCH 2/4] Fixed bug --- lib/Doctrine/ODM/PHPCR/UnitOfWork.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ODM/PHPCR/UnitOfWork.php b/lib/Doctrine/ODM/PHPCR/UnitOfWork.php index e70ef1745..501a5669e 100644 --- a/lib/Doctrine/ODM/PHPCR/UnitOfWork.php +++ b/lib/Doctrine/ODM/PHPCR/UnitOfWork.php @@ -1094,6 +1094,7 @@ public function computeChangeSet(ClassMetadata $class, $document) $id = $this->getDocumentId($document, false); $isNew = !isset($this->originalData[$oid]); + if ($isNew) { // Document is New and should be inserted $this->originalData[$oid] = $actualData; @@ -1807,14 +1808,13 @@ public function commit($document = null) } try { - while ($this->operationQueue->valid() && $operation = $this->operationQueue->dequeue()) { + while ($operation = $this->operationQueue->dequeue()) { list($operationType, $document, $data) = $operation; if (!$this->contains($document)) { continue; } - switch ($operationType) { case self::OP_INSERT: $this->executeInsert($document); @@ -1837,6 +1837,10 @@ public function commit($document = null) default: throw new RuntimeException('Unknown operation'); } + + if (false === $this->operationQueue->valid()) { + break; + } } $this->session->save(); From 2d24be78004f5f3fe7dd284f38e8d2dc14c29069 Mon Sep 17 00:00:00 2001 From: dantleech Date: Sun, 22 Dec 2013 17:54:15 +0100 Subject: [PATCH 3/4] Improved test passing --- lib/Doctrine/ODM/PHPCR/OperationQueue.php | 14 +++++++ lib/Doctrine/ODM/PHPCR/UnitOfWork.php | 37 ++++++++----------- tests/Doctrine/Tests/Models/Blog/NewsItem.php | 28 ++++++++++++++ .../ODM/PHPCR/Functional/BasicCrudTest.php | 3 ++ .../PHPCR/Functional/EventComputingTest.php | 2 +- 5 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 tests/Doctrine/Tests/Models/Blog/NewsItem.php diff --git a/lib/Doctrine/ODM/PHPCR/OperationQueue.php b/lib/Doctrine/ODM/PHPCR/OperationQueue.php index 437cba665..615fc6146 100644 --- a/lib/Doctrine/ODM/PHPCR/OperationQueue.php +++ b/lib/Doctrine/ODM/PHPCR/OperationQueue.php @@ -18,6 +18,20 @@ public function hasDocumentForOperation($document, $type) return false; } + public function removeOperationsForDocument($document, $type) + { + foreach ($this as $i => $operation) { + list($opType, $opDocument, $data) = $operation; + + if ($document === $opDocument && $type === $opType) { + $this->offsetUnset($i); + } + } + } + + + + public function filterByOperationType($targetOperationType) { $res = array(); diff --git a/lib/Doctrine/ODM/PHPCR/UnitOfWork.php b/lib/Doctrine/ODM/PHPCR/UnitOfWork.php index 501a5669e..e4bfbe465 100644 --- a/lib/Doctrine/ODM/PHPCR/UnitOfWork.php +++ b/lib/Doctrine/ODM/PHPCR/UnitOfWork.php @@ -75,7 +75,6 @@ class UnitOfWork const OP_INSERT = 'INSERT'; const OP_MOVE = 'MOVE'; const OP_REMOVE = 'REMOVE'; - const OP_UNREMOVE = 'UNREMOVE'; const OP_REORDER = 'REORDER'; /** @@ -592,7 +591,7 @@ private function doScheduleInsert($document, &$visited, $overrideIdGenerator = n // TODO: Change Tracking Deferred Explicit break; case self::STATE_REMOVED: - $this->operationQueue->push(self::OP_UNREMOVE, $document); + $this->operationQueue->removeOperationsForDocument($document, self::OP_REMOVE); $this->setDocumentState($oid, self::STATE_MANAGED); break; case self::STATE_DETACHED: @@ -951,6 +950,7 @@ public function computeSingleDocumentChangeSet($document) return; } + die('asd'); if (!$this->operationQueue->hasDocumentForOperation($document, self::OP_INSERT)) { $class = $this->dm->getClassMetadata(get_class($document)); $this->computeChangeSet($class, $document); @@ -1808,12 +1808,12 @@ public function commit($document = null) } try { - while ($operation = $this->operationQueue->dequeue()) { + $res = fopen('test', 'a'); + while (false === $this->operationQueue->isEmpty()) { + $operation = $this->operationQueue->dequeue(); list($operationType, $document, $data) = $operation; - if (!$this->contains($document)) { - continue; - } + fwrite($res, $operationType."\n"); switch ($operationType) { case self::OP_INSERT: @@ -1825,9 +1825,6 @@ public function commit($document = null) case self::OP_REMOVE: $this->executeRemoval($document); break; - case self::OP_UNREMOVE: - $this->executeInsert($document); - break; case self::OP_REORDER: $this->executeReorder($document, $data); break; @@ -1838,10 +1835,8 @@ public function commit($document = null) throw new RuntimeException('Unknown operation'); } - if (false === $this->operationQueue->valid()) { - break; - } } + fclose($res); $this->session->save(); @@ -2124,15 +2119,15 @@ private function executeUpdate($document, $dispatchEvents = true) } switch ($mapping['strategy']) { - case 'hard': - $strategy = PropertyType::REFERENCE; - break; - case 'path': - $strategy = PropertyType::PATH; - break; - default: - $strategy = PropertyType::WEAKREFERENCE; - break; + case 'hard': + $strategy = PropertyType::REFERENCE; + break; + case 'path': + $strategy = PropertyType::PATH; + break; + default: + $strategy = PropertyType::WEAKREFERENCE; + break; } if ($mapping['type'] === $class::MANY_TO_MANY) { diff --git a/tests/Doctrine/Tests/Models/Blog/NewsItem.php b/tests/Doctrine/Tests/Models/Blog/NewsItem.php new file mode 100644 index 000000000..de238139e --- /dev/null +++ b/tests/Doctrine/Tests/Models/Blog/NewsItem.php @@ -0,0 +1,28 @@ +dm->clear(); $userNew = $this->dm->find($this->type, '/functional/user2'); + + $this->assertNotNull($userNew); + $userNew->username = "test2"; $userNew->numbers = array(4, 5, 6); $userNew->id = '/functional/user2'; diff --git a/tests/Doctrine/Tests/ODM/PHPCR/Functional/EventComputingTest.php b/tests/Doctrine/Tests/ODM/PHPCR/Functional/EventComputingTest.php index 5c9d99be7..2a9cb7141 100644 --- a/tests/Doctrine/Tests/ODM/PHPCR/Functional/EventComputingTest.php +++ b/tests/Doctrine/Tests/ODM/PHPCR/Functional/EventComputingTest.php @@ -134,4 +134,4 @@ public function postMove(LifecycleEventArgs $e) $document = $e->getObject(); $document->username .= '-postmove'; } -} \ No newline at end of file +} From 648c20f33f4abc62d9cf5a5303c86b12e7b5fd54 Mon Sep 17 00:00:00 2001 From: dantleech Date: Wed, 25 Dec 2013 10:15:46 +0000 Subject: [PATCH 4/4] Test Run Completes --- lib/Doctrine/ODM/PHPCR/OperationQueue.php | 11 +--- lib/Doctrine/ODM/PHPCR/UnitOfWork.php | 75 +++++++++++------------ 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/lib/Doctrine/ODM/PHPCR/OperationQueue.php b/lib/Doctrine/ODM/PHPCR/OperationQueue.php index 615fc6146..0d782021f 100644 --- a/lib/Doctrine/ODM/PHPCR/OperationQueue.php +++ b/lib/Doctrine/ODM/PHPCR/OperationQueue.php @@ -4,13 +4,11 @@ class OperationQueue extends \SplQueue { - public function hasDocumentForOperation($document, $type) + public function hasOperationForDocument($document, $type) { - $documentOid = spl_object_hash($document); - foreach ($this as $operation) { - list($oid, $document, $data) = $operation; - if ($oid === $documentOid) { + list($opType, $opDocument, $data) = $operation; + if ($document === $opDocument) { return true; } } @@ -29,9 +27,6 @@ public function removeOperationsForDocument($document, $type) } } - - - public function filterByOperationType($targetOperationType) { $res = array(); diff --git a/lib/Doctrine/ODM/PHPCR/UnitOfWork.php b/lib/Doctrine/ODM/PHPCR/UnitOfWork.php index e4bfbe465..be35d4fb3 100644 --- a/lib/Doctrine/ODM/PHPCR/UnitOfWork.php +++ b/lib/Doctrine/ODM/PHPCR/UnitOfWork.php @@ -775,7 +775,7 @@ public function scheduleMove($document, $targetPath) throw new InvalidArgumentException('Detached document passed to move(): '.self::objToStr($document, $this->dm)); } - $this->operationQueue->push(self::OP_MOVE, $document); + $this->operationQueue->push(self::OP_MOVE, $document, array($targetPath)); $this->setDocumentState($oid, self::STATE_MANAGED); } @@ -925,7 +925,7 @@ public function getDocumentState($document) */ public function isScheduledForInsert($document) { - return $this->operationQueue->hasDocumentForOperation($document, self::OP_INSERT); + return $this->operationQueue->hasOperationForDocument($document, self::OP_INSERT); } /** @@ -950,8 +950,7 @@ public function computeSingleDocumentChangeSet($document) return; } - die('asd'); - if (!$this->operationQueue->hasDocumentForOperation($document, self::OP_INSERT)) { + if (!$this->operationQueue->hasOperationForDocument($document, self::OP_INSERT)) { $class = $this->dm->getClassMetadata(get_class($document)); $this->computeChangeSet($class, $document); } @@ -1264,11 +1263,13 @@ public function computeChangeSet(ClassMetadata $class, $document) if (count($actualData)) { if (empty($this->documentChangesets[$oid])) { $this->documentChangesets[$oid] = array('fields' => $actualData, 'reorderings' => array()); - } else { + $this->operationQueue->push(self::OP_UPDATE, $document); + } elseif ($this->documentChangesets[$oid]['fields'] != $actualData) { $this->documentChangesets[$oid]['fields'] = $actualData; + $this->operationQueue->push(self::OP_UPDATE, $document); } - $this->operationQueue->push(self::OP_UPDATE, $document); + } elseif (isset($this->documentChangesets[$oid])) { // make sure we don't keep an old changeset if an event changed // the document and no field changeset remains. @@ -1808,13 +1809,10 @@ public function commit($document = null) } try { - $res = fopen('test', 'a'); - while (false === $this->operationQueue->isEmpty()) { + while (!$this->operationQueue->isEmpty()) { $operation = $this->operationQueue->dequeue(); list($operationType, $document, $data) = $operation; - fwrite($res, $operationType."\n"); - switch ($operationType) { case self::OP_INSERT: $this->executeInsert($document); @@ -1836,7 +1834,6 @@ public function commit($document = null) } } - fclose($res); $this->session->save(); @@ -2280,6 +2277,7 @@ private function executeUpdate($document, $dispatchEvents = true) private function executeMove($document, $data) { list($targetPath) = $data; + $oid = spl_object_hash($document); $sourcePath = $this->getDocumentId($document); if ($sourcePath === $targetPath) { @@ -2347,37 +2345,34 @@ private function executeMove($document, $data) * * @param $documents */ - private function executeReorder($data) - { - list($document, $list) = $data; - foreach ($list as $value) { - list($parent, $src, $target, $before) = $value; - $parentNode = $this->session->getNode($this->getDocumentId($parent)); - - // check for src and target ... - $dest = $target; - if ($parentNode->hasNode($src) && $parentNode->hasNode($target)) { - // there is no orderAfter, so we need to find the child after target to use it in orderBefore - if (!$before) { - $dest = null; - $found = false; - foreach ($parentNode->getNodes() as $name => $child) { - if ($name === $target) { - $found = true; - } elseif ($found) { - $dest = $name; - break; - } + private function executeReorder($parent, $data) + { + list($src, $target, $before) = $data; + $parentNode = $this->session->getNode($this->getDocumentId($parent)); + + // check for src and target ... + $dest = $target; + if ($parentNode->hasNode($src) && $parentNode->hasNode($target)) { + // there is no orderAfter, so we need to find the child after target to use it in orderBefore + if (!$before) { + $dest = null; + $found = false; + foreach ($parentNode->getNodes() as $name => $child) { + if ($name === $target) { + $found = true; + } elseif ($found) { + $dest = $name; + break; } } + } - $parentNode->orderBefore($src, $dest); - // set all children collection to initialized = false to force reload after reordering - $class = $this->dm->getClassMetadata(get_class($parent)); - foreach ($class->childrenMappings as $fieldName) { - $children = $class->reflFields[$fieldName]->getValue($parent); - $children->setInitialized(false); - } + $parentNode->orderBefore($src, $dest); + // set all children collection to initialized = false to force reload after reordering + $class = $this->dm->getClassMetadata(get_class($parent)); + foreach ($class->childrenMappings as $fieldName) { + $children = $class->reflFields[$fieldName]->getValue($parent); + $children->setInitialized(false); } } } @@ -2618,7 +2613,7 @@ public function contains($document) { $oid = is_object($document) ? spl_object_hash($document) : $document; - return isset($this->documentIds[$oid]) && !$this->operationQueue->hasDocumentForOperation($document, self::OP_REMOVE); + return isset($this->documentIds[$oid]) && !$this->operationQueue->hasOperationForDocument($document, self::OP_REMOVE); } /**