paullund / nestedset
Laravel 4-5 的嵌套集模型
Requires
- php: >=7.1.3
- illuminate/database: ~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0
- illuminate/events: ~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0
- illuminate/support: ~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0
Requires (Dev)
- phpunit/phpunit: 8.*
- v6.x-dev
- v6.0.0
- v5.x-dev
- v5.0.4
- v5.0.3
- v4.x-dev
- v4.3.6
- v4.3.5
- v4.3.4
- v4.3.3
- v4.3.2
- v4.3.1
- v4.3.0
- v4.2.7
- v4.2.6
- v4.2.5
- v4.2.4
- 4.2.3
- v4.2.2
- v4.2.1
- v4.2.0
- v4.1.6
- 4.1.5
- v4.1.4
- v4.1.3
- v4.1.2
- v4.1.1
- v4.1.0
- v4.1.0-beta
- v4.0.1
- v4.0.0
- v3.x-dev
- v3.1.4
- v3.1.3
- v3.1.2
- v3.1.1
- v3.1.0
- v3.0.0
- v2.4.4
- v2.4.3
- v2.4.2
- v2.4.1
- v2.4.0
- v2.3.2
- v2.3.1
- v2.3.0
- v2.2.0
- v2.1.0
- v2.0.2
- v2.0.1
- v2.0
- v2.0-beta3
- v2.0-beta2
- v2.0-beta
- v1.1
- 1.0
- 1.0-beta
- dev-master / 4.2.x-dev
This package is auto-updated.
Last update: 2024-09-09 15:16:13 UTC
README
这是一个用于在关系型数据库中操作树结构的 Laravel 4-5 包。
- 支持 Laravel 5.5, 5.6, 5.7, 5.8,从 v4.3 版本开始
- 支持 Laravel 5.2, 5.3, 5.4,从 v4 版本开始
- Laravel 5.1 在 v3 版本中支持
- Laravel 4 在 v2 版本中支持
尽管这个项目完全免费使用,但我非常感激任何支持!
内容
什么是嵌套集?
嵌套集或嵌套集模型是一种在关系表中有效地存储层次数据的方法。来自维基百科
嵌套集模型是对节点按照树遍历进行编号,每个节点访问两次,按照访问顺序分配数字,并在两次访问时。这为每个节点留下两个数字,这两个数字存储为两个属性。查询变得非常便宜:可以通过比较这些数字来测试层次成员资格。更新需要重新编号,因此成本较高。
应用
当树很少更新时,NSM 显示出良好的性能。它被调整以快速获取相关节点。它非常适合构建多层菜单或商店的分类。
文档
假设我们有一个模型 Category;一个 $node 变量是该模型的一个实例,也是我们正在操作的节点。它可以是一个新的模型或来自数据库的一个。
关系
节点有以下关系,这些关系完全功能且可以懒加载
- 节点属于
parent - 节点有多个
children - 节点有多个
ancestors - 节点有多个
descendants
插入节点
移动和插入节点包括多个数据库查询,因此强烈建议使用事务。
重要!从 v4.2.0 版本开始,事务不会自动启动
另一个重要的注意事项是,结构操作被延迟到你在模型上调用 save 时(一些方法隐式调用 save 并返回操作的布尔结果)。
如果模型成功保存,并不意味着节点已经移动。如果你的应用程序依赖于节点是否实际上改变了位置,请使用 hasMoved 方法
if ($node->save()) { $moved = $node->hasMoved(); }
创建节点
当你简单地创建一个节点时,它将被追加到树的末尾
Category::create($attributes); // Saved as root
$node = new Category($attributes); $node->save(); // Saved as root
在这种情况下,该节点被视为一个 root,这意味着它没有父节点。
从现有节点创建根节点
// #1 Implicit save $node->saveAsRoot(); // #2 Explicit save $node->makeRoot()->save();
节点将被追加到树的末尾。
追加和预先指定父节点
如果你想使节点成为其他节点的子节点,你可以将其设置为最后一个或第一个子节点。
在以下示例中,$parent 是某个现有节点。
有几种方法可以追加节点
// #1 Using deferred insert $node->appendToNode($parent)->save(); // #2 Using parent node $parent->appendNode($node); // #3 Using parent's children relationship $parent->children()->create($attributes); // #5 Using node's parent relationship $node->parent()->associate($parent)->save(); // #6 Using the parent attribute $node->parent_id = $parent->id; $node->save(); // #7 Using static method Category::create($attributes, $parent);
并且只有几种方法可以预先追加
// #1 $node->prependToNode($parent)->save(); // #2 $parent->prependNode($node);
在指定节点之前或之后插入
你可以使用以下方法使 $node 成为 $neighbor 节点的邻居
$neighbor 必须存在,目标节点可以是新的。如果目标节点存在,它将被移动到新的位置,如果需要,父节点将更改。
# Explicit save $node->afterNode($neighbor)->save(); $node->beforeNode($neighbor)->save(); # Implicit save $node->insertAfterNode($neighbor); $node->insertBeforeNode($neighbor);
从数组构建树
当使用节点的静态方法 create 时,它会检查属性是否包含 children 键。如果包含,它会递归地创建更多节点。
$node = Category::create([ 'name' => 'Foo', 'children' => [ [ 'name' => 'Bar', 'children' => [ [ 'name' => 'Baz' ], ], ], ], ]);
$node->children 现在包含了一个已创建的子节点列表。
从数组重建树
您可以轻松地重建树。这在大量更改树结构时非常有用。
Category::rebuildTree($data, $delete);
$data 是一个节点数组
$data = [ [ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ], [ 'name' => 'bar' ], ];
对于名为 foo 的节点指定了一个 id,这意味着现有的节点将被填充并保存。如果节点不存在,则抛出 ModelNotFoundException。此外,此节点还指定了 children,它也是一个节点数组;它们将以相同的方式处理并保存为 foo 节点的子节点。
节点 bar 未指定主键,因此它将被创建。
$delete 显示是否删除已存在但不在 $data 中的节点。默认情况下,不删除节点。
重建子树
从 4.2.8 版本开始,您可以重建子树
Category::rebuildSubtree($root, $data);
这会将树重建限制在 $root 节点的后代。
检索节点
在某些情况下,我们将使用一个 $id 变量,它是目标节点的 id。
祖先和后代
祖先构成节点到其父节点的一条链。这对于显示当前类别的面包屑非常有用。
后代是子树中的所有节点,即节点的子节点、子节点的子节点等。
祖先和后代都可以进行懒加载。
// Accessing ancestors $node->ancestors; // Accessing descendants $node->descendants;
您可以使用自定义查询加载祖先和后代
$result = Category::ancestorsOf($id); $result = Category::ancestorsAndSelf($id); $result = Category::descendantsOf($id); $result = Category::descendantsAndSelf($id);
在大多数情况下,您需要按级别对祖先进行排序
$result = Category::defaultOrder()->ancestorsOf($id);
可以懒加载祖先集合
$categories = Category::with('ancestors')->paginate(30); // in view for breadcrumbs: @foreach($categories as $i => $category) <small>{{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' }}</small><br> {{ $category->name }} @endforeach
兄弟节点
兄弟节点是具有相同父节点的节点。
$result = $node->getSiblings(); $result = $node->siblings()->get();
获取仅下一个兄弟节点
// Get a sibling that is immediately after the node $result = $node->getNextSibling(); // Get all siblings that are after the node $result = $node->getNextSiblings(); // Get all siblings using a query $result = $node->nextSiblings()->get();
获取上一个兄弟节点
// Get a sibling that is immediately before the node $result = $node->getPrevSibling(); // Get all siblings that are before the node $result = $node->getPrevSiblings(); // Get all siblings using a query $result = $node->prevSiblings()->get();
从其他表中获取相关模型
想象一下,每个类别 has many 商品。即建立了 HasMany 关系。如何获取 $category 和其所有后代的商品?很简单!
// Get ids of descendants $categories = $category->descendants()->pluck('id'); // Include the id of category itself $categories[] = $category->getKey(); // Get goods $goods = Goods::whereIn('category_id', $categories)->get();
包含节点深度
如果您需要知道节点处于哪个级别
$result = Category::withDepth()->find($id); $depth = $result->depth;
根节点位于级别 0。根节点的子节点将具有级别 1,等等。
要获取指定级别的节点,可以应用 having 约束
$result = Category::withDepth()->having('depth', '=', 1)->get();
重要! 在数据库严格模式下,此方法将不起作用
默认排序
所有节点在内部严格组织。默认情况下,不应用排序,因此节点可能以随机顺序出现,这不会影响树显示。您可以通过字母或其它索引对节点进行排序。
但在某些情况下,层次结构顺序是必不可少的。它对于检索祖先和用于排序菜单项是必需的。
要应用树顺序,请使用 defaultOrder 方法
$result = Category::defaultOrder()->get();
您可以按相反的顺序获取节点
$result = Category::reversed()->get();
要将节点在父节点内部上移或下移以影响默认顺序
$bool = $node->down(); $bool = $node->up(); // Shift node by 3 siblings $bool = $node->down(3);
操作的结果是表示节点是否更改位置的布尔值。
约束
可以应用于查询构建器的各种约束
- whereIsRoot() 获取仅根节点;
- hasParent() 获取非根节点;
- whereIsLeaf() 获取仅叶节点;
- hasChildren() 获取非叶节点;
- whereIsAfter($id) 获取每个指定 id 后的节点(不仅仅是兄弟节点);
- whereIsBefore($id) 获取每个指定 id 前的节点。
后代约束
$result = Category::whereDescendantOf($node)->get(); $result = Category::whereNotDescendantOf($node)->get(); $result = Category::orWhereDescendantOf($node)->get(); $result = Category::orWhereNotDescendantOf($node)->get(); $result = Category::whereDescendantAndSelf($id)->get(); // Include target node into result set $result = Category::whereDescendantOrSelf($node)->get();
祖先约束
$result = Category::whereAncestorOf($node)->get(); $result = Category::whereAncestorOrSelf($id)->get();
$node 可以是模型的 primary key 或模型实例。
构建树
在获取一组节点后,您可以将其转换为树。例如
$tree = Category::get()->toTree();
这将填充集合中每个节点的 parent 和 children 关系,然后您可以使用递归算法渲染树
$nodes = Category::get()->toTree(); $traverse = function ($categories, $prefix = '-') use (&$traverse) { foreach ($categories as $category) { echo PHP_EOL.$prefix.' '.$category->name; $traverse($category->children, $prefix.'-'); } }; $traverse($nodes);
这将输出类似以下的内容
- Root
-- Child 1
--- Sub child 1
-- Child 2
- Another root
构建平面树
此外,您还可以构建一个扁平树:一个节点列表,其中子节点紧接在父节点之后。这在您获得具有自定义顺序(例如按字母顺序)的节点且不想使用递归遍历节点时很有帮助。
$nodes = Category::get()->toFlatTree();
上一个示例将输出
Root
Child 1
Sub child 1
Child 2
Another root
获取子树
有时您不需要加载整个树,只需要特定节点的子树。以下示例展示了这一点
$root = Category::descendantsAndSelf($rootId)->toTree()->first();
在单个查询中,我们获取子树的根节点及其所有可通过 children 关系访问的后代。
如果您不需要 $root 节点本身,可以这样做
$tree = Category::descendantsOf($rootId)->toTree($rootId);
删除节点
删除节点
$node->delete();
重要!该节点拥有的任何后代也将被删除!
重要!节点必须作为模型进行删除,不要尝试使用如下查询进行删除
Category::where('id', '=', $id)->delete();
这将破坏树!
SoftDeletes 特性也支持,包括模型级别。
辅助方法
检查节点是否是其他节点的后代
$bool = $node->isDescendantOf($parent);
检查节点是否是根节点
$bool = $node->isRoot();
其他检查
$node->isChildOf($other);$node->isAncestorOf($other);$node->isSiblingOf($other);$node->isLeaf()
检查一致性
您可以检查树是否损坏(即存在某些结构错误)
$bool = Category::isBroken();
可以获取错误统计信息
$data = Category::countErrors();
它将返回一个包含以下键的数组
oddness-- 具有错误lft和rgt值的节点数量duplicates-- 具有相同lft或rgt值的节点数量wrong_parent-- 具有无效的parent_id值的节点数量,该值与lft和rgt值不对应missing_parent-- 具有指向不存在节点的parent_id的节点数量
修复树
从 v3.1 版本开始,现在可以修复树。使用 parent_id 列的继承信息,为每个节点设置适当 的 _lft 和 _rgt 值。
Node::fixTree();
作用域
假设您有一个 Menu 模型和 MenuItems。在这些模型之间设置了单向多对多关系。 MenuItem 模型有一个 menu_id 属性,用于将模型连接起来。MenuItem 模型包含嵌套集。很明显,您希望根据 menu_id 属性分别处理每个树。为此,您需要将此属性指定为作用域属性
protected function getScopeAttributes() { return [ 'menu_id' ]; }
但现在,为了执行自定义查询,您需要提供用于作用域的属性
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OK MenuItem::descendantsOf($id)->get(); // WRONG: returns nodes from other scope MenuItem::scoped([ 'menu_id' => 5 ])->fixTree(); // OK
在请求模型实例的节点时,会根据该模型的属性自动应用作用域
$node = MenuItem::findOrFail($id); $node->siblings()->withDepth()->get(); // OK
使用实例获取作用域查询构建器
$node->newScopedQuery();
作用域和预加载
始终在预加载时使用作用域查询
MenuItem::scoped([ 'menu_id' => 5])->with('descendants')->findOrFail($id); // OK MenuItem::with('descendants')->findOrFail($id); // WRONG
需求
- PHP >= 5.4
- Laravel >= 4.1
强烈建议使用支持事务的数据库(如 MySQL 的 InnoDB)来确保树不受可能的损坏。
安装
要安装包,在终端中
composer require kalnoy/nestedset
从头开始设置
模式
对于 Laravel 5.5 及以上版本的用户
Schema::create('table', function (Blueprint $table) { ... $table->nestedSet(); }); // To drop columns Schema::table('table', function (Blueprint $table) { $table->dropNestedSet(); });
对于之前版本的 Laravel
... use Kalnoy\Nestedset\NestedSet; Schema::create('table', function (Blueprint $table) { ... NestedSet::columns($table); });
删除列
... use Kalnoy\Nestedset\NestedSet; Schema::table('table', function (Blueprint $table) { NestedSet::dropColumns($table); });
模型
您的模型应该使用 Kalnoy\Nestedset\NodeTrait 特性来启用嵌套集
use Kalnoy\Nestedset\NodeTrait; class Foo extends Model { use NodeTrait; }
迁移现有数据
从其他嵌套集扩展迁移
如果您的先前扩展使用了不同的列集,您只需在您的模型类中重写以下方法
public function getLftName() { return 'left'; } public function getRgtName() { return 'right'; } public function getParentIdName() { return 'parent'; } // Specify parent id attribute mutator public function setParentAttribute($value) { $this->setParentIdAttribute($value); }
从基本父级信息迁移
如果您的树包含 parent_id 信息,您需要向您的模式添加两列
$table->unsignedInteger('_lft'); $table->unsignedInteger('_rgt');
在设置好您的模型后,您只需调整树形结构来填充 _lft 和 _rgt 列
MyModel::fixTree();
许可协议
版权所有 (c) 2017 Alexander Kalnoy
在此,任何人无代价地获得本软件及其相关文档文件(以下简称“软件”)的副本,均被授予在软件中不受限制地处理软件的权利,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本,并允许向软件提供者授予此类权利的人进行此类操作,前提是遵守以下条件
上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。
软件按“原样”提供,不提供任何形式的保证,无论是明示的还是暗示的,包括但不限于适销性、特定用途适用性和非侵权性。在任何情况下,作者或版权所有者不应对任何索赔、损害或其他责任承担责任,无论这些责任是基于合同、侵权或其他原因,是否源于、涉及或与软件或软件的使用或其他方式有关。