. */ /** * Doctrine_Node_NestedSet * * @package Doctrine * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @category Object Relational Mapping * @link www.phpdoctrine.com * @since 1.0 * @version $Revision$ * @author Joe Simms * @author Roman Borschel */ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Interface { /** * 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(); $q = $q->where('rgt = ?', $this->getLeftValue() - 1); $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); $result = $q->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(); $q = $q->where('lft = ?', $this->getRightValue() + 1); $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); $result = $q->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(); $q = $q->where('lft = ?', $this->getLeftValue() + 1); $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); $result = $q->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(); $q = $q->where('rgt = ?', $this->getRightValue() - 1); $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); $result = $q->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(); $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(); 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(); $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()); $ancestors = $q->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; $newRoot = $dest->getNode()->getRootValue(); $this->shiftRLValues($dest->getNode()->getLeftValue(), 1, $newRoot); $this->shiftRLValues($dest->getNode()->getRightValue() + 2, 1, $newRoot); $newLeft = $dest->getNode()->getLeftValue(); $newRight = $dest->getNode()->getRightValue() + 2; $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; } /** * Accomplishes moving of nodes between different trees. * Used by the move* methods if the root value of the two nodes are different. * * @param Doctrine_Record $dest * @param unknown_type $newLeftValue * @param unknown_type $moveType */ private function _moveBetweenTrees(Doctrine_Record $dest, $newLeftValue, $moveType) { $conn = $this->record->getTable()->getConnection(); try { $conn->beginTransaction(); // Move between trees: Detach from old tree & insert into new tree $newRoot = $dest->getNode()->getRootValue(); $oldRoot = $this->getRootValue(); $oldLft = $this->getLeftValue(); $oldRgt = $this->getRightValue(); // Prepare target tree for insertion, make room $this->shiftRlValues($newLeftValue, $oldRgt - $oldLft - 1, $newRoot); // Set new root id for this node $this->setRootValue($newRoot); $this->record->save(); // Close gap in old tree $first = $oldRgt + 1; $delta = $oldLft - $oldRgt - 1; $this->shiftRLValues($first, $delta, $oldRoot); // Insert this node as a new node $this->setRightValue(0); $this->setLeftValue(0); switch ($moveType) { case 'moveAsPrevSiblingOf': $this->insertAsPrevSiblingOf($dest); break; case 'moveAsFirstChildOf': $this->insertAsFirstChildOf($dest); break; case 'moveAsNextSiblingOf': $this->insertAsNextSiblingOf($dest); break; case 'moveAsLastChildOf': $this->insertAsLastChildOf($dest); break; default: throw new Exception("Unknown move operation: $moveType."); } $diff = $oldRgt - $oldLft; $this->setRightValue($this->getLeftValue() + ($oldRgt - $oldLft)); $this->record->save(); // 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(); $q = $q->update($componentName) ->set($componentName . '.lft', 'lft + ' . $diff) ->set($componentName . '.rgt', 'rgt + ' . $diff) ->set($componentName . '.' . $rootColName, $newRoot) ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?', array($oldLft, $oldRgt)); $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $oldRoot); $q->execute(); $conn->commit(); } catch (Exception $e) { $conn->rollback(); throw $e; } } /** * moves node as prev sibling of dest record * */ public function moveAsPrevSiblingOf(Doctrine_Record $dest) { if ($dest->getNode()->getRootValue() != $this->getRootValue()) { // Move between trees $this->_moveBetweenTrees($dest, $dest->getNode()->getLeftValue(), __FUNCTION__); } else { // Move within the tree $this->updateNode($dest->getNode()->getLeftValue()); } } /** * moves node as next sibling of dest record * */ public function moveAsNextSiblingOf(Doctrine_Record $dest) { if ($dest->getNode()->getRootValue() != $this->getRootValue()) { // Move between trees $this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue() + 1, __FUNCTION__); } else { // Move within tree $this->updateNode($dest->getNode()->getRightValue() + 1); } } /** * moves node as first child of dest record * */ public function moveAsFirstChildOf(Doctrine_Record $dest) { if ($dest->getNode()->getRootValue() != $this->getRootValue()) { // Move between trees $this->_moveBetweenTrees($dest, $dest->getNode()->getLeftValue() + 1, __FUNCTION__); } else { // Move within tree $this->updateNode($dest->getNode()->getLeftValue() + 1); } } /** * moves node as last child of dest record * */ public function moveAsLastChildOf(Doctrine_Record $dest) { if ($dest->getNode()->getRootValue() != $this->getRootValue()) { // Move between trees $this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue(), __FUNCTION__); } else { // Move within tree $this->updateNode($dest->getNode()->getRightValue()); } } /** * Enter description here... * * @todo Exception handling/wrapping */ public function makeRoot($newRootId) { // TODO: throw exception instead? if ($this->getLeftValue() == 1 || !$this->record->getTable()->getTree()->getAttribute('hasManyRoots')) { return false; } $oldRgt = $this->getRightValue(); $oldLft = $this->getLeftValue(); $oldRoot = $this->getRootValue(); try { $conn = $this->record->getTable()->getConnection(); $conn->beginTransaction(); // Detach from old tree $first = $oldRgt + 1; $delta = $oldLft - $oldRgt - 1; $this->shiftRLValues($first, $delta, $this->getRootValue()); // Set new lft/rgt/root values for root node $this->setLeftValue(1); $this->setRightValue($oldRgt - $oldLft + 1); $this->setRootValue($newRootId); // Update descendants lft/rgt/root values $diff = 1 - $oldLft; $newRoot = $newRootId; $componentName = $this->record->getTable()->getComponentName(); $rootColName = $this->record->getTable()->getTree()->getAttribute('rootColumnName'); $q = $this->record->getTable()->createQuery(); $q = $q->update($componentName) ->set($componentName . '.lft', 'lft + ' . $diff) ->set($componentName . '.rgt', 'rgt + ' . $diff) ->set($componentName . '.' . $rootColName, $newRoot) ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?', array($oldLft, $oldRgt)); $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $oldRoot); $q->execute(); $conn->commit(); } catch (Exception $e) { $conn->rollback(); throw $e; } } /** * 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()) && ($this->getRightValue() == $subj->getNode()->getRightValue()) && ($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()) && ($this->getRightValue()<$subj->getNode()->getRightValue()) && ($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()) && ($this->getRightValue()<=$subj->getNode()->getRightValue()) && ($this->getRootValue() == $subj->getNode()->getRootValue())); } /** * determines if node is valid * * @return bool */ public function isValidNode(Doctrine_Record $record = null) { if ($record === null) { return ($this->getRightValue() > $this->getLeftValue()); } else { return ($record->getNode()->getRightValue() > $record->getNode()->getLeftValue()); } } /** * deletes node and it's descendants * */ public function delete() { // TODO: add the setting whether or not to delete descendants or relocate children $oldRoot = $this->getRootValue(); $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, $oldRoot); $coll = $q->execute(); $coll->delete(); $first = $this->getRightValue() + 1; $delta = $this->getLeftValue() - $this->getRightValue() - 1; $this->shiftRLValues($first, $delta, $oldRoot); 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(); $rootId = $this->getRootValue(); $treeSize = $right - $left + 1; $this->shiftRLValues($destLeft, $treeSize, $rootId); 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, $rootId); // correct values after source $this->shiftRLValues($right + 1, -$treeSize, $rootId); $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, $rootId = 1) { $qLeft = new Doctrine_Query(); $qRight = new Doctrine_Query(); // TODO: Wrap in transaction // shift left columns $componentName = $this->record->getTable()->getComponentName(); $qLeft = $qLeft->update($componentName) ->set($componentName . '.lft', 'lft + ' . $delta) ->where($componentName . '.lft >= ?', $first); $qLeft = $this->record->getTable()->getTree()->returnQueryWithRootId($qLeft, $rootId); $resultLeft = $qLeft->execute(); // shift right columns $resultRight = $qRight->update($componentName) ->set($componentName . '.rgt', 'rgt + ' . $delta) ->where($componentName . '.rgt >= ?', $first); $qRight = $this->record->getTable()->getTree()->returnQueryWithRootId($qRight, $rootId); $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, $rootId = 1) { $qLeft = new Doctrine_Query(); $qRight = new Doctrine_Query(); // TODO : Wrap in transaction // shift left column values $componentName = $this->record->getTable()->getComponentName(); $qLeft = $qLeft->update($componentName) ->set($componentName . '.lft', 'lft + ' . $delta) ->where($componentName . '.lft >= ? AND ' . $componentName . '.lft <= ?', array($first, $last)); $qLeft = $this->record->getTable()->getTree()->returnQueryWithRootId($qLeft, $rootId); $resultLeft = $qLeft->execute(); // shift right column values $qRight = $qRight->update($componentName) ->set($componentName . '.rgt', 'rgt + ' . $delta) ->where($componentName . '.rgt >= ? AND ' . $componentName . '.rgt <= ?', array($first, $last)); $qRight = $this->record->getTable()->getTree()->returnQueryWithRootId($qRight, $rootId); $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)) { $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->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); $coll = $q->execute(); $this->level = $coll->count() ? $coll->count() : 0; } 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('hasManyRoots')) return $this->record->get($this->record->getTable()->getTree()->getAttribute('rootColumnName')); return 1; } /** * sets records root id value * * @param int */ public function setRootValue($value) { if($this->record->getTable()->getTree()->getAttribute('hasManyRoots')) $this->record->set($this->record->getTable()->getTree()->getAttribute('rootColumnName'), $value); } }