nevadskiy / laravel-tree
为Eloquent模型提供树形结构。
Requires
- php: ^7.3|^8.0
- laravel/framework: ^8.79|^9.0|^10.0|^11.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- orchestra/testbench: ^6.25|^7.0|^8.0|^v9.0.0
- phpunit/phpunit: ^9.0|^10.5
README
🌳 为Eloquent模型提供树形结构
该包为您提供了一种简单的解决方案,使您能够轻松地为Eloquent模型创建层次结构。它利用物化路径模式来表示您数据中的层次结构。它可以用于各种用例,例如管理分类、嵌套评论等。
🔌 安装
通过Composer安装此包
composer require nevadskiy/laravel-tree
✨ 工作原理
当在应用程序中处理层次数据结构时,使用自引用的parent_id
列来存储结构是常见的方法。
虽然对于许多用例来说效果很好,但在需要执行复杂查询时(例如找到给定节点的所有后代),它可能会变得具有挑战性。一个简单而有效的解决方案是物化路径模式。
物化路径
"物化模式"涉及将每个节点在层次结构中的完整路径存储在单独的path
列中,作为字符串。每个节点的祖先由一系列用分隔符分隔的ID表示。
例如,分类数据库表可能看起来像这样
使用这种结构,您可以使用SQL查询轻松检索节点的所有后代
SELECT * FROM categories WHERE path LIKE '1.%'
PostgreSQL Ltree扩展
使用PostgreSQL ltree扩展,我们可以更进一步。此扩展提供了一种专为这种目的设计的额外ltree
列类型。结合GiST索引,它允许在整个树结构上执行轻量级和高效的查询。
现在SQL查询将如下所示
SELECT * FROM categories WHERE path ~ '1.*'
🔨 配置
您只需将AsTree
特质添加到模型中,并在模型表中添加一个与自引用的parent_id
列并排的path
列即可。
让我们从配置一个Category
模型开始
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Nevadskiy\Tree\AsTree; class Category extends Model { use AsTree; }
接下来,为模型创建迁移。对于数据库连接,path
列的定义取决于您的数据库。
使用PostgreSQL数据库
要添加具有ltree
类型和GiST索引的path
列,请使用以下代码
$table->ltree('path')->nullable()->spatialIndex();
完整的迁移文件可能如下所示
<?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'); } };
有时PostgreSQL中的Ltree扩展可能已被禁用。要启用它,您可以发布并运行一个包迁移
php artisan vendor:publish --tag=pgsql-ltree-migration
使用MySQL数据库
要添加具有索引的字符串path
列,请使用以下代码
$table->string('path')->nullable()->index();
🚊 使用
一旦您配置了模型,该包会自动处理所有基于父节点的path
属性的操作,因此您不需要手动设置它。
插入模型
要插入根节点,只需将模型保存到数据库中
$root = new Category(); $root->name = 'Science'; $root->save();
要插入子模型,只需分配parent_id
属性或使用parent
或children
关系即可
$child = new Category; $child->name = 'Physics'; $child->parent()->associate($root); $child->save();
正如您所看到的,它就像常规的Eloquent模型一样工作。
关系
AsTree
特质提供了以下关系
parent
children
ancestors
(只读)descendants
(只读)
父级和子级关系使用默认的 Laravel BelongsTo
和 HasMany
关系类。
祖先和子代关系只能在“读取”模式下使用,这意味着像 make
或 create
这样的方法不可用。因此,要保存相关节点,您需要使用 parent
或 children
关系。
父级
parent
关系使用默认的 Eloquent BelongsTo
关系类,该类需要一个 parent_id
列作为外键。它允许获取节点的父级。
echo $category->parent->name;
子级
children
关系使用默认的 Eloquent HasMany
关系类,是 parent
的反向关系。它允许获取节点的所有子级。
foreach ($category->children as $child) { echo $child->name; }
祖先
ancestors
关系是一个自定义关系,仅在“读取”模式下工作。它允许获取节点的所有祖先(不包括当前节点)。
使用属性
foreach ($category->ancestors as $ancestor) { echo $ancestor->name; }
使用查询构建器
$ancestors = $category->ancestors()->get();
获取包含当前节点及其祖先的集合
$hierarchy = $category->joinAncestors();
子代
descendants
关系是一个自定义关系,仅在“读取”模式下工作。它允许获取节点的所有子代(不包括当前节点)。
使用属性
foreach ($category->descendants as $descendant) { echo $descendant->name; }
使用查询构建器
$ancestors = $category->descendants()->get();
查询模型
获取根节点
$roots = Category::query()->root()->get();
按深度级别获取节点
$categories = Category::query()->whereDepth(3)->get();
获取节点的祖先(包括当前节点)
$ancestors = Category::query()->whereSelfOrAncestorOf($category)->get();
获取节点的子代(包括当前节点)
$descendants = Category::query()->whereSelfOrDescendantOf($category)->get();
按深度排序节点
$categories = Category::query()->orderByDepth()->get(); $categories = Category::query()->orderByDepthDesc()->get();
HasManyDeep
该包提供了一个 HasManyDeep
关系,可以用来连接,例如,使用 AsTree
特性的 Category
模型与 Product
模型。
这允许我们获取某个类别的产品及其所有子代。
以下是使用 HasManyDeep
关系的代码示例
<?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); } }
现在您可以获取产品
$products = $category->products()->paginate(20);
查询类别产品
您可以使用查询构建器轻松地获取类别及其所有子代的产品。
1st 方式(推荐)
$products = Product::query() ->join('categories', function (JoinClause $join) { $join->on('products.category_id', 'categories.id'); }) ->whereSelfOrDescendantOf($category) ->paginate(24, ['products.*']);
2nd 方式(较慢)
$products = Product::query() ->whereHas('category', function (Builder $query) use ($category) { $query->whereSelfOrDescendantOf($category); }) ->paginate(24);
移动节点
当您移动一个节点时,节点及其所有子代的 path
列也需要更新。幸运的是,该包在每次看到 parent_id
列被更新时都会自动使用单个查询来执行此操作。
因此,基本上要移动一个节点及其子树,您需要更新当前节点的父级节点
$science = Category::query()->where('name', 'Science')->firstOrFail(); $physics = Category::query()->where('name', 'Physics')->firstOrFail(); $physics->parent()->associate($science); $physics->save();
其他示例
构建树
要构建树,我们需要在 NodeCollection
上调用 tree
方法
$tree = Category::query()->orderBy('name')->get()->tree();
此方法使用 children
关系关联节点,并仅返回根节点。
构建面包屑
echo $category->joinAncestors()->reverse()->implode('name', ' > ');
删除子树
删除当前节点及其所有子代
$category->newQuery()->whereSelfOrDescendantOf($category)->delete();
📚 有用链接
- https://postgresql.ac.cn/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。
📜 许可证
MIT 许可证(MIT)。有关更多信息,请参阅 LICENSE。