nevadskiy/laravel-tree

为Eloquent模型提供树形结构。

0.5.0 2024-03-14 15:34 UTC

This package is auto-updated.

Last update: 2024-09-14 16:39:55 UTC


README

Stand With Ukraine

🌳 为Eloquent模型提供树形结构

PHPUnit Code Coverage Latest Stable Version License

该包为您提供了一种简单的解决方案,使您能够轻松地为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属性或使用parentchildren关系即可

$child = new Category;
$child->name = 'Physics';
$child->parent()->associate($root);
$child->save();

正如您所看到的,它就像常规的Eloquent模型一样工作。

关系

AsTree特质提供了以下关系

父级和子级关系使用默认的 Laravel BelongsToHasMany 关系类。

祖先和子代关系只能在“读取”模式下使用,这意味着像 makecreate 这样的方法不可用。因此,要保存相关节点,您需要使用 parentchildren 关系。

父级

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();

📚 有用链接

☕ 贡献

感谢您考虑贡献。有关更多信息,请参阅 CONTRIBUTING

📜 许可证

MIT 许可证(MIT)。有关更多信息,请参阅 LICENSE