nestedsetbehavior

Nested set behavior for AR models
80 followers

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.

Resources

Documentation

Installing and configuring

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.

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():

$roots=Category::model()->roots()->findAll();

Result:

Array of Active Record objects corresponding to Mobile phones and Cars nodes.

Getting all descendants of a node

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.

Getting all children of a node

Using NestedSetBehavior::children():

$category=Category::model()->findByPk(1);
$descendants=$category->children()->findAll();

Result:

Array of Active Record objects corresponding to iPhone, Samsung and Motorola.

Getting all ancestors of a node

Using NestedSetBehavior::ancestors():

$category=Category::model()->findByPk(5);
$descendants=$category->ancestors()->findAll();

Result:

Array of Active Record objects corresponding to Samsung and Mobile phones.

Getting parent of a node

Using NestedSetBehavior::getParent():

$category=Category::model()->findByPk(9);
$parent=$category->parent;

Result:

Array of Active Record objects corresponding to Cars.

Getting node siblings

Using NestedSetBehavior::getPrevSibling() or NestedSetBehavior::getNextSibling():

$category=Category::model()->findByPk(9);
$nextSibling=$category->nextSibling;

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:

Category::model()->findAll(array('order'=>'lft'));

For multiple trees per table:

Category::model()->findAll(array('condition'=>'root_id=?','order'=>'lft'),array($root_id));

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.

$root=new Root;
$root->title='Mobile Phones';
$root->saveNode();
$root=new Root;
$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:

$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

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:

// 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

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:

$node=Category::model()->findByPk(10);
$node->moveAsRoot();

Identifying node type

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;

Useful code

Non-recursive tree traversal

$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";
}

Change Log

1.05

  • External transaction support for NestedSetBehavior::delete() method (creocoder)

1.04

  • Fix for $depth parameter of NestedSetBehavior::ancestors() method (creocoder)

1.03

  • Class renamed to NestedSetBehavior (creocoder)
  • Rare bug with cache correction after NestedSetBehavior::addNode() fixed (creocoder)
  • Readmes non-recursive tree traversal algorithm fix (creocoder)

1.02

  • Added moveAsRoot() method (creocoder)
  • Updated readmes (Sam Dark)

1.01

  • Added not integer pk support (creocoder)
  • Remove final from class (creocoder)

1.00

  • Some behavior refactoring before release (creocoder)
  • Advanced doc added (creocoder)

0.99b

  • Allow to use multiply change tree operations (creocoder)
  • Method saveNode() now create root in "single root mode" (creocoder)
  • Unit tests refactored (creocoder)
  • Some behavior refactoring (creocoder)

0.99

  • Added note about removing fields from rules (Sam Dark)
  • Added Unit tests for single root mode (creocoder)
  • All attributes are now correctly quoted (creocoder)
  • Renamed fields: 'root'=>'rootAttribute', 'left'=>'leftAttribute', 'right'=>'rightAttribute', 'level'=>'levelAttribute' (creocoder)
  • Renamed method parent() => getParent() (creocoder)

0.95

  • Unit tests added (creocoder)
  • Incorrect usage of save() and delete() instead of behavior's saveNode() and deleteNode() is now detected (creocoder)

0.90

  • Moving a node to a different root is now supported (creocoder)

0.85

  • Initial public release (creocoder)

Total 20 comments

#7747 report it
cstdenis at 2012/04/14 06:22am
Looks useful

Could benefit from using of Postgresql's ltree data type where available: http://www.postgresql.org/docs/9.1/static/ltree.html

#7641 report it
br0sk at 2012/04/04 07:26am
Memory problem when using a large number of models instances

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:

$ancestor = $this->ancestors(1)->find();

If you look in the afterFind Method in the nested set behaviour it does this:

$owner=$this->getOwner();
self::$_cached[get_class($owner)][$this->_id=self::$_c++]=$owner;

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:

$ancestor->__destruct();

The nested set behaviour has a destructor that removes the object from the static cache.

unset(self::$_cached[get_class($this->getOwner())][$this->_id]);

Suggestion

Add possibility to turn caching off in the behaviour.

Something like:

public function behaviors()
    {
      return array(
            'NestedSetBehavior'=>array(
                'class'=>'application.components.behaviors.trees.NestedSetBehavior',
                'leftAttribute'=>'left',
                'rightAttribute'=>'right',
                'levelAttribute'=>'level',
                'hasManyRoots'=>true,
                'cache'=>false
        ));
    }
#6778 report it
mcsilvio at 2012/02/04 06:12pm
Multiple Users

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.

#6701 report it
yakoza at 2012/01/30 06:38am
use it with CMenu

hi, thanks for your great job is there a best solution to use it with CMenu?

#5491 report it
yiqing95 at 2011/10/16 12:54am
pagination with nestedset

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 ?

#5003 report it
drumaddict at 2011/09/05 08:26am
Nested Set Behavior Administration GUI with jsTree

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

#4978 report it
Spear at 2011/09/02 06:01pm
How to build a CTreeView

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?

#4678 report it
yiqing95 at 2011/08/03 02:18pm
glad to see the example about how to use this fantastic extension.

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 .

#4546 report it
mohitp at 2011/07/19 09:28am
jqGrid Tree with Nested Sets using multiple roots

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:

<?php
Yii::app()->clientScript->registerCoreScript('jquery.ui');
Yii::app()->clientScript->registerCssFile(Yii::app()->baseUrl . '/css/ui-lightness/jquery-ui-1.8.13.custom.css');
Yii::app()->clientScript->registerCssFile(Yii::app()->baseUrl . '/css/ui.jqgrid.css');
Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl . '/js/jqGrid/i18n/grid.locale-en.js', CClientScript::POS_HEAD);
Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl . '/js/jqGrid/jquery.jqGrid.min.js', CClientScript::POS_HEAD);
?>
<table id="list"></table> 
<div id="pager"></div> 
<?php
$script = '
    jQuery("#list").jqGrid(
    {
        treeGrid: true,
        treeGridModel: "adjacency",
        ExpandColumn: "name",
        rowNum: -1,
        url: "' . $this->createUrl("fetchTree") . '",
        datatype: "json",
        treedatatype: "json",
        treeIcons: {plus:"ui-icon-plus",minus:"ui-icon-minus",leaf:"ui-icon-cancel"},
        mtype: "POST",
        ExpandColClick: true,
        colNames:["id", "Group Name"],
        colModel :
            [ 
                {name:"id", index:"id", width:1, hidden: true, key: true},
                {name:"name", index:"name", width:300, sortable: false}
            ],
        height: "auto",
        caption: "View Groups"
    });
';
 
Yii::app()->clientScript->registerScript('jqGrid',$script);
?>

The actionFetchTree method:

public function actionFetchTree()
    {
        $ADDWHERE = '';
        if (isset ($_REQUEST["nodeid"]))
            $node = (integer)$_REQUEST["nodeid"];
        else
            $node = 0;
        // detect if here we post the data from allready loaded tree
        // we can make here other checks
        if( $node >0) 
        {
            //in jqGrid the level starts from 0, but in nested sets with multiple roots the level starts from 1
            $n_lvl = (integer)$_REQUEST["n_level"] + 1;         
            $parent = Yii::app()->db
                    ->createCommand("Select id, root, lft, rgt from tbl_tree WHERE id = $node")
                    ->queryRow();
            $ADDWHERE = " root = " . $parent["root"] . " AND level = " . ($n_lvl+1) . " AND lft > " . $parent["lft"] . " AND rgt < " . $parent["rgt"];
        } 
        else 
        { 
            // initial grid
            $n_lvl =0;
            $ADDWHERE = " level = 1 ";
        }        
        $SQL1 = "SELECT id, root, name, level-1 as lvl, lft, rgt, is_ledger from tbl_tree WHERE $ADDWHERE order by root, lft";
 
        header("Content-type: text/html;charset=utf-8");
        $response->page = 1;
        $response->total = 1;
        $cmd = Yii::app()->db->createCommand($SQL1);
        $result = $cmd->queryAll();
        $i=0;
        foreach ($result as $row)
        {
            if ($row['is_ledger'] == true) $leaf = 'true';else $leaf='false';
            if ($row['lvl'] == 0) $root = NULL; else $root = $parent['id'];
            $l = (integer)$row['lvl'];
            if( $n_lvl ==  $l)
            {
                $response->rows[$i]['cell']=array
                    (
                        $row['id'],
                        $row['name'],
                        $n_lvl,
                        $root,
                        $leaf,
                        'false'
                    );
                $i++;
            }
        }
        echo json_encode($response);
    }

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

