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:
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.
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.
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?
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
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
One more thing how to use the delete function? If just get the model instance and delete it will not be called… and will not remove the descendants. Shouldnt it use the beforeDelete event?
Because if i change it too:
/**
* Deletes node and it's descendants.
* @return boolean whether the deletion is successful.
*/
public function beforeDelete($event) {
parent::beforeDelete($event);
$owner=$this->getOwner();
if($owner->getIsNewRecord())
throw new CDbException(Yii::t('yiiext','The node cannot be deleted because it is new.'));
$transaction=$owner->getDbConnection()->beginTransaction();
try
{
$root=$this->hasManyRoots ? $owner->{$this->root} : null;
if($owner->isLeaf())
$result=$owner->delete();
else
{
$condition=$this->left.'>='.$owner->{$this->left}.' AND '.
$this->right.'<='.$owner->{$this->right};
if($root!==null)
$condition.=' AND '.$this->root.'='.$root;
$result=$owner->deleteAll($condition)>0;
}
if($result)
{
$first=$owner->{$this->right}+1;
$delta=$owner->{$this->left}-$owner->{$this->right}-1;
$this->shiftLeftRight($first,$delta,$root);
$transaction->commit();
return true;
}
}
catch(Exception $e)
{
$transaction->rollBack();
}
return false;
}