承接 baril/bonsai 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

baril/bonsai

Composer 安装命令:

composer require baril/bonsai

包简介

An implementation of the Closure Tables pattern for Eloquent.

README 文档

README

Version License Downloads Tests Coverage

This package is an implementation of the "Closure Table" design pattern 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.

Version compatibility

Laravel Bonsai
13.x 3.5+
12.x 3.3+
11.x 3.2+
10.x 3.1+
9.x 3.x
8.x 2.x / 3.x
7.x 1.x
6.x 1.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

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

bonsai:grow

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

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

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

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

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

💡 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

⚠️ 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

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

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

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

If you need each level of your tree to be explicitly ordered, install the Orderly package 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 to see all available methods.

Changelog

Please see CHANGELOG for more information on what has changed recently.

baril/bonsai 适用场景与选型建议

baril/bonsai 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 96.42k 次下载、GitHub Stars 达 35, 最近一次更新时间为 2020 年 09 月 07 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「tree」 「laravel」 「nested set」 「hierarchy」 「eloquent」 「closures」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 baril/bonsai 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 baril/bonsai 我们能提供哪些服务?
定制开发 / 二次开发

基于 baril/bonsai 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 96.42k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 35
  • 点击次数: 34
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 35
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2020-09-07