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 Kozhevnikov 34a081e929 Replace init with attach
On `init()` stage there's no owner(it is null).
Owner appears on `attach()`.
2014-01-17 11:05:06 +03:00

1237 lines
32 KiB
PHP

<?php
/**
* @link https://github.com/creocoder/yii2-nested-set-behavior
* @copyright Copyright (c) 2013 Alexander Kochetov
* @license http://opensource.org/licenses/BSD-3-Clause
*/
namespace creocoder\behaviors;
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;
/**
* @var bool
*/
public $hasManyRoots = false;
/**
* @var string
*/
public $rootAttribute = 'root';
/**
* @var string
*/
public $leftAttribute = 'lft';
/**
* @var string
*/
public $rightAttribute = 'rgt';
/**
* @var string
*/
public $levelAttribute = 'level';
/**
* @var bool
*/
private $_ignoreEvent = false;
/**
* @var bool
*/
private $_deleted = false;
/**
* @var int
*/
private $_id;
/**
* @var array
*/
private static $_cached;
/**
* @var int
*/
private static $_c = 0;
/**
* @inheritdoc
*/
public function events()
{
return [
ActiveRecord::EVENT_AFTER_FIND => 'afterFind',
ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert',
ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
];
}
/**
* @inheritdoc
*/
public function attach($owner)
{
parent::attach($owner);
self::$_cached[get_class($this->owner)][$this->_id = self::$_c++] = $this->owner;
}
/**
* Gets descendants for node.
* @param int $depth the depth.
* @return ActiveQuery.
*/
public function descendants($depth = null)
{
$query = $this->owner->find();
$db = $this->owner->getDb();
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '>'
. $this->owner->getAttribute($this->leftAttribute));
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '<'
. $this->owner->getAttribute($this->rightAttribute));
$query->addOrderBy($db->quoteColumnName($this->leftAttribute));
if ($depth !== null) {
$query->andWhere($db->quoteColumnName($this->levelAttribute) . '<='
. ($this->owner->getAttribute($this->levelAttribute) + $depth));
}
if ($this->hasManyRoots) {
$query->andWhere(
$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
[':' . $this->rootAttribute => $this->owner->getAttribute($this->rootAttribute)]
);
}
return $query;
}
/**
* Gets children for node (direct descendants only).
* @return ActiveQuery.
*/
public function children()
{
return $this->descendants(1);
}
/**
* Gets ancestors for node.
* @param int $depth the depth.
* @return ActiveQuery.
*/
public function ancestors($depth = null)
{
$query = $this->owner->find();
$db = $this->owner->getDb();
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '<'
. $this->owner->getAttribute($this->leftAttribute));
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '>'
. $this->owner->getAttribute($this->rightAttribute));
$query->addOrderBy($db->quoteColumnName($this->leftAttribute));
if ($depth !== null) {
$query->andWhere($db->quoteColumnName($this->levelAttribute) . '>='
. ($this->owner->getAttribute($this->levelAttribute) - $depth));
}
if ($this->hasManyRoots) {
$query->andWhere(
$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
[':' . $this->rootAttribute => $this->owner->getAttribute($this->rootAttribute)]
);
}
return $query;
}
/**
* Gets parent of node.
* @return ActiveQuery.
*/
public function parent()
{
$query = $this->owner->find();
$db = $this->owner->getDb();
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '<'
. $this->owner->getAttribute($this->leftAttribute));
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '>'
. $this->owner->getAttribute($this->rightAttribute));
$query->addOrderBy($db->quoteColumnName($this->rightAttribute));
if ($this->hasManyRoots) {
$query->andWhere(
$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
[':' . $this->rootAttribute => $this->owner->getAttribute($this->rootAttribute)]
);
}
return $query;
}
/**
* Gets previous sibling of node.
* @return ActiveQuery.
*/
public function prev()
{
$query = $this->owner->find();
$db = $this->owner->getDb();
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '='
. ($this->owner->getAttribute($this->leftAttribute) - 1));
if ($this->hasManyRoots) {
$query->andWhere(
$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
[':' . $this->rootAttribute => $this->owner->getAttribute($this->rootAttribute)]
);
}
return $query;
}
/**
* Gets next sibling of node.
* @return ActiveQuery.
*/
public function next()
{
$query = $this->owner->find();
$db = $this->owner->getDb();
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '='
. ($this->owner->getAttribute($this->rightAttribute) + 1));
if ($this->hasManyRoots) {
$query->andWhere(
$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
[':' . $this->rootAttribute => $this->owner->getAttribute($this->rootAttribute)]
);
}
return $query;
}
/**
* 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(false, $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('The node can\'t be deleted because it is new.');
}
if ($this->getIsDeletedRecord()) {
throw new Exception('The node can\'t 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->getAttribute($this->leftAttribute) . ' AND '
. $db->quoteColumnName($this->rightAttribute) . '<='
. $this->owner->getAttribute($this->rightAttribute);
$params = [];
if ($this->hasManyRoots) {
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute;
$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute);
}
$result = $this->owner->deleteAll($condition, $params) > 0;
}
if (!$result) {
if (isset($transaction)) {
$transaction->rollback();
}
return false;
}
$this->shiftLeftRight(
$this->owner->getAttribute($this->rightAttribute) + 1,
$this->owner->getAttribute($this->leftAttribute) - $this->owner->getAttribute($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->getAttribute($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->getAttribute($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->getAttribute($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->getAttribute($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->getAttribute($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->getAttribute($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->getAttribute($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->getAttribute($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('Many roots mode is off.');
}
if ($this->owner->getIsNewRecord()) {
throw new Exception('The node should not be new record.');
}
if ($this->getIsDeletedRecord()) {
throw new Exception('The node should not be deleted.');
}
if ($this->owner->isRoot()) {
throw new Exception('The node already is root node.');
}
$db = $this->owner->getDb();
if ($db->getTransaction() === null) {
$transaction = $db->beginTransaction();
}
try {
$left = $this->owner->getAttribute($this->leftAttribute);
$right = $this->owner->getAttribute($this->rightAttribute);
$levelDelta = 1 - $this->owner->getAttribute($this->levelAttribute);
$delta = 1 - $left;
$this->owner->updateAll(
[
$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,
[':' . $this->rootAttribute => $this->owner->getAttribute($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->getAttribute($this->leftAttribute) > $subj->getAttribute($this->leftAttribute))
&& ($this->owner->getAttribute($this->rightAttribute) < $subj->getAttribute($this->rightAttribute));
if ($this->hasManyRoots) {
$result = $result && ($this->owner->getAttribute($this->rootAttribute)
=== $subj->getAttribute($this->rootAttribute));
}
return $result;
}
/**
* Determines if node is leaf.
* @return boolean whether the node is leaf.
*/
public function isLeaf()
{
return $this->owner->getAttribute($this->rightAttribute)
- $this->owner->getAttribute($this->leftAttribute) === 1;
}
/**
* Determines if node is root.
* @return boolean whether the node is root.
*/
public function isRoot()
{
return $this->owner->getAttribute($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 '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('You should not use ActiveRecord::save() or ActiveRecord::insert() 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('You should not use ActiveRecord::save() or ActiveRecord::update() 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('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 ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
$condition = $db->quoteColumnName($attribute) . '>=' . $key;
$params = [];
if ($this->hasManyRoots) {
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute;
$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute);
}
$this->owner->updateAll(
[$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('The node can\'t be inserted because it is not new.');
}
if ($this->getIsDeletedRecord()) {
throw new Exception('The node can\'t be inserted because it is deleted.');
}
if ($target->getIsDeletedRecord()) {
throw new Exception('The node can\'t be inserted because target node is deleted.');
}
if ($this->owner->equals($target)) {
throw new Exception('The target node should not be self.');
}
if (!$levelUp && $target->isRoot()) {
throw new Exception('The target node should not be root.');
}
if ($runValidation && !$this->owner->validate()) {
return false;
}
if ($this->hasManyRoots) {
$this->owner->setAttribute($this->rootAttribute, $target->getAttribute($this->rootAttribute));
}
$db = $this->owner->getDb();
if ($db->getTransaction() === null) {
$transaction = $db->beginTransaction();
}
try {
$this->shiftLeftRight($key, 2);
$this->owner->setAttribute($this->leftAttribute, $key);
$this->owner->setAttribute($this->rightAttribute, $key + 1);
$this->owner->setAttribute($this->levelAttribute, $target->getAttribute($this->levelAttribute) + $levelUp);
$this->_ignoreEvent = true;
$result = $this->owner->insert(false, $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->setAttribute($this->leftAttribute, 1);
$this->owner->setAttribute($this->rightAttribute, 2);
$this->owner->setAttribute($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(false, $attributes);
$this->_ignoreEvent = false;
if (!$result) {
if (isset($transaction)) {
$transaction->rollback();
}
return false;
}
$this->owner->setAttribute($this->rootAttribute, $this->owner->getPrimaryKey());
$primaryKey = $this->owner->primaryKey();
if (!isset($primaryKey[0])) {
throw new Exception(get_class($this->owner) . ' must have a primary key.');
}
$this->owner->updateAll(
[$this->rootAttribute => $this->owner->getAttribute($this->rootAttribute)],
[$primaryKey[0] => $this->owner->getAttribute($this->rootAttribute)]
);
if (isset($transaction)) {
$transaction->commit();
}
} catch (\Exception $e) {
if (isset($transaction)) {
$transaction->rollback();
}
throw $e;
}
} else {
if ($this->owner->find()->roots()->exists()) {
throw new Exception('Can\'t create more than one root in single root mode.');
}
$this->_ignoreEvent = true;
$result = $this->owner->insert(false, $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('The node should not be new record.');
}
if ($this->getIsDeletedRecord()) {
throw new Exception('The node should not be deleted.');
}
if ($target->getIsDeletedRecord()) {
throw new Exception('The target node should not be deleted.');
}
if ($this->owner->equals($target)) {
throw new Exception('The target node should not be self.');
}
if ($target->isDescendantOf($this->owner)) {
throw new Exception('The target node should not be descendant.');
}
if (!$levelUp && $target->isRoot()) {
throw new Exception('The target node should not be root.');
}
$db = $this->owner->getDb();
if ($db->getTransaction() === null) {
$transaction = $db->beginTransaction();
}
try {
$left = $this->owner->getAttribute($this->leftAttribute);
$right = $this->owner->getAttribute($this->rightAttribute);
$levelDelta = $target->getAttribute($this->levelAttribute) - $this->owner->getAttribute($this->levelAttribute)
+ $levelUp;
if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute) !==
$target->getAttribute($this->rootAttribute)) {
foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
$this->owner->updateAll(
[$attribute => new Expression($db->quoteColumnName($attribute)
. sprintf('%+d', $right - $left + 1))],
$db->quoteColumnName($attribute) . '>=' . $key . ' AND '
. $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
[':' . $this->rootAttribute => $target->getAttribute($this->rootAttribute)]
);
}
$delta = $key - $left;
$this->owner->updateAll(
[
$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->getAttribute($this->rootAttribute),
],
$db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND '
. $db->quoteColumnName($this->rightAttribute) . '<=' . $right . ' AND '
. $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
[':' . $this->rootAttribute => $this->owner->getAttribute($this->rootAttribute)]
);
$this->shiftLeftRight($right + 1, $left - $right - 1);
if (isset($transaction)) {
$transaction->commit();
}
$this->correctCachedOnMoveBetweenTrees($key, $levelDelta, $target->getAttribute($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 = [];
if ($this->hasManyRoots) {
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute;
$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute);
}
$this->owner->updateAll(
[
$this->levelAttribute => new Expression($db->quoteColumnName($this->levelAttribute)
. sprintf('%+d', $levelDelta)),
],
$condition,
$params
);
foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
$condition = $db->quoteColumnName($attribute) . '>=' . $left . ' AND '
. $db->quoteColumnName($attribute) . '<=' . $right;
$params = [];
if ($this->hasManyRoots) {
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:'
. $this->rootAttribute;
$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute);
}
$this->owner->updateAll(
[$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 [[delete()]] and [[deleteNode()]].
*/
private function correctCachedOnDelete()
{
$left = $this->owner->getAttribute($this->leftAttribute);
$right = $this->owner->getAttribute($this->rightAttribute);
$key = $right + 1;
$delta = $left - $right - 1;
foreach (self::$_cached[get_class($this->owner)] as $node) {
/** @var $node ActiveRecord */
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
continue;
}
if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute)
!== $node->getAttribute($this->rootAttribute)) {
continue;
}
if ($node->getAttribute($this->leftAttribute) >= $left
&& $node->getAttribute($this->rightAttribute) <= $right) {
$node->setIsDeletedRecord(true);
} else {
if ($node->getAttribute($this->leftAttribute) >= $key) {
$node->setAttribute(
$this->leftAttribute,
$node->getAttribute($this->leftAttribute) + $delta
);
}
if ($node->getAttribute($this->rightAttribute) >= $key) {
$node->setAttribute(
$this->rightAttribute,
$node->getAttribute($this->rightAttribute) + $delta
);
}
}
}
}
/**
* Correct cache for [[addNode()]].
* @param int $key.
*/
private function correctCachedOnAddNode($key)
{
foreach (self::$_cached[get_class($this->owner)] as $node) {
/** @var $node ActiveRecord */
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
continue;
}
if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute)
!== $node->getAttribute($this->rootAttribute)) {
continue;
}
if ($this->owner === $node) {
continue;
}
if ($node->getAttribute($this->leftAttribute) >= $key) {
$node->setAttribute(
$this->leftAttribute,
$node->getAttribute($this->leftAttribute) + 2
);
}
if ($node->getAttribute($this->rightAttribute) >= $key) {
$node->setAttribute(
$this->rightAttribute,
$node->getAttribute($this->rightAttribute) + 2
);
}
}
}
/**
* Correct cache for [[moveNode()]].
* @param int $key.
* @param int $levelDelta.
*/
private function correctCachedOnMoveNode($key, $levelDelta)
{
$left = $this->owner->getAttribute($this->leftAttribute);
$right = $this->owner->getAttribute($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) {
/** @var $node ActiveRecord */
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
continue;
}
if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute)
!== $node->getAttribute($this->rootAttribute)) {
continue;
}
if ($node->getAttribute($this->leftAttribute) >= $key) {
$node->setAttribute(
$this->leftAttribute,
$node->getAttribute($this->leftAttribute) + $delta
);
}
if ($node->getAttribute($this->rightAttribute) >= $key) {
$node->setAttribute(
$this->rightAttribute,
$node->getAttribute($this->rightAttribute) + $delta
);
}
if ($node->getAttribute($this->leftAttribute) >= $left
&& $node->getAttribute($this->rightAttribute) <= $right) {
$node->setAttribute(
$this->levelAttribute,
$node->getAttribute($this->levelAttribute) + $levelDelta
);
}
if ($node->getAttribute($this->leftAttribute) >= $left
&& $node->getAttribute($this->leftAttribute) <= $right) {
$node->setAttribute(
$this->leftAttribute,
$node->getAttribute($this->leftAttribute) + $delta2
);
}
if ($node->getAttribute($this->rightAttribute) >= $left
&& $node->getAttribute($this->rightAttribute) <= $right) {
$node->setAttribute(
$this->rightAttribute,
$node->getAttribute($this->rightAttribute) + $delta2
);
}
if ($node->getAttribute($this->leftAttribute) >= $right + 1) {
$node->setAttribute(
$this->leftAttribute,
$node->getAttribute($this->leftAttribute) - $delta
);
}
if ($node->getAttribute($this->rightAttribute) >= $right + 1) {
$node->setAttribute(
$this->rightAttribute,
$node->getAttribute($this->rightAttribute) - $delta
);
}
}
}
/**
* Correct cache for [[moveNode()]].
* @param int $key.
* @param int $levelDelta.
* @param int $root.
*/
private function correctCachedOnMoveBetweenTrees($key, $levelDelta, $root)
{
$left = $this->owner->getAttribute($this->leftAttribute);
$right = $this->owner->getAttribute($this->rightAttribute);
$delta = $right - $left + 1;
$delta2 = $key - $left;
$delta3 = $left - $right - 1;
foreach (self::$_cached[get_class($this->owner)] as $node) {
/** @var $node ActiveRecord */
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
continue;
}
if ($node->getAttribute($this->rootAttribute) === $root) {
if ($node->getAttribute($this->leftAttribute) >= $key) {
$node->setAttribute(
$this->leftAttribute,
$node->getAttribute($this->leftAttribute) + $delta
);
}
if ($node->getAttribute($this->rightAttribute) >= $key) {
$node->setAttribute(
$this->rightAttribute,
$node->getAttribute($this->rightAttribute) + $delta
);
}
} elseif ($node->getAttribute($this->rootAttribute)
=== $this->owner->getAttribute($this->rootAttribute)) {
if ($node->getAttribute($this->leftAttribute) >= $left
&& $node->getAttribute($this->rightAttribute) <= $right) {
$node->setAttribute(
$this->leftAttribute,
$node->getAttribute($this->leftAttribute) + $delta2
);
$node->setAttribute(
$this->rightAttribute,
$node->getAttribute($this->rightAttribute) + $delta2
);
$node->setAttribute(
$this->levelAttribute,
$node->getAttribute($this->levelAttribute) + $levelDelta
);
$node->setAttribute($this->rootAttribute, $root);
} else {
if ($node->getAttribute($this->leftAttribute) >= $right + 1) {
$node->setAttribute(
$this->leftAttribute,
$node->getAttribute($this->leftAttribute) + $delta3
);
}
if ($node->getAttribute($this->rightAttribute) >= $right + 1) {
$node->setAttribute(
$this->rightAttribute,
$node->getAttribute($this->rightAttribute) + $delta3
);
}
}
}
}
}
/**
* Destructor.
*/
public function __destruct()
{
unset(self::$_cached[get_class($this->owner)][$this->_id]);
}
}