1198 lines
40 KiB
PHP
1198 lines
40 KiB
PHP
<?php
|
|
/*
|
|
* This file is part of the sfPropelActAsNestedSetBehavior package.
|
|
*
|
|
* (c) 2006-2007 Tristan Rivoallan <tristan@rivoallan.net>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
/**
|
|
* This behavior adds necessary logic for a propel model to behave as a nested set.
|
|
*
|
|
* To enable this behavior add this after Propel stub class declaration :
|
|
*
|
|
* <code>
|
|
* $columns_map = array('left' => MyClassPeer::TREE_LEFT,
|
|
* 'right' => MyClassPeer::TREE_RIGHT,
|
|
* 'parent' => MyClassPeer::TREE_PARENT,
|
|
* 'scope' => MyClassPeer::TOPIC_ID);
|
|
*
|
|
* sfPropelBehavior::add('MyClass', array('actasnestedset' => array('columns' => $columns_map)));
|
|
* </code>
|
|
*
|
|
* Column map values signification :
|
|
*
|
|
* - left : Model column holding nested set left value for a row
|
|
* - right : Model column holding nested set right value for a row
|
|
* - parent : Model column holding row's parent id
|
|
* (this is necessary because we use adjacency list tree traversal for some methods)
|
|
* - scope : Model column holding row's scope id. The scope is used to differenciate trees stored in
|
|
* the same table
|
|
*
|
|
* @author Tristan Rivoallan <tristan@rivoallan.net>
|
|
* @author Heltem (http://propel.phpdb.org/trac/ticket/312)
|
|
* @author Joe Simms (http://www.symfony-project.com/forum/index.php/m/20657/)
|
|
*
|
|
* @see http://www.symfony-project.com/trac/wiki/sfPropelActAsNestedSetBehaviorPlugin
|
|
*/
|
|
|
|
class sfPropelActAsNestedSetBehavior
|
|
{
|
|
# -- PROPERTIES
|
|
/**
|
|
* Nested set related columns.
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $columns = array();
|
|
|
|
/**
|
|
* Holds SQL queries propel objects that need to be processed before a node is saved.
|
|
* It acts as a FIFO stack.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $presave_stack = array();
|
|
|
|
/**
|
|
* Holds SQL queries propel objects that need to be processed before a node is deleted.
|
|
* It acts as a FIFO stack.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $predelete_stack = array();
|
|
|
|
# -- HOOKS
|
|
|
|
|
|
/**
|
|
* Runs necessary queries before node is saved.
|
|
*
|
|
* @param BaseObject $node
|
|
*/
|
|
public function preSave(BaseObject $node)
|
|
{
|
|
$this->processPreSaveStack();
|
|
}
|
|
|
|
/**
|
|
* Runs necessary queries before node is deleted.
|
|
*
|
|
* @param BaseObject $node
|
|
*/
|
|
public function preDelete(BaseObject $node)
|
|
{
|
|
$this->deleteDescendants($node);
|
|
}
|
|
|
|
# -- GETTERS AND SETTERS
|
|
|
|
|
|
/**
|
|
* Proxy method used to access column holding nested set's left value getter.
|
|
*
|
|
* @param BaseObject $node
|
|
*/
|
|
public function getLeftValue(BaseObject $node)
|
|
{
|
|
$getter = self::forgeMethodName($node, 'get', 'left');
|
|
return $node->$getter();
|
|
}
|
|
|
|
/**
|
|
* Proxy method used to access column holding nested set's left value setter.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param integer $value
|
|
*/
|
|
public function setLeftValue(BaseObject $node, $value)
|
|
{
|
|
$setter = self::forgeMethodName($node, 'set', 'left');
|
|
return $node->$setter($value);
|
|
}
|
|
|
|
/**
|
|
* Proxy method used to access column holding nested set's right value setter.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param integer $value
|
|
*/
|
|
public function setRightValue(BaseObject $node, $value)
|
|
{
|
|
$setter = self::forgeMethodName($node, 'set', 'right');
|
|
return $node->$setter($value);
|
|
}
|
|
|
|
/**
|
|
* Proxy method used to access column holding nested set's right value getter.
|
|
*
|
|
* @param BaseObject $node
|
|
*/
|
|
public function getRightValue(BaseObject $node)
|
|
{
|
|
$getter = self::forgeMethodName($node, 'get', 'right');
|
|
return $node->$getter();
|
|
}
|
|
|
|
/**
|
|
* Proxy method used to access column holding nested set's scope value setter.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param integer $value
|
|
*/
|
|
public function setScopeIdValue(BaseObject $node, $value)
|
|
{
|
|
$setter = self::forgeMethodName($node, 'set', 'scope');
|
|
return $node->$setter($value);
|
|
}
|
|
|
|
/**
|
|
* Proxy method used to access column holding nested set's scope value getter.
|
|
*
|
|
* @param BaseObject $node
|
|
*/
|
|
public function getScopeIdValue(BaseObject $node)
|
|
{
|
|
$getter = self::forgeMethodName($node, 'get', 'scope');
|
|
return $node->$getter();
|
|
}
|
|
|
|
/**
|
|
* Proxy method used to access column holding nested set's depth value getter.
|
|
*
|
|
* @param BaseObject $node
|
|
*/
|
|
public function getDepthValue(BaseObject $node)
|
|
{
|
|
$getter = self::forgeMethodName($node, 'get', 'depth');
|
|
return $node->$getter();
|
|
}
|
|
|
|
/**
|
|
* Proxy method used to access column holding nested set's depth value setter.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param integer $value
|
|
*/
|
|
public function setDepthValue(BaseObject $node, $value)
|
|
{
|
|
$setter = self::forgeMethodName($node, 'set', 'depth');
|
|
return $node->$setter($value);
|
|
}
|
|
|
|
/**
|
|
* Proxy method used to access column holding nested set's parent value setter.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param integer $value
|
|
*/
|
|
public function setParentIdValue(BaseObject $node, $value)
|
|
{
|
|
$setter = self::forgeMethodName($node, 'set', 'parent');
|
|
return $node->$setter($value);
|
|
}
|
|
|
|
/**
|
|
* Proxy method used to access column holding nested set's parent value getter.
|
|
*
|
|
* @param BaseObject $node
|
|
*/
|
|
public function getParentIdValue(BaseObject $node)
|
|
{
|
|
$getter = self::forgeMethodName($node, 'get', 'parent');
|
|
return $node->$getter();
|
|
}
|
|
|
|
# -- NESTED SETS PUBLIC API
|
|
|
|
|
|
/**
|
|
* Sets node properties to make it a root node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @throws Exception When trying to turn an existing non-root node into a root node
|
|
*/
|
|
public function makeRoot(BaseObject $node)
|
|
{
|
|
if ((bool)$node->getLeftValue())
|
|
{
|
|
throw new Exception('Cannot turn an existing node into a root node.');
|
|
}
|
|
|
|
$node->setDepthValue(0);
|
|
$node->setLeftValue(1);
|
|
$node->setRightValue(2);
|
|
}
|
|
|
|
# ---- INSERTION METHODS
|
|
|
|
|
|
/**
|
|
* Inserts node as first child of given node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $dest_node
|
|
*/
|
|
public function insertAsFirstChildOf(BaseObject $node, BaseObject $dest_node)
|
|
{
|
|
$node->setLeftValue($dest_node->getLeftValue() + 1);
|
|
$node->setRightValue($dest_node->getLeftValue() + 2);
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
$node->setParentIdValue($dest_node->getPrimaryKey());
|
|
$node->setDepthValue($dest_node->getDepthValue() + 1);
|
|
$this->addPreSaveStackEntries($this->shiftRLValues($node->getPeer(), $node->getLeftValue(), 2, $dest_node->getScopeIdValue()));
|
|
|
|
$dest_node->setRightValue($dest_node->getRightValue() + 2);
|
|
$this->addPreSaveStackEntries(array($dest_node));
|
|
}
|
|
|
|
/**
|
|
* Inserts node as last child of given node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $dest_node
|
|
*/
|
|
public function insertAsLastChildOf(BaseObject $node, BaseObject $dest_node)
|
|
{
|
|
$node->setLeftValue($dest_node->getRightValue());
|
|
$node->setRightValue($dest_node->getRightValue() + 1);
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
$node->setParentIdValue($dest_node->getPrimaryKey());
|
|
$node->setDepthValue($dest_node->getDepthValue() + 1);
|
|
$this->addPreSaveStackEntries($this->shiftRLValues($node->getPeer(), $node->getLeftValue(), 2, $dest_node->getScopeIdValue()));
|
|
|
|
$dest_node->setRightValue($dest_node->getRightValue() + 2);
|
|
$this->addPreSaveStackEntries(array($dest_node));
|
|
}
|
|
|
|
/**
|
|
* Inserts node as next sibling of given node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $dest_node
|
|
* @throws Exception When trying to create a sibling to a root node
|
|
*/
|
|
public function insertAsNextSiblingOf(BaseObject $node, BaseObject $dest_node)
|
|
{
|
|
if ($dest_node->isRoot())
|
|
{
|
|
$msg = 'Root nodes cannot have siblings';
|
|
throw new Exception($msg);
|
|
}
|
|
|
|
$node->setLeftValue($dest_node->getRightValue() + 1);
|
|
$node->setRightValue($dest_node->getRightValue() + 2);
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
$node->setParentIdValue($dest_node->getParentIdValue());
|
|
$node->setDepthValue($dest_node->getDepthValue());
|
|
|
|
$this->addPreSaveStackEntries($this->shiftRLValues($node->getPeer(), $node->getLeftValue(), 2, $dest_node->getScopeIdValue()));
|
|
}
|
|
|
|
/**
|
|
* Inserts node as previous sibling of given node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $dest_node
|
|
* @throws Exception When trying to create a sibling to a root node
|
|
*/
|
|
|
|
public function insertAsPrevSiblingOf(BaseObject $node, BaseObject $dest_node)
|
|
{
|
|
if ($dest_node->isRoot())
|
|
{
|
|
$msg = 'Root nodes cannot have siblings';
|
|
throw new Exception($msg);
|
|
}
|
|
|
|
$node->setLeftValue($dest_node->getLeftValue());
|
|
$node->setRightValue($dest_node->getLeftValue() + 1);
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
$node->setParentIdValue($dest_node->getParentIdValue());
|
|
$node->setDepthValue($dest_node->getDepthValue());
|
|
|
|
$this->addPreSaveStackEntries($this->shiftRLValues($node->getPeer(), $node->getLeftValue(), 2, $dest_node->getScopeIdValue()));
|
|
|
|
$dest_node->setLeftValue($dest_node->getLeftValue() + 2);
|
|
$dest_node->setRightValue($dest_node->getRightValue() + 2);
|
|
$this->addPreSaveStackEntries(array($dest_node));
|
|
}
|
|
|
|
/**
|
|
* Inserts node as parent of given node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $dest_node
|
|
* @throws Exception When trying to insert node as parent of a root node
|
|
*/
|
|
public function insertAsParentOf(BaseObject $node, BaseObject $dest_node)
|
|
{
|
|
if ($dest_node->isRoot())
|
|
{
|
|
$msg = 'Impossible to insert a node as parent of a root node';
|
|
throw new Exception($msg);
|
|
}
|
|
|
|
$peer_name = $node->getPeer();
|
|
|
|
$this->addPreSaveStackEntries(self::shiftRLValues($peer_name, $dest_node->getLeftValue(), 1, $dest_node->getScopeIdValue()));
|
|
$this->addPreSaveStackEntries(self::shiftRLValues($peer_name, $dest_node->getRightValue() + 2, 1, $dest_node->getScopeIdValue()));
|
|
|
|
$node->setLeftValue($dest_node->getLeftValue());
|
|
$node->setRightValue($dest_node->getRightValue() + 2);
|
|
|
|
$previous_parent = $dest_node->getParentIdValue();
|
|
$node->setParentIdValue($previous_parent);
|
|
$dest_node->setParentIdValue($node->getPrimaryKey());
|
|
|
|
$this->addPreSaveStackEntries(array($dest_node));
|
|
|
|
}
|
|
|
|
# ---- INFORMATIONAL METHODS
|
|
|
|
|
|
/**
|
|
* Returns true if given node as one or several children.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return bool
|
|
*/
|
|
public function hasChildren(BaseObject $node)
|
|
{
|
|
return $node->getRightValue() - $node->getLeftValue() > 1;
|
|
}
|
|
|
|
/**
|
|
* Returns true if given node is a root node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return bool
|
|
*/
|
|
public function isRoot(BaseObject $node)
|
|
{
|
|
return $node->getLeftValue() == 1;
|
|
}
|
|
|
|
/**
|
|
* Returns true if given node has a parent node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return bool
|
|
*/
|
|
public function hasParent(BaseObject $node)
|
|
{
|
|
return $node->getParentIdValue();
|
|
}
|
|
|
|
/**
|
|
* Returns true if given node has a next sibling.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return bool
|
|
*/
|
|
public function hasNextSibling(BaseObject $node)
|
|
{
|
|
return (bool)$node->retrieveNextSibling();
|
|
}
|
|
|
|
/**
|
|
* Returns true if given node has a previous sibling.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return bool
|
|
*/
|
|
public function hasPrevSibling(BaseObject $node)
|
|
{
|
|
return (bool)$node->retrievePrevSibling();
|
|
}
|
|
|
|
/**
|
|
* Returns true if given node does not have children.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return bool
|
|
*/
|
|
public function isLeaf(BaseObject $node)
|
|
{
|
|
return $node->getRightValue() - $node->getLeftValue() == 1;
|
|
}
|
|
|
|
/**
|
|
* Returns true if given node is identical to node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $compared_node
|
|
* @return bool
|
|
*/
|
|
public function isEqualTo(BaseObject $node, BaseObject $compared_node)
|
|
{
|
|
return ($node->getLeftValue() === $compared_node->getLeftValue() && $node->getRightValue() === $compared_node->getRightValue() && $node->getScopeIdValue() === $compared_node->getScopeIdValue() && $node->getParentIdValue() === $compared_node->getParentIdValue());
|
|
}
|
|
|
|
/**
|
|
* Returns true if given node is parent of node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $parent_node
|
|
*/
|
|
public function isChildOf(BaseObject $node, BaseObject $parent_node)
|
|
{
|
|
return ($node->getParentIdValue() === $parent_node->getPrimaryKey() && $node->getScopeIdValue() === $parent_node->getScopeIdValue());
|
|
}
|
|
|
|
/**
|
|
* Returns true if given node is descendant of node.
|
|
*
|
|
* @param BaseObject $descendant_node
|
|
* @param BaseObject $node
|
|
*/
|
|
public function isDescendantOf(BaseObject $descendant_node, BaseObject $node)
|
|
{
|
|
return ($node->getLeftValue() <= $descendant_node->getLeftValue() && $node->getRightValue() >= $descendant_node->getRightValue() && $node->getScopeIdValue() === $descendant_node->getScopeIdValue());
|
|
}
|
|
|
|
/**
|
|
* Returns given node number of direct children.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return integer
|
|
*/
|
|
public function getNumberOfChildren(BaseObject $node)
|
|
{
|
|
$peer_name = $node->getPeer();
|
|
$con = Propel::getConnection();
|
|
$scope_sql = '';
|
|
if (!is_null($node->getScopeIdValue()))
|
|
{
|
|
$scope_sql = sprintf(' AND %s = "%s"', self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue());
|
|
}
|
|
|
|
$sql = sprintf('SELECT COUNT(*) AS num_children FROM %s WHERE %s = %s %s', constant("$peer_name::TABLE_NAME"), self::getColumnConstant(get_class($node), 'parent'), $node->getPrimaryKey(), $scope_sql);
|
|
|
|
$stmt = $con->createStatement();
|
|
$rs = $stmt->executeQuery($sql);
|
|
$rs->next();
|
|
|
|
return $rs->getInt('num_children');
|
|
}
|
|
|
|
/**
|
|
* Returns given node number of descendants (n level).
|
|
*
|
|
* @param BaseObject $node
|
|
* @return integer
|
|
*/
|
|
public function getNumberOfDescendants(BaseObject $node)
|
|
{
|
|
$right = $node->getRightValue();
|
|
$left = $node->getLeftValue();
|
|
$num = ($right - $left - 1) / 2;
|
|
|
|
return $num;
|
|
}
|
|
|
|
/**
|
|
* Returns given node level.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return integer
|
|
*/
|
|
public function getLevel(BaseObject $node)
|
|
{
|
|
return (int)$node->getDepthValue();
|
|
}
|
|
|
|
/**
|
|
* Sets node level.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param int $level
|
|
*/
|
|
public function setLevel(BaseObject $node, $level)
|
|
{
|
|
$node->setDepthValue($level);
|
|
}
|
|
|
|
# ---- NODE RETRIEVAL METHODS
|
|
|
|
|
|
/**
|
|
* Returns node's parent.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param string $peer_method (optional) Method used for selecting node
|
|
* @return BaseObject or null if node does not have a parent.
|
|
*/
|
|
public function getParent(BaseObject $node, $peer_method = 'retrieveByPk')
|
|
{
|
|
return $node->isRoot() ? null : call_user_func(array($node->getPeer(), $peer_method), $node->getParentIdValue());
|
|
}
|
|
|
|
/**
|
|
* Returns given node direct children.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param string $peer_method (optional) Method used for selecting nodes
|
|
* @param Criteria $c (optional) Criteria object for restricting lookup
|
|
* @return array Node children
|
|
*/
|
|
public function getChildren(BaseObject $node, $peer_method = 'doSelect', Criteria $c = null)
|
|
{
|
|
if (!$c)
|
|
{
|
|
$c = new Criteria();
|
|
}
|
|
$c->addAnd(self::getColumnConstant(get_class($node), 'parent'), $node->getPrimaryKey(), Criteria::EQUAL);
|
|
$c->addAnd(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
|
|
$c->addAscendingOrderByColumn(self::getColumnConstant(get_class($node), 'left'));
|
|
|
|
$children = call_user_func(array($node->getPeer(), $peer_method), $c);
|
|
|
|
/*
|
|
* Set children level depending on node's.
|
|
* This prevents many further queries for getting children levels.
|
|
*/
|
|
// if (is_array($children))
|
|
// {
|
|
// $child_level = $node->getLevel() + 1;
|
|
// for($i = 0; $i < count($children); $i++)
|
|
// {
|
|
// $children[$i]->setLevel($child_level);
|
|
// }
|
|
// }
|
|
//
|
|
return $children;
|
|
}
|
|
|
|
/**
|
|
* Returns given node descendants (n level) in pre-order.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param string $peer_method (optional) Method used for selecting nodes
|
|
* @param Criteria $c (optional) Criteria object for restricting lookup
|
|
* @return array Node descendants, pre-order
|
|
*/
|
|
public function getDescendants(BaseObject $node, $peer_method = 'doSelect', Criteria $c = null)
|
|
{
|
|
$descendants = array();
|
|
|
|
if (!$node->isLeaf())
|
|
{
|
|
if (!$c)
|
|
{
|
|
$c = new Criteria();
|
|
}
|
|
$c->addAnd(self::getColumnConstant(get_class($node), 'left'), $node->getLeftValue(), Criteria::GREATER_THAN);
|
|
$c->addAnd(self::getColumnConstant(get_class($node), 'right'), $node->getRightValue(), Criteria::LESS_THAN);
|
|
$c->addAnd(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
|
|
$c->addAscendingOrderByColumn(self::getColumnConstant(get_class($node), 'left'));
|
|
|
|
$descendants = call_user_func(array($node->getPeer(), $peer_method), $c);
|
|
}
|
|
|
|
/*
|
|
* Set node levels to prevent further queries to database
|
|
*/
|
|
// $prev = array($node->getRightValue());
|
|
// $i = 0;
|
|
// if (count($descendants))
|
|
// {
|
|
// $initial_level = $descendants[0]->getLevel() - 1;
|
|
// }
|
|
//
|
|
// foreach ($descendants as $cur)
|
|
// {
|
|
// // get back to the parent
|
|
// while($cur->getRightValue() > $prev[$i])
|
|
// {
|
|
// $i--;
|
|
// }
|
|
//
|
|
// $cur->setLevel(++$i + $initial_level);
|
|
// $prev[$i] = $cur->getRightValue();
|
|
// }
|
|
|
|
return $descendants;
|
|
}
|
|
|
|
/**
|
|
* Returns given node next sibling.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return BaseObject
|
|
*/
|
|
public function retrieveNextSibling(BaseObject $node)
|
|
{
|
|
$c = new Criteria();
|
|
$c->add(self::getColumnConstant(get_class($node), 'left'), $node->getRightValue() + 1, Criteria::EQUAL);
|
|
$c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
|
|
|
|
return call_user_func(array($node->getPeer(), 'doSelectOne'), $c);
|
|
}
|
|
|
|
/**
|
|
* Returns given node previous sibling.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return BaseObject
|
|
*/
|
|
public function retrievePrevSibling(BaseObject $node)
|
|
{
|
|
$c = new Criteria();
|
|
$c->add(self::getColumnConstant(get_class($node), 'right'), $node->getLeftValue() - 1, Criteria::EQUAL);
|
|
$c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
|
|
|
|
return call_user_func(array($node->getPeer(), 'doSelectOne'), $c);
|
|
}
|
|
|
|
/**
|
|
* Returns given node first child.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return BaseObject
|
|
*/
|
|
public function retrieveFirstChild(BaseObject $node)
|
|
{
|
|
$c = new Criteria();
|
|
$c->add(self::getColumnConstant(get_class($node), 'left'), $node->getLeftValue() + 1, Criteria::EQUAL);
|
|
$c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
|
|
|
|
return call_user_func(array($node->getPeer(), 'doSelectOne'), $c);
|
|
}
|
|
|
|
/**
|
|
* Returns given node last child.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return BaseObject
|
|
*/
|
|
public function retrieveLastChild(BaseObject $node)
|
|
{
|
|
$c = new Criteria();
|
|
$c->add(self::getColumnConstant(get_class($node), 'right'), $node->getRightValue() - 1, Criteria::EQUAL);
|
|
$c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue(), Criteria::EQUAL);
|
|
|
|
return call_user_func(array($node->getPeer(), 'doSelectOne'), $c);
|
|
}
|
|
|
|
/**
|
|
* Returns given node parent.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return BaseObject
|
|
*/
|
|
public function retrieveParent(BaseObject $node, $peer_method = 'doSelectOne')
|
|
{
|
|
if ($node->isRoot())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Trick to get proper criteria
|
|
$clone = clone $node;
|
|
$clone->setId($node->getParentIdValue());
|
|
$c = $clone->buildPKeyCriteria();
|
|
|
|
return call_user_func(array($node->getPeer(), $peer_method), $c);
|
|
}
|
|
|
|
/**
|
|
* Returns node siblings.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param string $peer_method (optional) defaults to "doSelect"
|
|
* @return array
|
|
*/
|
|
public function retrieveSiblings(BaseObject $node, $peer_method = 'doSelect')
|
|
{
|
|
$c = new Criteria();
|
|
$c->add(self::getColumnConstant(get_class($node), 'parent'), $node->getParentIdValue());
|
|
$c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue());
|
|
|
|
$results = call_user_func(array($node->getPeer(), $peer_method), $c);
|
|
$final_results = array();
|
|
|
|
if (is_array($results) && count($results))
|
|
{
|
|
$level = $results[0]->getLevel();
|
|
foreach ($results as $r)
|
|
{
|
|
if ($node->isEqualTo($r))
|
|
{
|
|
continue;
|
|
}
|
|
$r->setLevel($level);
|
|
$final_results[] = $r;
|
|
}
|
|
}
|
|
|
|
return $final_results;
|
|
}
|
|
|
|
/**
|
|
* Returns path to a specific node as an array, useful to create breadcrumbs.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return array
|
|
*/
|
|
public function getPath(BaseObject $node, $peer_method = 'doSelect')
|
|
{
|
|
$c = new Criteria();
|
|
$c->add(self::getColumnConstant(get_class($node), 'left'), $node->getLeftValue(), Criteria::LESS_THAN);
|
|
$c->add(self::getColumnConstant(get_class($node), 'right'), $node->getRightValue(), Criteria::GREATER_THAN);
|
|
if ($node->getScopeIdValue())
|
|
{
|
|
$c->add(self::getColumnConstant(get_class($node), 'scope'), $node->getScopeIdValue());
|
|
}
|
|
$c->addAscendingOrderByColumn(self::getColumnConstant(get_class($node), 'left'));
|
|
|
|
return call_user_func(array($node->getPeer(), $peer_method), $c);
|
|
}
|
|
|
|
# ---- TREE MODIFICATIONS METHODS
|
|
|
|
|
|
/**
|
|
* Moves node to first child of given node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $dest_node
|
|
*/
|
|
public function moveToFirstChildOf(BaseObject $node, BaseObject $dest_node)
|
|
{
|
|
if ($dest_node->getScopeIdValue() != $node->getScopeIdValue())
|
|
{
|
|
$this->addPreSaveStackEntries(self::getUpdateNodesForTreeMigration($node, $dest_node->getLeftValue() + 1, $dest_node->getDepthValue() + 1, $dest_node->getScopeIdValue()));
|
|
$node->setParentIdValue($dest_node->getPrimaryKey());
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
$node->setDepthValue($dest_node->getDepthValue() + 1);
|
|
}
|
|
else
|
|
{
|
|
$node->setParentIdValue($dest_node->getPrimaryKey());
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
//$node->save();
|
|
$this->addPreSaveStackEntries(self::getUpdateTreeQueries($node, $dest_node->getLeftValue() + 1, $dest_node->getDepthValue() + 1));
|
|
$node->setDepthValue($dest_node->getDepthValue() + 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves node to last child of given node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $dest_node
|
|
*/
|
|
public function moveToLastChildOf(BaseObject $node, BaseObject $dest_node)
|
|
{
|
|
$this->addPreSaveStackEntries(self::getUpdateNodesForTreeMigration($node, $dest_node->getRightValue(), $dest_node->getDepthValue() + 1, $dest_node->getScopeIdValue()));
|
|
$node->setParentIdValue($dest_node->getPrimaryKey());
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
$node->setDepthValue($dest_node->getDepthValue() + 1);
|
|
}
|
|
|
|
/**
|
|
* Moves node to next sibling of given node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $dest_node
|
|
*/
|
|
public function moveToNextSiblingOf(BaseObject $node, BaseObject $dest_node)
|
|
{
|
|
if ($dest_node->getScopeIdValue() != $node->getScopeIdValue())
|
|
{
|
|
$this->addPreSaveStackEntries(self::getUpdateNodesForTreeMigration($node, $dest_node->getRightValue() + 1, $dest_node->getDepthValue(), $dest_node->getScopeIdValue()));
|
|
$node->setParentIdValue($dest_node->getParentIdValue());
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
$node->setDepthValue($dest_node->getDepthValue());
|
|
}
|
|
else
|
|
{
|
|
$node->setParentIdValue($dest_node->getParentIdValue());
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
$this->addPreSaveStackEntries(self::getUpdateTreeQueries($node, $dest_node->getRightValue() + 1, $dest_node->getDepthValue()));
|
|
$node->setDepthValue($dest_node->getDepthValue());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves node to previous sibling of given node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param BaseObject $dest_node
|
|
*/
|
|
public function moveToPrevSiblingOf(BaseObject $node, BaseObject $dest_node)
|
|
{
|
|
if ($dest_node->getScopeIdValue() != $node->getScopeIdValue())
|
|
{
|
|
$this->addPreSaveStackEntries(self::getUpdateNodesForTreeMigration($node, $dest_node->getLeftValue(), $dest_node->getDepthValue(), $dest_node->getScopeIdValue()));
|
|
$node->setParentIdValue($dest_node->getParentIdValue());
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
$node->setDepthValue($dest_node->getDepthValue());
|
|
}
|
|
else
|
|
{
|
|
$node->setParentIdValue($dest_node->getParentIdValue());
|
|
$node->setScopeIdValue($dest_node->getScopeIdValue());
|
|
$this->addPreSaveStackEntries(self::getUpdateTreeQueries($node, $dest_node->getLeftValue(), $dest_node->getDepthValue()));
|
|
$node->setDepthValue($dest_node->getDepthValue());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes given node direct children.
|
|
*
|
|
* @param BaseObject $node
|
|
*/
|
|
public function deleteChildren(BaseObject $node)
|
|
{
|
|
// array_reverse() call is necessary for root node properties to be correctly updated
|
|
foreach (array_reverse($node->getChildren()) as $child)
|
|
{
|
|
$child->delete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes all nodes below given node (n level).
|
|
*
|
|
* @param BaseObject $node
|
|
*/
|
|
public function deleteDescendants(BaseObject $node)
|
|
{
|
|
$peer_name = $node->getPeer();
|
|
$stub_name = get_class($node);
|
|
$scope = $node->getScopeIdValue();
|
|
|
|
$c = new Criteria();
|
|
$c1 = $c->getNewCriterion(self::getColumnConstant($stub_name, 'left'), $node->getLeftValue(), Criteria::GREATER_THAN);
|
|
$c2 = $c->getNewCriterion(self::getColumnConstant($stub_name, 'right'), $node->getRightValue(), Criteria::LESS_THAN);
|
|
|
|
$c1->addAnd($c2);
|
|
|
|
$c->add($c1);
|
|
$c->add(self::getColumnConstant($stub_name, 'scope'), $scope);
|
|
$c->addDescendingOrderByColumn(self::getColumnConstant($stub_name, 'right'));
|
|
|
|
// Nodes are not directly deleted because we need to maintain adjacency list properties
|
|
$descendants = call_user_func(array($peer_name, 'doDelete'), $c);
|
|
|
|
$this->addPreDeleteStackEntries(self::shiftRLValues($peer_name, $node->getLeftValue(), -($node->getRightValue() + 1 - $node->getLeftValue()), $scope));
|
|
$this->processPreDeleteStack();
|
|
|
|
}
|
|
|
|
# -- HELPER METHODS
|
|
|
|
|
|
/**
|
|
* Returns an up to date version of node.
|
|
*
|
|
* @param BaseObject $node
|
|
* @return BaseObject
|
|
*/
|
|
public function reload(BaseObject $node)
|
|
{
|
|
return call_user_func(array($node->getPeer(), 'retrieveByPk'), $node->getPrimaryKey());
|
|
}
|
|
|
|
private static function getUpdateNodesForTreeMigration($node, $dest_left, $dest_depth, $dest_scope)
|
|
{
|
|
$statements = array();
|
|
$peer_name = $node->getPeer();
|
|
$stub_name = self::getStubFromPeer($peer_name);
|
|
$table_name = constant("$peer_name::TABLE_NAME");
|
|
$scope_column = self::getColumnConstant($stub_name, 'scope');
|
|
$left_column = self::getColumnConstant($stub_name, 'left');
|
|
$right_column = self::getColumnConstant($stub_name, 'right');
|
|
$depth_column = self::getColumnConstant($stub_name, 'depth');
|
|
$tree_size = $node->getRightValue() - $node->getLeftValue() + 1;
|
|
$node_offset = $node->getScopeIdValue() == $dest_scope && $node->getLeftValue() > $dest_left ? $tree_size : 0;
|
|
|
|
if ($node->getScopeIdValue())
|
|
{
|
|
$scope_sql = sprintf(' AND %s = %s', $scope_column, $node->getScopeIdValue());
|
|
}
|
|
|
|
$statements = array_merge($statements, self::shiftRLValues($peer_name, $dest_left, $tree_size, $dest_scope));
|
|
|
|
$delta = $dest_left - $node->getLeftValue() - $node_offset;
|
|
|
|
$depth_delta = $dest_depth - $node->getDepthValue();
|
|
|
|
$sql = 'UPDATE %1$s SET %4$s = %7$d, %5$s = %5$s + %6$d, %2$s = %2$s + %10$d, %3$s = %3$s + %10$d WHERE %2$s >= %8$d AND %3$s <= %9$d %11$s';
|
|
|
|
$statements[] = sprintf($sql, $table_name, $left_column, $right_column, $scope_column, $depth_column, $depth_delta, $dest_scope, $node->getLeftValue() + $node_offset, $node->getRightValue() + $node_offset, $delta, $scope_sql);
|
|
|
|
$statements = array_merge($statements, self::shiftRLValues($peer_name, $node->getRightValue() + $node_offset + 1, -$tree_size, $node->getScopeIdValue()));
|
|
|
|
// throw new Exception("<pre>delta: $delta, dest_left: $dest_left, dest_depth: $dest_depth, tree_size: $tree_size\n".var_export($statements, true)."</pre>");
|
|
|
|
return $statements;
|
|
}
|
|
|
|
private static function changeParentIdValues($peer_name, $from, $by_parent_id, $parent_id, $scopeId = null)
|
|
{
|
|
$statements = array();
|
|
$stub_name = self::getStubFromPeer($peer_name);
|
|
$table_name = constant("$peer_name::TABLE_NAME");
|
|
$right_column = self::getColumnConstant($stub_name, 'right');
|
|
$scope_column = self::getColumnConstant($stub_name, 'scope');
|
|
$parent_column = self::getColumnConstant($stub_name, 'parent');
|
|
|
|
$scope_sql = '';
|
|
if (isset($scopeId))
|
|
{
|
|
$scope_sql = sprintf(' AND %s = %s', $scope_column, $scopeId);
|
|
}
|
|
|
|
$sql = 'UPDATE %1$s SET %2$s = %5$s WHERE %3$s >= %6$d AND %2$s = %4$s %7$s';
|
|
|
|
$statements[] = sprintf($sql, $table_name, $parent_column, $right_column, $by_parent_id, $parent_id, $from, $scope_sql);
|
|
|
|
return $statements;
|
|
|
|
}
|
|
|
|
public static function shiftRLValues($peer_name, $first, $delta, $scopeId = null)
|
|
{
|
|
$statements = array();
|
|
$stub_name = self::getStubFromPeer($peer_name);
|
|
$table_name = constant("$peer_name::TABLE_NAME");
|
|
$left_column = self::getColumnConstant($stub_name, 'left');
|
|
$right_column = self::getColumnConstant($stub_name, 'right');
|
|
$scope_column = self::getColumnConstant($stub_name, 'scope');
|
|
|
|
$scope_sql = '';
|
|
if (isset($scopeId))
|
|
{
|
|
$scope_sql = sprintf(' AND %s = %s', $scope_column, $scopeId);
|
|
}
|
|
|
|
$sql = 'UPDATE %1$s SET %2$s = %2$s + %3$d WHERE %2$s >= %4$d %5$s';
|
|
|
|
$statements[] = sprintf($sql, $table_name, $left_column, $delta, $first, $scope_sql);
|
|
$statements[] = sprintf($sql, $table_name, $right_column, $delta, $first, $scope_sql);
|
|
|
|
|
|
return $statements;
|
|
}
|
|
|
|
private static function shiftRLRange($peer_name, $first, $last, $delta, $scopeId = null)
|
|
{
|
|
$statements = array();
|
|
$stub_name = self::getStubFromPeer($peer_name);
|
|
$table_name = constant("$peer_name::TABLE_NAME");
|
|
$left_column = self::getColumnConstant($stub_name, 'left');
|
|
$right_column = self::getColumnConstant($stub_name, 'right');
|
|
$scope_column = self::getColumnConstant($stub_name, 'scope');
|
|
|
|
$scope_sql = '';
|
|
|
|
if (!is_null($scopeId))
|
|
{
|
|
$scope_sql = sprintf(' AND %s = "%s"', $scope_column, $scopeId);
|
|
}
|
|
|
|
$sql = 'UPDATE %1$s SET %2$s = %2$s + %3$d WHERE %2$s >= %4$d AND %2$s <= %5$d %6$s';
|
|
|
|
$statements[] = sprintf($sql, $table_name, $left_column, $delta, $first, $last, $scope_sql);
|
|
$statements[] = sprintf($sql, $table_name, $right_column, $delta, $first, $last, $scope_sql);
|
|
|
|
|
|
return $statements;
|
|
}
|
|
|
|
private static function shiftDepth($peer_name, $first, $last, $delta, $depth_delta, $scopeId = null)
|
|
{
|
|
$statements = array();
|
|
$stub_name = self::getStubFromPeer($peer_name);
|
|
$table_name = constant("$peer_name::TABLE_NAME");
|
|
$left_column = self::getColumnConstant($stub_name, 'left');
|
|
$right_column = self::getColumnConstant($stub_name, 'right');
|
|
$scope_column = self::getColumnConstant($stub_name, 'scope');
|
|
$depth_column = self::getColumnConstant($stub_name, 'depth');
|
|
|
|
$scope_sql = '';
|
|
|
|
if (!is_null($scopeId))
|
|
{
|
|
$scope_sql = sprintf(' AND %s = "%s"', $scope_column, $scopeId);
|
|
}
|
|
|
|
$sql = 'UPDATE %1$s SET %3$s = %3$s + %4$d WHERE %2$s >= %6$d AND %2$s <= %7$d %8$s';
|
|
|
|
$statements[] = sprintf($sql, $table_name, $right_column, $depth_column, $depth_delta, $delta, $first, $last, $scope_sql);
|
|
|
|
return $statements;
|
|
}
|
|
|
|
/**
|
|
* Returns getter / setter name for requested column.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param string $prefix Usually 'get' or 'set'
|
|
* @param string $column left|right|parent|scope
|
|
*/
|
|
private static function forgeMethodName($node, $prefix, $column)
|
|
{
|
|
$phpName = call_user_func(array($node->getPeer(), 'translateFieldName'), self::getColumnConstant(get_class($node), $column), BasePeer::TYPE_COLNAME, BasePeer::TYPE_PHPNAME);
|
|
return sprintf('%s%s', $prefix, $phpName);
|
|
}
|
|
|
|
/**
|
|
* Returns the appropriate column name.
|
|
*
|
|
* @param string $node_class Propel model class
|
|
* @param string $column "generic" column name (either parent, left, right, scope)
|
|
* @param bool $skip_table_name_prefix Removes table name from column name if true (defaults to false)
|
|
*
|
|
* @return string Column's name
|
|
*/
|
|
private static function getColumnConstant($node_class, $column, $skip_table_name_prefix = false)
|
|
{
|
|
$conf_directive = sprintf('propel_behavior_actasnestedset_%s_columns', $node_class);
|
|
$columns = sfConfig::get($conf_directive);
|
|
|
|
return $skip_table_name_prefix ? substr($columns[$column], strpos($columns[$column], '.') + 1) : $columns[$column];
|
|
}
|
|
|
|
/**
|
|
* Adds entries to given stack.
|
|
*
|
|
* @param string $stack_name
|
|
* @param array $entries
|
|
*/
|
|
private function addStackEntries($stack_name, $entries = array())
|
|
{
|
|
$stack = $this->$stack_name;
|
|
foreach ($entries as $entry)
|
|
{
|
|
$stack[] = $entry;
|
|
}
|
|
$this->$stack_name = $stack;
|
|
}
|
|
|
|
private function addPreDeleteStackEntries($entries = array())
|
|
{
|
|
$this->addStackEntries('predelete_stack', $entries);
|
|
}
|
|
|
|
private function addPreSaveStackEntries($entries = array())
|
|
{
|
|
$this->addStackEntries('presave_stack', $entries);
|
|
}
|
|
|
|
/**
|
|
* Processes presave stack : runs stacked queries and saves stackes objects.
|
|
*/
|
|
private function processStack($stack_name)
|
|
{
|
|
$con = Propel::getConnection();
|
|
try
|
|
{
|
|
$con->begin();
|
|
foreach ($this->$stack_name as $action)
|
|
{
|
|
array_shift($this->$stack_name);
|
|
|
|
// stack entry is an object, let's save it
|
|
if (is_object($action) && $action instanceof BaseObject)
|
|
{
|
|
$action->save();
|
|
}
|
|
// stack entry is an sql query, let's execute it
|
|
elseif (is_string($action))
|
|
{
|
|
|
|
$statement = $con->createStatement();
|
|
|
|
$result = $statement->executeQuery($action);
|
|
}
|
|
else
|
|
{
|
|
$msg = sprintf('Unable to process %s stack entry: %s', $stack_name, serialize($action));
|
|
throw new PropelException($msg);
|
|
}
|
|
|
|
}
|
|
}
|
|
catch(PropelException $e)
|
|
{
|
|
$con->rollback();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
private function processPreDeleteStack()
|
|
{
|
|
$this->processStack('predelete_stack');
|
|
}
|
|
|
|
private function processPreSaveStack()
|
|
{
|
|
$this->processStack('presave_stack');
|
|
}
|
|
|
|
/**
|
|
* Returns queries needed to update tree.
|
|
*
|
|
* @param BaseObject $node
|
|
* @param integer $dest_left
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function getUpdateTreeQueries(BaseObject $node, $dest_left, $dest_depth)
|
|
{
|
|
$statements = array();
|
|
$peer_name = $node->getPeer();
|
|
|
|
$left = $node->getLeftValue();
|
|
$right = $node->getRightValue();
|
|
$depth = $node->getDepthValue();
|
|
$tree_size = $right - $left + 1;
|
|
|
|
$statements = array_merge($statements, self::shiftRLValues($peer_name, $dest_left, $tree_size, $node->getScopeIdValue()));
|
|
|
|
if ($left >= $dest_left)
|
|
{
|
|
$left += $tree_size;
|
|
$right += $tree_size;
|
|
}
|
|
$statements = array_merge($statements, self::shiftDepth($peer_name, $left, $right, $dest_left - $left, $dest_depth - $depth, $node->getScopeIdValue()));
|
|
$statements = array_merge($statements, self::shiftRLRange($peer_name, $left, $right, $dest_left - $left, $node->getScopeIdValue()));
|
|
$statements = array_merge($statements, self::shiftRLValues($peer_name, $right + 1, -$tree_size, $node->getScopeIdValue()));
|
|
|
|
return $statements;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns peer's stub name.
|
|
*
|
|
* @param string $peer_name
|
|
* @return string
|
|
*/
|
|
public static function getStubFromPeer($peer_name)
|
|
{
|
|
return preg_replace('/^(\w+)Peer$/', '$1', $peer_name);
|
|
}
|
|
|
|
public function postHydrate(BaseObject $node, ResultSet $rs, $startcol = 1)
|
|
{
|
|
try
|
|
{
|
|
$node->depth = $rs->getInt($startcol);
|
|
}
|
|
catch(Exception $e)
|
|
{
|
|
$node->depth = 0;
|
|
}
|
|
|
|
}
|
|
|
|
}
|