staudenmeir / laravel-adjacency-list
使用公共表表达式(CTE)进行递归Laravel Eloquent关系
Requires
- php: ^8.2
- illuminate/database: ^11.0
- staudenmeir/eloquent-has-many-deep-contracts: ^1.2
- staudenmeir/laravel-cte: ^1.11
Requires (Dev)
- barryvdh/laravel-ide-helper: ^3.0
- harrygulliford/laravel-firebird: dev-laravel-11.x
- larastan/larastan: ^2.0
- mockery/mockery: ^1.5.1
- orchestra/testbench: ^9.0
- phpstan/phpstan-mockery: ^1.1
- phpunit/phpunit: ^11.0
- singlestoredb/singlestoredb-laravel: ^1.5.4
- staudenmeir/eloquent-has-many-deep: ^1.20
Suggests
- barryvdh/laravel-ide-helper: Provide type hints for attributes and relations.
- dev-main
- v1.22.2
- v1.22.1
- v1.22
- v1.21
- 1.20.x-dev
- v1.20.1
- v1.20
- v1.19
- v1.18
- v1.17
- v1.16
- v1.15
- v1.14
- v1.13.11
- v1.13.10
- v1.13.9
- v1.13.8
- v1.13.7
- v1.13.6
- v1.13.5
- v1.13.4
- v1.13.3
- v1.13.2
- v1.13.1
- v1.13
- 1.12.x-dev
- v1.12.11
- v1.12.10
- v1.12.9
- v1.12.8
- v1.12.7
- v1.12.6
- v1.12.5
- v1.12.4
- v1.12.3
- v1.12.2
- v1.12.1
- v1.12
- v1.11.4
- v1.11.3
- v1.11.2
- v1.11.1
- v1.11
- v1.10.3
- v1.10.2
- v1.10.1
- v1.10
- 1.9.x-dev
- v1.9.6
- v1.9.5
- v1.9.4
- v1.9.3
- v1.9.2
- v1.9.1
- v1.9
- v1.8
- v1.7.1
- v1.7
- v1.6.1
- v1.6
- 1.5.x-dev
- v1.5.1
- v1.5
- v1.4.1
- v1.4
- 1.3.x-dev
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3
- v1.2.3
- v1.2.2
- v1.2.1
- v1.2
- 1.1.x-dev
- v1.1.5
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1
- 1.0.x-dev
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0
- dev-firebird-tests
- dev-sqlsrv-varchar
- dev-TODO
This package is auto-updated.
Last update: 2024-09-11 18:18:31 UTC
README
此Laravel Eloquent扩展通过使用公共表表达式(CTE)提供对树和图的递归关系
兼容性
- MySQL 8.0+
- MariaDB 10.2+
- PostgreSQL 9.4+
- SQLite 3.8.3+
- SQL Server 2008+
- SingleStore 8.1+ (仅树)
- Firebird
安装
composer require staudenmeir/laravel-adjacency-list:"^1.0"
如果您在Windows的PowerShell中(例如在VS Code中),请使用此命令
composer require staudenmeir/laravel-adjacency-list:"^^^^1.0"
版本
用法
该包提供递归关系以遍历两种数据结构
树:每个节点一个父节点(一对多)
使用该包遍历具有每个节点一个父节点的树结构。用例可能是递归分类、页面层次结构或嵌套评论。
支持Laravel 5.5+。
入门
考虑以下表架构,用于树的层次数据
Schema::create('users', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('parent_id')->nullable(); });
在您的模型中使用HasRecursiveRelationships
特质来处理递归关系
class User extends Model { use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships; }
默认情况下,特质期望名为parent_id
的父键。您可以通过覆盖getParentKeyName()
来自定义它
class User extends Model { public function getParentKeyName() { return 'parent_id'; } }
默认情况下,特质使用模型的主键作为本地键。您可以通过覆盖getLocalKeyName()
来自定义它
class User extends Model { public function getLocalKeyName() { return 'id'; } }
包含的关系
特质提供各种关系
ancestors()
:模型的递归父级。ancestorsAndSelf()
:模型的递归父级和自身。bloodline()
:模型的祖先、后代和自身。children()
:模型直接的后代。childrenAndSelf()
:模型的直接后代和自身。descendants()
:模型的递归后代。descendantsAndSelf()
:模型的递归后代和自身。parent()
:模型直接的父亲。parentAndSelf()
:模型的直接父亲和自身。rootAncestor()
:模型的最顶层父亲。rootAncestorOrSelf()
:模型的最顶层父亲或自身。siblings()
:父亲的其它后代。siblingsAndSelf()
:父亲的所有后代。
$ancestors = User::find($id)->ancestors; $users = User::with('descendants')->get(); $users = User::whereHas('siblings', function ($query) { $query->where('name', 'John'); })->get(); $total = User::find($id)->descendants()->count(); User::find($id)->descendants()->update(['active' => false]); User::find($id)->siblings()->delete();
树
特质提供了tree()
查询作用域,从根开始获取所有模型
$tree = User::tree()->get();
treeOf()
允许您查询具有根模型自定义约束的树
$constraint = function ($query) { $query->whereNull('parent_id')->where('list_id', 1); }; $tree = User::treeOf($constraint)->get();
您还可以传递最大深度
$tree = User::tree(3)->get(); $tree = User::treeOf($constraint, 3)->get();
过滤器
特质提供了查询作用域,按模型在树中的位置过滤模型
hasChildren()
:有后代的模型。hasParent()
:有父亲的模型。isLeaf()
/doesntHaveChildren()
:没有后代的模型。isRoot()
:没有父亲的模型。
$noLeaves = User::hasChildren()->get(); $noRoots = User::hasParent()->get(); $leaves = User::isLeaf()->get(); $leaves = User::doesntHaveChildren()->get(); $roots = User::isRoot()->get();
排序
特质提供了查询作用域,按广度优先或深度优先排序模型
breadthFirst()
:在获取后代之前获取兄弟。depthFirst()
:先获取子节点,再获取兄弟节点。
$tree = User::tree()->breadthFirst()->get(); $descendants = User::find($id)->descendants()->depthFirst()->get();
深度
祖先、血统、后代和树查询的结果包括一个额外的 depth
列。
它包含模型相对于查询父级的深度。对于后代,深度为正;对于祖先,深度为负
$descendantsAndSelf = User::find($id)->descendantsAndSelf()->depthFirst()->get(); echo $descendantsAndSelf[0]->depth; // 0 echo $descendantsAndSelf[1]->depth; // 1 echo $descendantsAndSelf[2]->depth; // 2
如果您的表已经包含一个 depth
列,可以通过覆盖 getDepthName()
来更改列名
class User extends Model { public function getDepthName() { return 'depth'; } }
深度约束
您可以使用 whereDepth()
查询范围来根据模型的相对深度过滤模型
$descendants = User::find($id)->descendants()->whereDepth(2)->get(); $descendants = User::find($id)->descendants()->whereDepth('<', 3)->get();
带有 whereDepth()
约束的查询,即使限制了最大深度,也会在内部构建整个(子)树。使用 withMaxDepth()
来设置最大深度,这样可以提高查询性能,只需构建树请求的部分
$descendants = User::withMaxDepth(3, function () use ($id) { return User::find($id)->descendants; });
这也适用于负深度(技术上相当于最小值)
$ancestors = User::withMaxDepth(-3, function () use ($id) { return User::find($id)->ancestors; });
路径
祖先、血统、后代和树查询的结果还包括一个额外的 path
列。
它包含从查询父级到模型的点分隔的本地键路径
$descendantsAndSelf = User::find(1)->descendantsAndSelf()->depthFirst()->get(); echo $descendantsAndSelf[0]->path; // 1 echo $descendantsAndSelf[1]->path; // 1.2 echo $descendantsAndSelf[2]->path; // 1.2.3
如果您的表已经包含一个 path
列,可以通过覆盖 getPathName()
来更改列名
class User extends Model { public function getPathName() { return 'path'; }
您还可以通过覆盖 getPathSeparator()
来自定义路径分隔符
class User extends Model { public function getPathSeparator() { return '.'; } }
自定义路径
您还可以向查询结果中添加自定义路径列
class User extends Model { public function getCustomPaths() { return [ [ 'name' => 'slug_path', 'column' => 'slug', 'separator' => '/', ], ]; } } $descendantsAndSelf = User::find(1)->descendantsAndSelf; echo $descendantsAndSelf[0]->slug_path; // user-1 echo $descendantsAndSelf[1]->slug_path; // user-1/user-2 echo $descendantsAndSelf[2]->slug_path; // user-1/user-2/user-3
您还可以反转自定义路径
class User extends Model { public function getCustomPaths() { return [ [ 'name' => 'reverse_slug_path', 'column' => 'slug', 'separator' => '/', 'reverse' => true, ], ]; } }
嵌套结果
使用结果集合上的 toTree()
方法来生成嵌套树
$users = User::tree()->get(); $tree = $users->toTree();
它递归地设置 children
关系
[ { "id": 1, "children": [ { "id": 2, "children": [ { "id": 3, "children": [] } ] }, { "id": 4, "children": [ { "id": 5, "children": [] } ] } ] } ]
初始查询和递归查询约束
您可以在 CTE 的初始和递归查询中添加自定义约束。考虑一个在跳过非活动用户及其后代的同时遍历树的查询
$tree = User::withQueryConstraint(function (Builder $query) { $query->where('users.active', true); }, function () { return User::tree()->get(); });
您还可以使用 withInitialQueryConstraint()
/ withRecursiveQueryConstraint()
仅在初始或递归查询中添加自定义约束。
其他方法
特质还提供了检查模型之间关系的方法
isChildOf(Model $model)
:检查当前模型是否是给定模型的子模型。isParentOf(Model $model)
:检查当前模型是否是给定模型的父模型。getDepthRelatedTo(Model $model)
:返回当前模型相对于给定模型的深度。
$rootUser = User::create(['parent_id' => null]); $firstLevelUser = User::create(['parent_id' => $rootUser->id]); $secondLevelUser = User::create(['parent_id' => $firstLevelUser->id]); $isChildOf = $secondLevelUser->isChildOf($firstLevelUser); // Output: true $isParentOf = $rootUser->isParentOf($firstLevelUser); // Output: true $depthRelatedTo = $secondLevelUser->getDepthRelatedTo($rootUser); // Output: 2
自定义关系
您还可以定义自定义关系以递归检索相关模型。
- HasManyOfDescendants
- BelongsToManyOfDescendants
- MorphToManyOfDescendants
- MorphedByManyOfDescendants
- 中间作用域
- Laravel 之外的使用
HasManyOfDescendants
考虑 User
和 Post
之间的 HasMany
关系
class User extends Model { public function posts() { return $this->hasMany(Post::class); } }
定义一个 HasManyOfDescendants
关系以获取用户的全部帖子及其后代的帖子
class User extends Model { public function recursivePosts() { return $this->hasManyOfDescendantsAndSelf(Post::class); } } $recursivePosts = User::find($id)->recursivePosts; $users = User::withCount('recursivePosts')->get();
使用 hasManyOfDescendants()
仅获取后代的帖子
class User extends Model { public function descendantPosts() { return $this->hasManyOfDescendants(Post::class); } }
BelongsToManyOfDescendants
考虑 User
和 Role
之间的 BelongsToMany
关系
class User extends Model { public function roles() { return $this->belongsToMany(Role::class); } }
定义一个 BelongsToManyOfDescendants
关系以获取用户的全部角色及其后代的角色
class User extends Model { public function recursiveRoles() { return $this->belongsToManyOfDescendantsAndSelf(Role::class); } } $recursiveRoles = User::find($id)->recursiveRoles; $users = User::withCount('recursiveRoles')->get();
使用 belongsToManyOfDescendants()
仅获取后代的角色
class User extends Model { public function descendantRoles() { return $this->belongsToManyOfDescendants(Role::class); } }
MorphToManyOfDescendants
考虑 User
和 Tag
之间的 MorphToMany
关系
class User extends Model { public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } }
定义一个 MorphToManyOfDescendants
关系以获取用户的全部标签及其后代的标签
class User extends Model { public function recursiveTags() { return $this->morphToManyOfDescendantsAndSelf(Tag::class, 'taggable'); } } $recursiveTags = User::find($id)->recursiveTags; $users = User::withCount('recursiveTags')->get();
使用 morphToManyOfDescendants()
仅获取后代的标签
class User extends Model { public function descendantTags() { return $this->morphToManyOfDescendants(Tag::class, 'taggable'); } }
MorphedByManyOfDescendants
考虑 Category
和 Post
之间的 MorphedByMany
关系
class Category extends Model { public function posts() { return $this->morphedByMany(Post::class, 'categorizable'); } }
定义一个 MorphedByManyOfDescendants
关系以获取类别及其后代的全部帖子
class Category extends Model { public function recursivePosts() { return $this->morphedByManyOfDescendantsAndSelf(Post::class, 'categorizable'); } } $recursivePosts = Category::find($id)->recursivePosts; $categories = Category::withCount('recursivePosts')->get();
使用 morphedByManyOfDescendants()
仅获取后代的帖子
class Category extends Model { public function descendantPosts() { return $this->morphedByManyOfDescendants(Post::class, 'categorizable'); } }
中间作用域
您可以通过添加或删除中间作用域来调整后代查询(例如,子用户)
User::find($id)->recursivePosts()->withTrashedDescendants()->get(); User::find($id)->recursivePosts()->withIntermediateScope('active', new ActiveScope())->get(); User::find($id)->recursivePosts()->withIntermediateScope( 'depth', function ($query) { $query->whereDepth('<=', 10); } )->get(); User::find($id)->recursivePosts()->withoutIntermediateScope('active')->get();
Laravel 之外的使用
如果您在 Laravel 之外使用此包或已禁用 staudenmeir/laravel-cte
的包发现,则需要将公共表表达式(CTE)的支持添加到相关模型中
class Post extends Model { use \Staudenmeir\LaravelCte\Eloquent\QueriesExpressions; }
深度关系连接
您可以通过使用 staudenmeir/eloquent-has-many-deep 将递归关系与其他关系连接起来,从而将其纳入深层关系。这适用于 Ancestors
、Bloodline
和 Descendants
关系(Laravel 9+)。
考虑一个 HasMany
关系,在 User
和 Post
之间建立深层关系以获取一个用户的下级所有帖子。
User
→ 下级 → User
→ 多对一 → Post
安装额外的包,将 HasRelationships
特性添加到递归模型中,并 定义 深层关系。
class User extends Model { use \Staudenmeir\EloquentHasManyDeep\HasRelationships; use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships; public function descendantPosts(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep { return $this->hasManyDeepFromRelations( $this->descendants(), (new static)->posts() ); } public function posts() { return $this->hasMany(Post::class); } } $descendantPosts = User::find($id)->descendantPosts;
目前,递归关系只能位于深层关系的开始。
- 支持:
User
→ 下级 →User
→ 多对一 →Post
- 不支持:
Post
→ 属于 →User
→ 下级 →User
已知问题
MariaDB 尚未支持 子查询中的相关CTE。这影响了如 User::whereHas('descendants')
或 User::withCount('descendants')
的查询。
图:每个节点多个父节点(多对多)
您还可以使用此包遍历具有多个父节点的图,这些节点定义在辅助表中。用例可能是物料清单(BOM)或家谱。
支持 Laravel 9+。
入门
考虑以下表架构,用于存储作为节点和边的有向图
Schema::create('nodes', function (Blueprint $table) { $table->id(); }); Schema::create('edges', function (Blueprint $table) { $table->unsignedBigInteger('source_id'); $table->unsignedBigInteger('target_id'); $table->string('label'); $table->unsignedBigInteger('weight'); });
在您的模型中使用 HasGraphRelationships
特性来处理图关系,并指定辅助表名称
class Node extends Model { use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasGraphRelationships; public function getPivotTableName(): string { return 'edges'; } }
默认情况下,特性期望辅助表中有一个名为 parent_id
的父键和一个名为 child_id
的子键。您可以通过重写 getParentKeyName()
和 getChildKeyName()
来自定义它们。
class Node extends Model { public function getParentKeyName(): string { return 'source_id'; } public function getChildKeyName(): string { return 'target_id'; } }
默认情况下,特质使用模型的主键作为本地键。您可以通过覆盖getLocalKeyName()
来自定义它
class Node extends Model { public function getLocalKeyName(): string { return 'id'; } }
包含的关系
特质提供各种关系
ancestors()
:节点的递归父级。ancestorsAndSelf()
:节点的递归父级和自身。children()
:节点的直接子级。childrenAndSelf()
:节点的直接子级和自身。descendants()
:节点的递归子级。descendantsAndSelf()
:节点的递归子级和自身。parents()
:节点的直接父级。parentsAndSelf()
:节点的直接父级和自身。
$ancestors = Node::find($id)->ancestors; $nodes = Node::with('descendants')->get(); $nodes = Node::has('children')->get(); $total = Node::find($id)->descendants()->count(); Node::find($id)->descendants()->update(['active' => false]); Node::find($id)->parents()->delete();
辅助列
类似于 BelongsToMany
关系,除了父键和子键之外,您还可以从辅助表检索其他列。
class Node extends Model { public function getPivotColumns(): array { return ['label', 'weight']; } } $nodes = Node::find($id)->descendants; foreach ($nodes as $node) { dump( $node->pivot->label, $node->pivot->weight ); }
循环检测
如果您的图包含循环,您需要启用循环检测以防止无限循环。
class Node extends Model { public function enableCycleDetection(): bool { return true; } }
您还可以检索循环的开始,即第一个重复的节点。使用此选项,查询结果包括一个 is_cycle
列,指示节点是否为循环的一部分。
class Node extends Model { public function enableCycleDetection(): bool { return true; } public function includeCycleStart(): bool { return true; } } $nodes = Node::find($id)->descendants; foreach ($nodes as $node) { dump($node->is_cycle); }
子图
特性提供了 subgraph()
查询作用域,以获取自定义约束的子图。
$constraint = function ($query) { $query->whereIn('id', $ids); }; $subgraph = Node::subgraph($constraint)->get();
您可以将最大深度作为第二个参数传递。
$subgraph = Node::subgraph($constraint, 3)->get();
排序
特性提供了查询作用域来按广度优先或深度优先排序节点。
breadthFirst()
:在获取后代之前获取兄弟。depthFirst()
:先获取子节点,再获取兄弟节点。
$descendants = Node::find($id)->descendants()->breadthFirst()->get(); $descendants = Node::find($id)->descendants()->depthFirst()->get();
深度
祖先、子级和子图查询的结果包含一个额外的 depth
列。
它包含节点相对于查询父级的 相对 深度。对于子级,深度为正,对于祖先,深度为负。
$descendantsAndSelf = Node::find($id)->descendantsAndSelf()->depthFirst()->get(); echo $descendantsAndSelf[0]->depth; // 0 echo $descendantsAndSelf[1]->depth; // 1 echo $descendantsAndSelf[2]->depth; // 2
如果您的表已经包含一个 depth
列,可以通过覆盖 getDepthName()
来更改列名
class Node extends Model { public function getDepthName(): string { return 'depth'; } }
深度约束
您可以使用 whereDepth()
查询作用域通过节点的相对深度过滤节点。
$descendants = Node::find($id)->descendants()->whereDepth(2)->get(); $descendants = Node::find($id)->descendants()->whereDepth('<', 3)->get();
具有 whereDepth()
约束的查询限制最大深度仍然在内部构建整个(子)图。使用 withMaxDepth()
设置一个最大深度,通过仅构建请求的图部分来提高查询性能。
$descendants = Node::withMaxDepth(3, function () use ($id) { return Node::find($id)->descendants; });
这也适用于负深度(技术上相当于最小值)
$ancestors = Node::withMaxDepth(-3, function () use ($id) { return Node::find($id)->ancestors; });
路径
祖先、子级和子图查询的结果包含一个额外的 path
列。
它包含从查询的父节点到节点的点分隔的本地键路径
$descendantsAndSelf = Node::find(1)->descendantsAndSelf()->depthFirst()->get(); echo $descendantsAndSelf[0]->path; // 1 echo $descendantsAndSelf[1]->path; // 1.2 echo $descendantsAndSelf[2]->path; // 1.2.3
如果您的表已经包含一个 path
列,可以通过覆盖 getPathName()
来更改列名
class Node extends Model { public function getPathName(): string { return 'path'; } }
您还可以通过覆盖 getPathSeparator()
来自定义路径分隔符
class Node extends Model { public function getPathSeparator(): string { return '.'; } }
自定义路径
您还可以向查询结果中添加自定义路径列
class Node extends Model { public function getCustomPaths(): array { return [ [ 'name' => 'slug_path', 'column' => 'slug', 'separator' => '/', ], ]; } } $descendantsAndSelf = Node::find(1)->descendantsAndSelf; echo $descendantsAndSelf[0]->slug_path; // node-1 echo $descendantsAndSelf[1]->slug_path; // node-1/node-2 echo $descendantsAndSelf[2]->slug_path; // node-1/node-2/node-3
您还可以反转自定义路径
class Node extends Model { public function getCustomPaths(): array { return [ [ 'name' => 'reverse_slug_path', 'column' => 'slug', 'separator' => '/', 'reverse' => true, ], ]; } }
嵌套结果
使用结果集合上的 toTree()
方法来生成嵌套树
$nodes = Node::find($id)->descendants; $tree = $nodes->toTree();
它递归地设置 children
关系
[ { "id": 1, "children": [ { "id": 2, "children": [ { "id": 3, "children": [] } ] }, { "id": 4, "children": [ { "id": 5, "children": [] } ] } ] } ]
初始查询和递归查询约束
您可以为CTE的初始和递归查询添加自定义约束。考虑一个查询,您想要遍历节点的后代,同时跳过非活动节点及其后代
$descendants = Node::withQueryConstraint(function (Builder $query) { $query->where('nodes.active', true); }, function () { return Node::find($id)->descendants; });
您还可以使用 withInitialQueryConstraint()
/ withRecursiveQueryConstraint()
仅在初始或递归查询中添加自定义约束。
深度关系连接
您可以通过使用 staudenmeir/eloquent-has-many-deep (Laravel 9+) 将递归关系包含到深层关系中,通过将它们与其他关系连接起来。
考虑 Node
和 Post
之间的 HasMany
关系,并构建深层关系以获取节点后代的全部帖子
Node
→ 后代 → Node
→ 多个 → Post
安装额外的包,将 HasRelationships
特性添加到递归模型中,并 定义 深层关系。
class Node extends Model { use \Staudenmeir\EloquentHasManyDeep\HasRelationships; use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasGraphRelationships; public function descendantPosts(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep { return $this->hasManyDeepFromRelations( $this->descendants(), (new static)->posts() ); } public function posts() { return $this->hasMany(Post::class); } } $descendantPosts = Node::find($id)->descendantPosts;
目前,递归关系只能位于深层关系的开始。
- 支持:
Node
→ 后代 →Node
→ 多个 →Post
- 不支持:
Post
→ 属于 →Node
→ 后代 →Node
已知问题
MariaDB 尚未支持 子查询中的相关CTE。这影响了如 Node::whereHas('descendants')
或 Node::withCount('descendants')
的查询。
包冲突
staudenmeir/eloquent-param-limit-fix
:将两个包都替换为 staudenmeir/eloquent-param-limit-fix-x-laravel-adjacency-list 以在同一个模型上使用它们。
贡献
请参阅 CONTRIBUTING 和 CODE_OF_CONDUCT 了解详细信息。