Skip to content
244 changes: 242 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<a href="https://github.com/yii2-extensions/nested-sets-behavior" target="_blank">
<img src="https://www.yiiframework.com/image/yii_logo_light.svg" height="100px;">
</a>
<h1 align="center">Nested sets behavior.</h1>
<h1 align="center">Nested sets behavior</h1>
<br>
</p>

Expand All @@ -19,25 +19,265 @@
<a href="https://github.com/yii2-extensions/nested-sets-behavior/actions/workflows/build.yml" target="_blank">
<img src="https://github.com/yii2-extensions/nested-sets-behavior/actions/workflows/build.yml/badge.svg" alt="PHPUnit">
</a>
<a href="https://dashboard.stryker-mutator.io/reports/github.com/yii2-extensions/nested-sets-behavior/main" target="_blank">
<img src="https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyii2-extensions%2Fnested-sets-behavior%2Fmain" alt="Mutation Testing">
</a>
<a href="https://github.com/yii2-extensions/nested-sets-behavior/actions/workflows/static.yml" target="_blank">
<img src="https://github.com/yii2-extensions/nested-sets-behavior/actions/workflows/static.yml/badge.svg" alt="Static Analysis">
</a>
</p>

A powerful behavior for managing hierarchical data structures using the nested sets pattern in Yii ActiveRecord models.

Efficiently store and query tree structures like categories, menus, organizational charts, and any hierarchical data
with high-performance database operations.

## Features

- ✅ **Efficient Tree Operations** - Insert, move, delete nodes with automatic boundary management.
- ✅ **Flexible Queries** - Find ancestors, descendants, siblings, leaves, and roots.
- ✅ **Multiple Trees Support** - Manage multiple independent trees in the same table.
- ✅ **Query Optimization** - Single-query operations for maximum performance.
- ✅ **Transaction Safety** - All operations are wrapped in database transactions.
- ✅ **Validation & Error Handling** - Comprehensive validation with clear error messages.

## Quick start

### Installation

```bash
composer require --prefer-dist yii2-extensions/nested-sets-behavior
composer require yii2-extensions/nested-sets-behavior
```

### How it works

1. **Creates root nodes** using the nested sets pattern with `lft`, `rgt`, and `depth` fields.
2. **Manages hierarchy** automatically when inserting, moving, or deleting nodes.
3. **Optimizes queries** using boundary values for efficient tree traversal.
4. **Supports transactions** to ensure data integrity during complex operations.

### Basic Configuration

Add the behavior to your ActiveRecord model.

```php
<?php

declare(strict_types=1);

use yii\db\ActiveRecord;
use yii2\extensions\nestedsets\NestedSetsBehavior;

/**
* @phpstan-property int $depth
* @phpstan-property int $id
* @phpstan-property int $lft
* @phpstan-property int $rgt
*/
class Category extends ActiveRecord
{
public static function tableName(): string
{
return '{{%category}}';
}

public function behaviors(): array
{
return [
'nestedSets' => [
'class' => NestedSetsBehavior::class,
// 'treeAttribute' => 'tree', // Enable for multiple trees
// 'leftAttribute' => 'lft', // Default: 'lft'
// 'rightAttribute' => 'rgt', // Default: 'rgt'
// 'depthAttribute' => 'depth', // Default: 'depth'
],
];
}

public function transactions(): array
{
return [
self::SCENARIO_DEFAULT => self::OP_ALL,
];
}
}
```

### Database schema

Create the required database fields.

```sql
-- Single tree structure
CREATE TABLE category (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL,
depth INT NOT NULL
);

-- Multiple trees structure
CREATE TABLE category (
id INT PRIMARY KEY AUTO_INCREMENT,
tree INT,
name VARCHAR(255) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL,
depth INT NOT NULL
);
```

### Basic Usage

#### Creating and building trees

```php
<?php

declare(strict_types=1);

// Create root node
$root = new Category(['name' => 'Electronics']);
$root->makeRoot();

// Add children
$phones = new Category(['name' => 'Mobile Phones']);
$phones->appendTo($root);

$computers = new Category(['name' => 'Computers']);
$computers->appendTo($root);

// Add grandchildren
$smartphone = new Category(['name' => 'Smartphones']);
$smartphone->appendTo($phones);

$laptop = new Category(['name' => 'Laptops']);
$laptop->appendTo($computers);
```

#### Querying the tree

```php
<?php

declare(strict_types=1);

// Get all descendants of a node
$children = $root->children()->all();

// Get only direct children
$directChildren = $root->children(1)->all();

// Get all ancestors of a node
$parents = $smartphone->parents()->all();

// Get all leaf nodes (nodes without children)
$leaves = $root->leaves()->all();

// Navigate siblings
$nextSibling = $phones->next()->one();
$prevSibling = $computers->prev()->one();
```

#### Moving nodes

```php
<?php

declare(strict_types=1);

// Move as last child
$smartphone->appendTo($computers);

// Move as first child
$smartphone->prependTo($phones);

// Move as next sibling
$smartphone->insertAfter($laptop);

// Move as previous sibling
$smartphone->insertBefore($laptop);

// Make node a new root (multiple trees only)
$smartphone->makeRoot();
```

#### Deleting nodes

```php
<?php

declare(strict_types=1);

// Delete node only (children become children of parent)
$phones->delete();

// Delete node with all descendants
$phones->deleteWithChildren();
```

### Query builder integration

Add query behavior for advanced tree queries.

```php
<?php

declare(strict_types=1);

use yii\db\ActiveQuery;
use yii2\extensions\nestedsets\NestedSetsQueryBehavior;

/**
* @template T of Category
*
* @extends ActiveQuery<T>
*/
class CategoryQuery extends ActiveQuery
{
public function behaviors(): array
{
return [
'nestedSetsQuery' => NestedSetsQueryBehavior::class,
];
}
}

// In your Category model
/**
* @phpstan-return CategoryQuery<static>
*/
public static function find(): CategoryQuery
{
return new CategoryQuery(static::class);
}
```

Now you can use enhanced queries.

```php
<?php

declare(strict_types=1);

// Find all root nodes
$roots = Category::find()->roots()->all();

// Find all leaf nodes
$leaves = Category::find()->leaves()->all();
```

## Documentation

For detailed configuration options and advanced usage.

- 📚 [Installation Guide](docs/installation.md)
- ⚙️ [Configuration Reference](docs/configuration.md)
- 💡 [Usage Examples](docs/examples.md)
- 🧪 [Testing Guide](docs/testing.md)


## Quality code

[![Latest Stable Version](https://poser.pugx.org/yii2-extensions/nested-sets-behavior/v)](https://github.com/yii2-extensions/nested-sets-behavior/releases)
Expand Down
Loading