NestedSetBehavior for Yii 2

repository·master·Indexed 41 days ago

https://github.com/yiiext/nested-set-behavior

A Yii 2 Active Record behavior that implements the Nested Set model for managing hierarchical tree structures in a database. It provides methods for querying relationships (roots, descendants, ancestors), adding nodes (appendTo, prependTo), and repositioning subtrees (moveAsFirst, moveAsRoot). Supports both single-tree and multiple-tree modes via the hasManyRoots configuration.

Tokens
4.1K
Snippets
12
Records
13
Agent score
97%

What's inside NestedSetBehavior

  1. How to create and add nodes to the tree

    master

    To build a tree, you can create new nodes and position them relative to existing ones.

    Creating Root Nodes: Use saveNode() on a new model instance. Note: In single-tree mode, calling saveNode() when a root already exists will throw a CException.

    Adding Child Nodes: Use the following methods to attach a new node to an existing one:

    • appendTo($node): Adds the node as the last child of $node.
    • prependTo($node): Adds the node as the first child of $node.
    • insertAfter($node): Adds the node immediately after $node (as a sibling).
    • insertBefore($node): Adds the node immediately before $node (as a sibling).

    Example:

    $root = Category::model()->findByPk(1);
    $newChild = new Category;
    $newChild->title = 'New Category';
    $newChild->appendTo($root);
    // Create root nodes
    $root = new Category;
    $root->title = 'Mobile Phones';
    $root->saveNode();
    
    // Add children
    $category1 = new Category;
    $category1->title = 'Ford';
    $root = Category::model()->findByPk(1);
    $category1->appendTo($root);
  2. Install and configure NestedSetBehavior for Yii 2

    master

    To use nested sets in your Yii 2 Active Record models, attach the NestedSetBehavior in your model's behaviors() method. You must specify the attributes used to store the tree structure: leftAttribute, rightAttribute, and levelAttribute.

    Important: Do not add validation rules for these attributes in your model's rules() method, as it can interfere with the behavior's internal calculations.

    Database Schema:

    • For a single tree: Use a schema similar to extensions/yiiext/behaviors/trees/schema.sql.
    • For multiple trees: Use a schema similar to extensions/yiiext/behaviors/trees/schema_many_roots.sql.

    Modes of Operation:

    • Single Tree: Controlled by hasManyRoots => false (default). Only one root is allowed.
    • Multiple Trees: Set hasManyRoots => true. You can also specify a rootAttribute to identify which tree a node belongs to.
    public function behaviors()
    {
        return array(
            'nestedSetBehavior'=>array(
                'class'=>'ext.yiiext.behaviors.model.trees.NestedSetBehavior',
                'leftAttribute'=>'lft',
                'rightAttribute'=>'rgt',
                'levelAttribute'=>'level',
            ),
        );
    }
  3. Create and add nodes to a tree

    master

    Creating Root Nodes

    Use saveNode() on a new model instance.

    • In single tree mode, you can only create one root node. Attempting to create a second will throw a CException.
    • In multiple tree mode, you can create multiple roots.

    Adding Child Nodes

    Use the following methods to position a new node relative to an existing node:

    • appendTo($node): Adds the node as the last child of $node.
    • prependTo($node): Adds the node as the first child of $node.
    • insertAfter($node): Adds the node as a sibling immediately after $node.
    • insertBefore($node): Adds the node as a sibling immediately before $node.
    // Create a root
    $root = new Category;
    $root->title = 'Mobile Phones';
    $root->saveNode();
    
    // Add children
    $child = new Category;
    $child->title = 'iPhone';
    $root->appendTo($child); // Error in logic example, should be $child->appendTo($root);
    
    // Correct usage examples:
    $category1 = new Category; $category1->title = 'Ford';
    $root = Category::model()->findByPk(1);
    $category1->appendTo($root);
    
    $category2 = new Category; $category2->title = 'Mercedes';
    $category2->insertAfter($category1);
  4. Upgrade from v1.0.5 to v1.0.6

    master

    When upgrading from version 1.0.5 to 1.0.6, the methods used to retrieve parent and sibling nodes have changed. You must replace the old method calls with the new fluent API pattern.

    Specifically, replace direct getter methods like getParent(), getPrevSibling(), and getNextSibling() with the new chainable methods parent()->find(), prev()->find(), and next()->find().

    // Old way (v1.0.5)
    $parent = $node->getParent();
    $prevSibling = $node->getPrevSibling();
    $nextSibling = $node->getNextSibling();
    
    // New way (v1.0.6)
    $parent = $node->parent()->find();
    $prevSibling = $node->prev()->find();
    $nextSibling = $node->next()->find();
  5. Install and configure NestedSetBehavior in Yii 2

    master

    To use nested sets, attach NestedSetBehavior to your Yii 2 Active Record model via the behaviors() method.

    Configuration Options

    • leftAttribute: The field name for the left value (default: lft).
    • rightAttribute: The field name for the right value (default: rgt).
    • levelAttribute: The field name for the depth level (default: level).
    • hasManyRoots: Boolean. If false (default), the table represents a single tree. If true, the table can contain multiple trees.
    • rootAttribute: Used in multiple-tree mode to specify the field that identifies the root of a tree.

    Important: Do not add validation rules for the attributes used by the behavior (leftAttribute, rightAttribute, rootAttribute, or levelAttribute) in your model's rules() method, as this can cause issues during tree updates.

    public function behaviors()
    {
        return array(
            'nestedSetBehavior'=>array(
                'class'=>'ext.yiiext.behaviors.model.trees.NestedSetBehavior',
                'leftAttribute'=>'lft',
                'rightAttribute'=>'rgt',
                'levelAttribute'=>'level',
            ),
        );
    }
  6. Retrieve the whole tree

    master

    To fetch all nodes in the tree, use standard Active Record findAll() with an appropriate order.

    • Single tree mode: Order by the lft attribute.
    • Multiple trees mode: Order by root then lft.
    // Single tree per table
    Category::model()->findAll(array('order'=>'lft'));
    
    // Multiple trees per table
    Category::model()->findAll(array('condition'=>'root=?','order'=>'lft'), array($root_id));
  7. Database schemas for Nested Set

    master

    Depending on your tree structure requirements, use one of the following SQL schemas:

    • Single tree per table: Use extensions/yiiext/behaviors/trees/schema.sql.
    • Multiple trees per table: Use extensions/yiiext/behaviors/trees/schema_many_roots.sql.
  8. Traverse the tree without recursion

    master

    To avoid deep recursion when rendering or processing a tree, you can iterate through all nodes ordered by their lft attribute. This allows you to build a nested structure (like an HTML <ul>/<li> list) in a single loop by tracking the level attribute.

    Implementation Pattern:

    1. Fetch all nodes ordered by lft (and root if using multiple trees).
    2. Compare the current node's level with the previous node's level.
    3. If level increases, open a new nested container (e.g., <ul>).
    4. If level decreases, close the necessary number of containers.
    $criteria = new CDbCriteria;
    $criteria->order = 't.lft'; // Use 't.root, t.lft' for multiple trees
    $categories = Category::model()->findAll($criteria);
    $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 - $category->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--;)
    {
        echo CHtml::closeTag('li')."\n";
        echo CHtml::closeTag('ul')."\n";
    }
  9. How to move and modify tree nodes

    master

    The behavior provides several methods to reposition nodes within the hierarchy without manually recalculating lft and rgt values.

    Moving Nodes:

    • moveAsFirst($node): Moves the current node to be the first child of $node.
    • moveAsLast($node): Moves the current node to be the last child of $node.
    • moveBefore($node): Moves the current node to be the immediate sibling before $node.
    • moveAfter($node): Moves the current node to be the immediate sibling after $node.
    • moveAsRoot(): Converts the current node into a new root. All its descendants become descendants of this new root.

    Identifying Node Types: Use these methods to check the status of a node:

    • isRoot(): Returns true if the node is a root.
    • isLeaf(): Returns true if the node has no children.
    • isDescendantOf($node): Returns true if the current node is a descendant of $node.
    // Move a node to be the first child of another node
    $node->moveAsFirst($target);
    
    // Move a node to become a new root
    $node->moveAsRoot();
    
    // Check node status
    $node->isRoot();
    $node->isLeaf();
    $node->isDescendantOf($otherNode);
  10. How to query tree nodes (Selection methods)

    master

    The NestedSetBehavior provides several methods to retrieve nodes based on their position in the tree. These methods return Active Record query objects, allowing you to chain further methods like findAll() or find().

    Assuming a model Category:

    • Get all roots: Use roots() on the model class.
    • Get all descendants: Use descendants() on a specific node instance (includes all levels below).
    • Get direct children: Use children() on a specific node instance (only the immediate level below).
    • Get all ancestors: Use ancestors() on a specific node instance (all nodes above).
    • Get parent: Use parent() on a specific node instance (the immediate node above).
    • Get siblings: Use next() or prev() on a specific node instance.
    • Get the whole tree: Use standard Active Record findAll() methods.
      • Single tree mode: findAll(['order' => 'lft'])
      • Multiple trees mode: findAll(['condition' => 'root=?', 'order' => 'lft'], [$rootId])
    // Get all roots
    $roots = Category::model()->roots()->findAll();
    
    // Get all descendants of a node
    $category = Category::model()->findByPk(1);
    $descendants = $category->descendants()->findAll();
    
    // Get direct children
    $children = $category->children()->findAll();
    
    // Get all ancestors
    $ancestors = $category->ancestors()->findAll();
    
    // Get parent
    $parent = $category->parent()->find();
    
    // Get next sibling
    $nextSibling = $category->next()->find();
  11. Select nodes from a tree

    master

    The behavior provides several methods to query tree relationships using Active Record. These methods return an ActiveQuery object, allowing you to chain further methods like findAll() or find().

    Querying Methods

    • roots(): Returns all root nodes in the table.
    • descendants(): Returns all nodes below the current node.
    • children(): Returns only the immediate children of the current node.
    • ancestors(): Returns all nodes above the current node in the hierarchy.
    • parent(): Returns the immediate parent of the current node.
    • prev(): Returns the previous sibling.
    • next(): Returns the next sibling.
    // Get all roots
    $roots = Category::model()->roots()->findAll();
    
    // Get all descendants of a specific node
    $category = Category::model()->findByPk(1);
    $descendants = $category->descendants()->findAll();
    
    // Get immediate children
    $children = $category->children()->findAll();
    
    // Get all ancestors
    $ancestors = $category->ancestors()->findAll();
    
    // Get parent
    $parent = $category->parent()->find();
    
    // Get next sibling
    $nextSibling = $category->next()->find();
  12. Move nodes within a tree

    master

    The behavior provides several methods to reposition nodes and their entire subtrees:

    • moveAsFirst($node): Moves the current node to be the first child of $node.
    • moveAsLast($node): Moves the current node to be the last child of $node.
    • moveBefore($node): Moves the current node to be a sibling immediately before $node.
    • moveAfter($node): Moves the current node to be a sibling immediately after $node.
    • moveAsRoot(): Moves the current node (and all its descendants) to become a new root node.
    // Move a node to be the first child of another node
    $node->moveAsFirst($target);
    
    // Move a node to be a new root
    $node->moveAsRoot();