1
0
Fork 0
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:
Alexander Kochetov 2015-01-01 09:08:05 +03:00
parent 603e303001
commit 427deedd5a
8 changed files with 767 additions and 1654 deletions

32
LICENSE.md Normal file
View 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.

File diff suppressed because it is too large Load diff

690
NestedSetsBehavior.php Normal file
View 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;
}
}

View file

@ -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
View file

@ -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)

View file

@ -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\\": ""
}
}
}

View file

@ -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`)
);

View file

@ -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`)
);