Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
<?php
/**
* NestedSetBehavior class file.
*
* @author Alexander Kochetov <creocoder@gmail.com>
* @link https://github.com/yiiext/nested-set-behavior
*/
/**
* Provides nested set functionality for a model.
*
* @version 1.06
* @package yiiext.behaviors.model.trees
*/
class NestedSetBehavior extends CActiveRecordBehavior
{
public $rootAttribute = 'root';
public $leftAttribute = 'lft';
public $rightAttribute = 'rgt';
public $levelAttribute = 'level';
private $_ignoreEvent = false;
private $_deleted = false;
private $_id;
private static $_cached;
private static $_c = 0;
/**
* Named scope. Gets descendants for node.
* @param int $depth the depth.
* @return CActiveRecord the owner.
*/
public function descendants($depth = null)
{
$owner = $this->getOwner();
$db = $owner->getDbConnection();
$criteria = $owner->getDbCriteria();
$alias = $db->quoteColumnName($owner->getTableAlias());
$criteria->mergeWith(array(
'condition' => $alias . '.' . $db->quoteColumnName($this->leftAttribute) . '>' . $owner->{$this->leftAttribute} .
' AND ' . $alias . '.' . $db->quoteColumnName($this->rightAttribute) . '<' . $owner->{$this->rightAttribute},
'order' => $alias . '.' . $db->quoteColumnName($this->leftAttribute),
));
if ($depth !== null)
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->levelAttribute) . '<=' . ($owner->{$this->levelAttribute} + $depth));
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount);
$criteria->params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute};
return $owner;
}
/**
* Named scope. Gets children for node (direct descendants only).
* @return CActiveRecord the owner.
*/
public function children()
{
return $this->descendants(1);
}
/**
* Named scope. Gets ancestors for node.
* @param int $depth the depth.
* @return CActiveRecord the owner.
*/
public function ancestors($depth = null)
{
$owner = $this->getOwner();
$db = $owner->getDbConnection();
$criteria = $owner->getDbCriteria();
$alias = $db->quoteColumnName($owner->getTableAlias());
$criteria->mergeWith(array(
'condition' => $alias . '.' . $db->quoteColumnName($this->leftAttribute) . '<' . $owner->{$this->leftAttribute} .
' AND ' . $alias . '.' . $db->quoteColumnName($this->rightAttribute) . '>' . $owner->{$this->rightAttribute},
'order' => $alias . '.' . $db->quoteColumnName($this->leftAttribute),
));
if ($depth !== null)
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->levelAttribute) . '>=' . ($owner->{$this->levelAttribute} - $depth));
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount);
$criteria->params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute};
return $owner;
}
/**
* Named scope. Gets root node(s).
* @return CActiveRecord the owner.
*/
public function roots()
{
$owner = $this->getOwner();
$db = $owner->getDbConnection();
$owner->getDbCriteria()->addCondition($db->quoteColumnName($owner->getTableAlias()) . '.' . $db->quoteColumnName($this->leftAttribute) . '=1');
return $owner;
}
/**
* Named scope. Gets parent of node.
* @return CActiveRecord the owner.
*/
public function parent()
{
$owner = $this->getOwner();
$db = $owner->getDbConnection();
$criteria = $owner->getDbCriteria();
$alias = $db->quoteColumnName($owner->getTableAlias());
$criteria->mergeWith(array(
'condition' => $alias . '.' . $db->quoteColumnName($this->leftAttribute) . '<' . $owner->{$this->leftAttribute} .
' AND ' . $alias . '.' . $db->quoteColumnName($this->rightAttribute) . '>' . $owner->{$this->rightAttribute},
'order' => $alias . '.' . $db->quoteColumnName($this->rightAttribute),
));
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount);
$criteria->params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute};
return $owner;
}
/**
* Named scope. Gets previous sibling of node.
* @return CActiveRecord the owner.
*/
public function prev()
{
$owner = $this->getOwner();
$db = $owner->getDbConnection();
$criteria = $owner->getDbCriteria();
$alias = $db->quoteColumnName($owner->getTableAlias());
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rightAttribute) . '=' . ($owner->{$this->leftAttribute} - 1));
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount);
$criteria->params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute};
return $owner;
}
/**
* Named scope. Gets next sibling of node.
* @return CActiveRecord the owner.
*/
public function next()
{
$owner = $this->getOwner();
$db = $owner->getDbConnection();
$criteria = $owner->getDbCriteria();
$alias = $db->quoteColumnName($owner->getTableAlias());
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->leftAttribute) . '=' . ($owner->{$this->rightAttribute} + 1));
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount);
$criteria->params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute};
return $owner;
}
/**
* Create root node if multiple-root tree mode. Update node if it's not new.
* @param boolean $runValidation whether to perform validation.
* @param boolean $attributes list of attributes.
* @return boolean whether the saving succeeds.
*/
public function save($runValidation = true, $attributes = null)
{
$owner = $this->getOwner();
if ($runValidation && !$owner->validate($attributes))
return false;
if ($owner->getIsNewRecord())
return $this->makeRoot($attributes);
$this->_ignoreEvent = true;
$result = $owner->update($attributes);
$this->_ignoreEvent = false;
return $result;
}
/**
* Create root node if multiple-root tree mode. Update node if it's not new.
* @param boolean $runValidation whether to perform validation.
* @param boolean $attributes list of attributes.
* @return boolean whether the saving succeeds.
*/
public function saveNode($runValidation = true, $attributes = null)
{
return $this->save($runValidation, $attributes);
}
/**
* Deletes node and it's descendants.
* @return boolean whether the deletion is successful.
* @throws CDbException
* @throws Exception
*/
public function delete()
{
$owner = $this->getOwner();
if ($owner->getIsNewRecord())
throw new CDbException(Yii::t('yiiext', 'The node cannot be deleted because it is new.'));
if ($this->getIsDeletedRecord())
throw new CDbException(Yii::t('yiiext', 'The node cannot be deleted because it is already deleted.'));
$db = $owner->getDbConnection();
if ($db->getCurrentTransaction() === null)
$transaction = $db->beginTransaction();
try {
if ($owner->isLeaf()) {
$this->_ignoreEvent = true;
$result = $owner->delete();
$this->_ignoreEvent = false;
} else {
$condition = $db->quoteColumnName($this->leftAttribute) . '>=' . $owner->{$this->leftAttribute} . ' AND ' .
$db->quoteColumnName($this->rightAttribute) . '<=' . $owner->{$this->rightAttribute};
$params = array();
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount;
$params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute};
$result = $owner->deleteAll($condition, $params) > 0;
}
if (!$result) {
if (isset($transaction))
$transaction->rollback();
return false;
}
$this->shiftLeftRight($owner->{$this->rightAttribute} + 1, $owner->{$this->leftAttribute} - $owner->{$this->rightAttribute} - 1);
if (isset($transaction))
$transaction->commit();
$this->correctCachedOnDelete();
} catch (Exception $e) {
if (isset($transaction))
$transaction->rollback();
throw $e;
}
return true;
}
/**
* Deletes node and it's descendants.
* @return boolean whether the deletion is successful.
*/
public function deleteNode()
{
return $this->delete();
}
/**
* Prepends node to target as first child.
* @param CActiveRecord $target the target.
* @param boolean $runValidation whether to perform validation.
* @param array $attributes list of attributes.
* @return boolean whether the prepending succeeds.
*/
public function prependTo($target, $runValidation = true, $attributes = null)
{
return $this->addNode($target, $target->{$this->leftAttribute} + 1, 1, $runValidation, $attributes);
}
/**
* Prepends target to node as first child.
* @param CActiveRecord $target the target.
* @param boolean $runValidation whether to perform validation.
* @param array $attributes list of attributes.
* @return boolean whether the prepending succeeds.
*/
public function prepend($target, $runValidation = true, $attributes = null)
{
return $target->prependTo($this->getOwner(), $runValidation, $attributes);
}
/**
* Appends node to target as last child.
* @param CActiveRecord $target the target.
* @param boolean $runValidation whether to perform validation.
* @param array $attributes list of attributes.
* @return boolean whether the appending succeeds.
*/
public function appendTo($target, $runValidation = true, $attributes = null)
{
return $this->addNode($target, $target->{$this->rightAttribute}, 1, $runValidation, $attributes);
}
/**
* Appends target to node as last child.
* @param CActiveRecord $target the target.
* @param boolean $runValidation whether to perform validation.
* @param array $attributes list of attributes.
* @return boolean whether the appending succeeds.
*/
public function append($target, $runValidation = true, $attributes = null)
{
return $target->appendTo($this->getOwner(), $runValidation, $attributes);
}
/**
* Inserts node as previous sibling of target.
* @param CActiveRecord $target the target.
* @param boolean $runValidation whether to perform validation.
* @param array $attributes list of attributes.
* @return boolean whether the inserting succeeds.
*/
public function insertBefore($target, $runValidation = true, $attributes = null)
{
return $this->addNode($target, $target->{$this->leftAttribute}, 0, $runValidation, $attributes);
}
/**
* Inserts node as next sibling of target.
* @param CActiveRecord $target the target.
* @param boolean $runValidation whether to perform validation.
* @param array $attributes list of attributes.
* @return boolean whether the inserting succeeds.
*/
public function insertAfter($target, $runValidation = true, $attributes = null)
{
return $this->addNode($target, $target->{$this->rightAttribute} + 1, 0, $runValidation, $attributes);
}
/**
* Move node as previous sibling of target.
* @param CActiveRecord $target the target.
* @return boolean whether the moving succeeds.
*/
public function moveBefore($target)
{
return $this->moveNode($target, $target->{$this->leftAttribute}, 0);
}
/**
* Move node as next sibling of target.
* @param CActiveRecord $target the target.
* @return boolean whether the moving succeeds.
*/
public function moveAfter($target)
{
return $this->moveNode($target, $target->{$this->rightAttribute} + 1, 0);
}
/**
* Move node as first child of target.
* @param CActiveRecord $target the target.
* @return boolean whether the moving succeeds.
*/
public function moveAsFirst($target)
{
return $this->moveNode($target, $target->{$this->leftAttribute} + 1, 1);
}
/**
* Move node as last child of target.
* @param CActiveRecord $target the target.
* @return boolean whether the moving succeeds.
*/
public function moveAsLast($target)
{
return $this->moveNode($target, $target->{$this->rightAttribute}, 1);
}
/**
* Move node as new root.
* @return boolean whether the moving succeeds.
* @throws CDbException
* @throws CException
* @throws Exception
*/
public function moveAsRoot()
{
$owner = $this->getOwner();
if ($owner->getIsNewRecord())
throw new CException(Yii::t('yiiext', 'The node should not be new record.'));
if ($this->getIsDeletedRecord())
throw new CDbException(Yii::t('yiiext', 'The node should not be deleted.'));
if ($owner->isRoot())
throw new CException(Yii::t('yiiext', 'The node already is root node.'));
$db = $owner->getDbConnection();
if ($db->getCurrentTransaction() === null)
$transaction = $db->beginTransaction();
try {
$left = $owner->{$this->leftAttribute};
$right = $owner->{$this->rightAttribute};
$levelDelta = 1 - $owner->{$this->levelAttribute};
$delta = 1 - $left;
$lastRoot = $this->getLastRoot($owner);
$owner->updateAll(
array(
$this->leftAttribute => new CDbExpression($db->quoteColumnName($this->leftAttribute) . sprintf('%+d', $delta)),
$this->rightAttribute => new CDbExpression($db->quoteColumnName($this->rightAttribute) . sprintf('%+d', $delta)),
$this->levelAttribute => new CDbExpression($db->quoteColumnName($this->levelAttribute) . sprintf('%+d', $levelDelta)),
$this->rootAttribute => $lastRoot,
),
$db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND ' .
$db->quoteColumnName($this->rightAttribute) . '<=' . $right . ' AND ' .
$db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount,
array(CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++ => $owner->{$this->rootAttribute}));
$this->shiftLeftRight($right + 1, $left - $right - 1);
if (isset($transaction))
$transaction->commit();
$this->correctCachedOnMoveBetweenTrees(1, $levelDelta, $owner->getPrimaryKey());
} catch (Exception $e) {
if (isset($transaction))
$transaction->rollback();
throw $e;
}
return true;
}
/**
* Determines if node is descendant of subject node.
* @param CActiveRecord $subj the subject node.
* @return boolean whether the node is descendant of subject node.
*/
public function isDescendantOf($subj)
{
$owner = $this->getOwner();
$result = ($owner->{$this->leftAttribute} > $subj->{$this->leftAttribute})
&& ($owner->{$this->rightAttribute} < $subj->{$this->rightAttribute});
$result = $result && ($owner->{$this->rootAttribute} === $subj->{$this->rootAttribute});
return $result;
}
/**
* Determines if node is leaf.
* @return boolean whether the node is leaf.
*/
public function isLeaf()
{
$owner = $this->getOwner();
return $owner->{$this->rightAttribute} - $owner->{$this->leftAttribute} === 1;
}
/**
* Determines if node is root.
* @return boolean whether the node is root.
*/
public function isRoot()
{
return $this->getOwner()->{$this->leftAttribute} == 1;
}
/**
* Returns if the current node is deleted.
* @return boolean whether the node is deleted.
*/
public function getIsDeletedRecord()
{
return $this->_deleted;
}
/**
* Sets if the current node is deleted.
* @param boolean $value whether the node is deleted.
*/
public function setIsDeletedRecord($value)
{
$this->_deleted = $value;
}
/**
* Handle 'afterConstruct' event of the owner.
* @param CEvent $event event parameter.
*/
public function afterConstruct($event)
{
$owner = $this->getOwner();
self::$_cached[get_class($owner)][$this->_id = self::$_c++] = $owner;
}
/**
* Handle 'afterFind' event of the owner.
* @param CEvent $event event parameter.
*/
public function afterFind($event)
{
$owner = $this->getOwner();
self::$_cached[get_class($owner)][$this->_id = self::$_c++] = $owner;
}
/**
* Handle 'beforeSave' event of the owner.
* @param CEvent $event event parameter.
* @return boolean.
* @throws CDbException
*/
public function beforeSave($event)
{
if ($this->_ignoreEvent)
return true;
else
throw new CDbException(Yii::t('yiiext', 'You should not use CActiveRecord::save() method when NestedSetBehavior attached.'));
}
/**
* Handle 'beforeDelete' event of the owner.
* @param CEvent $event event parameter.
* @return boolean.
* @throws CDbException
*/
public function beforeDelete($event)
{
if ($this->_ignoreEvent)
return true;
else
throw new CDbException(Yii::t('yiiext', 'You should not use CActiveRecord::delete() method when NestedSetBehavior attached.'));
}
/**
* @param int $key .
* @param int $delta .
*/
private function shiftLeftRight($key, $delta)
{
$owner = $this->getOwner();
$db = $owner->getDbConnection();
foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) {
$condition = $db->quoteColumnName($attribute) . '>=' . $key;
$params = array();
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount;
$params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute};
$owner->updateAll(array($attribute => new CDbExpression($db->quoteColumnName($attribute) . sprintf('%+d', $delta))), $condition, $params);
}
}
/**
* @param CActiveRecord $target .
* @param int $key .
* @param int $levelUp .
* @param boolean $runValidation .
* @param array $attributes .
* @return boolean.
* @throws CDbException
* @throws CException
* @throws Exception
*/
private function addNode($target, $key, $levelUp, $runValidation, $attributes)
{
$owner = $this->getOwner();
if (!$owner->getIsNewRecord())
throw new CDbException(Yii::t('yiiext', 'The node cannot be inserted because it is not new.'));
if ($this->getIsDeletedRecord())
throw new CDbException(Yii::t('yiiext', 'The node cannot be inserted because it is deleted.'));
if ($target->getIsDeletedRecord())
throw new CDbException(Yii::t('yiiext', 'The node cannot be inserted because target node is deleted.'));
if ($owner->equals($target))
throw new CException(Yii::t('yiiext', 'The target node should not be self.'));
if (!$levelUp && $target->isRoot())
throw new CException(Yii::t('yiiext', 'The target node should not be root.'));
if ($runValidation && !$owner->validate())
return false;
$owner->{$this->rootAttribute} = $target->{$this->rootAttribute};
$db = $owner->getDbConnection();
if ($db->getCurrentTransaction() === null)
$transaction = $db->beginTransaction();
try {
$this->shiftLeftRight($key, 2);
$owner->{$this->leftAttribute} = $key;
$owner->{$this->rightAttribute} = $key + 1;
$owner->{$this->levelAttribute} = $target->{$this->levelAttribute} + $levelUp;
$this->_ignoreEvent = true;
$result = $owner->insert($attributes);
$this->_ignoreEvent = false;
if (!$result) {
if (isset($transaction))
$transaction->rollback();
return false;
}
if (isset($transaction))
$transaction->commit();
$this->correctCachedOnAddNode($key);
} catch (Exception $e) {
if (isset($transaction))
$transaction->rollback();
throw $e;
}
return true;
}
/**
* @param array $attributes .
* @return boolean.
* @throws CException
* @throws Exception
*/
private function makeRoot($attributes)
{
$owner = $this->getOwner();
$owner->{$this->leftAttribute} = 1;
$owner->{$this->rightAttribute} = 2;
$owner->{$this->levelAttribute} = 1;
$db = $owner->getDbConnection();
if ($db->getCurrentTransaction() === null)
$transaction = $db->beginTransaction();
try {
$this->_ignoreEvent = true;
$result = $owner->insert($attributes);
$this->_ignoreEvent = false;
if (!$result) {
if (isset($transaction))
$transaction->rollback();
return false;
}
$lastRoot = $this->getLastRoot($owner);
$owner->{$this->rootAttribute} = $lastRoot;
$owner->updateByPk($owner->getPrimaryKey(), array(
$this->rootAttribute => $lastRoot
));
if (isset($transaction))
$transaction->commit();
} catch (Exception $e) {
if (isset($transaction))
$transaction->rollback();
throw $e;
}
return true;
}
/**
* @param $owner CActiveRecord
* @return CActiveRecord
*/
protected function getLastRoot(CActiveRecord $owner)
{
$db = $owner->getDbConnection();
$criteria=new CDbCriteria();
$criteria->select = 'MAX(' . $db->quoteColumnName($this->rootAttribute) . ')+1 AS ' . $this->rootAttribute;
$lastRoot = $owner->model()->find($criteria);
return $lastRoot->{$this->rootAttribute} ? $lastRoot->{$this->rootAttribute} : 1;
}
/**
* @param CActiveRecord $target .
* @param int $key .
* @param int $levelUp .
* @return boolean.
* @throws CDbException
* @throws CException
* @throws Exception
*/
private function moveNode($target, $key, $levelUp)
{
$owner = $this->getOwner();
if ($owner->getIsNewRecord())
throw new CException(Yii::t('yiiext', 'The node should not be new record.'));
if ($this->getIsDeletedRecord())
throw new CDbException(Yii::t('yiiext', 'The node should not be deleted.'));
if ($target->getIsDeletedRecord())
throw new CDbException(Yii::t('yiiext', 'The target node should not be deleted.'));
if ($owner->equals($target))
throw new CException(Yii::t('yiiext', 'The target node should not be self.'));
if ($target->isDescendantOf($owner))
throw new CException(Yii::t('yiiext', 'The target node should not be descendant.'));
if (!$levelUp && $target->isRoot())
throw new CException(Yii::t('yiiext', 'The target node should not be root.'));
$db = $owner->getDbConnection();
if ($db->getCurrentTransaction() === null)
$transaction = $db->beginTransaction();
try {
$left = $owner->{$this->leftAttribute};
$right = $owner->{$this->rightAttribute};
$levelDelta = $target->{$this->levelAttribute} - $owner->{$this->levelAttribute} + $levelUp;
if ($owner->{$this->rootAttribute} !== $target->{$this->rootAttribute}) {
foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) {
$owner->updateAll(array($attribute => new CDbExpression($db->quoteColumnName($attribute) . sprintf('%+d', $right - $left + 1))),
$db->quoteColumnName($attribute) . '>=' . $key . ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount,
array(CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++ => $target->{$this->rootAttribute}));
}
$delta = $key - $left;
$owner->updateAll(
array(
$this->leftAttribute => new CDbExpression($db->quoteColumnName($this->leftAttribute) . sprintf('%+d', $delta)),
$this->rightAttribute => new CDbExpression($db->quoteColumnName($this->rightAttribute) . sprintf('%+d', $delta)),
$this->levelAttribute => new CDbExpression($db->quoteColumnName($this->levelAttribute) . sprintf('%+d', $levelDelta)),
$this->rootAttribute => $target->{$this->rootAttribute},
),
$db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND ' .
$db->quoteColumnName($this->rightAttribute) . '<=' . $right . ' AND ' .
$db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount,
array(CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++ => $owner->{$this->rootAttribute}));
$this->shiftLeftRight($right + 1, $left - $right - 1);
if (isset($transaction))
$transaction->commit();
$this->correctCachedOnMoveBetweenTrees($key, $levelDelta, $target->{$this->rootAttribute});
} else {
$delta = $right - $left + 1;
$this->shiftLeftRight($key, $delta);
if ($left >= $key) {
$left += $delta;
$right += $delta;
}
$condition = $db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND ' . $db->quoteColumnName($this->rightAttribute) . '<=' . $right;
$params = array();
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount;
$params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute};
$owner->updateAll(array($this->levelAttribute => new CDbExpression($db->quoteColumnName($this->levelAttribute) . sprintf('%+d', $levelDelta))), $condition, $params);
foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) {
$condition = $db->quoteColumnName($attribute) . '>=' . $left . ' AND ' . $db->quoteColumnName($attribute) . '<=' . $right;
$params = array();
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount;
$params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute};
$owner->updateAll(array($attribute => new CDbExpression($db->quoteColumnName($attribute) . sprintf('%+d', $key - $left))), $condition, $params);
}
$this->shiftLeftRight($right + 1, -$delta);
if (isset($transaction))
$transaction->commit();
$this->correctCachedOnMoveNode($key, $levelDelta);
}
} catch (Exception $e) {
if (isset($transaction))
$transaction->rollback();
throw $e;
}
return true;
}
/**
* Correct cache for {@link NestedSetBehavior::delete()} and {@link NestedSetBehavior::deleteNode()}.
*/
private function correctCachedOnDelete()
{
$owner = $this->getOwner();
$left = $owner->{$this->leftAttribute};
$right = $owner->{$this->rightAttribute};
$key = $right + 1;
$delta = $left - $right - 1;
foreach (self::$_cached[get_class($owner)] as $node) {
if ($node->getIsNewRecord() || $node->getIsDeletedRecord())
continue;
if ($owner->{$this->rootAttribute} !== $node->{$this->rootAttribute})
continue;
if ($node->{$this->leftAttribute} >= $left && $node->{$this->rightAttribute} <= $right)
$node->setIsDeletedRecord(true);
else {
if ($node->{$this->leftAttribute} >= $key)
$node->{$this->leftAttribute} += $delta;
if ($node->{$this->rightAttribute} >= $key)
$node->{$this->rightAttribute} += $delta;
}
}
}
/**
* Correct cache for {@link NestedSetBehavior::addNode()}.
* @param int $key .
*/
private function correctCachedOnAddNode($key)
{
$owner = $this->getOwner();
foreach (self::$_cached[get_class($owner)] as $node) {
if ($node->getIsNewRecord() || $node->getIsDeletedRecord())
continue;
if ($owner->{$this->rootAttribute} !== $node->{$this->rootAttribute})
continue;
if ($owner === $node)
continue;
if ($node->{$this->leftAttribute} >= $key)
$node->{$this->leftAttribute} += 2;
if ($node->{$this->rightAttribute} >= $key)
$node->{$this->rightAttribute} += 2;
}
}
/**
* Correct cache for {@link NestedSetBehavior::moveNode()}.
* @param int $key .
* @param int $levelDelta .
*/
private function correctCachedOnMoveNode($key, $levelDelta)
{
$owner = $this->getOwner();
$left = $owner->{$this->leftAttribute};
$right = $owner->{$this->rightAttribute};
$delta = $right - $left + 1;
if ($left >= $key) {
$left += $delta;
$right += $delta;
}
$delta2 = $key - $left;
foreach (self::$_cached[get_class($owner)] as $node) {
if ($node->getIsNewRecord() || $node->getIsDeletedRecord())
continue;
if ($owner->{$this->rootAttribute} !== $node->{$this->rootAttribute})
continue;
if ($node->{$this->leftAttribute} >= $key)
$node->{$this->leftAttribute} += $delta;
if ($node->{$this->rightAttribute} >= $key)
$node->{$this->rightAttribute} += $delta;
if ($node->{$this->leftAttribute} >= $left && $node->{$this->rightAttribute} <= $right)
$node->{$this->levelAttribute} += $levelDelta;
if ($node->{$this->leftAttribute} >= $left && $node->{$this->leftAttribute} <= $right)
$node->{$this->leftAttribute} += $delta2;
if ($node->{$this->rightAttribute} >= $left && $node->{$this->rightAttribute} <= $right)
$node->{$this->rightAttribute} += $delta2;
if ($node->{$this->leftAttribute} >= $right + 1)
$node->{$this->leftAttribute} -= $delta;
if ($node->{$this->rightAttribute} >= $right + 1)
$node->{$this->rightAttribute} -= $delta;
}
}
/**
* Correct cache for {@link NestedSetBehavior::moveNode()}.
* @param int $key .
* @param int $levelDelta .
* @param int $root .
*/
private function correctCachedOnMoveBetweenTrees($key, $levelDelta, $root)
{
$owner = $this->getOwner();
$left = $owner->{$this->leftAttribute};
$right = $owner->{$this->rightAttribute};
$delta = $right - $left + 1;
$delta2 = $key - $left;
$delta3 = $left - $right - 1;
foreach (self::$_cached[get_class($owner)] as $node) {
if ($node->getIsNewRecord() || $node->getIsDeletedRecord())
continue;
if ($node->{$this->rootAttribute} === $root) {
if ($node->{$this->leftAttribute} >= $key)
$node->{$this->leftAttribute} += $delta;
if ($node->{$this->rightAttribute} >= $key)
$node->{$this->rightAttribute} += $delta;
} else if ($node->{$this->rootAttribute} === $owner->{$this->rootAttribute}) {
if ($node->{$this->leftAttribute} >= $left && $node->{$this->rightAttribute} <= $right) {
$node->{$this->leftAttribute} += $delta2;
$node->{$this->rightAttribute} += $delta2;
$node->{$this->levelAttribute} += $levelDelta;
$node->{$this->rootAttribute} = $root;
} else {
if ($node->{$this->leftAttribute} >= $right + 1)
$node->{$this->leftAttribute} += $delta3;
if ($node->{$this->rightAttribute} >= $right + 1)
$node->{$this->rightAttribute} += $delta3;
}
}
}
}
/**
* Destructor.
*/
public function __destruct()
{
unset(self::$_cached[get_class($this->getOwner())][$this->_id]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment