From 532d3da4f13441fe3159e525a35b92a8325eac54 Mon Sep 17 00:00:00 2001 From: romanb Date: Sun, 8 Jul 2007 12:57:52 +0000 Subject: [PATCH] Bugfix for hydration. (zyne, please have a look at the diff). Improvements and enhancements to the NestedSet (not BC! please have a look at draft/nestedset_changes.tree). Added a model that was missing in the repos (model/BlogTag). Updated a testcase. --- lib/Doctrine/Hydrate.php | 13 + lib/Doctrine/Node.php | 8 + lib/Doctrine/Node/NestedSet.php | 339 ++++++++++++++--------- lib/Doctrine/Tree/NestedSet.php | 214 ++++++++++---- models/BlogTag.php | 10 + tests/Query/OneToOneFetchingTestCase.php | 14 +- 6 files changed, 400 insertions(+), 198 deletions(-) create mode 100644 models/BlogTag.php diff --git a/lib/Doctrine/Hydrate.php b/lib/Doctrine/Hydrate.php index 38a9e7c87..21f5051a1 100644 --- a/lib/Doctrine/Hydrate.php +++ b/lib/Doctrine/Hydrate.php @@ -1012,6 +1012,11 @@ class Doctrine_Hydrate extends Doctrine_Object implements Serializable } else { $parent = $map['parent']; $relation = $map['relation']; + + if (!isset($prev[$parent])) { + break; + } + // check the type of the relation if ( ! $relation->isOneToOne()) { // initialize the collection @@ -1082,6 +1087,10 @@ class Doctrine_Hydrate extends Doctrine_Object implements Serializable $parent = $this->_aliasMap[$alias]['parent']; $relation = $this->_aliasMap[$alias]['relation']; $componentAlias = $relation->getAlias(); + + if (!isset($prev[$parent])) { + break; + } // check the type of the relation if ( ! $relation->isOneToOne()) { @@ -1154,6 +1163,10 @@ class Doctrine_Hydrate extends Doctrine_Object implements Serializable } else { $prev[$alias] = $coll->getLast(); } + } else { + if (isset($prev[$alias])) { + unset($prev[$alias]); + } } } } diff --git a/lib/Doctrine/Node.php b/lib/Doctrine/Node.php index e92f844f7..3f22640ed 100644 --- a/lib/Doctrine/Node.php +++ b/lib/Doctrine/Node.php @@ -50,6 +50,13 @@ class Doctrine_Node implements IteratorAggregate * @param array $iteratorOptions */ protected $iteratorOptions; + + /** + * The tree to which the node belongs. + * + * @var unknown_type + */ + protected $_tree; /** * contructor, creates node with reference to record and any options @@ -61,6 +68,7 @@ class Doctrine_Node implements IteratorAggregate { $this->record = $record; $this->options = $options; + $this->_tree = $this->record->getTable()->getTree(); } /** diff --git a/lib/Doctrine/Node/NestedSet.php b/lib/Doctrine/Node/NestedSet.php index 31744bf3a..107f9b096 100644 --- a/lib/Doctrine/Node/NestedSet.php +++ b/lib/Doctrine/Node/NestedSet.php @@ -79,15 +79,22 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int */ public function getPrevSibling() { - $q = $this->record->getTable()->createQuery(); - $q = $q->where('rgt = ?', $this->getLeftValue() - 1); - $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); - $result = $q->execute()->getFirst(); + $q = $this->_tree->getBaseQuery(); + $q = $q->where('base.rgt = ?', $this->getLeftValue() - 1); + $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue()); + $result = $q->execute(); - if(!$result) - $result = $this->record->getTable()->create(); + if (count($result) <= 0) { + return false; + } - return $result; + if ($result instanceof Doctrine_Collection) { + $sibling = $result->getFirst(); + } else if (is_array($result)) { + $sibling = array_shift($result); + } + + return $sibling; } /** @@ -97,15 +104,22 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int */ public function getNextSibling() { - $q = $this->record->getTable()->createQuery(); - $q = $q->where('lft = ?', $this->getRightValue() + 1); - $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); - $result = $q->execute()->getFirst(); + $q = $this->_tree->getBaseQuery(); + $q = $q->where('base.lft = ?', $this->getRightValue() + 1); + $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue()); + $result = $q->execute(); - if(!$result) - $result = $this->record->getTable()->create(); + if (count($result) <= 0) { + return false; + } - return $result; + if ($result instanceof Doctrine_Collection) { + $sibling = $result->getFirst(); + } else if (is_array($result)) { + $sibling = array_shift($result); + } + + return $sibling; } /** @@ -117,17 +131,14 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int { $parent = $this->getParent(); $siblings = array(); - if($parent->exists()) - { - foreach($parent->getNode()->getChildren() as $child) - { - if($this->isEqualTo($child) && !$includeNode) + if ($parent->exists()) { + foreach ($parent->getNode()->getChildren() as $child) { + if ($this->isEqualTo($child) && !$includeNode) { continue; - + } $siblings[] = $child; - } + } } - return $siblings; } @@ -138,15 +149,22 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int */ public function getFirstChild() { - $q = $this->record->getTable()->createQuery(); - $q = $q->where('lft = ?', $this->getLeftValue() + 1); - $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); - $result = $q->execute()->getFirst(); + $q = $this->_tree->getBaseQuery(); + $q->where('base.lft = ?', $this->getLeftValue() + 1); + $this->_tree->returnQueryWithRootId($q, $this->getRootValue()); + $result = $q->execute(); + + if (count($result) <= 0) { + return false; + } - if(!$result) - $result = $this->record->getTable()->create(); + if ($result instanceof Doctrine_Collection) { + $child = $result->getFirst(); + } else if (is_array($result)) { + $child = array_shift($result); + } - return $result; + return $child; } /** @@ -156,35 +174,64 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int */ public function getLastChild() { - $q = $this->record->getTable()->createQuery(); - $q = $q->where('rgt = ?', $this->getRightValue() - 1); - $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); - $result = $q->execute()->getFirst(); + $q = $this->_tree->getBaseQuery(); + $q->where('base.rgt = ?', $this->getRightValue() - 1); + $this->_tree->returnQueryWithRootId($q, $this->getRootValue()); + $result = $q->execute(); - if(!$result) - $result = $this->record->getTable()->create(); + if (count($result) <= 0) { + return false; + } - return $result; + if ($result instanceof Doctrine_Collection) { + $child = $result->getFirst(); + } else if (is_array($result)) { + $child = array_shift($result); + } + + return $child; } /** * gets children for node (direct descendants only) * - * @return array array of sibling Doctrine_Record objects + * @return mixed The children of the node or FALSE if the node has no children. */ public function getChildren() { - return $this->getIterator('Pre', array('depth' => 1)); + return $this->getDescendants(1); } /** * gets descendants for node (direct descendants only) * - * @return iterator iterator to traverse descendants from node + * @return mixed The descendants of the node or FALSE if the node has no descendants. + * @todo Currently all descendants are fetched, no matter the depth. Maybe there is a better + * solution with less overhead. */ - public function getDescendants() + public function getDescendants($depth = null, $includeNode = false) { - return $this->getIterator(); + $q = $this->_tree->getBaseQuery(); + $params = array($this->record->get('lft'), $this->record->get('rgt')); + + if ($includeNode) { + $q->where("base.lft >= ? AND base.rgt <= ?", $params)->orderBy("base.lft asc"); + } else { + $q->where("base.lft > ? AND base.rgt < ?", $params)->orderBy("base.lft asc"); + } + + if ($depth !== null) { + $q->addWhere("base.level <= ?", $this->record['level'] + $depth); + } + + $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue()); + $result = $q->execute(); + + if (count($result) <= 0) { + return false; + } + + return $result; } /** @@ -194,18 +241,21 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int */ public function getParent() { - $q = $this->record->getTable()->createQuery(); - - $componentName = $this->record->getTable()->getComponentName(); - $q = $q->where("$componentName.lft < ? AND $componentName.rgt > ?", array($this->getLeftValue(), $this->getRightValue())) - ->orderBy("$componentName.rgt asc"); - $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); - - $parent = $q->execute()->getFirst(); + $q = $this->_tree->getBaseQuery(); + $q->where("base.lft < ? AND base.rgt > ?", array($this->getLeftValue(), $this->getRightValue())) + ->orderBy("base.rgt asc"); + $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue()); + $result = $q->execute(); - - if(!$parent) - $parent = $this->record->getTable()->create(); + if (count($result) <= 0) { + return false; + } + + if ($result instanceof Doctrine_Collection) { + $parent = $result->getFirst(); + } else if (is_array($result)) { + $parent = array_shift($result); + } return $parent; } @@ -213,18 +263,23 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int /** * gets ancestors for node * - * @return object Doctrine_Collection + * @param integer $deth The depth 'upstairs'. + * @return mixed The ancestors of the node or FALSE if the node has no ancestors (this + * basically means it's a root node). */ - public function getAncestors() + public function getAncestors($depth = null) { - $q = $this->record->getTable()->createQuery(); - - $componentName = $this->record->getTable()->getComponentName(); - $q = $q->where("$componentName.lft < ? AND $componentName.rgt > ?", array($this->getLeftValue(), $this->getRightValue())) - ->orderBy("$componentName.lft asc"); - $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); + $q = $this->_tree->getBaseQuery(); + $q->where("base.lft < ? AND base.rgt > ?", array($this->getLeftValue(), $this->getRightValue())) + ->orderBy("base.lft asc"); + if ($depth !== null) { + $q->addWhere("base.level >= ?", $this->record['level'] - $depth); + } + $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue()); $ancestors = $q->execute(); - + if (count($ancestors) <= 0) { + return false; + } return $ancestors; } @@ -239,13 +294,13 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int { $path = array(); $ancestors = $this->getAncestors(); - foreach($ancestors as $ancestor) - { + foreach ($ancestors as $ancestor) { $path[] = $ancestor->__toString(); } - if($includeRecord) + if ($includeRecord) { $path[] = $this->getRecord()->__toString(); - + } + return implode($seperator, $path); } @@ -258,7 +313,6 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int { $count = 0; $children = $this->getChildren(); - while ($children->next()) { $count++; } @@ -278,18 +332,19 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int /** * inserts node as parent of dest record * - * @return bool + * @return bool + * @todo Wrap in transaction */ public function insertAsParentOf(Doctrine_Record $dest) { // cannot insert a node that has already has a place within the tree - if ($this->isValidNode()) + if ($this->isValidNode()) { return false; - + } // cannot insert as parent of root - if ($dest->getNode()->isRoot()) + if ($dest->getNode()->isRoot()) { return false; - + } $newRoot = $dest->getNode()->getRootValue(); $this->shiftRLValues($dest->getNode()->getLeftValue(), 1, $newRoot); $this->shiftRLValues($dest->getNode()->getRightValue() + 2, 1, $newRoot); @@ -297,6 +352,7 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $newLeft = $dest->getNode()->getLeftValue(); $newRight = $dest->getNode()->getRightValue() + 2; + $this->record['level'] = $dest['level'] - 1; $this->insertNode($newLeft, $newRight, $newRoot); return true; @@ -305,12 +361,13 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int /** * inserts node as previous sibling of dest record * - * @return bool + * @return bool + * @todo Wrap in transaction */ public function insertAsPrevSiblingOf(Doctrine_Record $dest) { // cannot insert a node that has already has a place within the tree - if($this->isValidNode()) + if ($this->isValidNode()) return false; $newLeft = $dest->getNode()->getLeftValue(); @@ -318,6 +375,7 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $newRoot = $dest->getNode()->getRootValue(); $this->shiftRLValues($newLeft, 2, $newRoot); + $this->record['level'] = $dest['level']; $this->insertNode($newLeft, $newRight, $newRoot); // update destination left/right values to prevent a refresh // $dest->getNode()->setLeftValue($dest->getNode()->getLeftValue() + 2); @@ -329,7 +387,8 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int /** * inserts node as next sibling of dest record * - * @return bool + * @return bool + * @todo Wrap in transaction */ public function insertAsNextSiblingOf(Doctrine_Record $dest) { @@ -342,18 +401,20 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $newRoot = $dest->getNode()->getRootValue(); $this->shiftRLValues($newLeft, 2, $newRoot); + $this->record['level'] = $dest['level']; $this->insertNode($newLeft, $newRight, $newRoot); // update destination left/right values to prevent a refresh // no need, node not affected - return true; + return true; } /** * inserts node as first child of dest record * - * @return bool + * @return bool + * @todo Wrap in transaction */ public function insertAsFirstChildOf(Doctrine_Record $dest) { @@ -366,6 +427,7 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $newRoot = $dest->getNode()->getRootValue(); $this->shiftRLValues($newLeft, 2, $newRoot); + $this->record['level'] = $dest['level'] + 1; $this->insertNode($newLeft, $newRight, $newRoot); // update destination left/right values to prevent a refresh @@ -377,7 +439,8 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int /** * inserts node as last child of dest record * - * @return bool + * @return bool + * @todo Wrap in transaction */ public function insertAsLastChildOf(Doctrine_Record $dest) { @@ -390,6 +453,7 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $newRoot = $dest->getNode()->getRootValue(); $this->shiftRLValues($newLeft, 2, $newRoot); + $this->record['level'] = $dest['level'] + 1; $this->insertNode($newLeft, $newRight, $newRoot); // update destination left/right values to prevent a refresh @@ -405,6 +469,7 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int * @param Doctrine_Record $dest * @param unknown_type $newLeftValue * @param unknown_type $moveType + * @todo Better exception handling/wrapping */ private function _moveBetweenTrees(Doctrine_Record $dest, $newLeftValue, $moveType) { @@ -418,6 +483,7 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $oldRoot = $this->getRootValue(); $oldLft = $this->getLeftValue(); $oldRgt = $this->getRightValue(); + $oldLevel = $this->record['level']; // Prepare target tree for insertion, make room $this->shiftRlValues($newLeftValue, $oldRgt - $oldLft - 1, $newRoot); @@ -456,25 +522,28 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $this->setRightValue($this->getLeftValue() + ($oldRgt - $oldLft)); $this->record->save(); + $newLevel = $this->record['level']; + $levelDiff = $newLevel - $oldLevel; + // Relocate descendants of the node $diff = $this->getLeftValue() - $oldLft; $componentName = $this->record->getTable()->getComponentName(); $rootColName = $this->record->getTable()->getTree()->getAttribute('rootColumnName'); - // Update lft/rgt/root for all descendants - $q = $this->record->getTable()->createQuery(); + // Update lft/rgt/root/level for all descendants + $q = new Doctrine_Query($conn); $q = $q->update($componentName) ->set($componentName . '.lft', 'lft + ' . $diff) ->set($componentName . '.rgt', 'rgt + ' . $diff) + ->set($componentName . '.level', 'level + ' . $levelDiff) ->set($componentName . '.' . $rootColName, $newRoot) ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?', array($oldLft, $oldRgt)); - $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $oldRoot); + $q = $this->_tree->returnQueryWithRootId($q, $oldRoot); $q->execute(); $conn->commit(); - } - catch (Exception $e) { + } catch (Exception $e) { $conn->rollback(); throw $e; } @@ -491,7 +560,9 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $this->_moveBetweenTrees($dest, $dest->getNode()->getLeftValue(), __FUNCTION__); } else { // Move within the tree - $this->updateNode($dest->getNode()->getLeftValue()); + $oldLevel = $this->record['level']; + $this->record['level'] = $dest['level']; + $this->updateNode($dest->getNode()->getLeftValue(), $this->record['level'] - $oldLevel); } } @@ -506,7 +577,9 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue() + 1, __FUNCTION__); } else { // Move within tree - $this->updateNode($dest->getNode()->getRightValue() + 1); + $oldLevel = $this->record['level']; + $this->record['level'] = $dest['level']; + $this->updateNode($dest->getNode()->getRightValue() + 1, $this->record['level'] - $oldLevel); } } @@ -521,7 +594,9 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $this->_moveBetweenTrees($dest, $dest->getNode()->getLeftValue() + 1, __FUNCTION__); } else { // Move within tree - $this->updateNode($dest->getNode()->getLeftValue() + 1); + $oldLevel = $this->record['level']; + $this->record['level'] = $dest['level'] + 1; + $this->updateNode($dest->getNode()->getLeftValue() + 1, $this->record['level'] - $oldLevel); } } @@ -536,12 +611,14 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue(), __FUNCTION__); } else { // Move within tree - $this->updateNode($dest->getNode()->getRightValue()); + $oldLevel = $this->record['level']; + $this->record['level'] = $dest['level'] + 1; + $this->updateNode($dest->getNode()->getRightValue(), $this->record['level'] - $oldLevel); } } /** - * Enter description here... + * Makes this node a root node. Only used in multiple-root trees. * * @todo Exception handling/wrapping */ @@ -555,39 +632,42 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $oldRgt = $this->getRightValue(); $oldLft = $this->getLeftValue(); $oldRoot = $this->getRootValue(); + $oldLevel = $this->record['level']; try { $conn = $this->record->getTable()->getConnection(); $conn->beginTransaction(); - // Detach from old tree + // Detach from old tree (close gap in old tree) $first = $oldRgt + 1; $delta = $oldLft - $oldRgt - 1; $this->shiftRLValues($first, $delta, $this->getRootValue()); - // Set new lft/rgt/root values for root node + // Set new lft/rgt/root/level values for root node $this->setLeftValue(1); $this->setRightValue($oldRgt - $oldLft + 1); $this->setRootValue($newRootId); + $this->record['level'] = 0; - // Update descendants lft/rgt/root values + // Update descendants lft/rgt/root/level values $diff = 1 - $oldLft; $newRoot = $newRootId; $componentName = $this->record->getTable()->getComponentName(); $rootColName = $this->record->getTable()->getTree()->getAttribute('rootColumnName'); - $q = $this->record->getTable()->createQuery(); + $q = new Doctrine_Query($conn); $q = $q->update($componentName) ->set($componentName . '.lft', 'lft + ' . $diff) ->set($componentName . '.rgt', 'rgt + ' . $diff) + ->set($componentName . '.level', 'level - ' . $oldLevel) ->set($componentName . '.' . $rootColName, $newRoot) ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?', array($oldLft, $oldRgt)); - $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $oldRoot); + $q = $this->_tree->returnQueryWithRootId($q, $oldRoot); $q->execute(); $conn->commit(); - } - catch (Exception $e) { + + } catch (Exception $e) { $conn->rollback(); throw $e; } @@ -642,7 +722,9 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int */ public function isDescendantOf(Doctrine_Record $subj) { - return (($this->getLeftValue()>$subj->getNode()->getLeftValue()) && ($this->getRightValue()<$subj->getNode()->getRightValue()) && ($this->getRootValue() == $subj->getNode()->getRootValue())); + return (($this->getLeftValue() > $subj->getNode()->getLeftValue()) && + ($this->getRightValue() < $subj->getNode()->getRightValue()) && + ($this->getRootValue() == $subj->getNode()->getRootValue())); } /** @@ -652,7 +734,9 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int */ public function isDescendantOfOrEqualTo(Doctrine_Record $subj) { - return (($this->getLeftValue()>=$subj->getNode()->getLeftValue()) && ($this->getRightValue()<=$subj->getNode()->getRightValue()) && ($this->getRootValue() == $subj->getNode()->getRootValue())); + return (($this->getLeftValue() >= $subj->getNode()->getLeftValue()) && + ($this->getRightValue() <= $subj->getNode()->getRightValue()) && + ($this->getRootValue() == $subj->getNode()->getRootValue())); } /** @@ -671,17 +755,17 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int /** * deletes node and it's descendants - * + * @todo Delete more efficiently. Wrap in transaction if needed. */ public function delete() { // TODO: add the setting whether or not to delete descendants or relocate children $oldRoot = $this->getRootValue(); - $q = $this->record->getTable()->createQuery(); + $q = $this->_tree->getBaseQuery(); $componentName = $this->record->getTable()->getComponentName(); - $q = $q->where($componentName. '.lft >= ? AND ' . $componentName . '.rgt <= ?', array($this->getLeftValue(), $this->getRightValue())); + $q = $q->where('base.lft >= ? AND base.rgt <= ?', array($this->getLeftValue(), $this->getRightValue())); $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $oldRoot); @@ -714,26 +798,38 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int * move node's and its children to location $destLeft and updates rest of tree * * @param int $destLeft destination left value + * @todo Wrap in transaction */ - private function updateNode($destLeft) + private function updateNode($destLeft, $levelDiff) { + $componentName = $this->record->getTable()->getComponentName(); $left = $this->getLeftValue(); $right = $this->getRightValue(); $rootId = $this->getRootValue(); $treeSize = $right - $left + 1; + // Make room in the new branch $this->shiftRLValues($destLeft, $treeSize, $rootId); - if($left >= $destLeft){ // src was shifted too? + if ($left >= $destLeft){ // src was shifted too? $left += $treeSize; $right += $treeSize; } + // update level for descendants + $q = new Doctrine_Query(); + $q = $q->update($componentName) + ->set($componentName . '.level', 'level + ' . $levelDiff) + ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?', + array($left, $right)); + $q = $this->_tree->returnQueryWithRootId($q, $rootId); + $q->execute(); + // now there's enough room next to target to move the subtree $this->shiftRLRange($left, $right, $destLeft - $left, $rootId); - // correct values after source + // correct values after source (close gap in old tree) $this->shiftRLValues($right + 1, -$treeSize, $rootId); $this->record->save(); @@ -751,8 +847,6 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int $qLeft = new Doctrine_Query(); $qRight = new Doctrine_Query(); - // TODO: Wrap in transaction - // shift left columns $componentName = $this->record->getTable()->getComponentName(); $qLeft = $qLeft->update($componentName) @@ -785,8 +879,6 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int { $qLeft = new Doctrine_Query(); $qRight = new Doctrine_Query(); - - // TODO : Wrap in transaction // shift left column values $componentName = $this->record->getTable()->getComponentName(); @@ -855,30 +947,18 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int */ public function getLevel() { - if(!isset($this->level)) - { + if (!isset($this->record['level'])) { $componentName = $this->record->getTable()->getComponentName(); - $q = $this->record->getTable()->createQuery(); - $q = $q->where($componentName . '.lft < ? AND ' . $componentName . '.rgt > ?', array($this->getLeftValue(), $this->getRightValue())); + $q = $this->_tree->getBaseQuery(); + $q = $q->where('base.lft < ? AND base.rgt > ?', array($this->getLeftValue(), $this->getRightValue())); - $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); + $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue()); $coll = $q->execute(); - $this->level = $coll->count() ? $coll->count() : 0; + $this->record['level'] = count($coll) ? count($coll) : 0; } - - return $this->level; - } - - /** - * sets node's level - * - * @param int - */ - public function setLevel($level) - { - $this->level = $level; + return $this->record['level']; } /** @@ -887,9 +967,9 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int */ public function getRootValue() { - if($this->record->getTable()->getTree()->getAttribute('hasManyRoots')) - return $this->record->get($this->record->getTable()->getTree()->getAttribute('rootColumnName')); - + if ($this->_tree->getAttribute('hasManyRoots')) { + return $this->record->get($this->_tree->getAttribute('rootColumnName')); + } return 1; } @@ -900,7 +980,8 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int */ public function setRootValue($value) { - if($this->record->getTable()->getTree()->getAttribute('hasManyRoots')) - $this->record->set($this->record->getTable()->getTree()->getAttribute('rootColumnName'), $value); + if ($this->_tree->getAttribute('hasManyRoots')) { + $this->record->set($this->_tree->getAttribute('rootColumnName'), $value); + } } } diff --git a/lib/Doctrine/Tree/NestedSet.php b/lib/Doctrine/Tree/NestedSet.php index caa1240e3..cdb8767ec 100644 --- a/lib/Doctrine/Tree/NestedSet.php +++ b/lib/Doctrine/Tree/NestedSet.php @@ -31,6 +31,8 @@ */ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Interface { + private $_baseQuery; + /** * constructor, creates tree with reference to table and sets default root options * @@ -55,11 +57,12 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int public function setTableDefinition() { if ($root = $this->getAttribute('rootColumnName')) { - $this->table->setColumn($root, 'integer', 11); + $this->table->setColumn($root, 'integer', 4); } - $this->table->setColumn('lft', 'integer', 11); - $this->table->setColumn('rgt', 'integer', 11); + $this->table->setColumn('lft', 'integer', 4); + $this->table->setColumn('rgt', 'integer', 4); + $this->table->setColumn('level', 'integer', 2); } /** @@ -80,6 +83,7 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int $record->set('lft', '1'); $record->set('rgt', '2'); + $record->set('level', 0); $record->save(); @@ -90,107 +94,110 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int * returns root node * * @return object $record instance of Doctrine_Record + * @deprecated Use fetchRoot() */ public function findRoot($rootId = 1) { - $q = $this->table->createQuery(); - $q = $q->where('lft = ?', 1); + return $this->fetchRoot($rootId); + } + + /** + * Fetches a/the root node. + * + * @param integer $rootId + */ + public function fetchRoot($rootId = 1) + { + $q = $this->getBaseQuery(); + $q = $q->where('base.lft = ?', 1); // if tree has many roots, then specify root id $q = $this->returnQueryWithRootId($q, $rootId); + $data = $q->execute(); - $root = $q->execute()->getFirst(); - - // if no record is returned, create record - if ( ! $root) { - $root = $this->table->create(); + if (count($data) <= 0) { + return false; } - // set level to prevent additional query to determine level - $root->getNode()->setLevel(0); + if ($data instanceof Doctrine_Collection) { + $root = $data->getFirst(); + $root['level'] = 0; + } else if (is_array($data)) { + $root = array_shift($data); + $root['level'] = 0; + } else { + throw new Doctrine_Tree_Exception("Unexpected data structure returned."); + } return $root; } /** - * optimised method to returns iterator for traversal of the entire tree from root + * Fetches a tree. * - * @param array $options options - * @return object $iterator instance of Doctrine_Node_NestedSet_PreOrderIterator + * @param array $options Options + * @return mixed The tree or FALSE if the tree could not be found. */ public function fetchTree($options = array()) { // fetch tree - $q = $this->table->createQuery(); + $q = $this->getBaseQuery(); $componentName = $this->table->getComponentName(); - $q = $q->where("$componentName.lft >= ?", 1) - ->orderBy("$componentName.lft asc"); + $q = $q->addWhere("base.lft >= ?", 1); // if tree has many roots, then specify root id $rootId = isset($options['root_id']) ? $options['root_id'] : '1'; - $q = $this->returnQueryWithRootId($q, $rootId); + if (is_array($rootId)) { + $q->orderBy("base." . $this->getAttribute('rootColumnName') . ", base.lft ASC"); + } else { + $q->orderBy("base.lft ASC"); + } + $q = $this->returnQueryWithRootId($q, $rootId); $tree = $q->execute(); - - $root = $tree->getFirst(); - - // if no record is returned, create record - if ( ! $root) { - $root = $this->table->create(); + + if (count($tree) <= 0) { + return false; } - - if ($root->exists()) { - // set level to prevent additional query - $root->getNode()->setLevel(0); - - // default to include root node - $options = array_merge(array('include_record'=>true), $options); - - // remove root node from collection if not required - if ($options['include_record'] == false) { - $tree->remove(0); - } - - // set collection for iterator - $options['collection'] = $tree; - - return $root->getNode()->traverse('Pre', $options); - } - - // TODO: no default return value or exception thrown? + + return $tree; } /** - * optimised method that returns iterator for traversal of the tree from the given record primary key + * Fetches a branch of a tree. * - * @param mixed $pk primary key as used by table::find() to locate node to traverse tree from - * @param array $options options - * @return iterator instance of Doctrine_Node__PreOrderIterator + * @param mixed $pk primary key as used by table::find() to locate node to traverse tree from + * @param array $options Options. + * @return mixed The branch or FALSE if the branch could not be found. + * @todo Only fetch the lft and rgt values of the initial record. more is not needed. */ public function fetchBranch($pk, $options = array()) { $record = $this->table->find($pk); - if ( ! ($record instanceof Doctrine_Record)) { + if ( ! ($record instanceof Doctrine_Record) || !$record->exists()) { // TODO: if record doesn't exist, throw exception or similar? return false; } + //$depth = isset($options['depth']) ? $options['depth'] : null; - if ($record->exists()) { - $options = array_merge(array('include_record'=>true), $options); - return $record->getNode()->traverse('Pre', $options); - } + $q = $this->getBaseQuery(); + $params = array($record->get('lft'), $record->get('rgt')); + $q->where("base.lft >= ? AND base.rgt <= ?", $params)->orderBy("base.lft asc"); + $q = $this->returnQueryWithRootId($q, $record->getNode()->getRootValue()); + return $q->execute(); } /** - * fetch root nodes + * Fetches all root nodes. If the tree has only one root this is the same as + * fetchRoot(). * - * @return collection Doctrine_Collection + * @return mixed The root nodes. */ public function fetchRoots() { - $q = $this->table->createQuery(); - $q = $q->where('lft = ?', 1); + $q = $this->getBaseQuery(); + $q = $q->where('base.lft = ?', 1); return $q->execute(); } @@ -238,9 +245,98 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int public function returnQueryWithRootId($query, $rootId = 1) { if ($root = $this->getAttribute('rootColumnName')) { - $query->addWhere($root . ' = ?', $rootId); + if (is_array($rootId)) { + $query->addWhere($root . ' IN (' . implode(',', array_fill(0, count($rootId), '?')) . ')', + $rootId); + } else { + $query->addWhere($root . ' = ?', $rootId); + } } return $query; } + + /** + * Enter description here... + * + * @param array $options + * @return unknown + */ + public function getBaseQuery() + { + if (!isset($this->_baseQuery)) { + $this->_baseQuery = $this->_createBaseQuery(); + } + return clone $this->_baseQuery; + } + + /** + * Enter description here... + * + */ + private function _createBaseQuery() + { + $q = new Doctrine_Query(); + $q->select("base.*")->from($this->table->getComponentName() . " base"); + return $q; + } + + /** + * Enter description here... + * + * @param Doctrine_Query $query + */ + public function setBaseQuery(Doctrine_Query $query) + { + $query->addSelect("base.lft, base.rgt, base.level"); + if ($this->getAttribute('rootColumnName')) { + $query->addSelect("base." . $this->getAttribute('rootColumnName')); + } + $this->_baseQuery = $query; + } + + /** + * Enter description here... + * + */ + public function resetBaseQuery() + { + $this->_baseQuery = null; + } + + /** + * Enter description here... + * + * @param unknown_type $graph + */ + /* + public function computeLevels($tree) + { + $right = array(); + $isArray = is_array($tree); + $rootColumnName = $this->getAttribute('rootColumnName'); + + for ($i = 0, $count = count($tree); $i < $count; $i++) { + if ($rootColumnName && $i > 0 && $tree[$i][$rootColumnName] != $tree[$i-1][$rootColumnName]) { + $right = array(); + } + + if (count($right) > 0) { + while (count($right) > 0 && $right[count($right)-1] < $tree[$i]['rgt']) { + //echo count($right); + array_pop($right); + } + } + + if ($isArray) { + $tree[$i]['level'] = count($right); + } else { + $tree[$i]->getNode()->setLevel(count($right)); + } + + $right[] = $tree[$i]['rgt']; + } + return $tree; + } + */ } diff --git a/models/BlogTag.php b/models/BlogTag.php new file mode 100644 index 000000000..918354bfc --- /dev/null +++ b/models/BlogTag.php @@ -0,0 +1,10 @@ +hasMany('Photo', 'Phototag.photo_id'); + } + public function setTableDefinition() { + $this->hasColumn('tag', 'string', 100); + } +} diff --git a/tests/Query/OneToOneFetchingTestCase.php b/tests/Query/OneToOneFetchingTestCase.php index a2784e115..b5ecf7f93 100644 --- a/tests/Query/OneToOneFetchingTestCase.php +++ b/tests/Query/OneToOneFetchingTestCase.php @@ -190,7 +190,7 @@ class Doctrine_Query_OneToOneFetching_TestCase extends Doctrine_UnitTestCase { $query = new Doctrine_Query($this->connection); try { - $categories = $query->select("c.*, b.*, le.*, a.username, vr.title, vr.color, vr.icon") + $categories = $query->select("c.*, b.*, le.date, a.username, vr.title, vr.color, vr.icon") ->from("QueryTest_Category c") ->leftJoin("c.boards b") ->leftJoin("b.lastEntry le") @@ -204,19 +204,13 @@ class Doctrine_Query_OneToOneFetching_TestCase extends Doctrine_UnitTestCase // get the baord for inspection $board = $categories[0]['boards'][0]; - - // lastentry should've 2 fields. one regular field, one relation. - $this->assertEqual(2, count($board['lastEntry'])); + $this->assertEqual(1234, (int)$board['lastEntry']['date']); $this->assertTrue(isset($board['lastEntry']['author'])); - // author should've 2 fields. one regular field, one relation. - $this->assertEqual(2, count($board['lastEntry']['author'])); $this->assertEqual('romanbb', $board['lastEntry']['author']['username']); $this->assertTrue(isset($board['lastEntry']['author']['visibleRank'])); - // visibleRank should've 3 regular fields - $this->assertEqual(3, count($board['lastEntry']['author']['visibleRank'])); $this->assertEqual('Freak', $board['lastEntry']['author']['visibleRank']['title']); $this->assertEqual('red', $board['lastEntry']['author']['visibleRank']['color']); $this->assertEqual('freak.png', $board['lastEntry']['author']['visibleRank']['icon']); @@ -255,8 +249,8 @@ class Doctrine_Query_OneToOneFetching_TestCase extends Doctrine_UnitTestCase // get the board for inspection $tmpBoard = $categories[0]['boards'][0]; - - $this->assertTrue( ! isset($board['lastEntry'])); + + $this->assertTrue( ! isset($tmpBoard['lastEntry'])); } catch (Doctrine_Exception $e) { print $e;