nevadskiy/laravel-tree
Composer 安装命令:
composer require nevadskiy/laravel-tree
包简介
Tree-like structure for Eloquent models.
README 文档
README
🌳 Tree-like structure for Eloquent models
The package provides you with a simple solution that allows you to effortlessly create hierarchical structures for your Eloquent models. It leverages the materialized path pattern to represent the hierarchy of your data. It can be used for a wide range of use cases such as managing categories, nested comments, and more.
🔌 Installation
Install the package via Composer:
composer require nevadskiy/laravel-tree
✨ How it works
When working with hierarchical data structures in your application, storing the structure using a self-referencing parent_id column is a common approach.
While it works well for many use cases, it can become challenging when you need to make complex queries, such as finding all descendants of a given node.
One of the simples and effective solutions is the materialized path pattern.
Materialized path
The "materialized pattern" involves storing the full path of each node in the hierarchy in a separate path column as a string.
The ancestors of each node are represented by a series of IDs separated by a delimiter.
For example, the categories database table might look like this:
| id | name | parent_id | path |
|---|---|---|---|
| 1 | Science | null | 1 |
| 2 | Physics | 1 | 1.2 |
| 3 | Mechanics | 2 | 1.2.3 |
| 4 | Thermodynamics | 2 | 1.2.4 |
With this structure, you can easily retrieve all descendants of a node using a SQL query:
SELECT * FROM categories WHERE path LIKE '1.%'
PostgreSQL Ltree extension
Using the PostgreSQL ltree extension we can go even further. This extension provides an additional ltree column type designed specifically for this purpose.
In combination with a GiST index it allows executing lightweight and performant queries across an entire tree.
Now the SQL query will look like this:
SELECT * FROM categories WHERE path ~ '1.*'
🔨 Configuration
All you have to do is to add a AsTree trait to the model and add a path column alongside the self-referencing parent_id column to the model's table.
Let's get started by configuring a Category model:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Nevadskiy\Tree\AsTree; class Category extends Model { use AsTree; }
Next, create a migration for the model. The definition of the path column depends on your database connection.
Using PostgreSQL database
To add a path column with the ltree type and a GiST index, use the following code:
$table->ltree('path')->nullable()->spatialIndex();
The complete migration file may look like this:
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->string('name'); $table->ltree('path')->nullable()->spatialIndex(); $table->timestamps(); }); Schema::table('categories', function (Blueprint $table) { $table->foreignId('parent_id') ->nullable() ->index() ->constrained('categories') ->cascadeOnDelete(); }); } public function down(): void { Schema::dropIfExists('categories'); } };
Sometimes the Ltree extension may be disabled in PostgreSQL. To enable it, you can publish and run a package migration:
php artisan vendor:publish --tag=pgsql-ltree-migration
Using MySQL or SQLite database
To add a string path column with and an index, use the following code:
$table->string('path')->nullable()->index();
🚊 Usage
Once you have configured your model, the package automatically handles all manipulations with the path attribute based on the parent, so you do not need to set it manually.
Inserting models
To insert a root node, simply save the model to the database:
$root = new Category(); $root->name = 'Science'; $root->save();
To insert a child model, you only need to assign the parent_id attribute or use the parent or children relation:
$child = new Category; $child->name = 'Physics'; $child->parent()->associate($root); $child->save();
As you can see, it works just as regular Eloquent models.
Relations
The AsTree trait provides the following relations:
parentchildrenancestors(read-only)descendants(read-only)
The parent and children relations use default Laravel BelongsTo and HasMany relation classes.
The ancestors and descendants can be used only in the "read" mode, which means methods like make or create are not available.
So to save related nodes you need to use the parent or children relation.
Parent
The parent relation uses the default Eloquent BelongsTo relation class that needs the parent_id column as a foreign key.
It allows getting a parent of the node.
echo $category->parent->name;
Children
The children relation uses a default Eloquent HasMany relation class and is a reverse relation to the parent.
It allows getting all children of the node.
foreach ($category->children as $child) { echo $child->name; }
Ancestors
The ancestors relation is a custom relation that works only in "read" mode.
It allows getting all ancestors of the node (without the current node).
Using the attribute:
foreach ($category->ancestors as $ancestor) { echo $ancestor->name; }
Using the query builder:
$ancestors = $category->ancestors()->get();
Getting a collection with the current node and its ancestors:
$hierarchy = $category->joinAncestors();
Descendants
The descendants relation is a custom relation that works only in "read" mode.
It allows getting all descendants of the node (without the current node).
Using the attribute:
foreach ($category->descendants as $descendant) { echo $descendant->name; }
Using the query builder:
$ancestors = $category->descendants()->get();
Querying models
Getting root nodes:
$roots = Category::query()->root()->get();
Getting nodes by the depth level:
$categories = Category::query()->whereDepth(3)->get();
Getting ancestors of the node (including the current node):
$ancestors = Category::query()->whereSelfOrAncestorOf($category)->get();
Getting descendants of the node (including the current node):
$descendants = Category::query()->whereSelfOrDescendantOf($category)->get();
Ordering nodes by depth:
$categories = Category::query()->orderByDepth()->get(); $categories = Category::query()->orderByDepthDesc()->get();
HasManyDeep
The package provides the HasManyDeep relation that can be used to link, for example, a Category model that uses the AsTree trait with a Product model.
That allows us to get products of a category and each of its descendants.
Here is the code example on how to use the HasManyDeep relation:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Nevadskiy\Tree\AsTree; use Nevadskiy\Tree\Relations\HasManyDeep; class Category extends Model { use AsTree; public function products(): HasManyDeep { return HasManyDeep::between($this, Product::class); } }
Now you can get products:
$products = $category->products()->paginate(20);
Querying category products
You can easily get the products of a category and each of its descendants using a query builder.
1st way (recommended):
$products = Product::query() ->join('categories', function (JoinClause $join) { $join->on('products.category_id', 'categories.id'); }) ->whereSelfOrDescendantOf($category) ->paginate(24, ['products.*']);
2nd way (slower):
$products = Product::query() ->whereHas('category', function (Builder $query) use ($category) { $query->whereSelfOrDescendantOf($category); }) ->paginate(24);
Moving nodes
When you move a node, the path column of the node and each of its descendants have to be updated as well.
Fortunately, the package does this automatically using a single query every time it sees that the parent_id column has been updated.
So basically to move a node along with its subtree, you need to update the parent node of the current node:
$science = Category::query()->where('name', 'Science')->firstOrFail(); $physics = Category::query()->where('name', 'Physics')->firstOrFail(); $physics->parent()->associate($science); $physics->save();
Other examples
Building a tree
To build a tree, we need to call the tree method on the NodeCollection:
$tree = Category::query()->orderBy('name')->get()->tree();
This method associates nodes using the children relation and returns only root nodes.
Building breadcrumbs
echo $category->joinAncestors()->reverse()->implode('name', ' > ');
Deleting a subtree
Delete the current node and all its descendants:
$category->newQuery()->whereSelfOrDescendantOf($category)->delete();
📚 Useful links
- https://www.postgresql.org/docs/current/ltree.html
- https://patshaughnessy.net/2017/12/13/saving-a-tree-in-postgres-using-ltree
- https://patshaughnessy.net/2017/12/14/manipulating-trees-using-sql-and-the-postgres-ltree-extension
☕ Contributing
Thank you for considering contributing. Please see CONTRIBUTING for more information.
📜 License
The MIT License (MIT). Please see LICENSE for more information.
nevadskiy/laravel-tree 适用场景与选型建议
nevadskiy/laravel-tree 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 107.95k 次下载、GitHub Stars 达 67, 最近一次更新时间为 2023 年 01 月 08 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「tree」 「path」 「laravel」 「hierarchy」 「eloquent」 「materialized path」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 nevadskiy/laravel-tree 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 nevadskiy/laravel-tree 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 nevadskiy/laravel-tree 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
jsTree widget for yii2
JSONPath implementation for querying and updating JSON objects with support for json flattening into a table
Doctrine2 behavior traits - slowhop fork
The InstantConfigurationCopy module provides easy way to copy configuration field information for admin in back office Magento 2.
A fast and dynamic Merkle tree implementation
simple agnostic package for IO releted (files, dir, etc..)
统计信息
- 总下载量: 107.95k
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 67
- 点击次数: 17
- 依赖项目数: 3
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2023-01-08