|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* @link https://github.com/paulzi/yii2-nested-sets
|
|
|
|
* @copyright Copyright (c) 2015 PaulZi <pavel.zimakoff@gmail.com>
|
|
|
|
* @license MIT (https://github.com/paulzi/yii2-nested-sets/blob/master/LICENSE)
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace paulzi\nestedsets;
|
|
|
|
|
|
|
|
use Yii;
|
|
|
|
use yii\base\Behavior;
|
|
|
|
use yii\base\Exception;
|
|
|
|
use yii\base\NotSupportedException;
|
|
|
|
use yii\db\ActiveRecord;
|
|
|
|
use yii\db\Expression;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Nested Sets Behavior for Yii2
|
|
|
|
* @author PaulZi <pavel.zimakoff@gmail.com>
|
|
|
|
* @author Alexander Kochetov <https://github.com/creocoder>
|
|
|
|
*
|
|
|
|
* @property ActiveRecord $owner
|
|
|
|
*/
|
|
|
|
class NestedSetsBehavior extends Behavior
|
|
|
|
{
|
|
|
|
const OPERATION_MAKE_ROOT = 1;
|
|
|
|
const OPERATION_PREPEND_TO = 2;
|
|
|
|
const OPERATION_APPEND_TO = 3;
|
|
|
|
const OPERATION_INSERT_BEFORE = 4;
|
|
|
|
const OPERATION_INSERT_AFTER = 5;
|
|
|
|
const OPERATION_DELETE_ALL = 6;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string|null
|
|
|
|
*/
|
|
|
|
public $treeAttribute;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $leftAttribute = 'lft';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $rightAttribute = 'rgt';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $depthAttribute = 'depth';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string|null
|
|
|
|
*/
|
|
|
|
protected $operation;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var ActiveRecord|self|null
|
|
|
|
*/
|
|
|
|
protected $node;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $treeChange;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
public function events()
|
|
|
|
{
|
|
|
|
return [
|
|
|
|
ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert',
|
|
|
|
ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert',
|
|
|
|
ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
|
|
|
|
ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate',
|
|
|
|
ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
|
|
|
|
ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int|null $depth
|
|
|
|
* @return \yii\db\ActiveQuery
|
|
|
|
*/
|
|
|
|
public function getParents($depth = null)
|
|
|
|
{
|
|
|
|
$tableName = $this->owner->tableName();
|
|
|
|
$condition = [
|
|
|
|
'and',
|
|
|
|
['<', "{$tableName}.[[{$this->leftAttribute}]]", $this->owner->getAttribute($this->leftAttribute)],
|
|
|
|
['>', "{$tableName}.[[{$this->rightAttribute}]]", $this->owner->getAttribute($this->rightAttribute)],
|
|
|
|
];
|
|
|
|
if ($depth !== null) {
|
|
|
|
$condition[] = ['>=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->getAttribute($this->depthAttribute) - $depth];
|
|
|
|
}
|
|
|
|
|
|
|
|
$query = $this->owner->find()
|
|
|
|
->andWhere($condition)
|
|
|
|
->andWhere($this->treeCondition())
|
|
|
|
->addOrderBy(["{$tableName}.[[{$this->leftAttribute}]]" => SORT_ASC]);
|
|
|
|
$query->multiple = true;
|
|
|
|
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return \yii\db\ActiveQuery
|
|
|
|
*/
|
|
|
|
public function getParent()
|
|
|
|
{
|
|
|
|
$tableName = $this->owner->tableName();
|
|
|
|
$query = $this->getParents(1)
|
|
|
|
->orderBy(["{$tableName}.[[{$this->leftAttribute}]]" => SORT_DESC])
|
|
|
|
->limit(1);
|
|
|
|
$query->multiple = false;
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return \yii\db\ActiveQuery
|
|
|
|
*/
|
|
|
|
public function getRoot()
|
|
|
|
{
|
|
|
|
$tableName = $this->owner->tableName();
|
|
|
|
$query = $this->owner->find()
|
|
|
|
->andWhere(["{$tableName}.[[{$this->leftAttribute}]]" => 1])
|
|
|
|
->andWhere($this->treeCondition())
|
|
|
|
->limit(1);
|
|
|
|
$query->multiple = false;
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int|null $depth
|
|
|
|
* @param bool $andSelf
|
|
|
|
* @param bool $backOrder
|
|
|
|
* @return \yii\db\ActiveQuery
|
|
|
|
*/
|
|
|
|
public function getDescendants($depth = null, $andSelf = false, $backOrder = false)
|
|
|
|
{
|
|
|
|
$tableName = $this->owner->tableName();
|
|
|
|
$attribute = $backOrder ? $this->rightAttribute : $this->leftAttribute;
|
|
|
|
$condition = [
|
|
|
|
'and',
|
|
|
|
[$andSelf ? '>=' : '>', "{$tableName}.[[{$attribute}]]", $this->owner->getAttribute($this->leftAttribute)],
|
|
|
|
[$andSelf ? '<=' : '<', "{$tableName}.[[{$attribute}]]", $this->owner->getAttribute($this->rightAttribute)],
|
|
|
|
];
|
|
|
|
|
|
|
|
if ($depth !== null) {
|
|
|
|
$condition[] = ['<=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->getAttribute($this->depthAttribute) + $depth];
|
|
|
|
}
|
|
|
|
|
|
|
|
$query = $this->owner->find()
|
|
|
|
->andWhere($condition)
|
|
|
|
->andWhere($this->treeCondition())
|
|
|
|
->addOrderBy(["{$tableName}.[[{$attribute}]]" => $backOrder ? SORT_DESC : SORT_ASC]);
|
|
|
|
$query->multiple = true;
|
|
|
|
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return \yii\db\ActiveQuery
|
|
|
|
*/
|
|
|
|
public function getChildren()
|
|
|
|
{
|
|
|
|
return $this->getDescendants(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int|null $depth
|
|
|
|
* @return \yii\db\ActiveQuery
|
|
|
|
*/
|
|
|
|
public function getLeaves($depth = null)
|
|
|
|
{
|
|
|
|
$tableName = $this->owner->tableName();
|
|
|
|
$query = $this->getDescendants($depth)
|
|
|
|
->andWhere(["{$tableName}.[[{$this->leftAttribute}]]" => new Expression("{$tableName}.[[{$this->rightAttribute}]] - 1")]);
|
|
|
|
$query->multiple = true;
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return \yii\db\ActiveQuery
|
|
|
|
*/
|
|
|
|
public function getPrev()
|
|
|
|
{
|
|
|
|
$tableName = $this->owner->tableName();
|
|
|
|
$query = $this->owner->find()
|
|
|
|
->andWhere(["{$tableName}.[[{$this->rightAttribute}]]" => $this->owner->getAttribute($this->leftAttribute) - 1])
|
|
|
|
->andWhere($this->treeCondition())
|
|
|
|
->limit(1);
|
|
|
|
$query->multiple = false;
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return \yii\db\ActiveQuery
|
|
|
|
*/
|
|
|
|
public function getNext()
|
|
|
|
{
|
|
|
|
$tableName = $this->owner->tableName();
|
|
|
|
$query = $this->owner->find()
|
|
|
|
->andWhere(["{$tableName}.[[{$this->leftAttribute}]]" => $this->owner->getAttribute($this->rightAttribute) + 1])
|
|
|
|
->andWhere($this->treeCondition())
|
|
|
|
->limit(1);
|
|
|
|
$query->multiple = false;
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Populate children relations for self and all descendants
|
|
|
|
* @param int $depth = null
|
|
|
|
* @return static
|
|
|
|
*/
|
|
|
|
public function populateTree($depth = null)
|
|
|
|
{
|
|
|
|
/** @var ActiveRecord[]|static[] $nodes */
|
|
|
|
if ($depth === null) {
|
|
|
|
$nodes = $this->owner->descendants;
|
|
|
|
} else {
|
|
|
|
$nodes = $this->getDescendants($depth)->all();
|
|
|
|
}
|
|
|
|
|
|
|
|
$key = $this->owner->getAttribute($this->leftAttribute);
|
|
|
|
$relates = [];
|
|
|
|
$relates[$key] = [];
|
|
|
|
$parents = [$key];
|
|
|
|
$prev = $this->owner->getAttribute($this->depthAttribute);
|
|
|
|
foreach($nodes as $node)
|
|
|
|
{
|
|
|
|
$depth = $node->getAttribute($this->depthAttribute);
|
|
|
|
if ($depth <= $prev) {
|
|
|
|
$parents = array_slice($parents, 0, $depth - $prev - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
$key = end($parents);
|
|
|
|
if (!isset($relates[$key])) {
|
|
|
|
$relates[$key] = [];
|
|
|
|
}
|
|
|
|
$relates[$key][] = $node;
|
|
|
|
|
|
|
|
$parents[] = $node->getAttribute($this->leftAttribute);
|
|
|
|
$prev = $depth;
|
|
|
|
}
|
|
|
|
|
|
|
|
$nodes[] = $this->owner;
|
|
|
|
foreach ($nodes as $node) {
|
|
|
|
$key = $node->getAttribute($this->leftAttribute);
|
|
|
|
if (isset($relates[$key])) {
|
|
|
|
$node->populateRelation('children', $relates[$key]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->owner;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function isRoot()
|
|
|
|
{
|
|
|
|
return $this->owner->getAttribute($this->leftAttribute) === 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ActiveRecord $node
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function isChildOf($node)
|
|
|
|
{
|
|
|
|
$result = $this->owner->getAttribute($this->leftAttribute) > $node->getAttribute($this->leftAttribute)
|
|
|
|
&& $this->owner->getAttribute($this->rightAttribute) < $node->getAttribute($this->rightAttribute);
|
|
|
|
|
|
|
|
if ($result && $this->treeAttribute !== null) {
|
|
|
|
$result = $this->owner->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function isLeaf()
|
|
|
|
{
|
|
|
|
return $this->owner->getAttribute($this->rightAttribute) - $this->owner->getAttribute($this->leftAttribute) === 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return ActiveRecord
|
|
|
|
*/
|
|
|
|
public function makeRoot()
|
|
|
|
{
|
|
|
|
$this->operation = self::OPERATION_MAKE_ROOT;
|
|
|
|
return $this->owner;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ActiveRecord $node
|
|
|
|
* @return ActiveRecord
|
|
|
|
*/
|
|
|
|
public function prependTo($node)
|
|
|
|
{
|
|
|
|
$this->operation = self::OPERATION_PREPEND_TO;
|
|
|
|
$this->node = $node;
|
|
|
|
return $this->owner;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ActiveRecord $node
|
|
|
|
* @return ActiveRecord
|
|
|
|
*/
|
|
|
|
public function appendTo($node)
|
|
|
|
{
|
|
|
|
$this->operation = self::OPERATION_APPEND_TO;
|
|
|
|
$this->node = $node;
|
|
|
|
return $this->owner;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ActiveRecord $node
|
|
|
|
* @return ActiveRecord
|
|
|
|
*/
|
|
|
|
public function insertBefore($node)
|
|
|
|
{
|
|
|
|
$this->operation = self::OPERATION_INSERT_BEFORE;
|
|
|
|
$this->node = $node;
|
|
|
|
return $this->owner;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ActiveRecord $node
|
|
|
|
* @return ActiveRecord
|
|
|
|
*/
|
|
|
|
public function insertAfter($node)
|
|
|
|
{
|
|
|
|
$this->operation = self::OPERATION_INSERT_AFTER;
|
|
|
|
$this->node = $node;
|
|
|
|
return $this->owner;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Need for paulzi/auto-tree
|
|
|
|
*/
|
|
|
|
public function preDeleteWithChildren()
|
|
|
|
{
|
|
|
|
$this->operation = self::OPERATION_DELETE_ALL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return bool|int
|
|
|
|
* @throws \Exception
|
|
|
|
* @throws \yii\db\Exception
|
|
|
|
*/
|
|
|
|
public function deleteWithChildren()
|
|
|
|
{
|
|
|
|
$this->operation = self::OPERATION_DELETE_ALL;
|
|
|
|
if (!$this->owner->isTransactional(ActiveRecord::OP_DELETE)) {
|
|
|
|
$transaction = $this->owner->getDb()->beginTransaction();
|
|
|
|
try {
|
|
|
|
$result = $this->deleteWithChildrenInternal();
|
|
|
|
if ($result === false) {
|
|
|
|
$transaction->rollBack();
|
|
|
|
} else {
|
|
|
|
$transaction->commit();
|
|
|
|
}
|
|
|
|
return $result;
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
$transaction->rollBack();
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$result = $this->deleteWithChildrenInternal();
|
|
|
|
}
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @throws Exception
|
|
|
|
* @throws NotSupportedException
|
|
|
|
*/
|
|
|
|
public function beforeInsert()
|
|
|
|
{
|
|
|
|
if ($this->node !== null && !$this->node->getIsNewRecord()) {
|
|
|
|
$this->node->refresh();
|
|
|
|
}
|
|
|
|
switch ($this->operation) {
|
|
|
|
case self::OPERATION_MAKE_ROOT:
|
|
|
|
$condition = array_merge([$this->leftAttribute => 1], $this->treeCondition());
|
|
|
|
if ($this->owner->findOne($condition) !== null) {
|
|
|
|
throw new Exception('Can not create more than one root.');
|
|
|
|
}
|
|
|
|
$this->owner->setAttribute($this->leftAttribute, 1);
|
|
|
|
$this->owner->setAttribute($this->rightAttribute, 2);
|
|
|
|
$this->owner->setAttribute($this->depthAttribute, 0);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case self::OPERATION_PREPEND_TO:
|
|
|
|
$this->insertNode($this->node->getAttribute($this->leftAttribute) + 1, 1);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case self::OPERATION_APPEND_TO:
|
|
|
|
$this->insertNode($this->node->getAttribute($this->rightAttribute), 1);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case self::OPERATION_INSERT_BEFORE:
|
|
|
|
$this->insertNode($this->node->getAttribute($this->leftAttribute), 0);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case self::OPERATION_INSERT_AFTER:
|
|
|
|
$this->insertNode($this->node->getAttribute($this->rightAttribute) + 1, 0);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
throw new NotSupportedException('Method "'. $this->owner->className() . '::insert" is not supported for inserting new nodes.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
public function afterInsert()
|
|
|
|
{
|
|
|
|
if ($this->operation === self::OPERATION_MAKE_ROOT && $this->treeAttribute !== null && $this->owner->getAttribute($this->treeAttribute) === null) {
|
|
|
|
$id = $this->owner->getPrimaryKey();
|
|
|
|
$this->owner->setAttribute($this->treeAttribute, $id);
|
|
|
|
|
|
|
|
$primaryKey = $this->owner->primaryKey();
|
|
|
|
if (!isset($primaryKey[0])) {
|
|
|
|
throw new Exception('"' . $this->owner->className() . '" must have a primary key.');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->owner->updateAll([$this->treeAttribute => $id], [$primaryKey[0] => $id]);
|
|
|
|
}
|
|
|
|
$this->operation = null;
|
|
|
|
$this->node = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
public function beforeUpdate()
|
|
|
|
{
|
|
|
|
if ($this->node !== null && !$this->node->getIsNewRecord()) {
|
|
|
|
$this->node->refresh();
|
|
|
|
}
|
|
|
|
|
|
|
|
switch ($this->operation) {
|
|
|
|
case self::OPERATION_MAKE_ROOT:
|
|
|
|
if ($this->treeAttribute === null) {
|
|
|
|
throw new Exception('Can not move a node as the root when "treeAttribute" is not set.');
|
|
|
|
}
|
|
|
|
if ($this->owner->getOldAttribute($this->treeAttribute) !== $this->owner->getAttribute($this->treeAttribute)) {
|
|
|
|
$this->treeChange = $this->owner->getAttribute($this->treeAttribute);
|
|
|
|
$this->owner->setAttribute($this->treeAttribute, $this->owner->getOldAttribute($this->treeAttribute));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case self::OPERATION_INSERT_BEFORE:
|
|
|
|
case self::OPERATION_INSERT_AFTER:
|
|
|
|
if ($this->node->isRoot()) {
|
|
|
|
throw new Exception('Can not move a node before/after root.');
|
|
|
|
}
|
|
|
|
|
|
|
|
case self::OPERATION_PREPEND_TO:
|
|
|
|
case self::OPERATION_APPEND_TO:
|
|
|
|
if ($this->node->getIsNewRecord()) {
|
|
|
|
throw new Exception('Can not move a node when the target node is new record.');
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->owner->equals($this->node)) {
|
|
|
|
throw new Exception('Can not move a node when the target node is same.');
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->node->isChildOf($this->owner)) {
|
|
|
|
throw new Exception('Can not move a node when the target node is child.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function afterUpdate()
|
|
|
|
{
|
|
|
|
switch ($this->operation) {
|
|
|
|
case self::OPERATION_MAKE_ROOT:
|
|
|
|
$this->moveNodeAsRoot();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case self::OPERATION_PREPEND_TO:
|
|
|
|
$this->moveNode($this->node->getAttribute($this->leftAttribute) + 1, 1);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case self::OPERATION_APPEND_TO:
|
|
|
|
$this->moveNode($this->node->getAttribute($this->rightAttribute), 1);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case self::OPERATION_INSERT_BEFORE:
|
|
|
|
$this->moveNode($this->node->getAttribute($this->leftAttribute), 0);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case self::OPERATION_INSERT_AFTER:
|
|
|
|
$this->moveNode($this->node->getAttribute($this->rightAttribute) + 1, 0);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
$this->operation = null;
|
|
|
|
$this->node = null;
|
|
|
|
$this->treeChange = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
public function beforeDelete()
|
|
|
|
{
|
|
|
|
if ($this->owner->getIsNewRecord()) {
|
|
|
|
throw new Exception('Can not delete a node when it is new record.');
|
|
|
|
}
|
|
|
|
if ($this->isRoot() && $this->operation !== self::OPERATION_DELETE_ALL) {
|
|
|
|
throw new Exception('Method "'. $this->owner->className() . '::delete" is not supported for deleting root nodes.');
|
|
|
|
}
|
|
|
|
$this->owner->refresh();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function afterDelete()
|
|
|
|
{
|
|
|
|
$left = $this->owner->getAttribute($this->leftAttribute);
|
|
|
|
$right = $this->owner->getAttribute($this->rightAttribute);
|
|
|
|
if ($this->operation === static::OPERATION_DELETE_ALL || $this->isLeaf()) {
|
|
|
|
$this->shift($right + 1, null, $left - $right - 1);
|
|
|
|
} else {
|
|
|
|
$this->owner->updateAll(
|
|
|
|
[
|
|
|
|
$this->leftAttribute => new Expression("[[{$this->leftAttribute}]] - 1"),
|
|
|
|
$this->rightAttribute => new Expression("[[{$this->rightAttribute}]] - 1"),
|
|
|
|
$this->depthAttribute => new Expression("[[{$this->depthAttribute}]] - 1"),
|
|
|
|
],
|
|
|
|
$this->getDescendants()->where
|
|
|
|
);
|
|
|
|
$this->shift($right + 1, null, -2);
|
|
|
|
}
|
|
|
|
$this->operation = null;
|
|
|
|
$this->node = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
protected function deleteWithChildrenInternal()
|
|
|
|
{
|
|
|
|
if (!$this->owner->beforeDelete()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
$result = $this->owner->deleteAll($this->getDescendants(null, true)->where);
|
|
|
|
$this->owner->setOldAttributes(null);
|
|
|
|
$this->owner->afterDelete();
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int $to
|
|
|
|
* @param int $depth
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
protected function insertNode($to, $depth = 0)
|
|
|
|
{
|
|
|
|
if ($this->node->getIsNewRecord()) {
|
|
|
|
throw new Exception('Can not create a node when the target node is new record.');
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($depth === 0 && $this->node->isRoot()) {
|
|
|
|
throw new Exception('Can not insert a node before/after root.');
|
|
|
|
}
|
|
|
|
$this->owner->setAttribute($this->leftAttribute, $to);
|
|
|
|
$this->owner->setAttribute($this->rightAttribute, $to + 1);
|
|
|
|
$this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute) + $depth);
|
|
|
|
if ($this->treeAttribute !== null) {
|
|
|
|
$this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute));
|
|
|
|
}
|
|
|
|
$this->shift($to, null, 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int $to
|
|
|
|
* @param int $depth
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
protected function moveNode($to, $depth = 0)
|
|
|
|
{
|
|
|
|
$left = $this->owner->getAttribute($this->leftAttribute);
|
|
|
|
$right = $this->owner->getAttribute($this->rightAttribute);
|
|
|
|
$depth = $this->owner->getAttribute($this->depthAttribute) - $this->node->getAttribute($this->depthAttribute) - $depth;
|
|
|
|
if ($this->treeAttribute === null || $this->owner->getAttribute($this->treeAttribute) === $this->node->getAttribute($this->treeAttribute)) {
|
|
|
|
// same root
|
|
|
|
$this->owner->updateAll(
|
|
|
|
[$this->depthAttribute => new Expression("-[[{$this->depthAttribute}]]" . sprintf('%+d', $depth))],
|
|
|
|
$this->getDescendants(null, true)->where
|
|
|
|
);
|
|
|
|
$delta = $right - $left + 1;
|
|
|
|
if ($left >= $to) {
|
|
|
|
$this->shift($to, $left - 1, $delta);
|
|
|
|
$delta = $to - $left;
|
|
|
|
} else {
|
|
|
|
$this->shift($right + 1, $to - 1, -$delta);
|
|
|
|
$delta = $to - $right - 1;
|
|
|
|
}
|
|
|
|
$this->owner->updateAll(
|
|
|
|
[
|
|
|
|
$this->leftAttribute => new Expression("[[{$this->leftAttribute}]]" . sprintf('%+d', $delta)),
|
|
|
|
$this->rightAttribute => new Expression("[[{$this->rightAttribute}]]" . sprintf('%+d', $delta)),
|
|
|
|
$this->depthAttribute => new Expression("-[[{$this->depthAttribute}]]"),
|
|
|
|
],
|
|
|
|
[
|
|
|
|
'and',
|
|
|
|
$this->getDescendants(null, true)->where,
|
|
|
|
['<', $this->depthAttribute, 0],
|
|
|
|
]
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// move from other root
|
|
|
|
$tree = $this->node->getAttribute($this->treeAttribute);
|
|
|
|
$this->shift($to, null, $right - $left + 1, $tree);
|
|
|
|
$delta = $to - $left;
|
|
|
|
$this->owner->updateAll(
|
|
|
|
[
|
|
|
|
$this->leftAttribute => new Expression("[[{$this->leftAttribute}]]" . sprintf('%+d', $delta)),
|
|
|
|
$this->rightAttribute => new Expression("[[{$this->rightAttribute}]]" . sprintf('%+d', $delta)),
|
|
|
|
$this->depthAttribute => new Expression("[[{$this->depthAttribute}]]" . sprintf('%+d', -$depth)),
|
|
|
|
$this->treeAttribute => $tree,
|
|
|
|
],
|
|
|
|
$this->getDescendants(null, true)->where
|
|
|
|
);
|
|
|
|
$this->shift($right + 1, null, $left - $right - 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected function moveNodeAsRoot()
|
|
|
|
{
|
|
|
|
$left = $this->owner->getAttribute($this->leftAttribute);
|
|
|
|
$right = $this->owner->getAttribute($this->rightAttribute);
|
|
|
|
$depth = $this->owner->getAttribute($this->depthAttribute);
|
|
|
|
$tree = $this->treeChange ? $this->treeChange : $this->owner->getPrimaryKey();
|
|
|
|
|
|
|
|
$this->owner->updateAll(
|
|
|
|
[
|
|
|
|
$this->leftAttribute => new Expression("[[{$this->leftAttribute}]]" . sprintf('%+d', 1 - $left)),
|
|
|
|
$this->rightAttribute => new Expression("[[{$this->rightAttribute}]]" . sprintf('%+d', 1 - $left)),
|
|
|
|
$this->depthAttribute => new Expression("[[{$this->depthAttribute}]]" . sprintf('%+d', -$depth)),
|
|
|
|
$this->treeAttribute => $tree,
|
|
|
|
],
|
|
|
|
$this->getDescendants(null, true)->where
|
|
|
|
);
|
|
|
|
$this->shift($right + 1, null, $left - $right - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int $from
|
|
|
|
* @param int $to
|
|
|
|
* @param int $delta
|
|
|
|
* @param int|null $tree
|
|
|
|
*/
|
|
|
|
protected function shift($from, $to, $delta, $tree = null)
|
|
|
|
{
|
|
|
|
if ($delta !== 0 && ($to === null || $to >= $from)) {
|
|
|
|
if ($this->treeAttribute !== null && $tree === null) {
|
|
|
|
$tree = $this->owner->getAttribute($this->treeAttribute);
|
|
|
|
}
|
|
|
|
foreach ([$this->leftAttribute, $this->rightAttribute] as $i => $attribute) {
|
|
|
|
$this->owner->updateAll(
|
|
|
|
[$attribute => new Expression("[[{$attribute}]]" . sprintf('%+d', $delta))],
|
|
|
|
[
|
|
|
|
'and',
|
|
|
|
$to === null ? ['>=', $attribute, $from] : ['between', $attribute, $from, $to],
|
|
|
|
$this->treeAttribute !== null ? [$this->treeAttribute => $tree] : [],
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
protected function treeCondition()
|
|
|
|
{
|
|
|
|
$tableName = $this->owner->tableName();
|
|
|
|
if ($this->treeAttribute === null) {
|
|
|
|
return [];
|
|
|
|
} else {
|
|
|
|
return ["{$tableName}.[[{$this->treeAttribute}]]" => $this->owner->getAttribute($this->treeAttribute)];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|