thunderwolf/eloquent-nested-set

基于 Propel NestedSet 和 kalnoy/nestedset 的 Laravel Eloquent 的嵌套集

1.0.2 2024-08-25 22:11 UTC

This package is auto-updated.

Last update: 2024-09-25 20:26:36 UTC


README

nested_set 行为允许一个模型成为树结构,并提供多种方法以高效的方式遍历树。

许多应用程序需要在模型中存储层次数据。例如,论坛为每个讨论存储消息树。CMS 将部分和子部分视为导航树。在商业组织结构图中,每个人都是组织树的叶子。使用 嵌套集 是在关系数据库中存储此类层次数据并操作的最佳方式。"嵌套集" 这个名称描述了存储模型在树中位置的算法;它也被称为“修改后的前序树遍历”。

要使用它,需要在您的模型中包含 NestedSet 特质,配置如下

    public static function nestedSet(): array
    {
        return [];
    }

NestedSet 特质 将覆盖默认的 Builder,如下所示

    /**
     * Override -> Create a new Eloquent query builder for the model.
     * If you have more Behaviors using this kind on Override create own and use Trait NestedSetBuilderTrait
     *
     * @param  Builder  $query
     * @return NestedSetBuilder
     */
    public function newEloquentBuilder($query): NestedSetBuilder
    {
        return new NestedSetBuilder($query);
    }

如果您正在使用更多特质或自定义覆盖,请仅使用 NestedSetBuilderTrait 特质,因为 NestedSetBuilder 类看起来像这样

<?php

namespace Thunderwolf\EloquentNestedSet;

use Illuminate\Database\Eloquent\Builder;

class NestedSetBuilder extends Builder
{
    use NestedSetBuilderTrait;
}

基本用法

使用此包的最基本方法是在使用 NestedSet 特质 的模型中创建模型,如下所示

<?php

use Illuminate\Database\Eloquent\Model;
use Thunderwolf\EloquentNestedSet\NestedSet;

class Section extends Model
{
    use NestedSet;

    protected $table = 'sections';

    protected $fillable = ['title'];

    public $timestamps = false;

    public static function nestedSet(): array
    {
        return [];
    }
}

注册 NestedSetServiceProvider 之后,您还可以使用蓝图通过使用 createNestedSet 辅助方法创建表,如下所示

您还可以使用蓝图以这种方式创建表

$schema->create('sections', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->createNestedSet([]);
});

您将以类似的方式处理迁移。

模型具有将自身插入到树结构中的能力,如下所示

<?php
$s1 = new Section();
$s1->setAttribute('title', 'Home');
$s1->makeRoot(); // make this node the root of the tree
$s1->save();
$s2 = new Section();
$s2->setAttribute('title', 'World');
$s2->insertAsFirstChildOf($s1); // insert the node in the tree
$s2->save();
$s3 = new Section();
$s3->setAttribute('title', 'Europe');
$s3->insertAsFirstChildOf($s2); // insert the node in the tree
$s3->save();
$s4 = new Section();
$s4->setAttribute('title', 'Business');
$s4->insertAsNextSiblingOf($s2); // insert the node in the tree
$s4->save();
/* The sections are now stored in the database as a tree:
    $s1:Home
    |       \
$s2:World  $s4:Business
    |
$s3:Europe
*/

您可以使用 insertAsFirstChildOf()insertAsLastChildOf()insertAsPrevSiblingOf()insertAsNextSiblingOf() 中的任何一种方法,继续将新节点作为现有节点的子节点或兄弟节点插入。

一旦构建了树,您可以使用 nested_set 行为添加到查询和模型对象的众多方法中的任何一种来遍历它。例如

<?php
$rootNode = Section::query()->findRoot();       // $s1
$worldNode = $rootNode->getFirstChild();        // $s2
$businessNode = $worldNode->getNextSibling();   // $s4
$firstLevelSections = $rootNode->getChildren(); // array($s2, $s4)
$allSections = $rootNode->getDescendants();     // array($s2, $s3, $s4)

// you can also chain the methods
$europeNode = $rootNode->getLastChild()->getPrevSibling()->getFirstChild();  // $s3
$path = $europeNode->getAncestors();                                         // array($s1, $s2)

