mirror of
https://github.com/Oreolek/yii2-nested-sets.git
synced 2024-05-16 15:58:17 +03:00
Nested sets behavior complete overhaul for Yii 2 framework release version
This commit is contained in:
parent
603e303001
commit
427deedd5a
32
LICENSE.md
Normal file
32
LICENSE.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
The nested sets behavior for the Yii framework is free software.
|
||||
It is released under the terms of the following BSD License.
|
||||
|
||||
Copyright © 2015 by Alexander Kochetov (https://github.com/creocoder)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Yii Software LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
1238
NestedSet.php
1238
NestedSet.php
File diff suppressed because it is too large
Load diff
690
NestedSetsBehavior.php
Normal file
690
NestedSetsBehavior.php
Normal file
|
@ -0,0 +1,690 @@
|
|||
<?php
|
||||
/**
|
||||
* @link https://github.com/creocoder/yii2-nested-sets-behavior
|
||||
* @copyright Copyright (c) 2015 Alexander Kochetov
|
||||
* @license http://opensource.org/licenses/BSD-3-Clause
|
||||
*/
|
||||
|
||||
namespace creocoder\nestedsets;
|
||||
|
||||
use yii\base\Behavior;
|
||||
use yii\base\NotSupportedException;
|
||||
use yii\db\ActiveRecord;
|
||||
use yii\db\Exception;
|
||||
use yii\db\Expression;
|
||||
|
||||
/**
|
||||
* NestedSetsBehavior
|
||||
*
|
||||
* @property \yii\db\ActiveRecord $owner
|
||||
*
|
||||
* @author Alexander Kochetov <creocoder@gmail.com>
|
||||
*/
|
||||
class NestedSetsBehavior extends Behavior
|
||||
{
|
||||
const OPERATION_MAKE_ROOT = 'makeRoot';
|
||||
const OPERATION_PREPEND_TO = 'prependTo';
|
||||
const OPERATION_APPEND_TO = 'appendTo';
|
||||
const OPERATION_INSERT_BEFORE = 'insertBefore';
|
||||
const OPERATION_INSERT_AFTER = 'insertAfter';
|
||||
const OPERATION_DELETE_WITH_DESCENDANTS = 'deleteWithDescendants';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $leftAttribute = 'lft';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $rightAttribute = 'rgt';
|
||||
/**
|
||||
* @var string|false
|
||||
*/
|
||||
public $treeAttribute = false;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $depthAttribute = 'depth';
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $operation;
|
||||
/**
|
||||
* @var \yii\db\ActiveRecord|null
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
/**
|
||||
* @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',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the root node if the active record is new or moves it
|
||||
* as the root node.
|
||||
* @param boolean $runValidation
|
||||
* @param array $attributes
|
||||
* @return boolean
|
||||
*/
|
||||
public function makeRoot($runValidation = true, $attributes = null)
|
||||
{
|
||||
$this->operation = self::OPERATION_MAKE_ROOT;
|
||||
|
||||
return $this->owner->save($runValidation, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a node as the first child of the target node if the active
|
||||
* record is new or moves it as the first child of the target node.
|
||||
* @param \yii\db\ActiveRecord $node
|
||||
* @param boolean $runValidation
|
||||
* @param array $attributes
|
||||
* @return boolean
|
||||
*/
|
||||
public function prependTo($node, $runValidation = true, $attributes = null)
|
||||
{
|
||||
$this->operation = self::OPERATION_PREPEND_TO;
|
||||
$this->node = $node;
|
||||
|
||||
return $this->owner->save($runValidation, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a node as the last child of the target node if the active
|
||||
* record is new or moves it as the last child of the target node.
|
||||
* @param \yii\db\ActiveRecord $node
|
||||
* @param boolean $runValidation
|
||||
* @param array $attributes
|
||||
* @return boolean
|
||||
*/
|
||||
public function appendTo($node, $runValidation = true, $attributes = null)
|
||||
{
|
||||
$this->operation = self::OPERATION_APPEND_TO;
|
||||
$this->node = $node;
|
||||
|
||||
return $this->owner->save($runValidation, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a node as the previous sibling of the target node if the active
|
||||
* record is new or moves it as the previous sibling of the target node.
|
||||
* @param \yii\db\ActiveRecord $node
|
||||
* @param boolean $runValidation
|
||||
* @param array $attributes
|
||||
* @return boolean
|
||||
*/
|
||||
public function insertBefore($node, $runValidation = true, $attributes = null)
|
||||
{
|
||||
$this->operation = self::OPERATION_INSERT_BEFORE;
|
||||
$this->node = $node;
|
||||
|
||||
return $this->owner->save($runValidation, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a node as the next sibling of the target node if the active
|
||||
* record is new or moves it as the next sibling of the target node.
|
||||
* @param \yii\db\ActiveRecord $node
|
||||
* @param boolean $runValidation
|
||||
* @param array $attributes
|
||||
* @return boolean
|
||||
*/
|
||||
public function insertAfter($node, $runValidation = true, $attributes = null)
|
||||
{
|
||||
$this->operation = self::OPERATION_INSERT_AFTER;
|
||||
$this->node = $node;
|
||||
|
||||
return $this->owner->save($runValidation, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a node and its descendants.
|
||||
* @return integer|false the number of rows deleted or false if
|
||||
* the deletion is unsuccessful for some reason.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function deleteWithDescendants()
|
||||
{
|
||||
$this->operation = self::OPERATION_DELETE_WITH_DESCENDANTS;
|
||||
|
||||
try {
|
||||
if ($this->owner->isTransactional(ActiveRecord::OP_DELETE)) {
|
||||
$transaction = $this->owner->getDb()->beginTransaction();
|
||||
}
|
||||
|
||||
if (!$this->owner->beforeDelete()) {
|
||||
if (isset($transaction)) {
|
||||
$transaction->rollBack();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$condition = [
|
||||
'and',
|
||||
['>=', $this->leftAttribute, $this->owner->getAttribute($this->leftAttribute)],
|
||||
['<=', $this->rightAttribute, $this->owner->getAttribute($this->rightAttribute)]
|
||||
];
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$condition[] = [$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)];
|
||||
}
|
||||
|
||||
$result = $this->owner->deleteAll($condition);
|
||||
$this->owner->setOldAttributes(null);
|
||||
$this->owner->afterDelete();
|
||||
|
||||
if (isset($transaction)) {
|
||||
$transaction->commit();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
if (isset($transaction)) {
|
||||
$transaction->rollBack();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the descendants of the node.
|
||||
* @param integer $depth the depth
|
||||
* @return \yii\db\ActiveQuery
|
||||
*/
|
||||
public function descendants($depth = null)
|
||||
{
|
||||
$query = $this->owner->find();
|
||||
|
||||
$condition = [
|
||||
'and',
|
||||
['>', $this->leftAttribute, $this->owner->getAttribute($this->leftAttribute)],
|
||||
['<', $this->rightAttribute, $this->owner->getAttribute($this->rightAttribute)],
|
||||
];
|
||||
|
||||
if ($depth !== null) {
|
||||
$condition[] = ['<=', $this->depthAttribute, $this->owner->getAttribute($this->depthAttribute) + $depth];
|
||||
}
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$condition[] = [$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)];
|
||||
}
|
||||
|
||||
return $query->andWhere($condition)->addOrderBy([$this->leftAttribute => SORT_ASC]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the children of the node.
|
||||
* @return \yii\db\ActiveQuery
|
||||
*/
|
||||
public function children()
|
||||
{
|
||||
return $this->descendants(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ancestors of the node.
|
||||
* @param integer $depth the depth
|
||||
* @return \yii\db\ActiveQuery
|
||||
*/
|
||||
public function ancestors($depth = null)
|
||||
{
|
||||
$query = $this->owner->find();
|
||||
|
||||
$condition = [
|
||||
'and',
|
||||
['<', $this->leftAttribute, $this->owner->getAttribute($this->leftAttribute)],
|
||||
['>', $this->rightAttribute, $this->owner->getAttribute($this->rightAttribute)],
|
||||
];
|
||||
|
||||
if ($depth !== null) {
|
||||
$condition[] = ['>=', $this->depthAttribute, $this->owner->getAttribute($this->depthAttribute) - $depth];
|
||||
}
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$condition[] = [$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)];
|
||||
}
|
||||
|
||||
return $query->andWhere($condition)->addOrderBy([$this->leftAttribute => SORT_ASC]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent of the node.
|
||||
* @return \yii\db\ActiveQuery
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
$query = $this->owner->find();
|
||||
|
||||
$condition = [
|
||||
'and',
|
||||
['<', $this->leftAttribute, $this->owner->getAttribute($this->leftAttribute)],
|
||||
['>', $this->rightAttribute, $this->owner->getAttribute($this->rightAttribute)],
|
||||
];
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$condition[] = [$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)];
|
||||
}
|
||||
|
||||
return $query->andWhere($condition)->addOrderBy([$this->rightAttribute => SORT_ASC]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the previous sibling of the node.
|
||||
* @return \yii\db\ActiveQuery
|
||||
*/
|
||||
public function prev()
|
||||
{
|
||||
$query = $this->owner->find();
|
||||
$condition = [$this->rightAttribute => $this->owner->getAttribute($this->leftAttribute) - 1];
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$condition = ['and', $condition, [$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)]];
|
||||
}
|
||||
|
||||
return $query->andWhere($condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next sibling of the node.
|
||||
* @return \yii\db\ActiveQuery
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$query = $this->owner->find();
|
||||
$condition = [$this->leftAttribute => $this->owner->getAttribute($this->rightAttribute) + 1];
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$condition = ['and', $condition, [$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)]];
|
||||
}
|
||||
|
||||
return $query->andWhere($condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the node is descendant of the parent node.
|
||||
* @param \yii\db\ActiveRecord $node the parent node
|
||||
* @return boolean whether the node is descendant of the parent node
|
||||
*/
|
||||
public function isDescendantOf($node)
|
||||
{
|
||||
$result = ($this->owner->getAttribute($this->leftAttribute) > $node->getAttribute($this->leftAttribute))
|
||||
&& ($this->owner->getAttribute($this->rightAttribute) < $node->getAttribute($this->rightAttribute));
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$result = $result
|
||||
&& ($this->owner->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the 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 whether the node is root.
|
||||
* @return boolean whether the node is root
|
||||
*/
|
||||
public function isRoot()
|
||||
{
|
||||
return $this->owner->getAttribute($this->leftAttribute) == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param integer $value
|
||||
* @param integer $delta
|
||||
*/
|
||||
protected function shiftLeftRightAttribute($value, $delta)
|
||||
{
|
||||
$db = $this->owner->getDb();
|
||||
|
||||
foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
|
||||
$condition = ['>=', $attribute, $value];
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$condition = [
|
||||
'and',
|
||||
$condition,
|
||||
[$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)]
|
||||
];
|
||||
}
|
||||
|
||||
$this->owner->updateAll(
|
||||
[$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $delta))],
|
||||
$condition
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \yii\base\ModelEvent $event
|
||||
* @throws Exception
|
||||
* @throws NotSupportedException
|
||||
*/
|
||||
public function beforeInsert($event)
|
||||
{
|
||||
if ($this->node !== null && !$this->node->getIsNewRecord()) {
|
||||
$this->node->refresh();
|
||||
}
|
||||
|
||||
switch ($this->operation) {
|
||||
case self::OPERATION_MAKE_ROOT:
|
||||
$this->owner->setAttribute($this->leftAttribute, 1);
|
||||
$this->owner->setAttribute($this->rightAttribute, 2);
|
||||
$this->owner->setAttribute($this->depthAttribute, 0);
|
||||
|
||||
if ($this->treeAttribute === false && $this->owner->find()->roots()->exists()) {
|
||||
throw new Exception('Can not create more than one root when "treeAttribute" is false.');
|
||||
}
|
||||
|
||||
return;
|
||||
case self::OPERATION_PREPEND_TO:
|
||||
$value = $this->node->getAttribute($this->leftAttribute) + 1;
|
||||
$depth = 1;
|
||||
break;
|
||||
case self::OPERATION_APPEND_TO:
|
||||
$value = $this->node->getAttribute($this->rightAttribute);
|
||||
$depth = 1;
|
||||
break;
|
||||
case self::OPERATION_INSERT_BEFORE:
|
||||
$value = $this->node->getAttribute($this->leftAttribute);
|
||||
$depth = 0;
|
||||
break;
|
||||
case self::OPERATION_INSERT_AFTER:
|
||||
$value = $this->node->getAttribute($this->rightAttribute) + 1;
|
||||
$depth = 0;
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException('Method "'. get_class($this->owner) . '::insert" is not supported for inserting new nodes.');
|
||||
}
|
||||
|
||||
if ($this->node->getIsNewRecord()) {
|
||||
throw new Exception('Can not create a node when the target node is new record.');
|
||||
}
|
||||
|
||||
if ($this->owner->equals($this->node)) {
|
||||
throw new Exception('Can not create a node when the target node is same.');
|
||||
}
|
||||
|
||||
if ($depth === 0 && $this->node->isRoot()) {
|
||||
throw new Exception('Can not create a node when the target node is root.');
|
||||
}
|
||||
|
||||
$this->shiftLeftRightAttribute($value, 2);
|
||||
$this->owner->setAttribute($this->leftAttribute, $value);
|
||||
$this->owner->setAttribute($this->rightAttribute, $value + 1);
|
||||
$this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute) + $depth);
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \yii\db\AfterSaveEvent $event
|
||||
* @throws Exception
|
||||
*/
|
||||
public function afterInsert($event)
|
||||
{
|
||||
if ($this->operation === self::OPERATION_MAKE_ROOT && $this->treeAttribute !== false) {
|
||||
$this->owner->setAttribute($this->treeAttribute, $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->treeAttribute => $this->owner->getAttribute($this->treeAttribute)],
|
||||
[$primaryKey[0] => $this->owner->getAttribute($this->treeAttribute)]
|
||||
);
|
||||
}
|
||||
|
||||
$this->operation = null;
|
||||
$this->node = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \yii\base\ModelEvent $event
|
||||
* @throws Exception
|
||||
*/
|
||||
public function beforeUpdate($event)
|
||||
{
|
||||
if ($this->node !== null && !$this->node->getIsNewRecord()) {
|
||||
$this->node->refresh();
|
||||
}
|
||||
|
||||
switch ($this->operation) {
|
||||
case self::OPERATION_MAKE_ROOT:
|
||||
if ($this->treeAttribute === false) {
|
||||
throw new Exception('Can not move a node as the root when "treeAttribute" is false.');
|
||||
}
|
||||
|
||||
if ($this->owner->isRoot()) {
|
||||
throw new Exception('Can not move the root node as the root.');
|
||||
}
|
||||
|
||||
break;
|
||||
case self::OPERATION_PREPEND_TO:
|
||||
case self::OPERATION_APPEND_TO:
|
||||
case self::OPERATION_INSERT_BEFORE:
|
||||
case self::OPERATION_INSERT_AFTER:
|
||||
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->isDescendantOf($this->owner)) {
|
||||
throw new Exception('Can not move a node when the target node is descendant.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \yii\db\AfterSaveEvent $event
|
||||
* @throws Exception
|
||||
*/
|
||||
public function afterUpdate($event)
|
||||
{
|
||||
$db = $this->owner->getDb();
|
||||
$leftValue = $this->owner->getAttribute($this->leftAttribute);
|
||||
$rightValue = $this->owner->getAttribute($this->rightAttribute);
|
||||
$depthValue = $this->owner->getAttribute($this->depthAttribute);
|
||||
$treeValue = $this->owner->getAttribute($this->treeAttribute);
|
||||
$leftAttribute = $db->quoteColumnName($this->leftAttribute);
|
||||
$rightAttribute = $db->quoteColumnName($this->rightAttribute);
|
||||
$depthAttribute = $db->quoteColumnName($this->depthAttribute);
|
||||
|
||||
switch ($this->operation) {
|
||||
case self::OPERATION_MAKE_ROOT:
|
||||
$this->owner->updateAll(
|
||||
[
|
||||
$this->leftAttribute => new Expression($leftAttribute . sprintf('%+d', 1 - $leftValue)),
|
||||
$this->rightAttribute => new Expression($rightAttribute . sprintf('%+d', 1 - $leftValue)),
|
||||
$this->depthAttribute => new Expression($depthAttribute . sprintf('%+d', 1 - $depthValue)),
|
||||
$this->treeAttribute => $this->owner->getPrimaryKey(),
|
||||
],
|
||||
[
|
||||
'and',
|
||||
['>=', $this->leftAttribute, $leftValue],
|
||||
['<=', $this->rightAttribute, $rightValue],
|
||||
[$this->treeAttribute => $treeValue]
|
||||
]
|
||||
);
|
||||
|
||||
$this->shiftLeftRightAttribute($rightValue + 1, $leftValue - $rightValue - 1);
|
||||
$this->operation = null;
|
||||
$this->node = null;
|
||||
|
||||
return;
|
||||
case self::OPERATION_PREPEND_TO:
|
||||
$value = $this->node->getAttribute($this->leftAttribute) + 1;
|
||||
$depth = 1;
|
||||
break;
|
||||
case self::OPERATION_APPEND_TO:
|
||||
$value = $this->node->getAttribute($this->rightAttribute);
|
||||
$depth = 1;
|
||||
break;
|
||||
case self::OPERATION_INSERT_BEFORE:
|
||||
$value = $this->node->getAttribute($this->leftAttribute);
|
||||
$depth = 0;
|
||||
break;
|
||||
case self::OPERATION_INSERT_AFTER:
|
||||
$value = $this->node->getAttribute($this->rightAttribute) + 1;
|
||||
$depth = 0;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if ($depth === 0 && $this->node->isRoot()) {
|
||||
throw new Exception('Can not move a node when the target node is root.');
|
||||
}
|
||||
|
||||
$nodeRootValue = $this->node->getAttribute($this->treeAttribute);
|
||||
$depth = $this->node->getAttribute($this->depthAttribute) - $depthValue + $depth;
|
||||
|
||||
if ($this->treeAttribute === false || $treeValue === $nodeRootValue) {
|
||||
$delta = $rightValue - $leftValue + 1;
|
||||
$this->shiftLeftRightAttribute($value, $delta);
|
||||
|
||||
if ($leftValue >= $value) {
|
||||
$leftValue += $delta;
|
||||
$rightValue += $delta;
|
||||
}
|
||||
|
||||
$condition = ['and', ['>=', $this->leftAttribute, $leftValue], ['<=', $this->rightAttribute, $rightValue]];
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$condition[] = [$this->treeAttribute => $treeValue];
|
||||
}
|
||||
|
||||
$this->owner->updateAll(
|
||||
[$this->depthAttribute => new Expression($depthAttribute . sprintf('%+d', $depth))],
|
||||
$condition
|
||||
);
|
||||
|
||||
foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
|
||||
$condition = ['and', ['>=', $attribute, $leftValue], ['<=', $attribute, $rightValue]];
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$condition[] = [$this->treeAttribute => $treeValue];
|
||||
}
|
||||
|
||||
$this->owner->updateAll(
|
||||
[$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $value - $leftValue))],
|
||||
$condition
|
||||
);
|
||||
}
|
||||
|
||||
$this->shiftLeftRightAttribute($rightValue + 1, -$delta);
|
||||
} else {
|
||||
foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
|
||||
$this->owner->updateAll(
|
||||
[$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $rightValue - $leftValue + 1))],
|
||||
['and', ['>=', $attribute, $value], [$this->treeAttribute => $nodeRootValue]]
|
||||
);
|
||||
}
|
||||
|
||||
$delta = $value - $leftValue;
|
||||
|
||||
$this->owner->updateAll(
|
||||
[
|
||||
$this->leftAttribute => new Expression($leftAttribute . sprintf('%+d', $delta)),
|
||||
$this->rightAttribute => new Expression($rightAttribute . sprintf('%+d', $delta)),
|
||||
$this->depthAttribute => new Expression($depthAttribute . sprintf('%+d', $depth)),
|
||||
$this->treeAttribute => $nodeRootValue,
|
||||
],
|
||||
[
|
||||
'and',
|
||||
['>=', $this->leftAttribute, $leftValue],
|
||||
['<=', $this->rightAttribute, $rightValue],
|
||||
[$this->treeAttribute => $treeValue],
|
||||
]
|
||||
);
|
||||
|
||||
$this->shiftLeftRightAttribute($rightValue + 1, $leftValue - $rightValue - 1);
|
||||
$this->operation = null;
|
||||
$this->node = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \yii\base\ModelEvent $event
|
||||
* @throws Exception
|
||||
* @throws NotSupportedException
|
||||
*/
|
||||
public function beforeDelete($event)
|
||||
{
|
||||
if ($this->owner->getIsNewRecord()) {
|
||||
throw new Exception('Can not delete a node when it is new record.');
|
||||
}
|
||||
|
||||
if ($this->owner->isRoot() && $this->operation !== self::OPERATION_DELETE_WITH_DESCENDANTS) {
|
||||
throw new NotSupportedException('Method "'. get_class($this->owner) . '::delete" is not supported for deleting root nodes.');
|
||||
}
|
||||
|
||||
$this->owner->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \yii\base\Event $event
|
||||
* @throws Exception
|
||||
*/
|
||||
public function afterDelete($event)
|
||||
{
|
||||
$leftValue = $this->owner->getAttribute($this->leftAttribute);
|
||||
$rightValue = $this->owner->getAttribute($this->rightAttribute);
|
||||
|
||||
if ($this->owner->isLeaf() || $this->operation === self::OPERATION_DELETE_WITH_DESCENDANTS) {
|
||||
$this->shiftLeftRightAttribute($rightValue + 1, $leftValue - $rightValue - 1);
|
||||
} else {
|
||||
$condition = [
|
||||
'and',
|
||||
['>=', $this->leftAttribute, $this->owner->getAttribute($this->leftAttribute)],
|
||||
['<=', $this->rightAttribute, $this->owner->getAttribute($this->rightAttribute)]
|
||||
];
|
||||
|
||||
if ($this->treeAttribute !== false) {
|
||||
$condition[] = [$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)];
|
||||
}
|
||||
|
||||
$db = $this->owner->getDb();
|
||||
|
||||
$this->owner->updateAll(
|
||||
[
|
||||
$this->leftAttribute => new Expression($db->quoteColumnName($this->leftAttribute) . sprintf('%+d', -1)),
|
||||
$this->rightAttribute => new Expression($db->quoteColumnName($this->rightAttribute) . sprintf('%+d', -1)),
|
||||
$this->depthAttribute => new Expression($db->quoteColumnName($this->depthAttribute) . sprintf('%+d', -1)),
|
||||
],
|
||||
$condition
|
||||
);
|
||||
|
||||
$this->shiftLeftRightAttribute($rightValue + 1, -2);
|
||||
}
|
||||
|
||||
$this->operation = null;
|
||||
$this->node = null;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
/**
|
||||
* @link https://github.com/creocoder/yii2-nested-sets-behavior
|
||||
* @copyright Copyright (c) 2014 Alexander Kochetov
|
||||
* @copyright Copyright (c) 2015 Alexander Kochetov
|
||||
* @license http://opensource.org/licenses/BSD-3-Clause
|
||||
*/
|
||||
|
||||
|
@ -19,8 +19,8 @@ use yii\base\Behavior;
|
|||
class NestedSetsQueryBehavior extends Behavior
|
||||
{
|
||||
/**
|
||||
* Gets root node(s).
|
||||
* @return \yii\db\ActiveRecord the owner.
|
||||
* Gets the root nodes.
|
||||
* @return \yii\db\ActiveQuery the owner
|
||||
*/
|
||||
public function roots()
|
||||
{
|
||||
|
|
425
README.md
425
README.md
|
@ -1,420 +1,71 @@
|
|||
Nested Set behavior for Yii 2
|
||||
=============================
|
||||
# Nested Sets Behavior for Yii 2
|
||||
|
||||
This extension allows you to get functional for nested set trees.
|
||||
The nested sets behavior for the Yii framework.
|
||||
|
||||
Installation
|
||||
------------
|
||||
## Installation
|
||||
|
||||
The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
|
||||
|
||||
Either run
|
||||
|
||||
```sh
|
||||
php composer.phar require creocoder/yii2-nested-set-behavior "*"
|
||||
```
|
||||
php composer.phar require creocoder/yii2-nested-sets "dev-master"
|
||||
```
|
||||
|
||||
or add
|
||||
|
||||
```json
|
||||
"creocoder/yii2-nested-set-behavior": "*"
|
||||
"creocoder/yii2-nested-sets": "dev-master"
|
||||
```
|
||||
|
||||
to the require section of your `composer.json` file.
|
||||
|
||||
Configuring
|
||||
--------------------------
|
||||
## Configuring
|
||||
|
||||
First you need to configure model as follows:
|
||||
|
||||
```php
|
||||
class Category extends ActiveRecord
|
||||
{
|
||||
public function behaviors() {
|
||||
return [
|
||||
[
|
||||
'class' => NestedSet::className(),
|
||||
],
|
||||
];
|
||||
}
|
||||
use creocoder\nestedsets\NestedSetsBehavior;
|
||||
|
||||
public static function createQuery()
|
||||
{
|
||||
return new CategoryQuery(['modelClass' => get_called_class()]);
|
||||
}
|
||||
class Tree extends \yii\db\ActiveRecord
|
||||
{
|
||||
public function behaviors() {
|
||||
return [
|
||||
NestedSetsBehavior::className(),
|
||||
];
|
||||
}
|
||||
|
||||
public function transactions()
|
||||
{
|
||||
return [
|
||||
self::SCENARIO_DEFAULT => self::OP_ALL,
|
||||
];
|
||||
}
|
||||
|
||||
public static function find()
|
||||
{
|
||||
return new TreeQuery(get_called_class());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Second you need to configure query model as follows:
|
||||
|
||||
```php
|
||||
class CategoryQuery extends ActiveQuery
|
||||
use creocoder\nestedsets\NestedSetsQueryBehavior;
|
||||
|
||||
class TreeQuery extends \yii\db\ActiveQuery
|
||||
{
|
||||
public function behaviors() {
|
||||
return [
|
||||
[
|
||||
'class' => NestedSetQuery::className(),
|
||||
],
|
||||
];
|
||||
}
|
||||
public function behaviors() {
|
||||
return [
|
||||
NestedSetsQueryBehavior::className(),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
There is no need to validate fields specified in `leftAttribute`,
|
||||
`rightAttribute`, `rootAttribute` and `levelAttribute` options. Moreover,
|
||||
there could be problems if there are validation rules for these. Please
|
||||
check if there are no rules for fields mentioned in model's rules() method.
|
||||
## Usage
|
||||
|
||||
In case of storing a single tree per database, DB structure can be built with
|
||||
`schema/schema.sql`. If you're going to store multiple trees you'll need
|
||||
`schema/schema-many-roots.sql`.
|
||||
TBD.
|
||||
|
||||
By default `leftAttribute`, `rightAttribute` and `levelAttribute` values are
|
||||
matching field names in default DB schemas so you can skip configuring these.
|
||||
|
||||
There are two ways this behavior can work: one tree per table and multiple trees
|
||||
per table. The mode is selected based on the value of `hasManyRoots` option that
|
||||
is `false` by default meaning single tree mode. In multiple trees mode you can
|
||||
set `rootAttribute` option to match existing field in the table storing the tree.
|
||||
|
||||
Selecting from a tree
|
||||
---------------------
|
||||
|
||||
In the following we'll use an example model `Category` with the following in its
|
||||
DB:
|
||||
|
||||
~~~
|
||||
- 1. Mobile phones
|
||||
- 2. iPhone
|
||||
- 3. Samsung
|
||||
- 4. X100
|
||||
- 5. C200
|
||||
- 6. Motorola
|
||||
- 7. Cars
|
||||
- 8. Audi
|
||||
- 9. Ford
|
||||
- 10. Mercedes
|
||||
~~~
|
||||
|
||||
In this example we have two trees. Tree roots are ones with ID=1 and ID=7.
|
||||
|
||||
### Getting all roots
|
||||
|
||||
Using `NestedSet::roots()`:
|
||||
|
||||
```php
|
||||
$roots = Category::find()->roots()->all();
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
Array of Active Record objects corresponding to Mobile phones and Cars nodes.
|
||||
|
||||
### Getting all descendants of a node
|
||||
|
||||
Using `NestedSet::descendants()`:
|
||||
|
||||
```php
|
||||
$category = Category::find(1);
|
||||
$descendants = $category->descendants()->all();
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
Array of Active Record objects corresponding to iPhone, Samsung, X100, C200 and Motorola.
|
||||
|
||||
### Getting all children of a node
|
||||
|
||||
Using `NestedSet::children()`:
|
||||
|
||||
```php
|
||||
$category = Category::find(1);
|
||||
$descendants = $category->children()->all();
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
Array of Active Record objects corresponding to iPhone, Samsung and Motorola.
|
||||
|
||||
### Getting all ancestors of a node
|
||||
|
||||
Using `NestedSet::ancestors()`:
|
||||
|
||||
```php
|
||||
$category = Category::find(5);
|
||||
$ancestors = $category->ancestors()->all();
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
Array of Active Record objects corresponding to Samsung and Mobile phones.
|
||||
|
||||
### Getting parent of a node
|
||||
|
||||
Using `NestedSet::parent()`:
|
||||
|
||||
```php
|
||||
$category = Category::find(9);
|
||||
$parent = $category->parent()->one();
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
Array of Active Record objects corresponding to Cars.
|
||||
|
||||
### Getting node siblings
|
||||
|
||||
Using `NestedSet::prev()` or
|
||||
`NestedSet::next()`:
|
||||
|
||||
```php
|
||||
$category = Category::find(9);
|
||||
$nextSibling = $category->next()->one();
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
Array of Active Record objects corresponding to Mercedes.
|
||||
|
||||
### Getting the whole tree
|
||||
|
||||
You can get the whole tree using standard AR methods like the following.
|
||||
|
||||
For single tree per table:
|
||||
|
||||
```php
|
||||
Category::find()->addOrderBy('lft')->all();
|
||||
```
|
||||
|
||||
For multiple trees per table:
|
||||
|
||||
```php
|
||||
Category::find()->where('root = ?', [$root_id])->addOrderBy('lft')->all();
|
||||
```
|
||||
|
||||
Modifying a tree
|
||||
----------------
|
||||
|
||||
In this section we'll build a tree like the one used in the previous section.
|
||||
|
||||
### Creating root nodes
|
||||
|
||||
You can create a root node using `NestedSet::saveNode()`.
|
||||
In a single tree per table mode you can create only one root node. If you'll attempt
|
||||
to create more there will be CException thrown.
|
||||
|
||||
```php
|
||||
$root = new Category;
|
||||
$root->title = 'Mobile Phones';
|
||||
$root->saveNode();
|
||||
$root = new Category;
|
||||
$root->title = 'Cars';
|
||||
$root->saveNode();
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
~~~
|
||||
- 1. Mobile Phones
|
||||
- 2. Cars
|
||||
~~~
|
||||
|
||||
### Adding child nodes
|
||||
|
||||
There are multiple methods allowing you adding child nodes. To get more info
|
||||
about these refer to API. Let's use these
|
||||
to add nodes to the tree we have:
|
||||
|
||||
```php
|
||||
$category1 = new Category;
|
||||
$category1->title = 'Ford';
|
||||
$category2 = new Category;
|
||||
$category2->title = 'Mercedes';
|
||||
$category3 = new Category;
|
||||
$category3->title = 'Audi';
|
||||
$root = Category::find(1);
|
||||
$category1->appendTo($root);
|
||||
$category2->insertAfter($category1);
|
||||
$category3->insertBefore($category1);
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
~~~
|
||||
- 1. Mobile phones
|
||||
- 3. Audi
|
||||
- 4. Ford
|
||||
- 5. Mercedes
|
||||
- 2. Cars
|
||||
~~~
|
||||
|
||||
Logically the tree above doesn't looks correct. We'll fix it later.
|
||||
|
||||
```php
|
||||
$category1 = new Category;
|
||||
$category1->title = 'Samsung';
|
||||
$category2 = new Category;
|
||||
$category2->title = 'Motorola';
|
||||
$category3 = new Category;
|
||||
$category3->title = 'iPhone';
|
||||
$root = Category::find(2);
|
||||
$category1->appendTo($root);
|
||||
$category2->insertAfter($category1);
|
||||
$category3->prependTo($root);
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
~~~
|
||||
- 1. Mobile phones
|
||||
- 3. Audi
|
||||
- 4. Ford
|
||||
- 5. Mercedes
|
||||
- 2. Cars
|
||||
- 6. iPhone
|
||||
- 7. Samsung
|
||||
- 8. Motorola
|
||||
~~~
|
||||
|
||||
```php
|
||||
$category1 = new Category;
|
||||
$category1->title = 'X100';
|
||||
$category2 = new Category;
|
||||
$category2->title = 'C200';
|
||||
$node = Category::find(3);
|
||||
$category1->appendTo($node);
|
||||
$category2->prependTo($node);
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
~~~
|
||||
- 1. Mobile phones
|
||||
- 3. Audi
|
||||
- 9. С200
|
||||
- 10. X100
|
||||
- 4. Ford
|
||||
- 5. Mercedes
|
||||
- 2. Cars
|
||||
- 6. iPhone
|
||||
- 7. Samsung
|
||||
- 8. Motorola
|
||||
~~~
|
||||
|
||||
Modifying a tree
|
||||
----------------
|
||||
|
||||
In this section we'll finally make our tree logical.
|
||||
|
||||
### Tree modification methods
|
||||
|
||||
There are several methods allowing you to modify a tree. To get more info
|
||||
about these refer to API.
|
||||
|
||||
Let's start:
|
||||
|
||||
```php
|
||||
// move phones to the proper place
|
||||
$x100 = Category::find(10);
|
||||
$c200 = Category::find(9);
|
||||
$samsung = Category::find(7);
|
||||
$x100->moveAsFirst($samsung);
|
||||
$c200->moveBefore($x100);
|
||||
// now move all Samsung phones branch
|
||||
$mobile_phones = Category::find(1);
|
||||
$samsung->moveAsFirst($mobile_phones);
|
||||
// move the rest of phone models
|
||||
$iphone = Category::find(6);
|
||||
$iphone->moveAsFirst($mobile_phones);
|
||||
$motorola = Category::find(8);
|
||||
$motorola->moveAfter($samsung);
|
||||
// move car models to appropriate place
|
||||
$cars = Category::find(2);
|
||||
$audi = Category::find(3);
|
||||
$ford = Category::find(4);
|
||||
$mercedes = Category::find(5);
|
||||
|
||||
foreach([$audi, $ford, $mercedes] as $category) {
|
||||
$category->moveAsLast($cars);
|
||||
}
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
~~~
|
||||
- 1. Mobile phones
|
||||
- 6. iPhone
|
||||
- 7. Samsung
|
||||
- 10. X100
|
||||
- 9. С200
|
||||
- 8. Motorola
|
||||
- 2. Cars
|
||||
- 3. Audi
|
||||
- 4. Ford
|
||||
- 5. Mercedes
|
||||
~~~
|
||||
|
||||
### Moving a node making it a new root
|
||||
|
||||
There is a special `moveAsRoot()` method that allows moving a node and making it
|
||||
a new root. All descendants are moved as well in this case.
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
$node = Category::find(10);
|
||||
$node->moveAsRoot();
|
||||
```
|
||||
|
||||
### Identifying node type
|
||||
|
||||
There are three methods to get node type: `isRoot()`, `isLeaf()`, `isDescendantOf()`.
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
$root = Category::find(1);
|
||||
VarDumper::dump($root->isRoot()); //true;
|
||||
VarDumper::dump($root->isLeaf()); //false;
|
||||
$node = Category::find(9);
|
||||
VarDumper::dump($node->isDescendantOf($root)); //true;
|
||||
VarDumper::dump($node->isRoot()); //false;
|
||||
VarDumper::dump($node->isLeaf()); //true;
|
||||
$samsung = Category::find(7);
|
||||
VarDumper::dump($node->isDescendantOf($samsung)); //true;
|
||||
```
|
||||
|
||||
Useful code
|
||||
------------
|
||||
|
||||
### Non-recursive tree traversal
|
||||
|
||||
```php
|
||||
$categories = Category::find()->addOrderBy('lft')->all();
|
||||
$level = 0;
|
||||
|
||||
foreach ($categories as $n => $category)
|
||||
{
|
||||
if ($category->level == $level) {
|
||||
echo Html::endTag('li') . "\n";
|
||||
} elseif ($category->level > $level) {
|
||||
echo Html::beginTag('ul') . "\n";
|
||||
} else {
|
||||
echo Html::endTag('li') . "\n";
|
||||
|
||||
for ($i = $level - $category->level; $i; $i--) {
|
||||
echo Html::endTag('ul') . "\n";
|
||||
echo Html::endTag('li') . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo Html::beginTag('li');
|
||||
echo Html::encode($category->title);
|
||||
$level = $category->level;
|
||||
}
|
||||
|
||||
for ($i = $level; $i; $i--) {
|
||||
echo Html::endTag('li') . "\n";
|
||||
echo Html::endTag('ul') . "\n";
|
||||
}
|
||||
```
|
||||
[![PayPal - The safer, easier way to pay online!](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WJYG53DVUAALL)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"name": "creocoder/yii2-nested-set-behavior",
|
||||
"description": "The nested set model behavior for the Yii framework",
|
||||
"name": "creocoder/yii2-nested-sets",
|
||||
"description": "The nested sets behavior for the Yii framework",
|
||||
"keywords": [
|
||||
"yii2",
|
||||
"nested set"
|
||||
"nested sets"
|
||||
],
|
||||
"type": "yii2-extension",
|
||||
"license": "BSD-3-Clause",
|
||||
|
@ -18,7 +18,7 @@
|
|||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"creocoder\\nestedset\\": ""
|
||||
"creocoder\\nestedsets\\": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
CREATE TABLE `tbl_category` (
|
||||
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`root` INT(10) UNSIGNED DEFAULT NULL,
|
||||
`lft` INT(10) UNSIGNED NOT NULL,
|
||||
`rgt` INT(10) UNSIGNED NOT NULL,
|
||||
`level` SMALLINT(5) UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `root` (`root`),
|
||||
KEY `lft` (`lft`),
|
||||
KEY `rgt` (`rgt`),
|
||||
KEY `level` (`level`)
|
||||
);
|
|
@ -1,10 +0,0 @@
|
|||
CREATE TABLE `tbl_category` (
|
||||
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`lft` INT(10) UNSIGNED NOT NULL,
|
||||
`rgt` INT(10) UNSIGNED NOT NULL,
|
||||
`level` SMALLINT(5) UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `lft` (`lft`),
|
||||
KEY `rgt` (`rgt`),
|
||||
KEY `level` (`level`)
|
||||
);
|
Loading…
Reference in a new issue