#4520 report it
mohitp at 2011/07/16 07:41am
Transaction Checking while deleting node

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.

#4468 report it
mindplay at 2011/07/11 10:33pm
Fantastic work!

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!

#4310 report it
hav3fun at 2011/06/24 07:50am
Several trees in the same table, is it possible?

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

#4180 report it
yiqing95 at 2011/06/14 03:10am
want to know does it can apply to different ActiveRecord class?

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.

#4027 report it
mc.aser at 2011/05/30 02:11pm
How track event change node?, move, create, update, delete?

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 :) !

#3477 report it
juban at 2011/04/14 12:16pm
Hint - Get the position of a node

Hi,

If you need to find the position of a node into its parent branch, you can use something like that in your model:

public function getPosition() {
   return ($this->lft - ($this->parent->lft+1))/2;
}

then simply get the value by using $model->position

Very cool extension by the way ;)

Regards

#3423 report it
MSRQ at 2011/04/11 08:23pm
A complete save example with 'hasManyRoots' => false

Hi,

I am trying to implement this extension with a single root, but I can't find an example for saving nodes with it.

Could somebody please post a save snippet for this case?

Best regards,

#2998 report it
kazio at 2011/03/07 05:25am
default sort by lft

Can you add something like this :

[code]public function descendants($depth=null, $orderByLft=true)[/code]

order by lft is not always needed.

#2954 report it
lavium at 2011/03/01 07:58am
parent_id

It would be nice if we could save the parent_id of the node along with lft/rgt/level. If you save the parent_id then the query to find the direct descendants (not to mention the parent) becomes trivial. It won't affect your other queries, but 2 of the most common (for me, anyway) - direct children and getting the parent - should see big speed increases on large tables. Or am I missing something? :)

#2888 report it
tigo at 2011/02/22 10:30am
and thanks =)

Пасип, мужики, буду пользовать =)

#2887 report it
tigo at 2011/02/22 10:27am
if someone need it - func convert flat result array to hierarchical

If you will use it also need to add childs var in model class.

I put this function in overloaded class

class ENestedSetBehavior2 extends ENestedSetBehavior {
 
    /**
     * Tree must be sorted by left key
     * @param array $tree
     */
    public function hierarchical($tree = array()) {
        if(empty($tree))
            return false;
 
        $tmp = array();
        $parents = array();
        foreach($tree as $k=>$node){
            if($node->level == 1) {
                $tmp[$k] = $node;
                $parents[$node->level] = & $tmp[$k]->childs;
            } else {
                for($i = 1; $i < $node->level; $i++)
                    $parent = & $parents[$i];
 
                $parent[$k] = $node;    
                $parents[$node->level] = & $parent[$k]->childs;
            }
        }
        return $tmp;
    }
 
    /**
     * Named scope
     * @param int depth
     * @param boolean $self - return self node
     * @return CActiveRecord the owner.
     */
    public function descendants($depth=null, $self = false) {
...

Code example, in controller

$root = Category::model()->roots()->findByPk($id);
$branch = $root->descendants(null, true)->findAll();
 
$tree = Category::model()->tree->hierarchical($branch);
$this->render('tree', array(
    'model' => $root,
    'view' => $view,
    'tree' => $tree,
));

and views tree.php

<?php $this->renderPartial('_hierarchical', array('tree'=>$tree));?>

and _hierarchical.php

<?php if(!empty($tree)): ?>
<ul>
<?php
foreach($tree as $node):
?>
    <li>
        <?php echo CHtml::link($node->title, array('viewNode', 'id'=>$node->id)); ?>
        <?php if(!empty($node->childs))
                $this->renderPartial('_hierarchical', array('tree'=>$node->childs));
        ?>
    </li>
<?php 
endforeach;
?>
</ul>
<?php endif; ?>

this code will show somth like this example

Leave a comment

Please to leave your comment.

Create extension
Downloads
No downloadable files yet