这些方法返回的节点是常规的 Propel 模型对象,具有访问属性和关联模型的功能。《nested_set》行为还向节点添加了检查方法

<?php
echo $s2->isRoot();      // false
echo $s2->isLeaf();      // false
echo $s2->getLevel();    // 1
echo $s2->hasChildren(); // true
echo $s2->countChildren(); // 1
echo $s2->hasSiblings(); // true

遍历和检查方法中的每个方法都导致一个单独的数据库查询,无论节点在树中的位置如何。这是因为节点在树中的位置信息存储在模型的三个列中,分别命名为 tree_lefttree_righttree_level。这些列的值由嵌套集算法确定,这使得读取查询比使用简单的 parent_id 外键的树更有效。

节点操作

您可以使用 moveToFirstChildOf()moveToLastChildOf()moveToPrevSiblingOf()moveToNextSiblingOf() 中的任何一种方法移动节点及其子树。这些操作是立即的,不需要在之后保存模型

<?php
// move the entire "World" section under "Business"
$s2->moveToFirstChildOf($s4);
/* The tree is modified as follows:
$s1:Home
  |
$s4:Business
  |
$s2:World
  |
$s3:Europe
*/
// now move the "Europe" section directly under root, after "Business"
$s3->moveToNextSiblingOf($s4);
/* The tree is modified as follows:
    $s1:Home
    |        \
$s4:Business $s3:Europe
    |
$s2:World
*/

您可以使用 deleteDescendants() 删除节点的后代

<?php
// delete the entire "World" section of "Business"
$s4->deleteDescendants();
/* The tree is modified as follows:
    $s1:Home
    |        \
$s4:Business $s3:Europe
*/

如果您 delete() 一个节点,则所有后代都会级联删除。为了避免意外删除整个树,对根节点调用 delete() 会抛出异常。请使用 delete() 查询方法删除整个树。

过滤结果

《nested_set》行为为生成的Query对象添加了众多方法。您可以使用这些方法构建更复杂的查询。例如,要获取根节点的所有子节点并按标题排序,请按照以下方式构建查询:

<?php
$rootNode = Section::query()->findRoot();
$children = Section::query()
    ->childrenOf($rootNode)
    ->orderBy('title')
    ->get();

多个树

当您需要为单个模型存储多个树时——例如,论坛中多个帖子线程——请为每个树使用范围。这要求您通过将use_scope参数设置为true来在行为定义中启用范围树支持。使用具有如下配置的NestedSet 特性创建模型

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Thunderwolf\EloquentNestedSet\NestedSet;

class Post extends Model
{
    use NestedSet;

    protected $table = 'posts';

    protected $fillable = ['code', 'body'];

    public $timestamps = false;

    public static function nestedSet(): array
    {
        return ['use_scope' => true, 'scope_column' => 'posts_thread_id'];
    }

    public function thread(): BelongsTo
    {
        return $this->belongsTo(PostsThread::class, 'posts_thread_id');
    }
}

