PHPackages                             baril/bonsai - 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. baril/bonsai

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

baril/bonsai
============

An implementation of the Closure Tables pattern for Eloquent.

v3.4.0(1mo ago)3593.5k↓22.2%MITPHPCI passing

Since Sep 7Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/michaelbaril/bonsai)[ Packagist](https://packagist.org/packages/baril/bonsai)[ RSS](/packages/baril-bonsai/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (10)Dependencies (15)Versions (15)Used By (0)

Bonsai 🌳
========

[](#bonsai-deciduous_tree)

[![Version](https://camo.githubusercontent.com/1dfb23a378a5fb90468bd211bd6598a41bae4dabcc44fdb4e0980a15f73a55ff/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f626172696c2f626f6e7361693f6c6162656c3d737461626c65)](https://packagist.org/packages/baril/bonsai)[![License](https://camo.githubusercontent.com/d575c3493e8c03e2fd004385e9cfa5313b74f586befa30afd64169bb26c50a46/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f626172696c2f626f6e736169)](https://packagist.org/packages/baril/bonsai)[![Downloads](https://camo.githubusercontent.com/3e4713e5cb467e46321e07236b5897abe57170c56f59a82509d9fcbf0a009ff8/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f626172696c2f626f6e736169)](https://packagist.org/packages/baril/bonsai/stats)[![Tests](https://camo.githubusercontent.com/b771a3d94401fbe20590940069a541458163c9e8f1a44dc1e9134ddfb2ae7bda/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d69636861656c626172696c2f626f6e7361692f72756e2d74657374732e796d6c3f6272616e63683d6d6173746572266c6162656c3d7465737473)](https://github.com/michaelbaril/bonsai/actions/workflows/run-tests.yml)[![Coverage](https://camo.githubusercontent.com/bf5083d7bffcb7771d099408f17392ad1c343d9b12c68bb0f586a09142859784/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d68747470732533412532462532466d69636861656c626172696c2e6769746875622e696f253246626f6e736169253246636f76657261676525324662616467652e6a736f6e)](https://michaelbaril.github.io/bonsai/coverage/)

This package is an implementation of the ["Closure Table" design pattern](https://dirtsimple.org/2010/11/simplest-way-to-do-tree-based-queries.html)for Laravel Eloquent. This pattern allows for fast querying of tree-like structures stored in a relational database. It is an alternative to nested sets.

You can find the full API documentation [here](https://michaelbaril.github.io/bonsai/api/).

Version compatibility
---------------------

[](#version-compatibility)

LaravelBonsai12.x3.3+11.x3.2+10.x3.1+9.x3.x8.x2.x / 3.x7.x1.x6.x1.x⚠️ Up until version 3.2, only MySQL is supported. Starting with version 3.3, all DBMSs supported by Eloquent are supported by this package.

Setup
-----

[](#setup)

First, your main table needs a `parent_id` column (the name can be customized). This column is the one that holds the canonical data: the closures are merely a duplication of that information.

Then, your model must implement the `Baril\Bonsai\Concerns\BelongsToTree` trait.

You can use the following properties to specify the table and column names:

- `$parentForeignKey`: name of the self-referencing foreign key in the main table (defaults to `parent_id`),
- `$closureTable`: name of the closure table (defaults to the snake-cased model name suffixed with `_tree`, e.g. `tag_tree`).

```
use Baril\Bonsai\Concerns\BelongsToTree;

class Tag extends Model
{
    use BelongsToTree;

    protected $parentForeignKey = 'parent_tag_id';
    protected $closureTable = 'tag_closures';
}
```

Once your model is ready, you have to run the `bonsai:grow` command (described below).

Artisan commands
----------------

[](#artisan-commands)

### bonsai:grow

[](#bonsaigrow)

The `bonsai:grow` command will generate the migration file to create the closure table for your model:

```
php artisan bonsai:grow "App\\Models\\Tag"
php artisan migrate
```

### bonsai:fix

[](#bonsaifix)

If your `tag` table already contains data, you have to run another command to create the closures for the existing data:

```
php artisan bonsai:fix "App\\Models\\Tag"
```

This command is also useful at any time if your closures get corrupt somehow, as it will truncate the closure table and fill it again based on the data found in the main table's `parent_id` column.

### bonsai:show

[](#bonsaishow)

The `bonsai:show` command provides a quick-and-easy way to output the content of the tree. It takes a `label` parameter that defines which column (or accessor) to use as label. Optionally you can also specify a max depth.

```
php artisan bonsai:show "App\\Models\\Tag" --label=name --depth=3
```

Updating the tree
-----------------

[](#updating-the-tree)

Just fill the model's `parent_id` and save the model: the closure table will be updated accordingly.

```
$tag->parent()->associate($parentTag); // or just: $tag->parent_id = $parentTagId;
$tag->save();
```

The `save` method will throw a `\Baril\Bonsai\TreeException` in case of a redundancy error (i.e. if the `parent_id` corresponds to the model itself or one of its descendants).

You can also change the parent by using the `graft` and `graftOnto` methods:

```
$newParentTag->graft($childTag);
// and:
$childTag->graftOnto($newParentTag);
// are both equivalent to:
$childTag->parent()->associate($newParentTag);
$childTag->save();
```

The `cut` method turns the model into a root (with its descendants preserved):

```
$tag->cut();
// is equivalent to:
$tag->parent()->dissociate();
$tag->save();
```

When you delete a model, its closures will be deleted automatically. If the model has descendants, the `delete` method will throw a `TreeException`. If you want to delete the model and all its descendants, use the `deleteTree` method instead:

```
try {
    $tag->delete();
} catch (\Baril\Bonsai\TreeException $e) {
    // some specific treatment
    // ...
    $tag->deleteTree();
}
```

Relationships
-------------

[](#relationships)

The `BelongsToTree` trait provides the following relationships:

- `parent`: `BelongsTo` relation to the parent,
- `children`: `HasMany` relation to the children,
- `siblings`: `HasMany` relation to the children of the same parent.
- `ancestors`: `BelongsToMany` relation to the ancestors,
- `descendants`: `BelongsToMany` relation to the descendants.

### Siblings

[](#siblings)

💡 The `siblings` relation is a many-to-many relation, but under the hood, it extends `HasMany`.

The `siblings` relation has the following scopes:

- `withSelf()`: will include the item itself in the results of the relation.
- `withOrphans()`: by default, the relation doesn't consider "orphans" (i.e. the roots of the tree) as siblings. Thus, it won't return any result when called on roots. Using this scope changes this behavior: calling the relation on a root will now return all other roots.

### Ancestors and descendants

[](#ancestors-and-descendants)

⚠️ The `ancestors` and `descendants` relations are read-only. Using the `attach` or `detach`methods on these relations will throw an exception.

The `ancestors` and `descendants` relations have the following scopes:

- `withSelf()`: will include the item itself in the results of the relation.
- `orderByDepth($direction = 'asc')`: order the results by "depth", ie. distance from the referencing node.
- `maxDepth($depth)`: will retrieve ancestors/descendants up to (and including) the provided `$depth`.

Loading or eager-loading the `descendants` relation will automatically load the `children` relation (with no additional query). Furthermore, it will load the `children` relation recursively for all the eager-loaded descendants:

```
$tags = Tag::with('descendants')->get();

// The following code won't execute any new query:
foreach ($tags as $tag) {
    dump($tag->name);
    foreach ($tag->children as $child) {
        dump('-' . $child->name);
        foreach ($child->children as $grandchild) {
            dump('--' . $grandchild->name);
        }
    }
}
```

Similarly, loading the `ancestors` relation will load the `parent` relation recursively.

Methods
-------

[](#methods)

The `BelongsToTree` trait provides the following methods:

- `isRoot()`: returns `true` if the item has no parent.
- `isLeaf()`: returns `true` if the item has no child.
- `hasChildren()`
- `isChildOf($item)` (`$item` can be either a model or a model key)
- `isParentOf($item)`
- `isDescendantOf($item)`
- `isAncestorOf($item)`
- `isSiblingOf($item)`
- `findCommonAncestorWith($item)`: returns the first common ancestor between 2 items, or `null` if they don't have a common ancestor (which can happen if there are multiple roots).
- `getDistanceTo($item)`: returns the "distance" between 2 items (throws a `TreeException` if there's no common ancestor).
- `getDepth()`: returns the "depth" of the item in the tree (the root's depth being 0).
- `getHeight()`: returns the "height" of the subtree of which the item is the root (0 if the item is a leaf).

Query scopes
------------

[](#query-scopes)

The `BelongsToTree` trait provides the following query scopes:

- `onlyRoots()`
- `withoutRoots()`
- `onlyLeaves()`
- `withoutLeaves()`
- `hasChildren($bool = true)`: similar to either `onlyLeaves()` or `withoutLeaves()`, depending on the value of `$bool`.
- `descendantsOf($ancestor, $maxDepth = null, $withSelf = false)`: only return the descendants of `$ancestor`, with an optional `$maxDepth`. The `$ancestor` parameter can be either a model or a model key. If the `$withSelf` parameter is set to `true`, the ancestor will be included in the query results too.
- `ancestorsOf($descendant, $maxDepth = null, $withSelf = false)`
- `withDepth($as = 'depth')`: will add a `depth` column (or whatever alias you provided) to your resulting models.
- `withHeight($as = 'height')`: will add a `height` column (or whatever alias you provided) to your resulting models (will work only with Laravel 10+).

Special trees
-------------

[](#special-trees)

### Soft deleting tree

[](#soft-deleting-tree)

To implement soft delete on your model, use the `Baril\Bonsai\Concerns\SoftDeletes`trait instead of `Illuminate\Database\Eloquent\SoftDeletes`:

```
use Baril\Bonsai\Concerns\BelongsToTree;
use Baril\Bonsai\Concerns\SoftDeletes;

class Tag extends Model
{
    use BelongsToTree;
    use SoftDeletes;
}
```

The trait defines the `forceDeleteTree` method (which is similar to `deleteTree` for hard delete) and the `restoreTree` method. The latter method restores the model and all its soft-deleted descendants.

When you restore a model (either with `restore` or `restoreTree`), it will be restored under its original parent, assuming it still exists. If the parent has been deleted (either soft or hard) in the meantime, trying to restore the child will throw a `TreeException`. In this case, you may want to "graft" or "cut" the model before you restore it:

```
try {
    $tag->restore();
} catch (\Baril\Bonsai\TreeException $e) {
    $tag->cut()->restore(); // will restore $tag as a root
}
```

### Ordered tree

[](#ordered-tree)

If you need each level of your tree to be explicitly ordered, install [the Orderly package](https://github.com/michaelbaril/orderly) in addition to Bonsai:

```
composer require baril/orderly
```

You will need a `position` column in your main table (the name of the column can be customized using the `$orderColumn` property).

Your model must use either the `Baril\Bonsai\Concerns\Orderable` trait or the `Baril\Bonsai\Concerns\Ordered` trait.

```
use Baril\Bonsai\Concerns\BelongsToTree;
use Baril\Bonsai\Concerns\Orderable;

class Tag extends Model
{
    use BelongsToTree;
    use Orderable;

    protected $orderColumn = 'order';
}
```

If you're using `Orderable`, you can order the `children` relation like this:

```
$children = $this->children()->ordered()->get();
```

If you're using `Ordered`, the `children` relation is automatically ordered.

Check out the [documentation of the Orderly package](https://github.com/michaelbaril/orderly)to see all available methods.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

###  Health Score

54

—

FairBetter than 97% of packages

Maintenance89

Actively maintained with recent releases

Popularity39

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity62

Established project with proven stability

 Bus Factor1

Top contributor holds 98.3% 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

Every ~155 days

Recently: every ~167 days

Total

14

Last Release

58d ago

Major Versions

v1.0.0 → v2.0.02021-07-17

v2.0.2 → v3.0.02023-02-04

### Community

Maintainers

![](https://www.gravatar.com/avatar/95cbe396c7a3fe89d006b143506ee5e5645317318f2cf8cf00faf5d327ef6114?d=identicon)[michaelbaril](/maintainers/michaelbaril)

---

Top Contributors

[![michaelbaril](https://avatars.githubusercontent.com/u/11444287?v=4)](https://github.com/michaelbaril "michaelbaril (114 commits)")[![mbwbd](https://avatars.githubusercontent.com/u/231652605?v=4)](https://github.com/mbwbd "mbwbd (2 commits)")

---

Tags

laraveleloquenttreehierarchyadjacency listnested-setclosureshierarchical-dataclosure tables

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/baril-bonsai/health.svg)

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

###  Alternatives

[tucker-eric/eloquentfilter

An Eloquent way to filter Eloquent Models

1.8k4.8M26](/packages/tucker-eric-eloquentfilter)[typicms/nestablecollection

A Laravel Package that extends Collection to handle unlimited nested items following adjacency list model.

88327.2k20](/packages/typicms-nestablecollection)[toponepercent/baum

Baum is an implementation of the Nested Set pattern for Eloquent models.

3154.7k](/packages/toponepercent-baum)

PHPackages © 2026

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