Usage examples
این محتوا هنوز به زبان شما در دسترس نیست.
This document provides comprehensive examples of how to use the Yii Nested Sets Behavior in real-world hierarchical data
management scenarios. Use a model configured as in the configuration guide, with migrations applied.
Snippets are separate recipes; they do not form one executable script. Check structural methods’ false results and
model errors in application code. Refresh other loaded instances before querying their tree state. Echoed names below
illustrate CLI output; encode names when displaying them in HTML.
Building tree structures
Section titled “Building tree structures”Creating root nodes
Section titled “Creating root nodes”<?php
declare(strict_types=1);
use app\models\Category;
// Create a single root for the entire tree$root = new Category(['name' => 'All Categories']);$root->makeRoot();
echo "Root created: ID={$root->id}, Left={$root->lft}, Right={$root->rgt}, Depth={$root->depth}\n";// Output: Root created: ID=1, Left=1, Right=2, Depth=0Creating multiple tree roots
Section titled “Creating multiple tree roots”<?php
declare(strict_types=1);
// For models with treeAttribute enabled$electronicsRoot = new Category(['name' => 'Electronics']);$electronicsRoot->makeRoot();
$clothingRoot = new Category(['name' => 'Clothing']);$clothingRoot->makeRoot();
$homeRoot = new Category(['name' => 'Home & Garden']);$homeRoot->makeRoot();
// Each root has its own tree identifierecho "Electronics tree: {$electronicsRoot->tree}\n";echo "Clothing tree: {$clothingRoot->tree}\n";echo "Home tree: {$homeRoot->tree}\n";Building a complete category hierarchy
Section titled “Building a complete category hierarchy”<?php
declare(strict_types=1);
// Start with a root$root = new Category(['name' => 'Store Categories']);$root->makeRoot();
// Add main categories$electronics = new Category(['name' => 'Electronics']);$electronics->appendTo($root);
$clothing = new Category(['name' => 'Clothing']);$clothing->appendTo($root);
// Add subcategories to Electronics$phones = new Category(['name' => 'Mobile Phones']);$phones->appendTo($electronics);
$computers = new Category(['name' => 'Computers']);$computers->appendTo($electronics);
// Add sub-subcategories$smartphones = new Category(['name' => 'Smartphones']);$smartphones->appendTo($phones);
$featurePhones = new Category(['name' => 'Feature Phones']);$featurePhones->appendTo($phones);
$laptops = new Category(['name' => 'Laptops']);$laptops->appendTo($computers);
$desktops = new Category(['name' => 'Desktop Computers']);$desktops->appendTo($computers);
// Result tree structure:// Store Categories (1,18)// ├── Electronics (2,15)// │ ├── Mobile Phones (3,8)// │ │ ├── Smartphones (4,5)// │ │ └── Feature Phones (6,7)// │ └── Computers (9,14)// │ ├── Laptops (10,11)// │ └── Desktop Computers (12,13)// └── Clothing (16,17)Inserting and positioning nodes
Section titled “Inserting and positioning nodes”Adding nodes as children
Section titled “Adding nodes as children”<?php
declare(strict_types=1);
// Append as last child$accessories = new Category(['name' => 'Accessories']);$accessories->appendTo($electronics); // Becomes last child
// Prepend as first child$tablets = new Category(['name' => 'Tablets']);$tablets->prependTo($electronics); // Becomes first child
// Result: Electronics now has Tablets, Mobile Phones, Computers, AccessoriesInserting nodes as siblings
Section titled “Inserting nodes as siblings”<?php
declare(strict_types=1);
// Insert after a specific node$gaming = new Category(['name' => 'Gaming']);$gaming->insertAfter($computers); // Places Gaming after Computers
// Insert before a specific node$audio = new Category(['name' => 'Audio']);$audio->insertBefore($phones); // Places Audio before Mobile Phones
// If applied after the preceding example: Tablets, Audio, Mobile Phones, Computers, Gaming, AccessoriesPositioning nodes with specific ordering
Section titled “Positioning nodes with specific ordering”<?php
declare(strict_types=1);
class ProductController extends Controller{ public function actionOrganizeCategories(): void { $electronics = Category::findOne(['name' => 'Electronics']);
// Create categories in desired order $categories = [ 'Computers', 'Mobile Phones', 'Audio', 'Gaming', 'Accessories' ];
$previousCategory = null;
foreach ($categories as $categoryName) { $category = new Category(['name' => $categoryName]);
if ($previousCategory === null) { // First category becomes first child $category->prependTo($electronics); } else { // Subsequent categories inserted after previous $category->insertAfter($previousCategory); }
$previousCategory = $category; } }}Querying tree data
Section titled “Querying tree data”Finding ancestors and descendants
Section titled “Finding ancestors and descendants”<?php
declare(strict_types=1);
// Get all ancestors of a node (breadcrumb path)$smartphone = Category::findOne(['name' => 'Smartphones']);$breadcrumbs = $smartphone->parents()->all();
foreach ($breadcrumbs as $ancestor) { echo "{$ancestor->name} > ";}
echo $smartphone->name;// Output: Store Categories > Electronics > Mobile Phones > Smartphones
// Get all descendants of a node$electronics = Category::findOne(['name' => 'Electronics']);$allDescendants = $electronics->children()->all();
echo "Electronics has " . count($allDescendants) . " descendants:\n";
foreach ($allDescendants as $descendant) { echo str_repeat(' ', $descendant->depth - $electronics->depth) . $descendant->name . "\n";}Finding direct children and parents
Section titled “Finding direct children and parents”<?php
declare(strict_types=1);
// Get only direct children (depth = current depth + 1)$electronics = Category::findOne(['name' => 'Electronics']);$directChildren = $electronics->children(1)->all();
echo "Direct children of Electronics:\n";
foreach ($directChildren as $child) { echo "- {$child->name}\n";}
// Get only direct parent$smartphone = Category::findOne(['name' => 'Smartphones']);$directParent = $smartphone->parents(1)->one();
echo "Direct parent of {$smartphone->name}: {$directParent->name}\n";Finding siblings
Section titled “Finding siblings”<?php
declare(strict_types=1);
// Get the next sibling$phones = Category::findOne(['name' => 'Mobile Phones']);$nextSibling = $phones->next()->one();
if ($nextSibling) { echo "Next sibling: {$nextSibling->name}\n";}
// Get previous sibling$prevSibling = $phones->prev()->one();
if ($prevSibling) { echo "Previous sibling: {$prevSibling->name}\n";}
// Get all siblings (including self)$allSiblings = $phones->parents(1)->one()->children(1)->all();
echo "All siblings:\n";
foreach ($allSiblings as $sibling) { $current = ($sibling->id === $phones->id) ? ' (current)' : ''; echo "- {$sibling->name}{$current}\n";}Finding leaf nodes
Section titled “Finding leaf nodes”<?php
declare(strict_types=1);
// Get all leaf nodes in a subtree$electronics = Category::findOne(['name' => 'Electronics']);$leaves = $electronics->leaves()->all();
echo "Leaf categories in Electronics:\n";
foreach ($leaves as $leaf) { echo "- {$leaf->name}\n";}
// Get all leaf nodes in the entire tree$allLeaves = Category::find()->leaves()->all();
echo "All leaf categories:\n";
foreach ($allLeaves as $leaf) { echo "- {$leaf->name}\n";}Finding root nodes
Section titled “Finding root nodes”<?php
declare(strict_types=1);
// Get all root nodes (useful for multiple trees)$roots = Category::find()->roots()->all();
echo "All root categories:\n";
foreach ($roots as $root) { echo "- {$root->name} (Tree ID: {$root->tree})\n";}Moving and reorganizing nodes
Section titled “Moving and reorganizing nodes”Moving nodes within the same tree
Section titled “Moving nodes within the same tree”<?php
declare(strict_types=1);
// Move a node to a different parent$smartphones = Category::findOne(['name' => 'Smartphones']);$accessories = Category::findOne(['name' => 'Accessories']);
// Move Smartphones to be under Accessories$smartphones->appendTo($accessories);
// Move as sibling$tablets = Category::findOne(['name' => 'Tablets']);$computers = Category::findOne(['name' => 'Computers']);
// Move Tablets to be after Computers$tablets->insertAfter($computers);Reorganizing tree structure
Section titled “Reorganizing tree structure”<?php
declare(strict_types=1);
class CategoryController extends Controller{ public function actionReorganize(): void { // Reorganize Electronics category structure $electronics = Category::findOne(['name' => 'Electronics']);
// Create a new subcategory structure $mobile = new Category(['name' => 'Mobile Devices']); $mobile->appendTo($electronics);
// Move existing categories under new structure $phones = Category::findOne(['name' => 'Mobile Phones']); $tablets = Category::findOne(['name' => 'Tablets']);
$phones->appendTo($mobile); $tablets->appendTo($mobile);
// Result: Electronics > Mobile Devices > (Mobile Phones, Tablets) }}Moving nodes between trees (multiple trees only)
Section titled “Moving nodes between trees (multiple trees only)”<?php
declare(strict_types=1);
// Move a node from one tree to another$gaming = Category::findOne(['name' => 'Gaming']);$entertainmentRoot = Category::findOne(['name' => 'Entertainment']);
// Move Gaming from Electronics tree to Entertainment tree$gaming->appendTo($entertainmentRoot);
// The entire Gaming subtree moves to the new treeecho "Gaming moved to tree: {$gaming->tree}\n";Making existing nodes into new roots
Section titled “Making existing nodes into new roots”<?php
declare(strict_types=1);
// Convert a node and its subtree into a new independent tree$gaming = Category::findOne(['name' => 'Gaming']);
// Make Gaming a root of its own tree (multiple trees only)$gaming->makeRoot();
echo "Gaming is now root of tree: {$gaming->tree}\n";
// All descendants of Gaming maintain their relative positions$gamingChildren = $gaming->children()->all();
foreach ($gamingChildren as $child) { echo "- {$child->name} (Tree: {$child->tree})\n";}Deleting nodes
Section titled “Deleting nodes”Deleting individual nodes
Section titled “Deleting individual nodes”<?php
declare(strict_types=1);
// Delete a node - children move up to parent$mobilePhones = Category::findOne(['name' => 'Mobile Phones']);$children = $mobilePhones->children()->all();
echo "Before deletion, Mobile Phones has " . count($children) . " children\n";
// Delete the node - children become children of Electronics$mobilePhones->delete();
// Verify children moved up$electronics = Category::findOne(['name' => 'Electronics']);$newChildren = $electronics->children(1)->all();
echo "After deletion, Electronics direct children:\n";
foreach ($newChildren as $child) { echo "- {$child->name}\n";}Deleting nodes with all descendants
Section titled “Deleting nodes with all descendants”<?php
declare(strict_types=1);
// Delete a node and all its descendants$electronics = Category::findOne(['name' => 'Electronics']);$descendantsCount = count($electronics->children()->all());
echo "Electronics has {$descendantsCount} descendants\n";
// Delete Electronics and everything under it$deletedCount = $electronics->deleteWithChildren();
echo "Deleted {$deletedCount} nodes total\n";Deletion with a child check
Section titled “Deletion with a child check”Acquire the table lock before reading the state used to decide a write, and bypass query caching. This recipe owns a fresh transaction; when composing a larger unit of work, acquire the lock before establishing its read snapshot.
<?php
declare(strict_types=1);
use app\models\Category;
class CategoryService{ public function deleteCategory(int $categoryId, bool $deleteChildren = false): array { if (Category::getDb()->getTransaction() !== null) { throw new \LogicException('Start this operation outside an existing transaction.'); }
return Category::getDb()->transaction(static function () use ($categoryId, $deleteChildren): array { (new Category())->acquireTreeLock(); $category = Category::find()->where(['id' => $categoryId])->noCache()->one();
if ($category === null) { return ['success' => false, 'error' => 'Category not found']; }
if (!$deleteChildren && $category->children()->orderBy(null)->noCache()->exists()) { return ['success' => false, 'error' => 'Category has children']; }
// The child check above permits this for a leaf, including a childless root. $deletedCount = $category->deleteWithChildren();
if ($deletedCount === false) { throw new \RuntimeException('Category deletion was cancelled.'); }
return ['success' => true, 'deleted' => $deletedCount]; }); }}The existence check removes ordering for SQL Server compatibility. The count includes the selected node. Database
exceptions propagate after rollback; retry the complete transaction with fresh records when appropriate.
deleteWithChildren() runs delete hooks on the selected node only, not on each descendant. Put required descendant
cleanup in application code within the same transaction.
Advanced queries and operations
Section titled “Advanced queries and operations”Finding nodes by depth level
Section titled “Finding nodes by depth level”<?php
declare(strict_types=1);
// Find all categories at a specific depth level$electronics = Category::findOne(['name' => 'Electronics']);
// Get all grandchildren (depth = electronics.depth + 2)$grandchildren = $electronics->children(2) ->andWhere(['depth' => $electronics->depth + 2]) ->all();
echo "Grandchildren of Electronics:\n";
foreach ($grandchildren as $grandchild) { echo "- {$grandchild->name}\n";}Building tree menus
Section titled “Building tree menus”<?php
declare(strict_types=1);
use app\models\Category;use yii\base\Widget;use yii\helpers\{Html, Url};
class MenuWidget extends Widget{ public $rootId; public $maxDepth = 3;
public function run(): string { $root = Category::findOne($this->rootId);
if (!$root) { return ''; }
$tree = $this->buildTreeArray($root);
return $this->renderTree($tree); }
private function buildTreeArray($root): array { $children = $root->children($this->maxDepth)->all();
$tree = []; $currentDepth = $root->depth;
foreach ($children as $node) { $tree[] = [ 'id' => $node->id, 'name' => $node->name, 'depth' => $node->depth - $currentDepth, 'url' => Url::to(['category/view', 'id' => $node->id]), ]; }
return $tree; }
private function renderTree(array $tree): string { $html = '<ul class="tree-menu">';
foreach ($tree as $item) { $indent = str_repeat(' ', $item['depth']); $html .= '<li class="depth-' . $item['depth'] . '">'; $html .= Html::a(Html::encode($item['name']), $item['url']); $html .= '</li>'; }
$html .= '</ul>';
return $html; }}Checking node relationships
Section titled “Checking node relationships”<?php
declare(strict_types=1);
class CategoryService{ public function isAncestor(Category $ancestor, Category $descendant): bool { return $descendant->isChildOf($ancestor); }
public function isDescendant(Category $descendant, Category $ancestor): bool { return $descendant->isChildOf($ancestor); }
public function isSibling(Category $node1, Category $node2): bool { // Nodes are siblings if they have the same direct parent $parent1 = $node1->parents(1)->one(); $parent2 = $node2->parents(1)->one();
return $parent1 && $parent2 && $parent1->id === $parent2->id; }
public function getCommonAncestor(Category $node1, Category $node2): ?Category { $ancestors1 = $node1->parents()->all(); $ancestors2 = $node2->parents()->all();
// Find common ancestors $commonAncestors = array_intersect( array_column($ancestors1, 'id'), array_column($ancestors2, 'id') );
if (empty($commonAncestors)) { return null; }
// Return the deepest common ancestor return Category::find() ->where(['id' => $commonAncestors]) ->orderBy(['depth' => SORT_DESC]) ->one(); }}Basic tree checks
Section titled “Basic tree checks”These checks are diagnostic only: queries based on stored boundaries cannot discover every gap, overlap, orphan, or duplicate boundary. Run a complete integrity audit with writers stopped before accepting migrated data.
<?php
declare(strict_types=1);
class TreeValidator{ public function validateTreeIntegrity(?int $treeId = null): array { $errors = [];
$query = Category::find(); if ($treeId !== null) { $query->andWhere(['tree' => $treeId]); }
$categories = $query->all();
foreach ($categories as $category) { // Check left < right if ($category->lft >= $category->rgt) { $errors[] = "Invalid boundaries for {$category->name}: lft={$category->lft}, rgt={$category->rgt}"; }
// Check children boundaries $children = $category->children(1)->all(); foreach ($children as $child) { if ($child->lft <= $category->lft || $child->rgt >= $category->rgt) { $errors[] = "Child {$child->name} boundaries invalid relative to parent {$category->name}"; }
if ($child->depth !== $category->depth + 1) { $errors[] = "Child {$child->name} has incorrect depth"; } } }
return $errors; }}This comprehensive examples guide demonstrates practical usage patterns for the Yii Nested Sets Behavior across different scenarios, from basic tree building to complex hierarchical data management in real-world applications.