1
0
Fork 0
mirror of https://github.com/Oreolek/yii2-nested-sets.git synced 2024-07-03 06:55:00 +03:00
yii2-nested-sets/NestedSet.php
Alexander Kochetov 1b80a67e81 Preview release
2013-05-06 11:12:45 +04:00

949 lines
29 KiB
PHP

<?php
/**
* @link https://github.com/creocoder/nested-set-behavior-2
* @copyright Copyright (c) 2013 Alexander Kochetov
* @license http://www.yiiframework.com/license/
*/
use \yii\base\Behavior;
use \yii\base\Event;
use \yii\db\ActiveRecord;
use \yii\db\ActiveQuery;
use \yii\db\Expression;
use \yii\db\Exception;
/**
* @author Alexander Kochetov <creocoder@gmail.com>
*/
class NestedSet extends Behavior
{
/**
* @var ActiveRecord the owner of this behavior.
*/
public $owner;
public $hasManyRoots = false;
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;
/**
* Declares event handlers for the [[owner]]'s events.
* @return array events (array keys) and the corresponding event handler methods (array values).
*/
public function events()
{
return array(
ActiveRecord::EVENT_INIT => 'init',
ActiveRecord::EVENT_AFTER_FIND => 'afterFind',
ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert',
ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
);
}
/**
* Named scope. Gets descendants for node.
* @param ActiveQuery $query.
* @param int $depth the depth.
* @return ActiveRecord the owner.
*/
public function descendants($query, $depth = null)
{
$db = $this->owner->getDb();
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '>' . $this->owner->{$this->leftAttribute});
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '<' . $this->owner->{$this->rightAttribute});
$query->addOrderBy($db->quoteColumnName($this->leftAttribute));
if ($depth !== null) {
$query->andWhere($db->quoteColumnName($this->levelAttribute) . '<=' .
($this->owner->{$this->levelAttribute} + $depth));
}
if ($this->hasManyRoots) {
$query->andWhere($db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, array(
':' . $this->rootAttribute => $this->owner->{$this->rootAttribute}
));
}
}
/**
* Named scope. Gets children for node (direct descendants only).
* @return ActiveRecord the owner.
*/
public function children()
{
return $this->descendants(1);
}
/**
* Named scope. Gets ancestors for node.
* @param ActiveQuery $query.
* @param int $depth the depth.
* @return ActiveRecord the owner.
*/
public function ancestors($query, $depth = null)
{
$db = $this->owner->getDb();
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '<' . $this->owner->{$this->leftAttribute});
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '>' . $this->owner->{$this->rightAttribute});
$query->addOrderBy($db->quoteColumnName($this->leftAttribute));
if ($depth !== null) {
$query->andWhere($db->quoteColumnName($this->levelAttribute) . '>=' .
($this->owner->{$this->levelAttribute} - $depth));
}
if ($this->hasManyRoots) {
$query->andWhere($db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, array(
':' . $this->rootAttribute => $this->owner->{$this->rootAttribute}
));
}
}
/**
* Named scope. Gets root node(s).
* @param ActiveQuery $query.
* @return ActiveRecord the owner.
*/
public function roots($query)
{
$query->andWhere($this->leftAttribute . '=1');
}
/**
* Named scope. Gets parent of node.
* @param ActiveQuery $query.
* @return ActiveRecord the owner.
*/
public function parent($query)
{
$db = $this->owner->getDb();
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '<' . $this->owner->{$this->leftAttribute});
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '>' . $this->owner->{$this->rightAttribute});
$query->addOrderBy($db->quoteColumnName($this->rightAttribute));
if ($this->hasManyRoots) {
$query->andWhere($db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, array(
':' . $this->rootAttribute => $this->owner->{$this->rootAttribute}
));
}
}
/**
* Named scope. Gets previous sibling of node.
* @param ActiveQuery $query.
* @return ActiveRecord the owner.
*/
public function prev($query)
{
$db = $this->owner->getDb();
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '=' .
($this->owner->{$this->leftAttribute} - 1));
if ($this->hasManyRoots) {
$query->andWhere($db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, array(
':' . $this->rootAttribute => $this->owner->{$this->rootAttribute}
));
}
}
/**
* Named scope. Gets next sibling of node.
* @param ActiveQuery $query.
* @return ActiveRecord the owner.
*/
public function next($query)
{
$db = $this->owner->getDb();
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '=' .
($this->owner->{$this->rightAttribute} + 1));
if ($this->hasManyRoots) {
$query->andWhere($db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, array(
':' . $this->rootAttribute => $this->owner->{$this->rootAttribute}
));
}
}
/**
* Create root node if multiple-root tree mode. Update node if it's not new.
* @param boolean $runValidation whether to perform validation.
* @param array $attributes list of attributes.
* @return boolean whether the saving succeeds.
*/
public function save($runValidation = true, $attributes = null)
{
if ($runValidation && !$this->owner->validate($attributes)) {
return false;
}
if ($this->owner->getIsNewRecord()) {
return $this->makeRoot($attributes);
}
$this->_ignoreEvent = true;
$result = $this->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 array $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.
* @throws Exception.
* @throws \Exception.
* @return boolean whether the deletion is successful.
*/
public function delete()
{
if ($this->owner->getIsNewRecord()) {
throw new Exception(\Yii::t('nestedset', 'The node cannot be deleted because it is new.'));
}
if ($this->getIsDeletedRecord()) {
throw new Exception(\Yii::t('nestedset', 'The node cannot be deleted because it is already deleted.'));
}
$db = $this->owner->getDb();
if ($db->getTransaction() === null) {
$transaction = $db->beginTransaction();
}
try {
if ($this->owner->isLeaf()) {
$this->_ignoreEvent = true;
$result = $this->owner->delete();
$this->_ignoreEvent = false;
} else {
$condition = $db->quoteColumnName($this->leftAttribute) . '>=' . $this->owner->{$this->leftAttribute} .
' AND ' . $db->quoteColumnName($this->rightAttribute) . '<=' .
$this->owner->{$this->rightAttribute};
$params = array();
if ($this->hasManyRoots) {
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute;
$params[':' . $this->rootAttribute] = $this->owner->{$this->rootAttribute};
}
$result = $this->owner->deleteAll($condition, $params) > 0;
}
if (!$result) {
if (isset($transaction)) {
$transaction->rollback();
}
return false;
}
$this->shiftLeftRight(
$this->owner->{$this->rightAttribute} + 1,
$this->owner->{$this->leftAttribute} - $this->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 ActiveRecord $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 ActiveRecord $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->owner, $runValidation, $attributes);
}
/**
* Appends node to target as last child.
* @param ActiveRecord $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 ActiveRecord $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->owner, $runValidation, $attributes);
}
/**
* Inserts node as previous sibling of target.
* @param ActiveRecord $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 ActiveRecord $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 ActiveRecord $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 ActiveRecord $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 ActiveRecord $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 ActiveRecord $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.
* @throws Exception.
* @throws \Exception.
* @return boolean whether the moving succeeds.
*/
public function moveAsRoot()
{
if (!$this->hasManyRoots) {
throw new Exception(\Yii::t('nestedset', 'Many roots mode is off.'));
}
if ($this->owner->getIsNewRecord()) {
throw new Exception(\Yii::t('nestedset', 'The node should not be new record.'));
}
if ($this->getIsDeletedRecord()) {
throw new Exception(\Yii::t('nestedset', 'The node should not be deleted.'));
}
if ($this->owner->isRoot()) {
throw new Exception(\Yii::t('nestedset', 'The node already is root node.'));
}
$db = $this->owner->getDb();
if ($db->getTransaction() === null) {
$transaction = $db->beginTransaction();
}
try {
$left = $this->owner->{$this->leftAttribute};
$right = $this->owner->{$this->rightAttribute};
$levelDelta = 1 - $this->owner->{$this->levelAttribute};
$delta = 1 - $left;
$this->owner->updateAll(
array(
$this->leftAttribute => new Expression($db->quoteColumnName($this->leftAttribute) .
sprintf('%+d', $delta)),
$this->rightAttribute => new Expression($db->quoteColumnName($this->rightAttribute) .
sprintf('%+d', $delta)),
$this->levelAttribute => new Expression($db->quoteColumnName($this->levelAttribute) .
sprintf('%+d', $levelDelta)),
$this->rootAttribute => $this->owner->getPrimaryKey(),
),
$db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND ' .
$db->quoteColumnName($this->rightAttribute) . '<=' . $right . ' AND ' .
$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
array(':' . $this->rootAttribute => $this->owner->{$this->rootAttribute}));
$this->shiftLeftRight($right + 1, $left - $right - 1);
if (isset($transaction)) {
$transaction->commit();
}
$this->correctCachedOnMoveBetweenTrees(1, $levelDelta, $this->owner->getPrimaryKey());
} catch (\Exception $e) {
if (isset($transaction)) {
$transaction->rollback();
}
throw $e;
}
return true;
}
/**
* Determines if node is descendant of subject node.
* @param ActiveRecord $subj the subject node.
* @return boolean whether the node is descendant of subject node.
*/
public function isDescendantOf($subj)
{
$result = ($this->owner->{$this->leftAttribute} > $subj->{$this->leftAttribute})
&& ($this->owner->{$this->rightAttribute} < $subj->{$this->rightAttribute});
if ($this->hasManyRoots) {
$result = $result && ($this->owner->{$this->rootAttribute} === $subj->{$this->rootAttribute});
}
return $result;
}
/**
* Determines if node is leaf.
* @return boolean whether the node is leaf.
*/
public function isLeaf()
{
return $this->owner->{$this->rightAttribute} - $this->owner->{$this->leftAttribute} === 1;
}
/**
* Determines if node is root.
* @return boolean whether the node is root.
*/
public function isRoot()
{
return $this->owner->{$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 'init' event of the owner.
*/
public function init()
{
parent::init();
self::$_cached[get_class($this->owner)][$this->_id = self::$_c++] = $this->owner;
}
/**
* Handle 'afterFind' event of the owner.
* @param Event $event event parameter.
*/
public function afterFind($event)
{
self::$_cached[get_class($this->owner)][$this->_id = self::$_c++] = $this->owner;
}
/**
* Handle 'beforeInsert' event of the owner.
* @param Event $event event parameter.
* @throws Exception.
* @return boolean.
*/
public function beforeInsert($event)
{
if ($this->_ignoreEvent) {
return true;
} else {
throw new Exception(\Yii::t('nestedset', 'You should not use ActiveRecord::insert() or ActiveRecord::save() methods when NestedSet behavior attached.'));
}
}
/**
* Handle 'beforeUpdate' event of the owner.
* @param Event $event event parameter.
* @throws Exception.
* @return boolean.
*/
public function beforeUpdate($event)
{
if ($this->_ignoreEvent) {
return true;
} else {
throw new Exception(\Yii::t('nestedset', 'You should not use ActiveRecord::update() or ActiveRecord::save() methods when NestedSet behavior attached.'));
}
}
/**
* Handle 'beforeDelete' event of the owner.
* @param Event $event event parameter.
* @throws Exception.
* @return boolean.
*/
public function beforeDelete($event)
{
if ($this->_ignoreEvent) {
return true;
} else {
throw new Exception(\Yii::t('nestedset', 'You should not use ActiveRecord::delete() method when NestedSet behavior attached.'));
}
}
/**
* @param int $key.
* @param int $delta.
*/
private function shiftLeftRight($key, $delta)
{
$db = $this->owner->getDb();
foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) {
$condition = $db->quoteColumnName($attribute) . '>=' . $key;
$params = array();
if ($this->hasManyRoots) {
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute;
$params[':' . $this->rootAttribute] = $this->owner->{$this->rootAttribute};
}
$this->owner->updateAll(array(
$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $delta)),
), $condition, $params);
}
}
/**
* @param ActiveRecord $target.
* @param int $key.
* @param int $levelUp.
* @param boolean $runValidation.
* @param array $attributes.
* @throws Exception.
* @throws \Exception.
* @return boolean.
*/
private function addNode($target, $key, $levelUp, $runValidation, $attributes)
{
if (!$this->owner->getIsNewRecord()) {
throw new Exception(\Yii::t('nestedset', 'The node cannot be inserted because it is not new.'));
}
if ($this->getIsDeletedRecord()) {
throw new Exception(\Yii::t('nestedset', 'The node cannot be inserted because it is deleted.'));
}
if ($target->getIsDeletedRecord()) {
throw new Exception(\Yii::t('nestedset', 'The node cannot be inserted because target node is deleted.'));
}
if ($this->owner->equals($target)) {
throw new Exception(\Yii::t('nestedset', 'The target node should not be self.'));
}
if (!$levelUp && $target->isRoot()) {
throw new Exception(\Yii::t('nestedset', 'The target node should not be root.'));
}
if ($runValidation && !$this->owner->validate()) {
return false;
}
if ($this->hasManyRoots) {
$this->owner->{$this->rootAttribute} = $target->{$this->rootAttribute};
}
$db = $this->owner->getDb();
if ($db->getTransaction() === null) {
$transaction = $db->beginTransaction();
}
try {
$this->shiftLeftRight($key, 2);
$this->owner->{$this->leftAttribute} = $key;
$this->owner->{$this->rightAttribute} = $key + 1;
$this->owner->{$this->levelAttribute} = $target->{$this->levelAttribute} + $levelUp;
$this->_ignoreEvent = true;
$result = $this->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.
* @throws Exception.
* @throws \Exception.
* @return boolean.
*/
private function makeRoot($attributes)
{
$this->owner->{$this->leftAttribute} = 1;
$this->owner->{$this->rightAttribute} = 2;
$this->owner->{$this->levelAttribute} = 1;
if ($this->hasManyRoots) {
$db = $this->owner->getDb();
if ($db->getTransaction() === null) {
$transaction = $db->beginTransaction();
}
try {
$this->_ignoreEvent = true;
$result = $this->owner->insert($attributes);
$this->_ignoreEvent = false;
if (!$result) {
if (isset($transaction)) {
$transaction->rollback();
}
return false;
}
$pk = $this->owner->{$this->rootAttribute} = $this->owner->getPrimaryKey();
$this->owner->updateAll(
array($this->rootAttribute => $pk),
array($this->owner->primaryKey()[0] => $pk)
);
if (isset($transaction)) {
$transaction->commit();
}
} catch (\Exception $e) {
if (isset($transaction)) {
$transaction->rollback();
}
throw $e;
}
} else {
if ($this->owner->roots()->exists()) {
throw new Exception(\Yii::t('nestedset', 'Cannot create more than one root in single root mode.'));
}
$this->_ignoreEvent = true;
$result = $this->owner->insert($attributes);
$this->_ignoreEvent = false;
if (!$result) {
return false;
}
}
return true;
}
/**
* @param ActiveRecord $target.
* @param int $key.
* @param int $levelUp.
* @throws Exception.
* @throws \Exception.
* @return boolean.
*/
private function moveNode($target, $key, $levelUp)
{
if ($this->owner->getIsNewRecord()) {
throw new Exception(\Yii::t('nestedset', 'The node should not be new record.'));
}
if ($this->getIsDeletedRecord()) {
throw new Exception(\Yii::t('nestedset', 'The node should not be deleted.'));
}
if ($target->getIsDeletedRecord()) {
throw new Exception(\Yii::t('nestedset', 'The target node should not be deleted.'));
}
if ($this->owner->equals($target)) {
throw new Exception(\Yii::t('nestedset', 'The target node should not be self.'));
}
if ($target->isDescendantOf($this->owner)) {
throw new Exception(\Yii::t('nestedset', 'The target node should not be descendant.'));
}
if (!$levelUp && $target->isRoot()) {
throw new Exception(\Yii::t('nestedset', 'The target node should not be root.'));
}
$db = $this->owner->getDb();
if ($db->getTransaction() === null) {
$transaction = $db->beginTransaction();
}
try {
$left = $this->owner->{$this->leftAttribute};
$right = $this->owner->{$this->rightAttribute};
$levelDelta = $target->{$this->levelAttribute} - $this->owner->{$this->levelAttribute} + $levelUp;
if ($this->hasManyRoots && $this->owner->{$this->rootAttribute} !== $target->{$this->rootAttribute}) {
foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) {
$this->owner->updateAll(array(
$attribute => new Expression($db->quoteColumnName($attribute) .
sprintf('%+d', $right - $left + 1)),
),
$db->quoteColumnName($attribute) . '>=' . $key . ' AND ' .
$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
array(':' . $this->rootAttribute => $target->{$this->rootAttribute})
);
}
$delta = $key - $left;
$this->owner->updateAll(
array(
$this->leftAttribute => new Expression($db->quoteColumnName($this->leftAttribute) .
sprintf('%+d', $delta)),
$this->rightAttribute => new Expression($db->quoteColumnName($this->rightAttribute) .
sprintf('%+d', $delta)),
$this->levelAttribute => new Expression($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) . '=:' . $this->rootAttribute,
array(':' . $this->rootAttribute => $this->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();
if ($this->hasManyRoots) {
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute;
$params[':' . $this->rootAttribute] = $this->owner->{$this->rootAttribute};
}
$this->owner->updateAll(array(
$this->levelAttribute => new Expression($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();
if ($this->hasManyRoots) {
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' .
$this->rootAttribute;
$params[':' . $this->rootAttribute] = $this->owner->{$this->rootAttribute};
}
$this->owner->updateAll(array(
$attribute => new Expression($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 NestedSet behavior::delete()} and {@link NestedSet behavior::deleteNode()}.
*/
private function correctCachedOnDelete()
{
$left = $this->owner->{$this->leftAttribute};
$right = $this->owner->{$this->rightAttribute};
$key = $right + 1;
$delta = $left - $right - 1;
foreach (self::$_cached[get_class($this->owner)] as $node) {
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
continue;
}
if ($this->hasManyRoots && $this->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 NestedSet behavior::addNode()}.
* @param int $key.
*/
private function correctCachedOnAddNode($key)
{
foreach (self::$_cached[get_class($this->owner)] as $node) {
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
continue;
}
if ($this->hasManyRoots && $this->owner->{$this->rootAttribute} !== $node->{$this->rootAttribute}) {
continue;
}
if ($this->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 NestedSet behavior::moveNode()}.
* @param int $key.
* @param int $levelDelta.
*/
private function correctCachedOnMoveNode($key, $levelDelta)
{
$left = $this->owner->{$this->leftAttribute};
$right = $this->owner->{$this->rightAttribute};
$delta = $right - $left + 1;
if ($left >= $key) {
$left += $delta;
$right += $delta;
}
$delta2 = $key - $left;
foreach (self::$_cached[get_class($this->owner)] as $node) {
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
continue;
}
if ($this->hasManyRoots && $this->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 NestedSet behavior::moveNode()}.
* @param int $key.
* @param int $levelDelta.
* @param int $root.
*/
private function correctCachedOnMoveBetweenTrees($key, $levelDelta, $root)
{
$left = $this->owner->{$this->leftAttribute};
$right = $this->owner->{$this->rightAttribute};
$delta = $right - $left + 1;
$delta2 = $key - $left;
$delta3 = $left - $right - 1;
foreach (self::$_cached[get_class($this->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;
}
} elseif ($node->{$this->rootAttribute} === $this->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->owner)][$this->_id]);
}
}