Nested set

This extension allows AR models to work with nested sets tree.

http://www.yiiframework.com/extension/nestedsetbehavior/

Can be used with http://www.yiiframework.com/forum/index.php?/topic/9434-extension-ejnestedtreeactions/.

Tried using this for nested comments, so $manyRoots == true.

I got a DB error when trying to save my first comment: "root does not have a default value"

Adding


$owner->{$this->root}=0;

at line 552 fixed the issue.

Maybe this could go into Zii: http://code.google.com/p/zii/issues/detail?id=2

Yeti

What are your schema and the code you are using?

The schema is as provided in the download.

$comment = new Comment();

// If form submitted

$comment->saveNode();

When added to the behavior, the following methods return the descendants of a node as a tree of related nodes;

The returned array is a list of child nodes.

Each child has it’s children as a list of related models in the descendants related models and so on.

The benefit is that this allows the application to use recursive code to traverse the descendants.




/**

 * Returns descendants of the current node as a tree.

 * The returned array is a list of child nodes.

 * Each child has it's children as a list of related models in the

 * [i]descendants[/i] related models and so on.

 * @param integer The depth of descendants to fetch

 * @return array Descendants of the current node

 */

public function getDescendants($depth = null) {

  $descendants = $this->descendants($depth)->findAll();

  $_descendants = array();

  while ($descendants)

    $_descendants[] = $this->descendants2tree($descendants);

  return $_descendants;

} 

	

/**

 * Recursive function to add descendants as related records

 * @param array Remaining descendants

 * @return CModel Branch with descendants as related records

 */

private function descendants2tree(&$descendants) {

  $branch = array_shift($descendants);

  while ($descendants) {

    if ($descendants[0]->{$this->left} < $branch->{$this->right})

      $branch->addRelatedRecord('descendants', $this->descendants2tree($descendants), true);

    else

      break;

  }

  return $branch;

}



Usage


$descendants = $node->getDescendants($depth);

or if all descendants are required - i.e. not depth limited


$descendants = $node->descendants;

Now - for example - in a view (called "descendants") you can do something like this to render the whole descendant tree in a nested list:




<ul>

<?php foreach ($descendants as $descendant): ?>

  <li>

    <div><?php echo $descendant->content; ?></div>

<?php $this->renderPartial('descendants', array('descendants'=>$descendant->descendants)); ?>

  </li>

<?php endforeach; ?>

<ul>



And finally (for today :) ); very minor, but I found it made more sense in my code to have the level starting at zero rather one. This way when using foreach($nodes as $level=>$node) at the top of the tree $level == $node->level.

There is a new version in SVN. Will be released as archive soon.

changelog

— Unit tests added (creocoder)

— Incorrect usage of save() and delete() instead of behavior’s saveNode() and deleteNode() is now detected (creocoder)

Yeti

Please try to reproduce your issue with DB error.

getDescendants looks like presentational method. I think it should not be included into behavior itself. Maybe a widget will fit.

Level starting at zero isn’t better in any way plus most implementations are using level started at one so it will be easier to migrate when needed.

to reproduce just remove the change above then call the saveNode() method with $manyRoots true.

Tracing through what is going on is:

  • The schema supplied declares root as NOT NULL.

  • the makeRoot() method does not (as supplied) set root to any value before inserting the record, i.e. root for the record is NULL when it is saved.

To prevent the error root needs to be set to something prior to saving the record (zero made sense to me for a new root node and the behavior I think is the right place to do it), or the schema needs to allow root to be NULL; of the two setting root to a value for me seems the better solution.

We might be getting into semantics; getDescendants returns the data in a particular format which of itself it is not presentational. Though as the example shows the view may become simpler; kind of depends what you are doing in the view I guess.

Whether it goes in your behavior is of course your call; for those that want to use it perhaps a widget or a class extending ENestedSetBehavior.

Said it was minor :)

Fixed SQL schema.

hey, guys!

