2013-12-30 14:02:19 +02:00
|
|
|
|
Nested Set behavior for Yii 2
|
|
|
|
|
=============================
|
2013-05-05 19:19:56 +03:00
|
|
|
|
|
2014-01-16 12:48:19 +02:00
|
|
|
|
This extension allows you to get functional for nested set trees.
|
2013-05-06 10:12:45 +03:00
|
|
|
|
|
2013-12-30 14:02:19 +02:00
|
|
|
|
Installation
|
|
|
|
|
------------
|
|
|
|
|
|
|
|
|
|
The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
|
|
|
|
|
|
|
|
|
|
Either run
|
|
|
|
|
|
2014-01-07 19:51:59 +02:00
|
|
|
|
```sh
|
2014-01-10 00:17:13 +02:00
|
|
|
|
php composer.phar require creocoder/yii2-nested-set-behavior "*"
|
2013-12-30 14:02:19 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
or add
|
|
|
|
|
|
|
|
|
|
```json
|
2014-01-10 00:17:13 +02:00
|
|
|
|
"creocoder/yii2-nested-set-behavior": "*"
|
2013-12-30 14:02:19 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
to the require section of your `composer.json` file.
|
|
|
|
|
|
|
|
|
|
Configuring
|
2013-05-06 10:12:45 +03:00
|
|
|
|
--------------------------
|
|
|
|
|
|
|
|
|
|
First you need to configure model as follows:
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-07 10:32:13 +03:00
|
|
|
|
class Category extends ActiveRecord
|
2013-05-06 10:12:45 +03:00
|
|
|
|
{
|
2013-05-07 10:32:13 +03:00
|
|
|
|
public function behaviors() {
|
2014-01-16 10:53:01 +02:00
|
|
|
|
return [
|
2014-01-16 10:54:21 +02:00
|
|
|
|
[
|
2014-01-17 02:57:15 +02:00
|
|
|
|
'class' => NestedSet::className(),
|
2014-01-16 10:53:01 +02:00
|
|
|
|
],
|
|
|
|
|
];
|
2013-05-07 10:32:13 +03:00
|
|
|
|
}
|
2014-01-29 17:05:38 +02:00
|
|
|
|
|
|
|
|
|
public static function createQuery()
|
|
|
|
|
{
|
|
|
|
|
return new CategoryQuery(['modelClass' => get_called_class()]);
|
|
|
|
|
}
|
2013-05-07 10:32:13 +03:00
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Second you need to configure query model as follows:
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
class CategoryQuery extends ActiveQuery
|
|
|
|
|
{
|
|
|
|
|
public function behaviors() {
|
2014-01-16 10:53:01 +02:00
|
|
|
|
return [
|
2014-01-16 10:54:21 +02:00
|
|
|
|
[
|
2014-01-17 02:57:15 +02:00
|
|
|
|
'class' => NestedSetQuery::className(),
|
2014-01-16 10:53:01 +02:00
|
|
|
|
],
|
|
|
|
|
];
|
2013-05-07 10:32:13 +03:00
|
|
|
|
}
|
2013-05-06 10:12:45 +03:00
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
In case of storing a single tree per database, DB structure can be built with
|
2013-05-06 11:27:26 +03:00
|
|
|
|
`schema/schema.sql`. If you're going to store multiple trees you'll need
|
2014-01-04 03:42:57 +02:00
|
|
|
|
`schema/schema-many-roots.sql`.
|
2013-05-06 10:12:45 +03:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2013-05-06 10:57:43 +03:00
|
|
|
|
Using `NestedSet::roots()`:
|
2013-05-06 10:12:45 +03:00
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$roots = Category::find()->roots()->all();
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Result:
|
|
|
|
|
|
|
|
|
|
Array of Active Record objects corresponding to Mobile phones and Cars nodes.
|
|
|
|
|
|
|
|
|
|
### Getting all descendants of a node
|
|
|
|
|
|
2013-05-06 10:57:43 +03:00
|
|
|
|
Using `NestedSet::descendants()`:
|
2013-05-06 10:12:45 +03:00
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$category = Category::find(1);
|
|
|
|
|
$descendants = $category->descendants()->all();
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Result:
|
|
|
|
|
|
|
|
|
|
Array of Active Record objects corresponding to iPhone, Samsung, X100, C200 and Motorola.
|
|
|
|
|
|
|
|
|
|
### Getting all children of a node
|
|
|
|
|
|
2013-05-06 10:57:43 +03:00
|
|
|
|
Using `NestedSet::children()`:
|
2013-05-06 10:12:45 +03:00
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$category = Category::find(1);
|
|
|
|
|
$descendants = $category->children()->all();
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Result:
|
|
|
|
|
|
|
|
|
|
Array of Active Record objects corresponding to iPhone, Samsung and Motorola.
|
|
|
|
|
|
|
|
|
|
### Getting all ancestors of a node
|
|
|
|
|
|
2013-05-06 10:57:43 +03:00
|
|
|
|
Using `NestedSet::ancestors()`:
|
2013-05-06 10:12:45 +03:00
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$category = Category::find(5);
|
|
|
|
|
$ancestors = $category->ancestors()->all();
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Result:
|
|
|
|
|
|
|
|
|
|
Array of Active Record objects corresponding to Samsung and Mobile phones.
|
|
|
|
|
|
|
|
|
|
### Getting parent of a node
|
|
|
|
|
|
2013-05-06 10:57:43 +03:00
|
|
|
|
Using `NestedSet::parent()`:
|
2013-05-06 10:12:45 +03:00
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$category = Category::find(9);
|
2013-05-06 11:01:52 +03:00
|
|
|
|
$parent = $category->parent()->one();
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Result:
|
|
|
|
|
|
|
|
|
|
Array of Active Record objects corresponding to Cars.
|
|
|
|
|
|
|
|
|
|
### Getting node siblings
|
|
|
|
|
|
2013-05-06 10:57:43 +03:00
|
|
|
|
Using `NestedSet::prev()` or
|
|
|
|
|
`NestedSet::next()`:
|
2013-05-06 10:12:45 +03:00
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$category = Category::find(9);
|
2013-05-06 11:01:52 +03:00
|
|
|
|
$nextSibling = $category->next()->one();
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
2013-05-06 18:21:58 +03:00
|
|
|
|
Category::find()->addOrderBy('lft')->all();
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For multiple trees per table:
|
|
|
|
|
|
|
|
|
|
```php
|
2014-01-15 12:00:47 +02:00
|
|
|
|
Category::find()->where('root = ?', [$root_id])->addOrderBy('lft')->all();
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Modifying a tree
|
|
|
|
|
----------------
|
|
|
|
|
|
|
|
|
|
In this section we'll build a tree like the one used in the previous section.
|
|
|
|
|
|
|
|
|
|
### Creating root nodes
|
|
|
|
|
|
2013-05-06 10:57:43 +03:00
|
|
|
|
You can create a root node using `NestedSet::saveNode()`.
|
2013-05-06 10:12:45 +03:00
|
|
|
|
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
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$root = new Category;
|
|
|
|
|
$root->title = 'Mobile Phones';
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$root->saveNode();
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$root = new Category;
|
|
|
|
|
$root->title = 'Cars';
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$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
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$category1 = new Category;
|
|
|
|
|
$category1->title = 'Ford';
|
|
|
|
|
$category2 = new Category;
|
|
|
|
|
$category2->title = 'Mercedes';
|
|
|
|
|
$category3 = new Category;
|
|
|
|
|
$category3->title = 'Audi';
|
|
|
|
|
$root = Category::find(1);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$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
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$category1 = new Category;
|
|
|
|
|
$category1->title = 'Samsung';
|
|
|
|
|
$category2 = new Category;
|
|
|
|
|
$category2->title = 'Motorola';
|
|
|
|
|
$category3 = new Category;
|
|
|
|
|
$category3->title = 'iPhone';
|
|
|
|
|
$root = Category::find(2);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$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
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$category1 = new Category;
|
|
|
|
|
$category1->title = 'X100';
|
2013-05-06 10:59:30 +03:00
|
|
|
|
$category2 = new Category;
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$category2->title = 'C200';
|
|
|
|
|
$node = Category::find(3);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$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
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$x100 = Category::find(10);
|
|
|
|
|
$c200 = Category::find(9);
|
|
|
|
|
$samsung = Category::find(7);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$x100->moveAsFirst($samsung);
|
|
|
|
|
$c200->moveBefore($x100);
|
|
|
|
|
// now move all Samsung phones branch
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$mobile_phones = Category::find(1);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$samsung->moveAsFirst($mobile_phones);
|
|
|
|
|
// move the rest of phone models
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$iphone = Category::find(6);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$iphone->moveAsFirst($mobile_phones);
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$motorola = Category::find(8);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$motorola->moveAfter($samsung);
|
|
|
|
|
// move car models to appropriate place
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$cars = Category::find(2);
|
|
|
|
|
$audi = Category::find(3);
|
|
|
|
|
$ford = Category::find(4);
|
|
|
|
|
$mercedes = Category::find(5);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
|
2014-01-15 12:00:47 +02:00
|
|
|
|
foreach([$audi, $ford, $mercedes] as $category) {
|
2014-01-16 10:53:01 +02:00
|
|
|
|
$category->moveAsLast($cars);
|
2013-05-06 10:57:43 +03:00
|
|
|
|
}
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$node = Category::find(10);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$node->moveAsRoot();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Identifying node type
|
|
|
|
|
|
|
|
|
|
There are three methods to get node type: `isRoot()`, `isLeaf()`, `isDescendantOf()`.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:57:43 +03:00
|
|
|
|
$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;
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Useful code
|
|
|
|
|
------------
|
|
|
|
|
|
|
|
|
|
### Non-recursive tree traversal
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 18:09:34 +03:00
|
|
|
|
$categories = Category::find()->addOrderBy('lft')->all();
|
|
|
|
|
$level = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($categories as $n => $category)
|
|
|
|
|
{
|
|
|
|
|
if ($category->level == $level) {
|
2013-11-13 05:55:56 +02:00
|
|
|
|
echo Html::endTag('li') . "\n";
|
2013-05-06 18:09:34 +03:00
|
|
|
|
} elseif ($category->level > $level) {
|
2013-11-13 05:55:56 +02:00
|
|
|
|
echo Html::beginTag('ul') . "\n";
|
2013-05-06 18:09:34 +03:00
|
|
|
|
} else {
|
2013-11-13 05:55:56 +02:00
|
|
|
|
echo Html::endTag('li') . "\n";
|
2013-05-06 18:09:34 +03:00
|
|
|
|
|
|
|
|
|
for ($i = $level - $category->level; $i; $i--) {
|
2013-11-13 05:55:56 +02:00
|
|
|
|
echo Html::endTag('ul') . "\n";
|
|
|
|
|
echo Html::endTag('li') . "\n";
|
2013-05-06 18:09:34 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-11-13 05:55:56 +02:00
|
|
|
|
echo Html::beginTag('li');
|
2013-05-06 18:09:34 +03:00
|
|
|
|
echo Html::encode($category->title);
|
|
|
|
|
$level = $category->level;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for ($i = $level; $i; $i--) {
|
2013-11-13 05:55:56 +02:00
|
|
|
|
echo Html::endTag('li') . "\n";
|
|
|
|
|
echo Html::endTag('ul') . "\n";
|
2013-05-06 18:09:34 +03:00
|
|
|
|
}
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|