thunderwolf / eloquent-nested-set
基于 Propel NestedSet 和 kalnoy/nestedset 的 Laravel Eloquent 的嵌套集
Requires
- php: >=7.4
- illuminate/database: ^7.0|^8.0|^9.0|^10.0
- illuminate/events: ^7.0|^8.0|^9.0|^10.0
- illuminate/support: ^7.0|^8.0|^9.0|^10.0
Requires (Dev)
- phpunit/phpunit: ^9.5
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_left
、tree_right
和 tree_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