first of all, Samdark, thanks for this extension!

I have a little doubt: how do you create a CRUD for this kind of table?

I mean, when you crud the model, all you get is a form where you have to manually fill the root, left, right and level fields…this is not that easy imho and i’m sure you guys have solved this more elegantly

could you please share your experience on how to crud this model?

any help is very appreciated

:)

regards!

scoob.junior

Creocoder implemented it. I’m just tested it and wrote documentation.

To manage tree (taxonomy) I’m personally using this one: http://www.yiiframework.com/forum/index.php?/topic/9434-extension-ejnestedtreeactions/

thank you very much, Samdark, i’m testing this right now…facing some troubles in rendering but have already posted in the EJNestedTreeActions forum topic

regards!!

:)

I am trying somoething very simple like…




$root=Categories::model()->roots()->findByPk(1);        

        $cat = new Categories();

        $cat->name = 'Cool';


        $root->append($cat);

        $root->save();


// Does nothing too <img src='http://www.yiiframework.com/forum/public/style_emoticons/default/sad.gif' class='bbc_emoticon' alt=':(' />

$cat->save();



Or…




$cat = new Categories();

        $cat->name = 'Cool';

        $cat->appendTo($root);

        $cat->save();



But having no luck nothing get’s saved :( Could you please add some documentation how to actually add / modify / delete nodes :)

Also please add to the documentation what values a root needs left: 1, right:0, root: (primary id) Else it would throw some sql errors…

Ah sorry… I had validation in my model on lft / rgt / lvl… But the behavior runs after validation.

And I notice the root needst left = 1 / rgt = 2 else it will move the root to child :)

Any reason why there is not a addRoot method ?

Also it be nice to have some convience method to have the tree return in an multi dimensional associative array… As far as I can see now descendants()->findAll() just returns all records in a flat array.

Also how to get the the path? Like I select a category but want to get the entire path all the way up, usefull for breadcrumbs and filtering in categories.

like…


SELECT parent.name

FROM nested_category AS node,

nested_category AS parent

WHERE node.lft BETWEEN parent.lft AND parent.rgt

AND node.name = 'FLASH'

ORDER BY parent.lft;

Also I see now that my first error was because validation is called before the makeRoot function… maybe better to call this after the makeRoot so the model can still have validation rules. And not have to call validate = false

Ok i needed this badly so i added my own function… but if anybody can show how to do with activerecord plz do :)




    /**

     * Returns the path of the node

     * @return array

     */

    public function getPath()

    {

        $owner = $this->getOwner();

        $alias = $owner->getTableAlias();

        $pk = $owner->primaryKey;

        

        $tbl_select = "";

        $total = count($owner->tableSchema->columns);

        $i = 0;

        foreach($owner->tableSchema->columns as $column => $value){

            $i++;

            $tbl_select .= "parent.".$column;

            if($i < $total){

                $tbl_select .= ", ";

            }

        }

        

        $sql = "SELECT $tbl_select

            FROM ".$owner->tableSchema->name." AS node,

            ".$owner->tableSchema->name." AS parent

            WHERE node.lft BETWEEN parent.lft AND parent.rgt

            AND node.".$owner->tableSchema->primaryKey." = ".$pk."

            AND parent.".$this->level." > 0 ";

        if($this->hasManyRoots){

            $sql .= "AND parent.".$this->root." = ".$owner->{$this->root}." ";

        }

        $sql .= " ORDER BY parent.lft;";


        $command = Yii::app()->db->createCommand($sql);        

        $result = $command->queryAll();

        

        return $result;

    }



One more thing… I think tables now are not being locked when making modifications?

Or is the transaction enough?

Transactions should be enough but if you’ll get a problem with data, let me know.

Btw how to move the node to a new parent? I only see there is moveBefore / moveAsFirst and append does not seem to work :(

And why if I want to use the move methods there are restrictions that the node cannot be a descendant? If I want to move a child to top cannot ?