diff --git a/draft/EXAMPLE.tree.php b/draft/EXAMPLE.tree.php index d62198e49..11c18628e 100644 --- a/draft/EXAMPLE.tree.php +++ b/draft/EXAMPLE.tree.php @@ -39,9 +39,8 @@ class Menu extends Doctrine_Record { } - // this toString() function is used to get the name for the path, see node::getPath - // maybe change to actually use __toString(), howvever, i wondered if this had any significance ?? - public function toString() { + // this __toString() function is used to get the name for the path, see node::getPath + public function __toString() { return $this->get('name'); } } diff --git a/draft/EXAMPLE2.tree.php b/draft/EXAMPLE2.tree.php new file mode 100644 index 000000000..5f9f5ba06 --- /dev/null +++ b/draft/EXAMPLE2.tree.php @@ -0,0 +1,217 @@ +setTableName('menu_many_roots'); + + // add this your table definition to set the table as NestedSet tree implementation + // with many roots + $this->actsAsTree('NestedSet', array('has_many_roots' => true)); + + // you do not need to add any columns specific to the nested set implementation, these are added for you + $this->hasColumn("name","string",30); + + } + + // this __toString() function is used to get the name for the path, see node::getPath + public function __toString() { + return $this->get('name'); + } +} + +// set connections to database +$dsn = 'mysql:dbname=nestedset;host=localhost'; +$user = 'user'; +$password = 'pass'; + +try { + $dbh = new PDO($dsn, $user, $password); +} catch (PDOException $e) { + echo 'Connection failed: ' . $e->getMessage(); +} + +$manager = Doctrine_Manager::getInstance(); + +$conn = $manager->openConnection($dbh); + +// create root +$root = new Menu(); +$root->set('name', 'root'); + +$manager->getTable('Menu')->getTree()->createRoot($root); + +// build tree +$two = new Menu(); +$two->set('name', '2'); +$root->getNode()->addChild($two); + +$one = new Menu(); +$one->set('name', '1'); +$one->getNode()->insertAsPrevSiblingOf($two); + +// refresh as node's lft and rgt values have changed, zYne, can we automate this? +$two->refresh(); + +$three = new Menu(); +$three->set('name', '3'); +$three->getNode()->insertAsNextSiblingOf($two); +$two->refresh(); + +$one_one = new Menu(); +$one_one->set('name', '1.1'); +$one_one->getNode()->insertAsFirstChildOf($one); +$one->refresh(); + +$one_two = new Menu(); +$one_two->set('name', '1.2'); +$one_two->getNode()->insertAsLastChildOf($one); +$one_two->refresh(); + +$one_two_one = new Menu(); +$one_two_one->set('name', '1.2.1'); +$one_two->getNode()->addChild($one_two_one); + +$root->refresh(); +$four = new Menu(); +$four->set('name', '4'); +$root->getNode()->addChild($four); + +$root->refresh(); +$five = new Menu(); +$five->set('name', '5'); +$root->getNode()->addChild($five); + +$root->refresh(); +$six = new Menu(); +$six->set('name', '6'); +$root->getNode()->addChild($six); + +output_message('initial root'); +output_tree($root); + +// create a new root with a tree +$root2 = new Menu(); +$root2->set('name', 'new root'); + +$manager->getTable('Menu')->getTree()->createRoot($root2); + +// build tree +$two2 = new Menu(); +$two2->set('name', '2'); +$root2->getNode()->addChild($two2); + +$one2 = new Menu(); +$one2->set('name', '1'); +$one2->getNode()->insertAsPrevSiblingOf($two2); + +// refresh as node's lft and rgt values have changed, zYne, can we automate this? +$two2->refresh(); + +$three2 = new Menu(); +$three2->set('name', '3'); +$three2->getNode()->insertAsNextSiblingOf($two2); +$two2->refresh(); + +$one_one2 = new Menu(); +$one_one2->set('name', '1.1'); +$one_one2->getNode()->insertAsFirstChildOf($one2); +$one2->refresh(); + +$one_two2 = new Menu(); +$one_two2->set('name', '1.2'); +$one_two2->getNode()->insertAsLastChildOf($one2); +$one_two2->refresh(); + +$one_two_one2 = new Menu(); +$one_two_one2->set('name', '1.2.1'); +$one_two2->getNode()->addChild($one_two_one2); + +$root2->refresh(); +$four2 = new Menu(); +$four2->set('name', '4'); +$root2->getNode()->addChild($four2); + +output_message('new root'); +output_tree($root2); + +$one_one->refresh(); +$six->set('name', '1.0 (was 6)'); +$six->getNode()->moveAsPrevSiblingOf($one_one); + +$one_two->refresh(); +$five->refresh(); +$five->set('name', '1.3 (was 5)'); +$five->getNode()->moveAsNextSiblingOf($one_two); + +$one_one->refresh(); +$four->refresh(); +$four->set('name', '1.1.1 (was 4)'); +$four->getNode()->moveAsFirstChildOf($one_one); + +$root->refresh(); +$one_two_one->refresh(); +$one_two_one->set('name', 'last (was 1.2.1)'); +$one_two_one->getNode()->moveAsLastChildOf($root); + +output_message('transformed initial root'); +output_tree($root); + +function output_tree($root) +{ + // display tree + // first we must refresh the node as the tree has been transformed + $root->refresh(); + + // next we must get the iterator to traverse the tree from the root node + $traverse = $root->getNode()->traverse(); + + output_node($root); + // now we traverse the tree and output the menu items + while($item = $traverse->next()) + { + output_node($item); + } + + unset($traverse); +} + +function output_node($record) +{ + echo str_repeat('-', $record->getNode()->getLevel()) . $record->get('name') . ' (has children:'.$record->getNode()->hasChildren().') '. ' (is leaf:'.$record->getNode()->isLeaf().') '.'
'; +} + +function output_message($msg) +{ + echo "
$msg".'
'; +} + + + + diff --git a/draft/Node/NestedSet.php b/draft/Node/NestedSet.php index b0adf6511..b00b22de2 100644 --- a/draft/Node/NestedSet.php +++ b/draft/Node/NestedSet.php @@ -31,654 +31,693 @@ */ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Interface { - /** - * test if node has previous sibling - * - * @return bool - */ - public function hasPrevSibling() + /** + * test if node has previous sibling + * + * @return bool + */ + public function hasPrevSibling() + { + return $this->isValidNode($this->getPrevSibling()); + } + + /** + * test if node has next sibling + * + * @return bool + */ + public function hasNextSibling() + { + return $this->isValidNode($this->getNextSibling()); + } + + /** + * test if node has children + * + * @return bool + */ + public function hasChildren() + { + return (($this->getRightValue() - $this->getLeftValue() ) >1 ); + } + + /** + * test if node has parent + * + * @return bool + */ + public function hasParent() + { + return !$this->isRoot(); + } + + /** + * gets record of prev sibling or empty record + * + * @return object Doctrine_Record + */ + public function getPrevSibling() + { + $q = $this->record->getTable()->createQuery(); + $result = $q->where('rgt = ?', $this->getLeftValue() - 1)->execute()->getFirst(); + + if(!$result) + $result = $this->record->getTable()->create(); + + return $result; + } + + /** + * gets record of next sibling or empty record + * + * @return object Doctrine_Record + */ + public function getNextSibling() + { + $q = $this->record->getTable()->createQuery(); + $result = $q->where('lft = ?', $this->getRightValue() + 1)->execute()->getFirst(); + + if(!$result) + $result = $this->record->getTable()->create(); + + return $result; + } + + /** + * gets siblings for node + * + * @return array array of sibling Doctrine_Record objects + */ + public function getSiblings($includeNode = false) + { + $parent = $this->getParent(); + $siblings = array(); + if($parent->exists()) { - return $this->isValidNode($this->getPrevSibling()); + foreach($parent->getNode()->getChildren() as $child) + { + if($this->isEqualTo($child) && !$includeNode) + continue; + + $siblings[] = $child; + } + } + + return $siblings; + } + + /** + * gets record of first child or empty record + * + * @return object Doctrine_Record + */ + public function getFirstChild() + { + $q = $this->record->getTable()->createQuery(); + $result = $q->where('lft = ?', $this->getLeftValue() + 1)->execute()->getFirst(); + + if(!$result) + $result = $this->record->getTable()->create(); + + return $result; + } + + /** + * gets record of last child or empty record + * + * @return object Doctrine_Record + */ + public function getLastChild() + { + $q = $this->record->getTable()->createQuery(); + $result = $q->where('rgt = ?', $this->getRightValue() - 1)->execute()->getFirst(); + + if(!$result) + $result = $this->record->getTable()->create(); + + return $result; + } + + /** + * gets children for node (direct descendants only) + * + * @return array array of sibling Doctrine_Record objects + */ + public function getChildren() + { + return $this->getIterator('Pre', array('depth' => 1)); + } + + /** + * gets descendants for node (direct descendants only) + * + * @return iterator iterator to traverse descendants from node + */ + public function getDescendants() + { + return $this->getIterator(); + } + + /** + * gets record of parent or empty record + * + * @return object Doctrine_Record + */ + public function getParent() + { + $q = $this->record->getTable()->createQuery(); + + $parent = $q->where('lft < ? AND rgt > ?', array($this->getLeftValue(), $this->getRightValue())) + ->orderBy('rgt asc') + ->execute() + ->getFirst(); + + if(!$parent) + $parent = $this->record->getTable()->create(); + + return $parent; + } + + /** + * gets ancestors for node + * + * @return object Doctrine_Collection + */ + public function getAncestors() + { + $q = $this->record->getTable()->createQuery(); + + $ancestors = $q->where('lft < ? AND rgt > ?', array($this->getLeftValue(), $this->getRightValue())) + ->orderBy('lft asc') + ->execute(); + + return $ancestors; + } + + /** + * gets path to node from root, uses record::toString() method to get node names + * + * @param string $seperator path seperator + * @param bool $includeNode whether or not to include node at end of path + * @return string string representation of path + */ + public function getPath($seperator = ' > ', $includeRecord = false) + { + $path = array(); + $ancestors = $this->getAncestors(); + foreach($ancestors as $ancestor) + { + $path[] = $ancestor->__toString(); + } + if($includeRecord) + $path[] = $this->getRecord()->__toString(); + + return implode($seperator, $path); + } + + /** + * gets number of children (direct descendants) + * + * @return int + */ + public function getNumberChildren() + { + $count = 0; + $children = $this->getChildren(); + + while($children->next()) + { + $count++; + } + return $count; + } + + /** + * gets number of descendants (children and their children) + * + * @return int + */ + public function getNumberDescendants() + { + return ($this->getRightValue() - $this->getLeftValue() - 1) / 2; + } + + /** + * inserts node as parent of dest record + * + * @return bool + */ + public function insertAsParentOf(Doctrine_Record $dest) + { + // cannot insert a node that has already has a place within the tree + if($this->isValidNode()) + return false; + + // cannot insert as parent of root + if($dest->getNode()->isRoot()) + return false; + + $this->shiftRLValues($dest->getNode()->getLeftValue(), 1); + $this->shiftRLValues($dest->getNode()->getRightValue() + 2, 1); + + $newLeft = $dest->getNode()->getLeftValue(); + $newRight = $dest->getNode()->getRightValue() + 2; + $newRoot = $dest->getNode()->getRootValue(); + + $this->insertNode($newLeft, $newRight, $newRoot); + + return true; + } + + /** + * inserts node as previous sibling of dest record + * + * @return bool + */ + public function insertAsPrevSiblingOf(Doctrine_Record $dest) + { + // cannot insert a node that has already has a place within the tree + if($this->isValidNode()) + return false; + + $newLeft = $dest->getNode()->getLeftValue(); + $newRight = $dest->getNode()->getLeftValue() + 1; + $newRoot = $dest->getNode()->getRootValue(); + + $this->shiftRLValues($newLeft, 2, $newRoot); + $this->insertNode($newLeft, $newRight, $newRoot); + // update destination left/right values to prevent a refresh + // $dest->getNode()->setLeftValue($dest->getNode()->getLeftValue() + 2); + // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2); + + return true; + } + + /** + * inserts node as next sibling of dest record + * + * @return bool + */ + public function insertAsNextSiblingOf(Doctrine_Record $dest) + { + // cannot insert a node that has already has a place within the tree + if($this->isValidNode()) + return false; + + $newLeft = $dest->getNode()->getRightValue() + 1; + $newRight = $dest->getNode()->getRightValue() + 2; + $newRoot = $dest->getNode()->getRootValue(); + + $this->shiftRLValues($newLeft, 2, $newRoot); + $this->insertNode($newLeft, $newRight, $newRoot); + + // update destination left/right values to prevent a refresh + // no need, node not affected + + return true; + } + + /** + * inserts node as first child of dest record + * + * @return bool + */ + public function insertAsFirstChildOf(Doctrine_Record $dest) + { + // cannot insert a node that has already has a place within the tree + if($this->isValidNode()) + return false; + + $newLeft = $dest->getNode()->getLeftValue() + 1; + $newRight = $dest->getNode()->getLeftValue() + 2; + $newRoot = $dest->getNode()->getRootValue(); + + $this->shiftRLValues($newLeft, 2, $newRoot); + $this->insertNode($newLeft, $newRight, $newRoot); + + // update destination left/right values to prevent a refresh + // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2); + + return true; + } + + /** + * inserts node as last child of dest record + * + * @return bool + */ + public function insertAsLastChildOf(Doctrine_Record $dest) + { + // cannot insert a node that has already has a place within the tree + if($this->isValidNode()) + return false; + + $newLeft = $dest->getNode()->getRightValue(); + $newRight = $dest->getNode()->getRightValue() + 1; + $newRoot = $dest->getNode()->getRootValue(); + + $this->shiftRLValues($newLeft, 2, $newRoot); + $this->insertNode($newLeft, $newRight, $newRoot); + + // update destination left/right values to prevent a refresh + // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2); + + return true; + } + + /** + * moves node as prev sibling of dest record + * + */ + public function moveAsPrevSiblingOf(Doctrine_Record $dest) + { + $this->updateNode($dest->getNode()->getLeftValue()); + } + + /** + * moves node as next sibling of dest record + * + */ + public function moveAsNextSiblingOf(Doctrine_Record $dest) + { + $this->updateNode($dest->getNode()->getRightValue() + 1); + } + + /** + * moves node as first child of dest record + * + */ + public function moveAsFirstChildOf(Doctrine_Record $dest) + { + $this->updateNode($dest->getNode()->getLeftValue() + 1); + } + + /** + * moves node as last child of dest record + * + */ + public function moveAsLastChildOf(Doctrine_Record $dest) + { + $this->updateNode($dest->getNode()->getRightValue()); + } + + /** + * adds node as last child of record + * + */ + public function addChild(Doctrine_Record $record) + { + $record->getNode()->insertAsLastChildOf($this->getRecord()); + } + + /** + * determines if node is leaf + * + * @return bool + */ + public function isLeaf() + { + return (($this->getRightValue()-$this->getLeftValue())==1); + } + + /** + * determines if node is root + * + * @return bool + */ + public function isRoot() + { + return ($this->getLeftValue()==1); + } + + /** + * determines if node is equal to subject node + * + * @return bool + */ + public function isEqualTo(Doctrine_Record $subj) + { + return (($this->getLeftValue()==$subj->getNode()->getLeftValue()) and ($this->getRightValue()==$subj->getNode()->getRightValue()) and ($this->getRootValue() == $subj->getNode()->getRootValue())); + } + + /** + * determines if node is child of subject node + * + * @return bool + */ + public function isDescendantOf(Doctrine_Record $subj) + { + return (($this->getLeftValue()>$subj->getNode()->getLeftValue()) and ($this->getRightValue()<$subj->getNode()->getRightValue()) and ($this->getRootValue() == $subj->getNode()->getRootValue())); + } + + /** + * determines if node is child of or sibling to subject node + * + * @return bool + */ + public function isDescendantOfOrEqualTo(Doctrine_Record $subj) + { + return (($this->getLeftValue()>=$subj->getNode()->getLeftValue()) and ($this->getRightValue()<=$subj->getNode()->getRightValue()) and ($this->getRootValue() == $subj->getNode()->getRootValue())); + } + + /** + * determines if node is valid + * + * @return bool + */ + public function isValidNode() + { + return ($this->getRightValue() > $this->getLeftValue()); + } + + /** + * deletes node and it's descendants + * + */ + public function delete() + { + // TODO: add the setting whether or not to delete descendants or relocate children + + $q = $this->record->getTable()->createQuery(); + + $componentName = $this->record->getTable()->getComponentName(); + + $q = $q->where("$componentName.lft >= ? AND $componentName.rgt <= ?", array($this->getLeftValue(), $this->getRightValue())); + + $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); + + $coll = $q->execute(); + + $coll->delete(); + + $first = $this->getRightValue() + 1; + $delta = $this->getLeftValue() - $this->getRightValue() - 1; + $this->shiftRLValues($first, $delta); + + return true; + } + + /** + * sets node's left and right values and save's it + * + * @param int $destLeft node left value + * @param int $destRight node right value + */ + private function insertNode($destLeft = 0, $destRight = 0, $destRoot = 1) + { + $this->setLeftValue($destLeft); + $this->setRightValue($destRight); + $this->setRootValue($destRoot); + $this->record->save(); + } + + /** + * move node's and its children to location $destLeft and updates rest of tree + * + * @param int $destLeft destination left value + */ + private function updateNode($destLeft) + { + $left = $this->getLeftValue(); + $right = $this->getRightValue(); + + $treeSize = $right - $left + 1; + + $this->shiftRLValues($destLeft, $treeSize); + + if($left >= $destLeft){ // src was shifted too? + $left += $treeSize; + $right += $treeSize; + } + + // now there's enough room next to target to move the subtree + $this->shiftRLRange($left, $right, $destLeft - $left); + + // correct values after source + $this->shiftRLValues($right + 1, -$treeSize); + + $this->record->save(); + $this->record->refresh(); + } + + /** + * adds '$delta' to all Left and Right values that are >= '$first'. '$delta' can also be negative. + * + * @param int $first First node to be shifted + * @param int $delta Value to be shifted by, can be negative + */ + private function shiftRLValues($first, $delta, $root_id = 1) + { + $qLeft = $this->record->getTable()->createQuery(); + $qRight = $this->record->getTable()->createQuery(); + + // TODO: Wrap in transaction + + // shift left columns + $qLeft = $qLeft->update($this->record->getTable()->getComponentName()) + ->set('lft', "lft + $delta") + ->where('lft >= ?', $first); + + $qLeft = $this->record->getTable()->getTree()->returnQueryWithRootId($qLeft, $root_id); + + $resultLeft = $qLeft->execute(); + + // shift right columns + $resultRight = $qRight->update($this->record->getTable()->getComponentName()) + ->set('rgt', "rgt + $delta") + ->where('rgt >= ?', $first); + + $qRight = $this->record->getTable()->getTree()->returnQueryWithRootId($qRight, $root_id); + + $resultRight = $qRight->execute(); + } + + /** + * adds '$delta' to all Left and Right values that are >= '$first' and <= '$last'. + * '$delta' can also be negative. + * + * @param int $first First node to be shifted (L value) + * @param int $last Last node to be shifted (L value) + * @param int $delta Value to be shifted by, can be negative + */ + private function shiftRLRange($first, $last, $delta, $root_id = 1) + { + $qLeft = $this->record->getTable()->createQuery(); + $qRight = $this->record->getTable()->createQuery(); + + // TODO : Wrap in transaction + + // shift left column values + $qLeft = $qLeft->update($this->record->getTable()->getComponentName()) + ->set('lft', "lft + $delta") + ->where('lft >= ? AND lft <= ?', array($first, $last)); + + $qLeft = $this->record->getTable()->getTree()->returnQueryWithRootId($qLeft, $root_id); + + $resultLeft = $qLeft->execute(); + + // shift right column values + $qRight = $qRight->update($this->record->getTable()->getComponentName()) + ->set('rgt', "rgt + $delta") + ->where('rgt >= ? AND rgt <= ?', array($first, $last)); + + $qRight = $this->record->getTable()->getTree()->returnQueryWithRootId($qRight, $root_id); + + $resultRight = $qRight->execute(); + } + + /** + * gets record's left value + * + * @return int + */ + public function getLeftValue() + { + return $this->record->get('lft'); + } + + /** + * sets record's left value + * + * @param int + */ + public function setLeftValue($lft) + { + $this->record->set('lft', $lft); + } + + /** + * gets record's right value + * + * @return int + */ + public function getRightValue() + { + return $this->record->get('rgt'); + } + + /** + * sets record's right value + * + * @param int + */ + public function setRightValue($rgt) + { + $this->record->set('rgt', $rgt); + } + + /** + * gets level (depth) of node in the tree + * + * @return int + */ + public function getLevel() + { + if(!isset($this->level)) + { + $q = $this->record->getTable()->createQuery(); + $q = $q->where('lft < ? AND rgt > ?', array($this->getLeftValue(), $this->getRightValue())); + + $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); + + $coll = $q->execute(); + + $this->level = $coll->count() ? $coll->count() : 0; } - /** - * test if node has next sibling - * - * @return bool - */ - public function hasNextSibling() - { - return $this->isValidNode($this->getNextSibling()); - } - - /** - * test if node has children - * - * @return bool - */ - public function hasChildren() - { - return (($this->getRightValue() - $this->getLeftValue() ) >1); - } - - /** - * test if node has parent - * - * @return bool - */ - public function hasParent() - { - return !$this->isRoot(); - } - - /** - * gets record of prev sibling or empty record - * - * @return object Doctrine_Record - */ - public function getPrevSibling() - { - $q = $this->record->getTable()->createQuery(); - $result = $q->where('rgt = ?', $this->getLeftValue() - 1)->execute()->getFirst(); - - if (!$result) { - $result = $this->record->getTable()->create(); - } - - return $result; - } - - /** - * gets record of next sibling or empty record - * - * @return object Doctrine_Record - */ - public function getNextSibling() - { - $q = $this->record->getTable()->createQuery(); - $result = $q->where('lft = ?', $this->getRightValue() + 1)->execute()->getFirst(); - - if (!$result) { - $result = $this->record->getTable()->create(); - } - - return $result; - } - - /** - * gets siblings for node - * - * @return array array of sibling Doctrine_Record objects - */ - public function getSiblings($includeNode = false) - { - $parent = $this->getParent(); - $siblings = array(); - if ($parent->exists()) { - foreach($parent->getNode()->getChildren() as $child) { - if ($this->isEqualTo($child) && !$includeNode) { - continue; - } - $siblings[] = $child; - } - } - - return $siblings; - } - - /** - * gets record of first child or empty record - * - * @return object Doctrine_Record - */ - public function getFirstChild() - { - $q = $this->record->getTable()->createQuery(); - $result = $q->where('lft = ?', $this->getLeftValue() + 1)->execute()->getFirst(); - - if (!$result) { - $result = $this->record->getTable()->create(); - } - - return $result; - } - - /** - * gets record of last child or empty record - * - * @return object Doctrine_Record - */ - public function getLastChild() - { - $q = $this->record->getTable()->createQuery(); - $result = $q->where('rgt = ?', $this->getRightValue() - 1)->execute()->getFirst(); - - if (!$result) { - $result = $this->record->getTable()->create(); - } - - return $result; - } - - /** - * gets children for node (direct descendants only) - * - * @return array array of sibling Doctrine_Record objects - */ - public function getChildren() - { - return $this->getIterator('Pre', array('depth' => 1)); - } - - /** - * gets descendants for node (direct descendants only) - * - * @return iterator iterator to traverse descendants from node - */ - public function getDescendants() - { - return $this->getIterator(); - } - - /** - * gets record of parent or empty record - * - * @return object Doctrine_Record - */ - public function getParent() - { - $q = $this->record->getTable()->createQuery(); - - $parent = $q->where('lft < ? AND rgt > ?', array($this->getLeftValue(), $this->getRightValue())) - ->orderBy('rgt asc') - ->execute() - ->getFirst(); - - if (!$parent) { - $parent = $this->record->getTable()->create(); - } - - return $parent; - } - - /** - * gets ancestors for node - * - * @return object Doctrine_Collection - */ - public function getAncestors() - { - $q = $this->record->getTable()->createQuery(); - - $ancestors = $q->where('lft < ? AND rgt > ?', array($this->getLeftValue(), $this->getRightValue())) - ->orderBy('lft asc') - ->execute(); - - return $ancestors; - } - - /** - * gets path to node from root, uses record::toString() method to get node names - * - * @param string $seperator path seperator - * @param bool $includeNode whether or not to include node at end of path - * @return string string representation of path - */ - public function getPath($seperator = ' > ', $includeRecord = false) - { - $path = array(); - $ancestors = $this->getAncestors(); - foreach ($ancestors as $ancestor) { - $path[] = $ancestor->toString(); - } - if ($includeRecord) { - $path[] = $this->getRecord()->toString(); - } - - return implode($seperator, $path); - } - - /** - * gets number of children (direct descendants) - * - * @return int - */ - public function getNumberChildren() - { - $count = 0; - $children = $this->getChildren(); - - while ($children->next()) { - $count++; - } - return $count; - } - - /** - * gets number of descendants (children and their children) - * - * @return int - */ - public function getNumberDescendants() - { - return ($this->getRightValue() - $this->getLeftValue() - 1) / 2; - } - - /** - * inserts node as parent of dest record - * - * @return bool - */ - public function insertAsParentOf(Doctrine_Record $dest) - { - // cannot insert a node that has already has a place within the tree - if ($this->isValidNode()) { - return false; - } - - // cannot insert as parent of root - if ($dest->getNode()->isRoot()) { - return false; - } - - $this->shiftRLValues($dest->getNode()->getLeftValue(), 1); - $this->shiftRLValues($dest->getNode()->getRightValue() + 2, 1); - - $newLeft = $dest->getNode()->getLeftValue(); - $newRight = $dest->getNode()->getRightValue() + 2; - $this->insertNode($newLeft, $newRight); - - return true; - } - - /** - * inserts node as previous sibling of dest record - * - * @return bool - */ - public function insertAsPrevSiblingOf(Doctrine_Record $dest) - { - // cannot insert a node that has already has a place within the tree - if ($this->isValidNode()) { - return false; - } - - $newLeft = $dest->getNode()->getLeftValue(); - $newRight = $dest->getNode()->getLeftValue() + 1; - - $this->shiftRLValues($newLeft, 2); - $this->insertNode($newLeft, $newRight); - - // update destination left/right values to prevent a refresh - // $dest->getNode()->setLeftValue($dest->getNode()->getLeftValue() + 2); - // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2); - - return true; - } - - /** - * inserts node as next sibling of dest record - * - * @return bool - */ - public function insertAsNextSiblingOf(Doctrine_Record $dest) - { - // cannot insert a node that has already has a place within the tree - if ($this->isValidNode()) { - return false; - } - - $newLeft = $dest->getNode()->getRightValue() + 1; - $newRight = $dest->getNode()->getRightValue() + 2; - - $this->shiftRLValues($newLeft, 2); - $this->insertNode($newLeft, $newRight); - - // update destination left/right values to prevent a refresh - // no need, node not affected - - return true; - } - - /** - * inserts node as first child of dest record - * - * @return bool - */ - public function insertAsFirstChildOf(Doctrine_Record $dest) - { - // cannot insert a node that has already has a place within the tree - if ($this->isValidNode()) { - return false; - } - - $newLeft = $dest->getNode()->getLeftValue() + 1; - $newRight = $dest->getNode()->getLeftValue() + 2; - - $this->shiftRLValues($newLeft, 2); - $this->insertNode($newLeft, $newRight); - - // update destination left/right values to prevent a refresh - // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2); - - return true; - } - - /** - * inserts node as last child of dest record - * - * @return bool - */ - public function insertAsLastChildOf(Doctrine_Record $dest) - { - // cannot insert a node that has already has a place within the tree - if ($this->isValidNode()) { - return false; - } - - $newLeft = $dest->getNode()->getRightValue(); - $newRight = $dest->getNode()->getRightValue() + 1; - - $this->shiftRLValues($newLeft, 2); - $this->insertNode($newLeft, $newRight); - - // update destination left/right values to prevent a refresh - // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2); - - return true; - } - - /** - * moves node as prev sibling of dest record - * - */ - public function moveAsPrevSiblingOf(Doctrine_Record $dest) - { - $this->updateNode($dest->getNode()->getLeftValue()); - } - - /** - * moves node as next sibling of dest record - * - */ - public function moveAsNextSiblingOf(Doctrine_Record $dest) - { - $this->updateNode($dest->getNode()->getRightValue() + 1); - } - - /** - * moves node as first child of dest record - * - */ - public function moveAsFirstChildOf(Doctrine_Record $dest) - { - $this->updateNode($dest->getNode()->getLeftValue() + 1); - } - - /** - * moves node as last child of dest record - * - */ - public function moveAsLastChildOf(Doctrine_Record $dest) - { - $this->updateNode($dest->getNode()->getRightValue()); - } - - /** - * adds node as last child of record - * - */ - public function addChild(Doctrine_Record $record) - { - $record->getNode()->insertAsLastChildOf($this->getRecord()); - } - - /** - * determines if node is leaf - * - * @return bool - */ - public function isLeaf() - { - return (($this->getRightValue() - $this->getLeftValue()) == 1); - } - - /** - * determines if node is root - * - * @return bool - */ - public function isRoot() - { - return ($this->getLeftValue() == 1); - } - - /** - * determines if node is equal to subject node - * - * @return bool - */ - public function isEqualTo(Doctrine_Record $subj) - { - // TODO: break this out to reduce line length <120 (Zend) - return (($this->getLeftValue()==$subj->getNode()->getLeftValue()) and ($this->getRightValue()==$subj->getNode()->getRightValue())); - } - - /** - * determines if node is child of subject node - * - * @return bool - */ - public function isDescendantOf(Doctrine_Record $subj) - { - // TODO: break this out to reduce line length <120 (Zend) - return (($this->getLeftValue()>$subj->getNode()->getLeftValue()) and ($this->getRightValue()<$subj->getNode()->getRightValue())); - } - - /** - * determines if node is child of or sibling to subject node - * - * @return bool - */ - public function isDescendantOfOrEqualTo(Doctrine_Record $subj) - { - // TODO: break this out to reduce line length <120 (Zend) - return (($this->getLeftValue()>=$subj->getNode()->getLeftValue()) and ($this->getRightValue()<=$subj->getNode()->getRightValue())); - } - - /** - * determines if node is valid - * - * @return bool - */ - public function isValidNode() - { - return ($this->getRightValue() > $this->getLeftValue()); - } - - /** - * deletes node and it's descendants - * - */ - public function delete() - { - // TODO: add the setting whether or not to delete descendants or relocate children - - $q = $this->record->getTable()->createQuery(); - - $componentName = $this->record->getTable()->getComponentName(); - - $params = array($this->getLeftValue(), $this->getRightValue()); - $coll = $q->where("$componentName.lft >= ? AND $componentName.rgt <= ?", $params)->execute(); - $coll->delete(); - - $first = $this->getRightValue() + 1; - $delta = $this->getLeftValue() - $this->getRightValue() - 1; - $this->shiftRLValues($first, $delta); - - return true; - } - - /** - * sets node's left and right values and save's it - * - * @param int $destLeft node left value - * @param int $destRight node right value - */ - private function insertNode($destLeft = 0, $destRight = 0) - { - $this->setLeftValue($destLeft); - $this->setRightValue($destRight); - $this->record->save(); - } - - /** - * move node's and its children to location $destLeft and updates rest of tree - * - * @param int $destLeft destination left value - */ - private function updateNode($destLeft) - { - $left = $this->getLeftValue(); - $right = $this->getRightValue(); - - $treeSize = $right - $left + 1; - - $this->shiftRLValues($destLeft, $treeSize); - - if ($left >= $destLeft) { // src was shifted too? - $left += $treeSize; - $right += $treeSize; - } - - // now there's enough room next to target to move the subtree - $this->shiftRLRange($left, $right, $destLeft - $left); - - // correct values after source - $this->shiftRLValues($right + 1, -$treeSize); - - $this->record->save(); - $this->record->refresh(); - } - - /** - * adds '$delta' to all Left and Right values that are >= '$first'. '$delta' can also be negative. - * - * @param int $first first node to be shifted - * @param int $delta value to be shifted by, can be negative - */ - private function shiftRLValues($first, $delta) - { - $qLeft = $this->record->getTable()->createQuery(); - $qRight = $this->record->getTable()->createQuery(); - - // TODO: Wrap in transaction - - // shift left columns - $resultLeft = $qLeft->update($this->record->getTable()->getComponentName()) - ->set('lft', "lft + $delta") - ->where('lft >= ?', $first) - ->execute(); - - // shift right columns - $resultRight = $qRight->update($this->record->getTable()->getComponentName()) - ->set('rgt', "rgt + $delta") - ->where('rgt >= ?', $first) - ->execute(); - } - - /** - * adds '$delta' to all Left and Right values that are >= '$first' and <= '$last'. - * '$delta' can also be negative. - * - * @param int $first first node to be shifted (L value) - * @param int $last last node to be shifted (L value) - * @param int $delta value to be shifted by, can be negative - */ - private function shiftRLRange($first, $last, $delta) - { - $qLeft = $this->record->getTable()->createQuery(); - $qRight = $this->record->getTable()->createQuery(); - - // TODO : Wrap in transaction - - // shift left column values - $result = $qLeft->update($this->record->getTable()->getComponentName()) - ->set('lft', "lft + $delta") - ->where('lft >= ? AND lft <= ?', array($first, $last)) - ->execute(); - - // shift right column values - $result = $qRight->update($this->record->getTable()->getComponentName()) - ->set('rgt', "rgt + $delta") - ->where('rgt >= ? AND rgt <= ?', array($first, $last)) - ->execute(); - } - - /** - * gets record's left value - * - * @return int - */ - public function getLeftValue() - { - return $this->record->get('lft'); - } - - /** - * sets record's left value - * - * @param int - */ - public function setLeftValue($lft) - { - $this->record->set('lft', $lft); - } - - /** - * gets record's right value - * - * @return int - */ - public function getRightValue() - { - return $this->record->get('rgt'); - } - - /** - * sets record's right value - * - * @param int - */ - public function setRightValue($rgt) - { - $this->record->set('rgt', $rgt); - } - - /** - * gets level (depth) of node in the tree - * - * @return int - */ - public function getLevel() - { - if (!isset($this->level)) { - $q = $this->record->getTable()->createQuery(); - $coll = $q->where('lft < ? AND rgt > ?', array($this->getLeftValue(), $this->getRightValue())) - ->execute(); - $this->level = $coll->count() ? $coll->count() : 0; - } - - return $this->level; - } - - /** - * sets node's level - * - * @param int - */ - public function setLevel($level) - { - $this->level = $level; - } -} + return $this->level; + } + + /** + * sets node's level + * + * @param int + */ + public function setLevel($level) + { + $this->level = $level; + } + + /** + * get records root id value + * + */ + public function getRootValue() + { + if($this->record->getTable()->getTree()->getAttribute('has_many_roots')) + return $this->record->get($this->record->getTable()->getTree()->getAttribute('root_column_name')); + + return 1; + } + + /** + * sets records root id value + * + * @param int + */ + public function setRootValue($value) + { + if($this->record->getTable()->getTree()->getAttribute('has_many_roots')) + $this->record->set($this->record->getTable()->getTree()->getAttribute('root_column_name'), $value); + } +} \ No newline at end of file diff --git a/draft/Node/NestedSet/PreOrderIterator.php b/draft/Node/NestedSet/PreOrderIterator.php index 71c8782c1..bc2e87d5a 100644 --- a/draft/Node/NestedSet/PreOrderIterator.php +++ b/draft/Node/NestedSet/PreOrderIterator.php @@ -72,6 +72,8 @@ class Doctrine_Node_NestedSet_PreOrderIterator implements Iterator } else { $query = $q->where("$componentName.lft > ? AND $componentName.rgt < ?", $params)->orderBy('lft asc'); } + + $query = $record->getTable()->getTree()->returnQueryWithRootId($query, $record->getNode()->getRootValue()); $this->maxLevel = isset($opts['depth']) ? ($opts['depth'] + $record->getNode()->getLevel()) : 0; $this->options = $opts; diff --git a/draft/Tree.php b/draft/Tree.php index 1279cfb08..690f5b569 100644 --- a/draft/Tree.php +++ b/draft/Tree.php @@ -39,7 +39,7 @@ class Doctrine_Tree /** * @param array $options */ - protected $options; + protected $options = array(); /** * constructor, creates tree with reference to table and any options @@ -88,4 +88,23 @@ class Doctrine_Tree } return new $class($table, $options); } + + /** + * gets tree attribute value + * + */ + public function getAttribute($name) + { + return isset($this->options[$name]) ? $this->options[$name] : null; + } + + /** + * sets tree attribute value + * + * @param mixed + */ + public function setAttribute($name, $value) + { + $this->options[$name] = $value; + } } diff --git a/draft/Tree/Interface.php b/draft/Tree/Interface.php index d97f2f8df..7c365fee5 100644 --- a/draft/Tree/Interface.php +++ b/draft/Tree/Interface.php @@ -43,7 +43,7 @@ interface Doctrine_Tree_Interface { * * @return object $record instance of Doctrine_Record */ - public function findRoot(); + public function findRoot($root_id = 1); /** * optimised method to returns iterator for traversal of the entire tree from root diff --git a/draft/Tree/NestedSet.php b/draft/Tree/NestedSet.php index 316753b43..35f17f925 100644 --- a/draft/Tree/NestedSet.php +++ b/draft/Tree/NestedSet.php @@ -31,6 +31,21 @@ */ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Interface { + /** + * constructor, creates tree with reference to table and sets default root options + * + * @param object $table instance of Doctrine_Table + * @param array $options options + */ + public function __construct(Doctrine_Table $table, $options) + { + // set default many root attributes + $options['has_many_roots'] = isset($options['has_many_roots']) ? $options['has_many_roots'] : false; + $options['root_column_name'] = isset($options['root_column_name']) ? $options['root_column_name'] : 'root_id'; + + parent::__construct($table, $options); + } + /** * used to define table attributes required for the NestetSet implementation * adds lft and rgt columns for corresponding left and right values @@ -38,6 +53,9 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int */ public function setTableDefinition() { + if($this->getAttribute('has_many_roots')) + $this->table->setColumn($this->getAttribute('root_column_name'),"integer",11); + $this->table->setColumn("lft","integer",11); $this->table->setColumn("rgt","integer",11); } @@ -53,8 +71,13 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int $record = $this->table->create(); } + // if tree is many roots, then get next root id + if($this->getAttribute('has_many_roots')) + $record->getNode()->setRootValue($this->getNextRootId()); + $record->set('lft', '1'); $record->set('rgt', '2'); + $record->save(); return $record; @@ -65,11 +88,15 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int * * @return object $record instance of Doctrine_Record */ - public function findRoot() + public function findRoot($root_id = 1) { $q = $this->table->createQuery(); - $root = $q->where('lft = ?', 1) - ->execute()->getFirst(); + $q = $q->where('lft = ?', 1); + + // if tree has many roots, then specify root id + $q = $this->returnQueryWithRootId($q, $root_id); + + $root = $q->execute()->getFirst(); // if no record is returned, create record if (!$root) { @@ -93,9 +120,14 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int // fetch tree $q = $this->table->createQuery(); - $tree = $q->where('lft >= ?', 1) - ->orderBy('lft asc') - ->execute(); + $q = $q->where('lft >= ?', 1) + ->orderBy('lft asc'); + + // if tree has many roots, then specify root id + $root_id = isset($options['root_id']) ? $options['root_id'] : '1'; + $q = $this->returnQueryWithRootId($q, $root_id); + + $tree = $q->execute(); $root = $tree->getFirst(); @@ -142,4 +174,65 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int // TODO: if record doesn't exist, throw exception or similar? } + + /** + * fetch root nodes + * + * @return collection Doctrine_Collection + */ + public function fetchRoots() + { + $q = $this->table->createQuery(); + $q = $q->where('lft = ?', 1); + return $q->execute(); + } + + /** + * calculates the next available root id + * + * @return integer + */ + public function getNextRootId() + { + return $this->getMaxRootId() + 1; + } + + /** + * calculates the current max root id + * + * @return integer + */ + public function getMaxRootId() + { + $component = $this->table->getComponentName(); + $column = $this->getAttribute('root_column_name'); + + // cannot get this dql to work, cannot retrieve result using $coll[0]->max + //$dql = "SELECT MAX(c.$column) FROM $component c"; + + $dql = "SELECT c.$column FROM $component c ORDER BY c.$column desc LIMIT 1"; + + $coll = $this->table->getConnection()->query($dql); + + $max = $coll[0]->get($column); + + $max = !is_null($max) ? $max : 0; + + return $max; + } + + /** + * returns parsed query with root id where clause added if applicable + * + * @param object $query Doctrine_Query + * @param integer $root_id id of destination root + * @return object Doctrine_Query + */ + public function returnQueryWithRootId($query, $root_id = 1) + { + if($this->getAttribute('has_many_roots')) + $query->addWhere($this->getAttribute('root_column_name') . ' = ?', $root_id); + + return $query; + } }