Note: Latest release and documentation are available from extension github page.
This extension allows managing trees stored in database as nested sets. It's implemented as Active Record behavior.
Implemented by creocoder.
First you need to configure model as follows:
public function behaviors() { return array( 'NestedSetBehavior'=>array( 'class'=>'ext.yiiext.behaviors.trees.NestedSetBehavior', '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 filed 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 filed in the table storing the 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.
Using NestedSetBehavior::roots():
$roots=Category::model()->roots()->findAll();
Result:
Array of Active Record objects corresponding to Mobile phones and Cars nodes.
Using NestedSetBehavior::descendants():
$category=Category::model()->findByPk(1); $descendants=$category->descendants()->findAll();
Result:
Array of Active Record objects corresponding to iPhone, Samsung, X100, C200 and Motorola.
Using NestedSetBehavior::children():
$category=Category::model()->findByPk(1); $descendants=$category->children()->findAll();
Result:
Array of Active Record objects corresponding to iPhone, Samsung and Motorola.
Using NestedSetBehavior::ancestors():
$category=Category::model()->findByPk(5); $descendants=$category->ancestors()->findAll();
Result:
Array of Active Record objects corresponding to Samsung and Mobile phones.
Using NestedSetBehavior::getParent():
$category=Category::model()->findByPk(9); $parent=$category->parent;
Result:
Array of Active Record objects corresponding to Cars.
Using NestedSetBehavior::getPrevSibling() or
NestedSetBehavior::getNextSibling():
$category=Category::model()->findByPk(9); $nextSibling=$category->nextSibling;
Result:
Array of Active Record objects corresponding to Mercedes.
You can get the whole tree using standard AR methods like the following.
For single tree per table:
Category::model()->findAll(array('order'=>'lft'));
For multiple trees per table:
Category::model()->findAll(array('condition'=>'root_id=?','order'=>'lft'),array($root_id));
In this section we'll build a tree like the one used in the previous section.
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.
$root=new Root; $root->title='Mobile Phones'; $root->saveNode(); $root=new Root; $root->title='Cars'; $root->saveNode();
Result:
- 1. Mobile Phones - 2. Cars
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:
$category1=new Category; $category1->title='Ford'; $category2=new Category; $category2->title='Mercedes'; $category3=new Category; $category3->title='Audi'; $root=Category::model()->findByPk(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.
$category1=new Category; $category1->title='Samsung'; $category2=new Category; $category2->title='Motorola'; $category3=new Category; $category3->title='iPhone'; $root=Category::model()->findByPk(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
$category1=new Category; $category1->title='X100'; $category2=new Category; $category2->title='C200'; $node=Category::model()->findByPk(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
In this section we'll finally make our tree logical.
There are several methods allowing you to modify a tree. To get more info about these refer to API.
Let's start:
// move phones to the proper place $x100=Category::model()->findByPk(10); $c200=Category::model()->findByPk(9); $samsung=Category::model()->findByPk(7); $x100->moveAsFirst($samsung); $c200->moveBefore($x100); // now move all Samsung phones branch $mobile_phones=Category::model()->findByPk(1); $samsung->moveAsFirst($mobile_phones); // move the rest of phone models $iphone=Category::model()->findByPk(6); $iphone->moveAsFirst($mobile_phones); $motorola=Category::model()->findByPk(8); $motorola->moveAfter($samsung); // move car models to appropriate place $cars=Category::model()->findByPk(2); $audi=Category::model()->findByPk(3); $ford=Category::model()->findByPk(4); $mercedes=Category::model()->findByPk(5); 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
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:
$node=Category::model()->findByPk(10); $node->moveAsRoot();
There are three methods to get node type: isRoot(), isLeaf(), isDescendantOf().
Example:
$root=Category::model()->findByPk(1); CVarDumper::dump($root->isRoot()); //true; CVarDumper::dump($root->isLeaf()); //false; $node=Category::model()->findByPk(9); CVarDumper::dump($node->isDescendantOf($root)); //true; CVarDumper::dump($node->isRoot()); //false; CVarDumper::dump($root->isLeaf()); //true; $samsung=Category::model()->findByPk(7); CVarDumper::dump($node->isDescendantOf($samsung)); //true;
$level=0; foreach($categories as $n=>$category) { if($category->level==$level) echo CHtml::closeTag('li')."\n"; else if($category->level>$level) echo CHtml::openTag('ul')."\n"; else { echo CHtml::closeTag('li')."\n"; for($i=$level-$model->level;$i;$i--) { echo CHtml::closeTag('ul')."\n"; echo CHtml::closeTag('li')."\n"; } } echo CHtml::openTag('li'); echo CHtml::encode($category->title); $level=$category->level; } for($i=$level;$i;$i--) { echo CHtml::closeTag('li')."\n"; echo CHtml::closeTag('ul')."\n"; }
final from class (creocoder)rules (Sam Dark)
Total 20 comments
debido a que no tenemos demasiada informacion aqui, es necesario ver el código directamente, pues es el lugar donde mencionan los parametros adicionales al momento de crear nodos sin realizar la validación. here
and
Just look closer to the "Selecting from tree" section and "Getting all children of a node" example.
Example data structure:
but CORRECT example data structure shoud be:
Please fix the extension description and the readme file on github.
BTW - this is a great extension anyway - thank you!
Can you give a recursive traversal of useful code?
Great extension. I got it work! But I have a question:
If I have a tbl_presentation and categories extension configured for these presentations. How can I get all the the categories (roots for example) for those presentations that have certain condition? (ie: tbl_presentation.status = 'active') ?
@andrew1 just fixed the error.
Would love to be able to use this extension but can't access it on the link provided to github.
Getting Fatal error: Cannot use object of type stdClass as array in /var/lib/jenkins/workspace/yiiext.github.com/app/components/YiiextGithubApi.php on line 72
Could benefit from using of Postgresql's ltree data type where available: http://www.postgresql.org/docs/9.1/static/ltree.html
The Problem
I have noticed a bit of a problem with it if you use it for exports or similar tasks where you are traversing through the whole database getting data and pushing it out to a file.
The extension has an internal cache stored in a static variable. This can be disastrous since this means that the memory of the model you just used will be stored in memory and because of that is not released when you loop through to the next element.
I was traversing through about 100000 items and after adding a call to a model that uses the nested set behaviour my export script started running out of memory.
I am using a call like this in a loop:
If you look in the afterFind Method in the nested set behaviour it does this:
This stores the object in a static array. Quite quickly this will fill the memory up. This problem is probably only ever going to show up in the rare event of you traversing through a large number of nodes and I can see that this is the wanted behaviour in most cases.
The Solution
Make sure to destruct the model manually after you have used it. Like so:
The nested set behaviour has a destructor that removes the object from the static cache.
Suggestion
Add possibility to turn caching off in the behaviour.
Something like:
Hi there, I'd like to attach a user_id field to this table and have 1 tree per user. Do you think this plugin will work in this way?
Thanks for the great work, Marco.
hi, thanks for your great job is there a best solution to use it with CMenu?
when a top level root has too many children , if want to use the pagination . how to ? i noticed this method : public function descendants($depth=null, $self = false)
too many children means the tree may be too deep or too wide . both situation need consider how to apply pagination , any idea ?
Hi,I have just uploaded an extension for Nested Set Behavior Administration with jsTree plugin.This is the Link: Nested Set Behavior Administration GUI with jsTree
A very useful extension, thank you so much for sharing! Could you please help me get this clear — how exactly can I display the whole tree via CTreeView widget? What is the right way to get the whole tree, when three has many roots?
the usage example is very helpful , also we can check it out from svn , in the unit test directory one can find more usage!
how about developing an extension that bridge the nestedset model and the adjacency model , we can translate one model easily to another . there are some code pieces in the internet converting the nestedset to adjacency model . you need create both table and config the mapping for two table fields( because some table may be exist earlier and may have different field name ) ,often converting take place in midnight ,it will cost a lot of time .
I just implemented jqGrid Tree view using Nested Sets with multiple nodes. It took me sometime to work out the logic, so i thought i could share my code, if some other beginner like me needs to check.
Using Nested Sets in multiple roots mode requires the use of the 'adjacency' jqGrid tree Model. Atleast that is what i understand.
The view file details:
The actionFetchTree method:
The explanation is given in the jqGrid wiki site where the usage of Adjacency Model is described. The only point to remember here is that while sending the children nodes we need to send the immediate parent id, and not the root id for that tree. So, in the beginning of the code, we try to obtain the root details through a queryRow() call
This is really a great extension.
Please notice that while performing the moveNode action, the checking is being done to find out if a txn has already been set (i.e., if there was a beginTransaction() issued by some controller). And only if it has not been set, the extension tries to set one by itself.
But while deleting a Node, the same is not being done. I feel that this checking could have been done in the delete method also. In my implementation, i have enabled this checking and it is working fine, as of now.
Beautiful work!
One note, this is technically not a nested set, but an adjacency list - this type of tree implementation is optimized for fast reading, but writes are more expensive than a nested set. It also had the advantage (over a nested set) of maintaining sibling order.
Personally I prefer adjacency lists - and this is an awesome implementation, queries neatly packaged as scopes, operations fully wrapped in transactions, fully unit tested, etc. - very, very nice work!
Thanks!
First of all, great extension.
Now I need to extend its functionality in order to keep several trees in the same table. Not one tree with several roots, but several trees within the same table. I would need this "profile_id" filter to paint one tree or another.
I have a table "profile_component" with fields "id", "left", "right" and "level" to manage the nested set behavior. But it also has a field "profile_id", so that the records with profile_id=0 represent one tree, the records with profile_id=1 represent a second tree, and so on.
Does anybody know how should I extend the component so that I can get one tree or another by setting this "profile_id" attribute?
I´m using also JsTree to render the tree and EJNestedTreeActions to deal with the tree actions.
Thanks in advance
in a project there may have many models , which have the tree struct . and if this extension can apply to multiple ar or can only to one specific ar . if attach to many different ar , does it need to modify the category table schema , may be add some field like : cate_type varchar(20) .. or i misunderstand the usage , i will modify the ar's table add the table field come from the category .
the tree struct can added to a exist table(which in first version may be plain ) ,let's say : menu(id,text,url). some time later , we need the menu to support tree struct , there are two choices : 1.add some another fields to the menu table ,like pid, level etc..; 2.- use another table such as node to maintain the relation :node(id ,type, id_type,type_pid,type_level)
the choice 2 may need do a lots things when perform the crud operations. for query may use the left join or inner join . and create , update , delete become even complex . so if i use this extension in my project , i have to modify the existing ar's table.
How track event change node, moved, created, updated, deleted? Event onAfterSave not work! I need delete cache if changed structur of tree. And it is I have to do in model.
Thanks :) !
Leave a comment
Please login to leave your comment.