Source for file NestedSet.php
Documentation is available at NestedSet.php
* $Id: NestedSet.php 2263 2007-08-20 07:45:29Z romanb $
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.phpdoctrine.com>.
* Doctrine_Node_NestedSet
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @category Object Relational Mapping
* @link www.phpdoctrine.com
* @version $Revision: 2263 $
* @author Joe Simms <joe.simms@websites4.com>
* @author Roman Borschel <roman@code-factory.org>
* test if node has previous sibling
* test if node has next sibling
* test if node has children
* test if node has parent
* gets record of prev sibling or empty record
* @return object Doctrine_Record
$baseAlias =
$this->_tree->getBaseAlias();
$q =
$this->_tree->getBaseQuery();
$q =
$q->addWhere("$baseAlias.rgt = ?", $this->getLeftValue() -
1);
if (count($result) <=
0) {
$sibling =
$result->getFirst();
* gets record of next sibling or empty record
* @return object Doctrine_Record
$baseAlias =
$this->_tree->getBaseAlias();
$q =
$this->_tree->getBaseQuery();
$q =
$q->addWhere("$baseAlias.lft = ?", $this->getRightValue() +
1);
if (count($result) <=
0) {
$sibling =
$result->getFirst();
* @return array array of sibling Doctrine_Record objects
foreach ($parent->getNode()->getChildren() as $child) {
if ($this->isEqualTo($child) &&
!$includeNode) {
* gets record of first child or empty record
* @return object Doctrine_Record
$baseAlias =
$this->_tree->getBaseAlias();
$q =
$this->_tree->getBaseQuery();
$q->addWhere("$baseAlias.lft = ?", $this->getLeftValue() +
1);
if (count($result) <=
0) {
$child =
$result->getFirst();
* gets record of last child or empty record
* @return object Doctrine_Record
$baseAlias =
$this->_tree->getBaseAlias();
$q =
$this->_tree->getBaseQuery();
if (count($result) <=
0) {
$child =
$result->getFirst();
* gets children for node (direct descendants only)
* @return mixed The children of the node or FALSE if the node has no children.
* gets descendants for node (direct descendants only)
* @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.
$baseAlias =
$this->_tree->getBaseAlias();
$q =
$this->_tree->getBaseQuery();
$params =
array($this->record->get('lft'), $this->record->get('rgt'));
$q->addWhere("$baseAlias.lft >= ? AND $baseAlias.rgt <= ?", $params)->addOrderBy("$baseAlias.lft asc");
$q->addWhere("$baseAlias.lft > ? AND $baseAlias.rgt < ?", $params)->addOrderBy("$baseAlias.lft asc");
$q->addWhere("$baseAlias.level <= ?", $this->record['level'] +
$depth);
if (count($result) <=
0) {
* gets record of parent or empty record
* @return object Doctrine_Record
$baseAlias =
$this->_tree->getBaseAlias();
$q =
$this->_tree->getBaseQuery();
->addOrderBy("$baseAlias.rgt asc");
if (count($result) <=
0) {
$parent =
$result->getFirst();
* gets ancestors for node
* @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).
$baseAlias =
$this->_tree->getBaseAlias();
$q =
$this->_tree->getBaseQuery();
->addOrderBy("$baseAlias.lft asc");
$q->addWhere("$baseAlias.level >= ?", $this->record['level'] -
$depth);
$ancestors =
$q->execute();
if (count($ancestors) <=
0) {
* 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)
foreach ($ancestors as $ancestor) {
$path[] =
$ancestor->__toString();
* gets number of children (direct descendants)
* gets number of descendants (children and their children)
* inserts node as parent of dest record
* @todo Wrap in transaction
// cannot insert a node that has already has a place within the tree
// cannot insert as parent of root
if ($dest->getNode()->isRoot()) {
$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->record['level'] =
$dest['level'] -
1;
* inserts node as previous sibling of dest record
* @todo Wrap in transaction
// cannot insert a node that has already has a place within the tree
$newLeft =
$dest->getNode()->getLeftValue();
$newRight =
$dest->getNode()->getLeftValue() +
1;
$newRoot =
$dest->getNode()->getRootValue();
$this->shiftRLValues($newLeft, 2, $newRoot);
$this->record['level'] =
$dest['level'];
// update destination left/right values to prevent a refresh
// $dest->getNode()->setLeftValue($dest->getNode()->getLeftValue() + 2);
// $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
* inserts node as next sibling of dest record
* @todo Wrap in transaction
// cannot insert a node that has already has a place within the tree
$newLeft =
$dest->getNode()->getRightValue() +
1;
$newRight =
$dest->getNode()->getRightValue() +
2;
$newRoot =
$dest->getNode()->getRootValue();
$this->shiftRLValues($newLeft, 2, $newRoot);
$this->record['level'] =
$dest['level'];
// update destination left/right values to prevent a refresh
// no need, node not affected
* inserts node as first child of dest record
* @todo Wrap in transaction
// cannot insert a node that has already has a place within the tree
$newLeft =
$dest->getNode()->getLeftValue() +
1;
$newRight =
$dest->getNode()->getLeftValue() +
2;
$newRoot =
$dest->getNode()->getRootValue();
$this->shiftRLValues($newLeft, 2, $newRoot);
$this->record['level'] =
$dest['level'] +
1;
// update destination left/right values to prevent a refresh
// $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
* inserts node as last child of dest record
* @todo Wrap in transaction
// cannot insert a node that has already has a place within the tree
$newLeft =
$dest->getNode()->getRightValue();
$newRight =
$dest->getNode()->getRightValue() +
1;
$newRoot =
$dest->getNode()->getRootValue();
$this->shiftRLValues($newLeft, 2, $newRoot);
$this->record['level'] =
$dest['level'] +
1;
// update destination left/right values to prevent a refresh
// $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
* Accomplishes moving of nodes between different trees.
* Used by the move* methods if the root values of the two nodes are different.
* @param Doctrine_Record $dest
* @param unknown_type $newLeftValue
* @param unknown_type $moveType
* @todo Better exception handling/wrapping
$conn =
$this->record->getTable()->getConnection();
$conn->beginTransaction();
// Move between trees: Detach from old tree & insert into new tree
$newRoot =
$dest->getNode()->getRootValue();
$oldLevel =
$this->record['level'];
// Prepare target tree for insertion, make room
$this->shiftRlValues($newLeftValue, $oldRgt -
$oldLft -
1, $newRoot);
// Set new root id for this node
$delta =
$oldLft -
$oldRgt -
1;
$this->shiftRLValues($first, $delta, $oldRoot);
// Insert this node as a new node
case 'moveAsPrevSiblingOf':
case 'moveAsFirstChildOf':
case 'moveAsNextSiblingOf':
case 'moveAsLastChildOf':
throw
new Exception("Unknown move operation: $moveType.");
$diff =
$oldRgt -
$oldLft;
$newLevel =
$this->record['level'];
$levelDiff =
$newLevel -
$oldLevel;
// Relocate descendants of the node
$componentName =
$this->_tree->getBaseComponent();
$rootColName =
$this->record->getTable()->getTree()->getAttribute('rootColumnName');
// Update lft/rgt/root/level for all descendants
$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->_tree->returnQueryWithRootId($q, $oldRoot);
* moves node as prev sibling of dest record
if ($dest->getNode()->getRootValue() !=
$this->getRootValue()) {
$oldLevel =
$this->record['level'];
$this->record['level'] =
$dest['level'];
$this->updateNode($dest->getNode()->getLeftValue(), $this->record['level'] -
$oldLevel);
* moves node as next sibling of dest record
if ($dest->getNode()->getRootValue() !=
$this->getRootValue()) {
$this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue() +
1, __FUNCTION__
);
$oldLevel =
$this->record['level'];
$this->record['level'] =
$dest['level'];
$this->updateNode($dest->getNode()->getRightValue() +
1, $this->record['level'] -
$oldLevel);
* moves node as first child of dest record
if ($dest->getNode()->getRootValue() !=
$this->getRootValue()) {
$oldLevel =
$this->record['level'];
$this->record['level'] =
$dest['level'] +
1;
$this->updateNode($dest->getNode()->getLeftValue() +
1, $this->record['level'] -
$oldLevel);
* moves node as last child of dest record
if ($dest->getNode()->getRootValue() !=
$this->getRootValue()) {
$oldLevel =
$this->record['level'];
$this->record['level'] =
$dest['level'] +
1;
$this->updateNode($dest->getNode()->getRightValue(), $this->record['level'] -
$oldLevel);
* Makes this node a root node. Only used in multiple-root trees.
* @todo Exception handling/wrapping
// TODO: throw exception instead?
if ($this->getLeftValue() ==
1 ||
!$this->record->getTable()->getTree()->getAttribute('hasManyRoots')) {
$oldLevel =
$this->record['level'];
$conn =
$this->record->getTable()->getConnection();
$conn->beginTransaction();
// Detach from old tree (close gap in old tree)
$delta =
$oldLft -
$oldRgt -
1;
$this->shiftRLValues($first, $delta, $this->getRootValue());
// Set new lft/rgt/root/level values for root node
// Update descendants lft/rgt/root/level values
$componentName =
$this->_tree->getBaseComponent();
$rootColName =
$this->record->getTable()->getTree()->getAttribute('rootColumnName');
$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->_tree->returnQueryWithRootId($q, $oldRoot);
* adds node as last child of record
public function addChild(Doctrine_Record $record)
$record->getNode()->insertAsLastChildOf($this->getRecord());
* determines if node is leaf
* determines if node is root
* determines if node is equal to subject node
public function isEqualTo(Doctrine_Record $subj)
return (($this->getLeftValue() ==
$subj->getNode()->getLeftValue()) &&
* determines if node is child of subject node
return (($this->getLeftValue() >
$subj->getNode()->getLeftValue()) &&
($this->getRootValue() ==
$subj->getNode()->getRootValue()));
* determines if node is child of or sibling to subject node
return (($this->getLeftValue() >=
$subj->getNode()->getLeftValue()) &&
($this->getRootValue() ==
$subj->getNode()->getRootValue()));
* determines if node is valid
return ($record->getNode()->getRightValue() >
$record->getNode()->getLeftValue());
* deletes node and it's descendants
* @todo Delete more efficiently. Wrap in transaction if needed.
// TODO: add the setting whether or not to delete descendants or relocate children
$q =
$this->_tree->getBaseQuery();
$baseAlias =
$this->_tree->getBaseAlias();
$componentName =
$this->_tree->getBaseComponent();
$q =
$this->record->getTable()->getTree()->returnQueryWithRootId($q, $oldRoot);
$this->shiftRLValues($first, $delta, $oldRoot);
* 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)
* 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, $levelDiff)
$componentName =
$this->_tree->getBaseComponent();
$treeSize =
$right -
$left +
1;
// Make room in the new branch
$this->shiftRLValues($destLeft, $treeSize, $rootId);
if ($left >=
$destLeft){ // src was shifted too?
// update level for descendants
$q =
$q->update($componentName)
->set($componentName .
'.level', 'level + ?')
->where($componentName .
'.lft > ? AND ' .
$componentName .
'.rgt < ?',
array($levelDiff, $left, $right));
$q =
$this->_tree->returnQueryWithRootId($q, $rootId);
// now there's enough room next to target to move the subtree
$this->shiftRLRange($left, $right, $destLeft -
$left, $rootId);
// correct values after source (close gap in old tree)
$this->shiftRLValues($right +
1, -
$treeSize, $rootId);
* 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
$componentName =
$this->_tree->getBaseComponent();
$qLeft =
$qLeft->update($componentName)
->set($componentName .
'.lft', 'lft + ?')
->where($componentName .
'.lft >= ?', array($delta, $first));
$qLeft =
$this->record->getTable()->getTree()->returnQueryWithRootId($qLeft, $rootId);
$resultLeft =
$qLeft->execute();
$resultRight =
$qRight->update($componentName)
->set($componentName .
'.rgt', 'rgt + ?')
->where($componentName .
'.rgt >= ?', array($delta, $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)
// shift left column values
$componentName =
$this->_tree->getBaseComponent();
$qLeft =
$qLeft->update($componentName)
->set($componentName .
'.lft', 'lft + ?')
->where($componentName .
'.lft >= ? AND ' .
$componentName .
'.lft <= ?', array($delta, $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 + ?')
->where($componentName .
'.rgt >= ? AND ' .
$componentName .
'.rgt <= ?', array($delta, $first, $last));
$qRight =
$this->record->getTable()->getTree()->returnQueryWithRootId($qRight, $rootId);
$resultRight =
$qRight->execute();
* gets record's left value
return $this->record->get('lft');
* sets record's left value
$this->record->set('lft', $lft);
* gets record's right value
return $this->record->get('rgt');
* sets record's right value
$this->record->set('rgt', $rgt);
* gets level (depth) of node in the tree
if (!isset
($this->record['level'])) {
$baseAlias =
$this->_tree->getBaseAlias();
$componentName =
$this->_tree->getBaseComponent();
$q =
$this->_tree->getBaseQuery();
return $this->record['level'];
* get records root id value
if ($this->_tree->getAttribute('hasManyRoots')) {
return $this->record->get($this->_tree->getAttribute('rootColumnName'));
* sets records root id value
if ($this->_tree->getAttribute('hasManyRoots')) {
$this->record->set($this->_tree->getAttribute('rootColumnName'), $value);