PHPackages                             lunarphp/nestedset - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. lunarphp/nestedset

ActiveLibrary[Database &amp; ORM](/categories/database)

lunarphp/nestedset
==================

Nested Set Model for Laravel 12+ (Lunar-maintained fork of kalnoy/nestedset)

1.0.0(1mo ago)011.4k↑421%MITPHPPHP ^8.3CI passing

Since Jun 1Pushed 1mo agoCompare

[ Source](https://github.com/lunarphp/nestedset)[ Packagist](https://packagist.org/packages/lunarphp/nestedset)[ RSS](/packages/lunarphp-nestedset/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (7)Versions (2)Used By (0)

Nested Set for Laravel
======================

[](#nested-set-for-laravel)

A Laravel package for storing and querying trees (hierarchical data) in relational databases using the [Nested Set Model](http://en.wikipedia.org/wiki/Nested_set_model).

This is a **Lunar-maintained fork** of [`kalnoy/nestedset`](https://github.com/lazychaser/laravel-nestedset)by Alexander Kalnoy, kept current for modern Laravel.

> **Supports Laravel 12 &amp; 13 on PHP 8.3+.** If you need an older Laravel version, use the upstream [`kalnoy/nestedset`](https://packagist.org/packages/kalnoy/nestedset).

Contents
--------

[](#contents)

- [What are nested sets?](#what-are-nested-sets)
- [Requirements](#requirements)
- [Installation](#installation)
- [Documentation](#documentation)
    - [Inserting nodes](#inserting-nodes)
    - [Retrieving nodes](#retrieving-nodes)
    - [Deleting nodes](#deleting-nodes)
    - [Helper methods](#helper-methods)
    - [Checking consistency](#checking-consistency)
    - [Scoping](#scoping)
- [Contributing](#contributing)
- [License](#license)

What are nested sets?
---------------------

[](#what-are-nested-sets)

Nested sets, or the [Nested Set Model](http://en.wikipedia.org/wiki/Nested_set_model), are a way to effectively store hierarchical data in a relational table. From Wikipedia:

> The nested set model is to number the nodes according to a tree traversal, which visits each node twice, assigning numbers in the order of visiting, and at both visits. This leaves two numbers for each node, which are stored as two attributes. Querying becomes inexpensive: hierarchy membership can be tested by comparing these numbers. Updating requires renumbering and is therefore expensive.

The model performs well when the tree is updated rarely. It is tuned to be fast at fetching related nodes, which makes it ideal for things like multi-depth menus or shop categories.

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 12 or 13

It is strongly recommended to use a database that supports transactions (such as MySQL's InnoDB or PostgreSQL) to protect the tree from corruption during updates.

Installation
------------

[](#installation)

```
composer require lunarphp/nestedset

```

### The schema

[](#the-schema)

Add the nested set columns to your table with the `nestedSet()` Blueprint macro:

```
Schema::create('table', function (Blueprint $table) {
    // ...
    $table->nestedSet();
});

// To drop the columns:
Schema::table('table', function (Blueprint $table) {
    $table->dropNestedSet();
});
```

### The model

[](#the-model)

Add the `Lunar\Nestedset\NodeTrait` trait to your model to enable nested sets:

```
use Illuminate\Database\Eloquent\Model;
use Lunar\Nestedset\NodeTrait;

class Category extends Model
{
    use NodeTrait;
}
```

Documentation
-------------

[](#documentation)

Suppose we have a model `Category`. A `$node` variable is an instance of that model and the node we are manipulating. It can be a fresh model or one loaded from the database.

### Relationships

[](#relationships)

A node has the following relationships, which are fully functional and can be eager-loaded:

- Node belongs to `parent`
- Node has many `children`
- Node has many `ancestors`
- Node has many `descendants`

### Inserting nodes

[](#inserting-nodes)

Moving and inserting nodes runs several database queries, so it is highly recommended to wrap these operations in a transaction. Transactions are **not**started automatically.

Structural manipulations are **deferred** until you call `save()` on the model. Some methods call `save()` implicitly and return the boolean result of the operation.

A successful `save()` does not necessarily mean the node was moved. If your application depends on whether the node actually changed position, use `hasMoved()`:

```
if ($node->save()) {
    $moved = $node->hasMoved();
}
```

#### Creating nodes

[](#creating-nodes)

When you create a node without specifying a parent, it is appended to the end of the tree as a *root* (a node without a parent):

```
Category::create($attributes); // Saved as root
```

```
$node = new Category($attributes);
$node->save(); // Saved as root
```

#### Making a node a root

[](#making-a-node-a-root)

```
// Implicit save
$node->saveAsRoot();

// Explicit save
$node->makeRoot()->save();
```

The node is appended to the end of the tree.

#### Appending and prepending to a parent

[](#appending-and-prepending-to-a-parent)

To make a node a child of another node, you can make it the last or first child. *In the following examples, `$parent` is an existing node.*

There are several ways to append a node:

```
// #1 Using deferred insert
$node->appendToNode($parent)->save();

// #2 Using the parent node
$parent->appendNode($node);

// #3 Using the parent's children relationship
$parent->children()->create($attributes);

// #4 Using the node's parent relationship
$node->parent()->associate($parent)->save();

// #5 Using the parent attribute
$node->parent_id = $parent->id;
$node->save();

// #6 Using the static method
Category::create($attributes, $parent);
```

And a couple of ways to prepend:

```
// #1
$node->prependToNode($parent)->save();

// #2
$parent->prependNode($node);
```

#### Inserting before or after a node

[](#inserting-before-or-after-a-node)

You can make `$node` a neighbor of the `$neighbor` node. `$neighbor` must exist; the target node may be fresh. If the target node exists, it is moved to the new position and its parent is changed if required.

```
// Explicit save
$node->afterNode($neighbor)->save();
$node->beforeNode($neighbor)->save();

// Implicit save
$node->insertAfterNode($neighbor);
$node->insertBeforeNode($neighbor);
```

#### Building a tree from an array

[](#building-a-tree-from-an-array)

When you call the static `create` method, it checks whether the attributes contain a `children` key. If they do, it creates the child nodes recursively:

```
$node = Category::create([
    'name' => 'Foo',
    'children' => [
        [
            'name' => 'Bar',
            'children' => [
                ['name' => 'Baz'],
            ],
        ],
    ],
]);
```

`$node->children` now contains the list of created child nodes.

#### Rebuilding a tree from an array

[](#rebuilding-a-tree-from-an-array)

You can rebuild a tree, which is useful for mass-changing its structure:

```
Category::rebuildTree($data, $delete);
```

`$data` is an array of nodes:

```
$data = [
    ['id' => 1, 'name' => 'foo', 'children' => [ /* ... */ ]],
    ['name' => 'bar'],
];
```

The node named `foo` has an `id`, so the existing node is filled and saved. If the node does not exist, a `ModelNotFoundException` is thrown. Its `children` are processed in the same way and saved as children of `foo`.

The node named `bar` has no primary key, so it is created.

`$delete` controls whether existing nodes that are not present in `$data` are deleted. By default, nodes are not deleted.

##### Rebuilding a subtree

[](#rebuilding-a-subtree)

You can constrain rebuilding to the descendants of a given node:

```
Category::rebuildSubtree($root, $data);
```

### Retrieving nodes

[](#retrieving-nodes)

*In some examples below, `$id` is the primary key of the target node.*

#### Ancestors and descendants

[](#ancestors-and-descendants)

Ancestors form the chain of parents up to a node — useful for breadcrumbs. Descendants are all nodes in a subtree: children, children of children, and so on.

Both can be eager-loaded:

```
$node->ancestors;
$node->descendants;
```

You can also load them with a custom query:

```
$result = Category::ancestorsOf($id);
$result = Category::ancestorsAndSelf($id);
$result = Category::descendantsOf($id);
$result = Category::descendantsAndSelf($id);
```

In most cases you want ancestors ordered by level:

```
$result = Category::defaultOrder()->ancestorsOf($id);
```

Ancestors can be eager-loaded for breadcrumbs:

```
$categories = Category::with('ancestors')->paginate(30);
```

```
@foreach ($categories as $category)
    {{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' }}
    {{ $category->name }}
@endforeach
```

#### Siblings

[](#siblings)

Siblings are nodes that share the same parent:

```
$result = $node->getSiblings();
$result = $node->siblings()->get();
```

Next siblings:

```
// The sibling immediately after the node
$result = $node->getNextSibling();

// All siblings after the node
$result = $node->getNextSiblings();

// All siblings after the node, as a query
$result = $node->nextSiblings()->get();
```

Previous siblings:

```
// The sibling immediately before the node
$result = $node->getPrevSibling();

// All siblings before the node
$result = $node->getPrevSiblings();

// All siblings before the node, as a query
$result = $node->prevSiblings()->get();
```

#### Getting related models from another table

[](#getting-related-models-from-another-table)

Imagine each category `has many` goods (a `HasMany` relationship). To get all goods of `$category` and every one of its descendants:

```
// Get the ids of the descendants
$categories = $category->descendants()->pluck('id');

// Include the category's own id
$categories[] = $category->getKey();

// Get the goods
$goods = Goods::whereIn('category_id', $categories)->get();
```

#### Including node depth

[](#including-node-depth)

To know the level of a node:

```
$result = Category::withDepth()->find($id);

$depth = $result->depth;
```

Root nodes are at level 0, their children at level 1, and so on.

To get nodes at a specific level, apply a `having` constraint:

```
$result = Category::withDepth()->having('depth', '=', 1)->get();
```

> **Note:** this will not work in database strict mode.

#### Default order

[](#default-order)

Nodes are organized internally, but no order is applied by default, so they may appear in an arbitrary order. This does not affect displaying a tree, and you are free to order nodes alphabetically or by another index.

When hierarchical order is essential (it is required for retrieving ancestors and useful for ordering menu items), apply `defaultOrder`:

```
$result = Category::defaultOrder()->get();
```

Reversed order:

```
$result = Category::reversed()->get();
```

To shift a node up or down among its siblings, affecting the default order:

```
$bool = $node->down();
$bool = $node->up();

// Shift the node down by 3 siblings
$bool = $node->down(3);
```

The return value is a boolean indicating whether the node changed position.

#### Constraints

[](#constraints)

Various constraints can be applied to the query builder:

- `whereIsRoot()` — only root nodes
- `hasParent()` — only non-root nodes
- `whereIsLeaf()` — only leaves
- `hasChildren()` — only non-leaf nodes
- `whereIsAfter($id)` — every node (not just siblings) after the node with the given id
- `whereIsBefore($id)` — every node before the node with the given id

Descendant constraints:

```
$result = Category::whereDescendantOf($node)->get();
$result = Category::whereNotDescendantOf($node)->get();
$result = Category::orWhereDescendantOf($node)->get();
$result = Category::orWhereNotDescendantOf($node)->get();
$result = Category::whereDescendantAndSelf($id)->get();

// Include the target node in the result set
$result = Category::whereDescendantOrSelf($node)->get();
```

Ancestor constraints:

```
$result = Category::whereAncestorOf($node)->get();
$result = Category::whereAncestorOrSelf($id)->get();
```

`$node` can be either a primary key or a model instance.

#### Building a tree

[](#building-a-tree)

After fetching a set of nodes, you can convert it into a tree:

```
$tree = Category::get()->toTree();
```

This fills the `parent` and `children` relationships on every node so you can render the tree recursively:

```
$nodes = Category::get()->toTree();

$traverse = function ($categories, $prefix = '-') use (&$traverse) {
    foreach ($categories as $category) {
        echo PHP_EOL.$prefix.' '.$category->name;

        $traverse($category->children, $prefix.'-');
    }
};

$traverse($nodes);
```

This outputs something like:

```
- Root
-- Child 1
--- Sub child 1
-- Child 2
- Another root

```

##### Building a flat tree

[](#building-a-flat-tree)

You can also build a flat tree: a list of nodes where each child node immediately follows its parent. This is helpful when nodes have a custom order (e.g. alphabetical) and you want to avoid recursion:

```
$nodes = Category::get()->toFlatTree();
```

The previous example then outputs:

```
Root
Child 1
Sub child 1
Child 2
Another root

```

##### Getting a subtree

[](#getting-a-subtree)

Sometimes you only need a subtree rather than the whole tree:

```
$root = Category::descendantsAndSelf($rootId)->toTree()->first();
```

In a single query this gets a subtree root and all of its descendants, accessible via the `children` relation.

If you don't need the `$root` node itself:

```
$tree = Category::descendantsOf($rootId)->toTree($rootId);
```

### Deleting nodes

[](#deleting-nodes)

To delete a node:

```
$node->delete();
```

> **Important:** any descendants the node has are also deleted.

> **Important:** nodes must be deleted as models. **Do not** delete them with a query like the following — it will break the tree:
>
> ```
> Category::where('id', '=', $id)->delete(); // DON'T
> ```

The `SoftDeletes` trait is supported, including at the model level.

### Helper methods

[](#helper-methods)

```
$bool = $node->isDescendantOf($parent);
$bool = $node->isRoot();
$bool = $node->isChildOf($other);
$bool = $node->isAncestorOf($other);
$bool = $node->isSiblingOf($other);
$bool = $node->isLeaf();
```

### Checking consistency

[](#checking-consistency)

Check whether a tree is broken (has structural errors):

```
$bool = Category::isBroken();
```

Get error statistics:

```
$data = Category::countErrors();
```

The returned array has the following keys:

- `oddness` — nodes with a wrong set of `lft`/`rgt` values
- `duplicates` — nodes that share a `lft` or `rgt` value
- `wrong_parent` — nodes with a `parent_id` that doesn't match their `lft`/`rgt` values
- `missing_parent` — nodes with a `parent_id` pointing to a node that doesn't exist

#### Fixing a tree

[](#fixing-a-tree)

A broken tree can be fixed. Using the inheritance information from the `parent_id`column, the correct `_lft` and `_rgt` values are set for every node:

```
Category::fixTree();

// Or constrain it to a subtree:
Category::fixSubtree($root);
```

#### Customising the query

[](#customising-the-query)

If your model defines a global scope that interferes with the integrity checks (for example one that adds columns or self-join-unsafe `where` clauses), pass a callback to customise the query used by `isBroken()`, `countErrors()`, `getTotalErrors()`, `fixTree()` and `fixSubtree()`. The most common use is dropping a global scope:

```
Category::isBroken(fn ($query) => $query->withoutGlobalScopes());

Category::fixTree(null, fn ($query) => $query->withoutGlobalScopes());
```

### Scoping

[](#scoping)

Imagine you have `Menu` and `MenuItem` models with a one-to-many relationship, where `MenuItem` has a `menu_id` attribute and uses nested sets. You'd want to process each tree separately based on `menu_id`. To do so, declare the scope attribute on the model:

```
protected function getScopeAttributes()
{
    return ['menu_id'];
}
```

Now, to run a custom query, you must provide the attributes used for scoping:

```
MenuItem::scoped(['menu_id' => 5])->withDepth()->get(); // OK
MenuItem::descendantsOf($id)->get();                     // WRONG: returns nodes from other scopes
MenuItem::scoped(['menu_id' => 5])->fixTree();           // OK
```

When requesting nodes from a model instance, scopes are applied automatically based on that instance's attributes:

```
$node = MenuItem::findOrFail($id);

$node->siblings()->withDepth()->get(); // OK
```

To get a scoped query builder from an instance:

```
$node->newScopedQuery();
```

#### Scoping and eager loading

[](#scoping-and-eager-loading)

Always use a scoped query when eager loading:

```
MenuItem::scoped(['menu_id' => 5])->with('descendants')->findOrFail($id); // OK
MenuItem::with('descendants')->findOrFail($id);                           // WRONG
```

Migrating existing data
-----------------------

[](#migrating-existing-data)

### Migrating from another nested set extension

[](#migrating-from-another-nested-set-extension)

If your previous extension used a different set of columns, override these methods on your model:

```
public function getLftName()
{
    return 'left';
}

public function getRgtName()
{
    return 'right';
}

public function getParentIdName()
{
    return 'parent';
}

// Map the parent id attribute mutator
public function setParentAttribute($value)
{
    $this->setParentIdAttribute($value);
}
```

### Migrating from basic parentage info

[](#migrating-from-basic-parentage-info)

If your tree only has `parent_id` information, add the two nested set columns to your schema:

```
$table->unsignedInteger('_lft');
$table->unsignedInteger('_rgt');
```

After [setting up your model](#the-model), fix the tree to populate `_lft` and `_rgt`:

```
MyModel::fixTree();
```

Contributing
------------

[](#contributing)

Contributions are welcome. The test suite runs against SQLite, MySQL and PostgreSQL:

```
composer test      # run the test suite
composer lint      # check code style (Laravel Pint)
composer format    # fix code style
composer analyse   # run static analysis (Larastan)

```

See [CONTRIBUTING.md](CONTRIBUTING.md) for details, and [CHANGELOG.md](CHANGELOG.md) for release notes.

License
-------

[](#license)

Released under the [MIT license](LICENSE.md).

This package is a fork of [`kalnoy/nestedset`](https://github.com/lazychaser/laravel-nestedset). Copyright (c) 2017 Alexander Kalnoy. Maintained for Lunar by Neon Digital.

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance89

Actively maintained with recent releases

Popularity28

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

53d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/112434999?v=4)[Lunar - Headless E-Commerce for Laravel](/maintainers/lunarphp)[@lunarphp](https://github.com/lunarphp)

---

Top Contributors

[![glennjacobs](https://avatars.githubusercontent.com/u/647407?v=4)](https://github.com/glennjacobs "glennjacobs (15 commits)")

---

Tags

laraveldatabasenested setshierarchynsmlunar

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/lunarphp-nestedset/health.svg)

```
[![Health](https://phpackages.com/badges/lunarphp-nestedset/health.svg)](https://phpackages.com/packages/lunarphp-nestedset)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[mongodb/laravel-mongodb

A MongoDB based Eloquent model and Query builder for Laravel

7.1k8.4M98](/packages/mongodb-laravel-mongodb)[kalnoy/nestedset

Nested Set Model for Laravel 5.7 and up

3.8k14.2M263](/packages/kalnoy-nestedset)[aimeos/laravel-nestedset

Nested Set Model for Laravel

3714.4k7](/packages/aimeos-laravel-nestedset)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