在这个示例中,我们还使用了一个SingleScopedUser模型,如下所示

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class PostsThread extends Model
{
    protected $table = 'posts-threads';

    protected $fillable = ['title'];

    public $timestamps = false;

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

在注册了NestedSetServiceProvider之后,对于上面的模型,您也可以使用Blueprints通过类似以下方式创建表格,使用createNestedSet辅助方法:

$schema->create('posts-threads', function (Blueprint $table3) {
    $table3->increments('id');
    $table3->string('title');
});

$schema->create('posts', function (Blueprint $table4) {
    $table4->increments('id');
    $table4->unsignedInteger('posts_thread_id');
    $table4->string('code');
    $table4->string('body');
    $table4->createNestedSet(['use_scope' => true, 'scope_column' => 'posts_thread_id']);
});

您将以类似的方式处理迁移。

对于上面的示例,您可以拥有所需数量的树

<?php
$thread1 = PostsThread::query()->find(1);
$thread2 = PostsThread::query()->find(2);
$thread3 = PostsThread::query()->find(3);

$firstPost = Post::query()->findRoot($thread2->getKey());  // first message of the discussion
$discussion = Post::query()->findTree($thread3->getKey()); // all messages of the discussion

// first messages of every discussion
$firstPostOfEveryDiscussion = Post::query()->findRoots();

Post::query()->inTree($thread1->getKey())->delete(); // delete an entire discussion

配置包

警告!范围功能尚未移动到Eloquent,仅计划从Propel移动

默认情况下,该行为为模型添加了三列——如果您使用范围功能,则为四列。您可以使用自定义名称为嵌套集合列命名。

您还可以按如下方式配置要创建和使用的列:

<?php

use Illuminate\Database\Eloquent\Model;
use Thunderwolf\EloquentNestedSet\NestedSet;

class Category extends Model
{

    use NestedSet;

    protected $table = 'categories';

    protected $fillable = ['name'];

    public $timestamps = false;

    public static function nestedSet(): array
    {
        return ['left' => 'lft', 'right' => 'rgt', 'level' => 'lvl'];
    }

    public static function resetActionsPerformed()
    {
        static::$actionsPerformed = 0;
    }
}

然后,您的蓝图将如下所示

$schema->create('categories', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->createNestedSet(['left' => 'lft', 'right' => 'rgt', 'level' => 'lvl']);
});

完整的API

以下是该行为添加到模型对象的列表

<?php
// storage columns accessors
public function getLftName(): string
public function getRgtName(): string
public function getLvlName(): string
public function getLft(): ?int
public function setLft(int $value)
public function getRgt(): ?int
public function setRgt(int $value)
public function getLvl(): ?int
public function setLvl(int $value)

// only for behavior with use_scope
public function isNestedSetScopeUsed(): bool
public function getNestedSetScopeName(): string
public function getNestedSetScope(): ?int
public function setNestedSetScope(int $rank): void

// root maker (requires calling save() afterwards)
public function makeRoot(): self

// inspection methods
public function isInTree(): bool
public function isRoot(): bool
public function isLeaf(): bool
public function isDescendantOf(self $parent): bool
public function isAncestorOf(self $child): bool
public function hasParent(): bool
public function hasPrevSibling(): bool
public function hasNextSibling(): bool
public function hasChildren(): bool
public function countChildren(): int
public function countDescendants(): int

// tree traversal methods
public function getParent(): ?Model
public function getPrevSibling(): ?Model
public function getNextSibling(): ?Model
public function getChildren(): ?Collection
public function getFirstChild(): ?Model
public function getLastChild(): ?Model
public function getSiblings(bool $includeCurrent = false): ?Collection
public function getDescendants(): ?Collection
public function getBranch(): ?Collection
public function getAncestors(): ?Collection

// node insertion methods (immediate, no need to save() afterwards) - automatic object refresh
public function addChild(Model $child, string $where = 'first'): self

// node insertion methods (require calling save() afterwards)
public function insertAsFirstChildOf(Model $parent): self
public function insertAsLastChildOf(Model $parent): self
public function insertAsPrevSiblingOf(Model $sibling): self
public function insertAsNextSiblingOf(Model $sibling): self

// node move methods (immediate, no need to save() afterwards)
public function moveToFirstChildOf(Model $parent): self
public function moveToLastChildOf(Model $parent): self
public function moveToPrevSiblingOf(Model $sibling): self
public function moveToNextSiblingOf(Model $sibling): self

// deletion methods
public function deleteDescendants(): int

// refresh metod
public function reload(): Model

以下是该行为添加到Builder的列表

<?php
// tree filter methods
public function descendantsOf(Model $node): self
public function branchOf(Model $node): self
public function childrenOf(Model $node): self
public function siblingsOf(Model $node): self
public function ancestorsOf(Model $node): self
public function rootsOf(Model $node): self

// only for behavior with use_scope
public function treeRoots(): self
public function inTree(int $scope = null): self
public function findRoots()

// order methods
public function orderByBranch(bool $reverse = false): self
public function orderByLevel(bool $reverse = false): self

// termination methods
public function findRoot(int $scope = null)
public function findTree(int $scope = null)

// delete method
public function deleteTree($scope = null, ConnectionInterface $con = null): int