2013-05-06 10:12:45 +03:00
|
|
|
|
Nested Set behavior 2 (preview version)
|
|
|
|
|
=======================================
|
2013-05-05 19:19:56 +03:00
|
|
|
|
|
2013-05-06 10:12:45 +03:00
|
|
|
|
This extension allows managing trees stored in database as nested sets.
|
|
|
|
|
It's implemented as Active Record behavior.
|
|
|
|
|
|
|
|
|
|
Installing and configuring
|
|
|
|
|
--------------------------
|
|
|
|
|
|
|
|
|
|
First you need to configure model as follows:
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
public function behaviors()
|
|
|
|
|
{
|
|
|
|
|
return array(
|
|
|
|
|
'nestedSetBehavior'=>array(
|
|
|
|
|
'class'=>'NestedSet',
|
|
|
|
|
'leftAttribute'=>'lft',
|
|
|
|
|
'rightAttribute'=>'rgt',
|
|
|
|
|
'levelAttribute'=>'level',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
`extensions/yiiext/behaviors/trees/schema.sql`. If you're going to store multiple
|
|
|
|
|
trees you'll need `extensions/yiiext/behaviors/trees/schema_many_roots.sql`.
|
|
|
|
|
|
|
|
|
|
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 `NestedSetBehavior::roots()`:
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:17:09 +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
|
|
|
|
|
|
|
|
|
|
Using `NestedSetBehavior::descendants()`:
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:43:57 +03:00
|
|
|
|
$category=Category::find(1);
|
2013-05-06 10:17:09 +03:00
|
|
|
|
$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
|
|
|
|
|
|
|
|
|
|
Using `NestedSetBehavior::children()`:
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:43:57 +03:00
|
|
|
|
$category=Category::find(1);
|
2013-05-06 10:17:09 +03:00
|
|
|
|
$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
|
|
|
|
|
|
|
|
|
|
Using `NestedSetBehavior::ancestors()`:
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:43:57 +03:00
|
|
|
|
$category=Category::find(5);
|
2013-05-06 10:17:09 +03:00
|
|
|
|
$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
|
|
|
|
|
|
|
|
|
|
Using `NestedSetBehavior::parent()`:
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:43:57 +03:00
|
|
|
|
$category=Category::find(9);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$parent=$category->parent()->find();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Result:
|
|
|
|
|
|
|
|
|
|
Array of Active Record objects corresponding to Cars.
|
|
|
|
|
|
|
|
|
|
### Getting node siblings
|
|
|
|
|
|
|
|
|
|
Using `NestedSetBehavior::prev()` or
|
|
|
|
|
`NestedSetBehavior::next()`:
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:43:57 +03:00
|
|
|
|
$category=Category::find(9);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$nextSibling=$category->next()->find();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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 10:43:57 +03:00
|
|
|
|
TBD.
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For multiple trees per table:
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:43:57 +03:00
|
|
|
|
TBD.
|
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
|
|
|
|
|
|
|
|
|
|
You can create a root node using `NestedSetBehavior::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';
|
2013-05-06 10:43:57 +03:00
|
|
|
|
$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
|
|
|
|
|
$category1=new Category;
|
|
|
|
|
$category1->title='Samsung';
|
|
|
|
|
$category2=new Category;
|
|
|
|
|
$category2->title='Motorola';
|
|
|
|
|
$category3=new Category;
|
|
|
|
|
$category3->title='iPhone';
|
2013-05-06 10:43:57 +03:00
|
|
|
|
$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
|
|
|
|
|
$category1=new Category;
|
|
|
|
|
$category1->title='X100';
|
|
|
|
|
$category2=new Category;
|
|
|
|
|
$category2->title='C200';
|
2013-05-06 10:43:57 +03:00
|
|
|
|
$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:43:57 +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:43:57 +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:43:57 +03:00
|
|
|
|
$iphone=Category::find(6);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
$iphone->moveAsFirst($mobile_phones);
|
2013-05-06 10:43:57 +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:43:57 +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
|
|
|
|
|
|
|
|
|
foreach(array($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
|
2013-05-06 10:43:57 +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:43:57 +03:00
|
|
|
|
$root=Category::find(1);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
CVarDumper::dump($root->isRoot()); //true;
|
|
|
|
|
CVarDumper::dump($root->isLeaf()); //false;
|
2013-05-06 10:43:57 +03:00
|
|
|
|
$node=Category::find(9);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
CVarDumper::dump($node->isDescendantOf($root)); //true;
|
|
|
|
|
CVarDumper::dump($node->isRoot()); //false;
|
|
|
|
|
CVarDumper::dump($node->isLeaf()); //true;
|
2013-05-06 10:43:57 +03:00
|
|
|
|
$samsung=Category::find(7);
|
2013-05-06 10:12:45 +03:00
|
|
|
|
CVarDumper::dump($node->isDescendantOf($samsung)); //true;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Useful code
|
|
|
|
|
------------
|
|
|
|
|
|
|
|
|
|
### Non-recursive tree traversal
|
|
|
|
|
|
|
|
|
|
```php
|
2013-05-06 10:43:57 +03:00
|
|
|
|
TBD.
|
2013-05-06 10:12:45 +03:00
|
|
|
|
```
|