这是一个 Laravel 4-10 包,用于处理关系数据库中的树。
自 v6.0.4 起支持Laravel 11.0
自 v6.0.2 起支持Laravel 10.0
自 v6.0.1 起支持Laravel 9.0
自 v6.0.0 起支持Laravel 8.0
自 v5 起支持Laravel 5.7、5.8、6.0、7.0
Laravel 5.5、5.6从 v4.3 开始支持
自 v4 起支持Laravel 5.2、5.3、5.4
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(); }
当您简单地创建一个节点时,它将被附加到树的末尾:
类别::create($attributes); // 保存为root
$node = 新类别($attributes);$node->save(); // 保存为root
在这种情况下,该节点被视为根,这意味着它没有父节点。
// #1 隐式 save$node->saveAsRoot();// #2 显式 save$node->makeRoot()->save();
该节点将被附加到树的末尾。
如果想让节点成为其他节点的子节点,可以将其设为最后一个或第一个子节点。
在以下示例中, $parent
是某个现有节点。
追加节点的方法有以下几种:
// #1 使用延迟插入$node->appendToNode($parent)->save();// #2 使用父节点$parent->appendNode($node);// #3 使用父节点的子节点关系$parent- >children()->create($attributes);// #5 使用节点的父关系$node->parent()->associate($parent)->save();// #6 使用父节点attribute$node->parent_id = $parent->id;$node->save();// #7 使用静态方法Category::create($attributes, $parent);
只有几种前置方法:
// #1$node->prependToNode($parent)->save();// #2$parent->prependNode($node);
您可以使用以下方法使$node
成为$neighbor
节点的邻居:
$neighbor
必须存在,目标节点可以是新鲜的。如果目标节点存在,它将被移动到新位置,并且如果需要,父节点将被更改。
# 显式 save$node->afterNode($neighbor)->save();$node->beforeNode($neighbor)->save();# 隐式 save$node->insertAfterNode($neighbor);$node-> insertBeforeNode($neighbor);
当在节点上使用静态方法create
时,它会检查属性是否包含children
键。如果是,它会递归地创建更多节点。
$node = Category::create(['name' => 'Foo','children' => [ ['姓名' => '酒吧','儿童' => [ [ '名字' => '巴兹' ], ], ], ], ]);
$node->children
现在包含已创建的子节点的列表。
您可以轻松地重建一棵树。这对于大规模改变树的结构很有用。
类别::rebuildTree($data, $delete);
$data
是一个节点数组:
$数据 = [ [ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ], [ '名称' => '酒吧' ], ];
为名为foo
的节点指定了一个 id,这意味着现有节点将被填充并保存。如果节点不存在,则抛出ModelNotFoundException
。此外,该节点还指定了children
,这也是一个节点数组;它们将以相同的方式处理并保存为节点foo
的子节点。
节点bar
没有指定主键,因此将创建它。
$delete
显示是否删除已存在但不存在于$data
中的节点。默认情况下,不会删除节点。
从 4.2.8 开始,您可以重建子树:
类别::rebuildSubtree($root, $data);
这限制了树重建到$root
节点的后代。
在某些情况下,我们将使用$id
变量,它是目标节点的 id。
祖先为节点创建了一条父链。有助于显示当前类别的面包屑。
后代是子树中的所有节点,即节点的子节点、子节点的子节点等。
祖先和后代都可以急切地加载。
// 访问祖先$node->ancestors;// 访问后代$node->descendants;
可以使用自定义查询加载祖先和后代:
$结果 = 类别::ancestorsOf($id);$结果 = 类别::ancestorsAndSelf($id);$结果 = 类别::descendantsOf($id);$结果 = 类别::descendantsAndSelf($id);
在大多数情况下,你需要你的祖先按级别排序:
$result = Category::defaultOrder()->ancestorsOf($id);
可以急切地加载祖先集合:
$categories = Category::with('ancestors')->paginate(30);// 在面包屑视图中:@foreach($categories as $i => $category) <小>{{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : '顶级' }}</small><br> {{ $类别->名称 }} @endforeach
兄弟节点是具有相同父节点的节点。
$结果 = $node->getSiblings();$结果 = $node->siblings()->get();
仅获取下一个兄弟姐妹:
// 获取紧随该节点之后的同级节点$result = $node->getNextSibling();// 获取该节点之后的所有同级节点$result = $node->getNextSiblings();// 使用 a 获取所有同级节点查询$结果 = $node->nextSiblings()->get();
要获取以前的兄弟姐妹:
// 获取紧邻该节点之前的同级节点$result = $node->getPrevSibling();// 获取该节点之前的所有同级节点$result = $node->getPrevSiblings();// 使用 a 获取所有同级节点查询$结果 = $node->prevSiblings()->get();
想象一下,每个类别has many
商品。即HasMany
关系成立。如何获得$category
及其所有后代的所有商品?简单的!
// 获取后代的 id$categories = $category->descendants()->pluck('id');// 包含类别本身的 id$categories[] = $category->getKey();// 获取商品$goods = Goods::whereIn('category_id', $categories)->get();
如果需要知道节点处于哪个级别:
$结果 = 类别::withDepth()->find($id);$深度 = $结果->深度;
根节点的级别为 0。根节点的子节点的级别为 1,依此类推。
要获取指定级别的节点,您可以应用having
约束:
$结果 = 类别::withDepth()->having('深度', '=', 1)->get();
重要的!这在数据库严格模式下不起作用
所有节点都在内部严格组织。默认情况下,不应用任何顺序,因此节点可能会以随机顺序出现,这不会影响树的显示。您可以按字母表或其他索引对节点进行排序。
但在某些情况下,层级顺序是至关重要的。它是检索祖先所必需的,并且可用于订购菜单项。
要应用树顺序,请使用defaultOrder
方法:
$结果 = 类别::defaultOrder()->get();
您可以按相反的顺序获取节点:
$结果 = 类别::reversed()->get();
要在父级内部向上或向下移动节点以影响默认顺序:
$bool = $node->down();$bool = $node->up();// 将节点移动 3 个兄弟节点$bool = $node->down(3);
运算结果是节点是否改变位置的布尔值。
可应用于查询生成器的各种约束:
whereIsRoot()仅获取根节点;
hasParent()获取非根节点;
whereIsLeaf()仅获取叶子;
hasChildren()获取非离开节点;
whereIsAfter($id)获取具有指定 id 的节点之后的每个节点(不仅仅是兄弟节点);
whereIsBefore($id)获取具有指定 id 的节点之前的每个节点。
后代约束:
$结果 = 类别::whereDescendantOf($node)->get();$结果 = 类别::whereNotDescendantOf($node)->get();$结果 = 类别::orWhereDescendantOf($node)->get() ;$结果 = 类别::orWhereNotDescendantOf($node)->get();$结果 = Category::whereDescendantAndSelf($id)->get();// 将目标节点纳入结果集中$result = Category::whereDescendantOrSelf($node)->get();
祖先约束:
$结果 = 类别::whereAncestorOf($node)->get();$结果 = 类别::whereAncestorOrSelf($id)->get();
$node
可以是模型或模型实例的主键。
获得一组节点后,可以将其转换为树。例如:
$tree = 类别::get()->toTree();
这将填充集合中每个节点上children
parent
关系,您可以使用递归算法渲染树:
$nodes = Category::get()->toTree();$traverse = function ($categories, $prefix = '-') use (&$traverse) {foreach ($categories as $category) {echo PHP_EOL.$前缀。' '.$category->name;$traverse($category->children, $prefix.'-'); } };$遍历($节点);
这将输出类似这样的内容:
- 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);
删除节点:
$节点->删除();
重要的!该节点的任何后代也将被删除!
重要的!节点需要作为模型删除,不要尝试使用如下查询删除它们:
类别::where('id', '=', $id)->delete();
这会破坏树的!
在模型级别也支持SoftDeletes
特征。
检查节点是否是其他节点的后代:
$bool = $node->isDescendantOf($parent);
检查节点是否为根节点:
$bool = $node->isRoot();
其他检查:
$node->isChildOf($other);
$node->isAncestorOf($other);
$node->isSiblingOf($other);
$node->isLeaf()
您可以检查树是否损坏(即有一些结构错误):
$bool = 类别::isBroken();
可以获得错误统计信息:
$data = Category::countErrors();
它将返回一个包含以下键的数组:
oddness
-- 具有错误的lft
和rgt
值集的节点数
duplicates
-- 具有相同lft
或rgt
值的节点数
wrong_parent
——具有与lft
和rgt
值不对应的无效parent_id
值的节点数
missing_parent
parent_id
指向不存在的节点的节点数
由于 v3.1 树现在可以修复。使用parent_id
列中的继承信息,为每个节点设置正确的_lft
和_rgt
值。
节点::fixTree();
假设您有Menu
模型和MenuItems
。这些模型之间建立了一对多的关系。 MenuItem
具有用于将模型连接在一起的menu_id
属性。 MenuItem
包含嵌套集。显然,您希望根据menu_id
属性单独处理每棵树。为此,您需要将此属性指定为范围属性:
受保护函数 getScopeAttributes() {返回['菜单_id']; }
但现在,为了执行一些自定义查询,您需要提供用于确定范围的属性:
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OKMenuItem::descendantsOf($id)->get(); // 错误:从其他scopeMenuItem::scoped([ 'menu_id' => 5 ])->fixTree()返回节点; // 好的
使用模型实例请求节点时,会根据该模型的属性自动应用范围:
$node = MenuItem::findOrFail($id);$node->siblings()->withDepth()->get(); // 好的
要使用实例获取范围查询生成器:
$node->newScopedQuery();
急切加载时始终使用范围查询:
MenuItem::scoped([ 'menu_id' => 5])->with('后代')->findOrFail($id); // OKMenuItem::with('descendants')->findOrFail($id); // 错误的
PHP >= 5.4
Laravel >= 4.1
强烈建议使用支持事务的数据库(如 MySql 的 InnoDb)来保护树免受可能的损坏。
要安装该软件包,请在终端中:
composer require kalnoy/nestedset
对于 Laravel 5.5 及以上版本的用户:
Schema::create('table', function (Blueprint $table) {...$table->nestedSet(); });// 删除 columnsSchema::table('table', function (Blueprint $table) {$table->dropNestedSet(); });
对于之前的 Laravel 版本:
...使用 KalnoyNestedsetNestedSet; Schema::create('table', function (Blueprint $table) {...NestedSet::columns($table); });
删除列:
...使用 KalnoyNestedsetNestedSet; Schema::table('table', function (Blueprint $table) { NestedSet::dropColumns($table); });
您的模型应使用KalnoyNestedsetNodeTrait
特征来启用嵌套集:
use KalnoyNestedsetNodeTrait;class Foo 扩展 Model {use NodeTrait; }
如果您之前的扩展使用了不同的列集,您只需重写模型类上的以下方法:
公共函数 getLftName() {返回'左'; }公共函数 getRgtName() {返回'正确'; }公共函数 getParentIdName() {返回'父级'; }//指定父id属性变元public function setParentAttribute($value) {$this->setParentIdAttribute($value); }
如果您的树包含parent_id
信息,您需要向您的模式添加两列:
$table->unsignedInteger('_lft');$table->unsignedInteger('_rgt');
设置模型后,您只需修复树即可填充_lft
和_rgt
列:
MyModel::fixTree();
版权所有 (c) 2017 亚历山大·卡尔诺伊
特此免费授予获得本软件和相关文档文件(“软件”)副本的任何人不受限制地使用本软件,包括但不限于使用、复制、修改、合并的权利、发布、分发、再许可和/或销售软件的副本,并允许向其提供软件的人员这样做,但须满足以下条件:
上述版权声明和本许可声明应包含在本软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途的适用性和不侵权的保证。在任何情况下,作者或版权持有者均不对因本软件或本软件中的使用或其他交易而产生或与之相关的任何索赔、损害或其他责任负责,无论是合同、侵权行为还是其他行为。软件。