kalnoy/nestedset

Laravel 5.7 及以上版本的嵌套集模型

安装: 8,646,864

依赖: 221

建议者: 0

安全性: 0

星标: 3,632

关注者: 97

分支: 471

开放问题: 213

v6.0.4 2024-04-08 06:10 UTC

README

Build Status Total Downloads Latest Stable Version Latest Unstable Version License

这是一个用于在关系型数据库中处理树的 Laravel 4-10 包。

  • Laravel 11.0 自 v6.0.4 版本开始支持
  • Laravel 10.0 自 v6.0.2 版本开始支持
  • Laravel 9.0 自 v6.0.1 版本开始支持
  • Laravel 8.0 自 v6.0.0 版本开始支持
  • Laravel 5.7, 5.8, 6.0, 7.0 自 v5 版本开始支持
  • Laravel 5.5, 5.6 自 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

在这种情况下,该节点被认为是 根节点,这意味着它没有父节点。

从现有节点创建根节点

// #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。

祖先和后代

祖先构成节点到其父节点的链条。对于显示当前类别的导航栏非常有用。

后代是指子树中的所有节点,即节点的子节点、子节点的子节点等。

祖先和后代都可以 eager 加载。

// 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);

可以 eager 加载祖先集合

$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 可以是模型的主键或者模型实例。

构建树

获取一组节点后,你可以将其转换为树。例如

$tree = Category::get()->toTree();

这将填充每个节点上的 parentchildren 关系,并可以使用递归算法渲染树

$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 -- 具有错误 lftrgt 值的节点数
  • duplicates -- 具有相同 lftrgt 值的节点数
  • wrong_parent -- 具有无效 parent_id 值的节点数,该值与 lftrgt 值不对应
  • 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

特此授予任何获得本软件及其相关文档文件(“软件”)副本的个人免费使用软件的权利,不受任何限制,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或出售软件副本的权利,并允许向软件提供的人士从事此类活动,但受以下条件约束

上述版权声明和本许可声明应包含在软件的所有副本或实质性部分中。

软件按“原样”提供,不包括任何明示或暗示的保证,包括但不限于适销性、特定目的适用性和非侵权性保证。在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任承担责任,无论该责任是基于合同、侵权或其他原因,不论该责任产生于、源于或与软件或使用或以其他方式处理软件有关。