构建未来 / baum
Baum 是 Eloquent 模型嵌套集模式的实现。
Requires
- php: >=5.4.0
- illuminate/console: *
- illuminate/database: *
- illuminate/events: *
- illuminate/filesystem: *
- illuminate/support: *
Requires (Dev)
- d11wtq/boris: ~1.0.10
- mockery/mockery: ~0.9
- phpunit/phpunit: ~4.0
README
Baum 是针对 Laravel 5 的 Eloquent ORM 的嵌套集模式的实现。
针对 Laravel 4.2.x 兼容性,请查看 1.0.x 分支 或使用最新的 1.0.x 标签版本。
文档
关于嵌套集
嵌套集是一种智能地实现有序树的方法,它允许进行快速的非递归查询。例如,您可以在单个查询中获取一个节点的所有后代,无论树有多深。缺点是插入/移动/删除需要复杂的 SQL 语句,但这个包在幕后处理这些操作!
嵌套集适用于有序树(例如菜单、商业类别)和必须高效查询的大型树(例如线程帖子)。
有关嵌套集的更多信息,请参阅 维基百科条目。此外,这是一个很好的入门教程: http://www.evanpetersen.com/item/nested-sets.html
理论背后的内容,简要版
一个简单的方式来可视化嵌套集是如何工作的是,考虑一个父实体包围所有的子实体,然后是父实体的父实体,等等。所以这个树
root
|_ Child 1
|_ Child 1.1
|_ Child 1.2
|_ Child 2
|_ Child 2.1
|_ Child 2.2
可以像这样可视化
___________________________________________________________________
| Root |
| ____________________________ ____________________________ |
| | Child 1 | | Child 2 | |
| | __________ _________ | | __________ _________ | |
| | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | |
1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14
| |___________________________| |___________________________| |
|___________________________________________________________________|
数字代表左边界和右边界。然后表可能看起来像这样
id | parent_id | lft | rgt | depth | data
1 | | 1 | 14 | 0 | root
2 | 1 | 2 | 7 | 1 | Child 1
3 | 2 | 3 | 4 | 2 | Child 1.1
4 | 2 | 5 | 6 | 2 | Child 1.2
5 | 1 | 8 | 13 | 1 | Child 2
6 | 5 | 9 | 10 | 2 | Child 2.1
7 | 5 | 11 | 12 | 2 | Child 2.2
要获取父节点的所有子节点,你需要
SELECT * WHERE lft IS BETWEEN parent.lft AND parent.rgt
要获取子节点数,它是
(right - left - 1)/2
要获取一个节点及其所有祖先节点,直到根节点,你需要
SELECT * WHERE node.lft IS BETWEEN lft AND rgt
正如你所看到的,在普通树中可能是递归的并且非常慢的查询现在变得非常快。很酷,不是吗?
安装
Baum 与 Laravel 5 及以上版本兼容。您可以使用以下方式将其添加到您的 composer.json 文件中
"baum/baum": "~1.1"
运行 composer install 安装它。
与大多数 Laravel 5 包一样,您接下来需要注册 Baum 服务提供者。要这样做,请打开您的 config/app.php 文件并将以下行添加到 providers 数组中
'Baum\Providers\BaumServiceProvider',
入门
安装包正确后,最简单的方法是运行提供的生成器
php artisan baum:install MODEL
将模型替换为您计划用于嵌套集模型的类名。
生成器将在您的应用程序中安装一个迁移和一个模型文件,该文件配置为与 Baum 提供的嵌套集行为一起工作。您应该查看这些文件,因为每个文件都描述了它们可以如何进行自定义。
接下来,您可能会运行 artisan migrate 以应用迁移。
模型配置
为了与 Baum 一起工作,您必须确保您的模型类扩展了 Baum\Node。
这是最简单的方式
class Category extends Baum\Node { }
这是一个稍微复杂一些的例子,其中列名已经定制
class Dictionary extends Baum\Node { protected $table = 'dictionary'; // 'parent_id' column name protected $parentColumn = 'parent_id'; // 'lft' column name protected $leftColumn = 'lidx'; // 'rgt' column name protected $rightColumn = 'ridx'; // 'depth' column name protected $depthColumn = 'nesting'; // guard attributes from mass-assignment protected $guarded = array('id', 'parent_id', 'lidx', 'ridx', 'nesting'); }
请记住,显然,列名必须与数据库表中的列名匹配。
迁移配置
您必须确保支持您的 Baum 模型的数据库表具有以下列
parent_id:父引用(整数)lft:左索引边界(整数)rgt:右索引边界(整数)depth:深度或嵌套级别(整数)
以下是一个示例迁移文件
class Category extends Migration { public function up() { Schema::create('categories', function(Blueprint $table) { $table->increments('id'); $table->integer('parent_id')->nullable(); $table->integer('lft')->nullable(); $table->integer('rgt')->nullable(); $table->integer('depth')->nullable(); $table->string('name', 255); $table->timestamps(); }); } public function down() { Schema::drop('categories'); } }
您可以自由修改列名,前提是在迁移和模型中同时更改。
使用
配置完您的模型并运行迁移后,现在您可以使用Baum了。以下是一些示例。
- 创建根节点
- 插入节点
- 删除节点
- 获取节点的嵌套级别
- 移动节点
- 向您的节点提问
- 关系
- 根和叶作用域
- 访问血统/后代链
- 限制返回的子节点级别
- 自定义排序列
- 转储层次结构树
- 模型事件:
moving和moved - 作用域支持
- 验证
- 树重建
- 软删除
- 种子/批量赋值
- 其他/实用函数
创建根节点
默认情况下,所有节点都创建为根节点
$root = Category::create(['name' => 'Root category']);
或者,您可能需要将现有的节点转换为 根节点
$node->makeRoot();
您还可以将其 parent_id 列设置为null以实现相同的行为
// This works the same as makeRoot() $node->parent_id = null; $node->save();
插入节点
// Directly with a relation $child1 = $root->children()->create(['name' => 'Child 1']); // with the `makeChildOf` method $child2 = Category::create(['name' => 'Child 2']); $child2->makeChildOf($root);
删除节点
$child1->delete();
删除节点的后代也将被删除,所有 lft 和 rgt 边界都将重新计算。请注意,目前不会触发后代 deleting 和 deleted 模型事件。
获取节点的嵌套级别
getLevel() 方法将返回节点的当前嵌套级别或深度。
$node->getLevel() // 0 when root
移动节点
Baum提供了一些移动节点的方法
moveLeft():找到左兄弟并将其移动到其左侧。moveRight():找到右兄弟并将其移动到其右侧。moveToLeftOf($otherNode):移动到 ... 的左侧节点moveToRightOf($otherNode):移动到 ... 的右侧节点makeNextSiblingOf($otherNode):moveToRightOf的别名。makeSiblingOf($otherNode):makeNextSiblingOf的别名。makePreviousSiblingOf($otherNode):moveToLeftOf的别名。makeChildOf($otherNode):使节点成为 ... 的子节点。makeFirstChildOf($otherNode):使节点成为 ... 的第一个子节点。makeLastChildOf($otherNode):makeChildOf的别名。makeRoot():使当前节点成为根节点。
例如
$root = Creatures::create(['name' => 'The Root of All Evil']); $dragons = Creatures::create(['name' => 'Here Be Dragons']); $dragons->makeChildOf($root); $monsters = new Creatures(['name' => 'Horrible Monsters']); $monsters->save(); $monsters->makeSiblingOf($dragons); $demons = Creatures::where('name', '=', 'demons')->first(); $demons->moveToLeftOf($dragons);
向您的节点提问
您可以向Baum节点提出一些问题
isRoot():如果是根节点,则返回true。isLeaf():如果是叶节点(分支的末尾),则返回true。isChild():如果是子节点,则返回true。isDescendantOf($other):如果是其他节点的后代,则返回true。isSelfOrDescendantOf($other):如果是自我或后代,则返回true。isAncestorOf($other):如果是其他节点的祖先,则返回true。isSelfOrAncestorOf($other):如果是自我或祖先,则返回true。equals($node):当前节点实例等于其他。insideSubtree($node):检查给定的节点是否在由左索引和右索引定义的子树内。inSameScope($node):如果给定的节点与当前节点具有相同的作用域,则返回true。即,如果 每个 在scoped属性中的列在两个节点中都具有相同的值。
使用前面的示例中的节点
$demons->isRoot(); // => false $demons->isDescendantOf($root) // => true
关系
Baum为您提供了两个自我引用的Eloquent关系:parent 和 children。
$parent = $node->parent()->get(); $children = $node->children()->get();
根和叶作用域
Baum提供了一些基本的查询作用域来访问根节点和叶节点
// Query scope which targets all root nodes Category::roots() // All leaf nodes (nodes at the end of a branch) Category:allLeaves()
您可能还对仅第一个根节点感兴趣
$firstRootNode = Category::root();
访问血统/后代链
Baum 提供了多种方法来访问嵌套集合树中节点的前辈/后代链。需要注意的是,它们以两种方式提供:
首先作为 查询范围,返回一个 Illuminate\Database\Eloquent\Builder 实例以继续查询。要获取 实际 的结果,请记住调用 get() 或 first()。
ancestorsAndSelf():定位所有祖先链节点,包括当前节点。ancestors():查询祖先链节点,不包括当前节点。siblingsAndSelf():实例范围,定位父节点的所有子节点,包括自身。siblings():实例范围,定位父节点的所有子节点,不包括自身。leaves():实例范围,定位所有没有子节点的嵌套子节点。descendantsAndSelf():定位自身及其所有嵌套子节点。descendants():所有子节点和嵌套子节点集合。immediateDescendants():所有子节点集合(非递归)。
其次,作为返回实际 Baum\Node 实例(在适当的 Collection 对象中)的方法
getRoot():返回从当前节点开始的根节点。getAncestorsAndSelf():检索包括当前节点在内的所有祖先链。getAncestorsAndSelfWithoutRoot():所有祖先(包括当前节点)但不包括根节点。getAncestors():从数据库中获取所有祖先链,不包括当前节点。getAncestorsWithoutRoot():所有祖先,不包括当前节点和根节点。getSiblingsAndSelf():获取所有子节点,包括自身。getSiblings():返回所有子节点,不包括自身。getLeaves():返回所有没有子节点的嵌套子节点。getDescendantsAndSelf():检索所有嵌套子节点和自身。getDescendants():检索所有子节点和嵌套子节点。getImmediateDescendants():检索所有子节点(非递归)。
以下是一个迭代节点后代的简单示例(假设有一个名称属性可用)
$node = Category::where('name', '=', 'Books')->first(); foreach($node->getDescendantsAndSelf() as $descendant) { echo "{$descendant->name}"; }
限制返回的子级层级
在某些情况下,当层次深度很大时,可能希望限制返回的子级层级(深度)。您可以通过在 Baum 中使用 limitDepth 查询范围来实现这一点。
以下片段将获取当前节点的后代,最多5个深度级别以下
$node->descendants()->limitDepth(5)->get();
类似地,您可以通过为 getDescendants 和 getDescendantsAndSelf 方法提供所需的深度限制作为第一个参数来限制后代层级。
// This will work without depth limiting // 1. As usual $node->getDescendants(); // 2. Selecting only some attributes $other->getDescendants(array('id', 'parent_id', 'name')); ... // With depth limiting // 1. A maximum of 5 levels of children will be returned $node->getDescendants(5); // 2. A max. of 5 levels of children will be returned selecting only some attrs $other->getDescendants(5, array('id', 'parent_id', 'name'));
自定义排序列
默认情况下,Baum 按照一致性返回所有结果,按 lft 索引列的值排序。
如果您希望更改此默认行为,您需要在模型中指定要用于排序结果的列名,如下所示
protected $orderColumn = 'name';
转储层次结构树
Baum 扩展了默认的 Eloquent\Collection 类,并为它提供了一个 toHierarchy 方法,该方法返回表示查询树的嵌套集合。
将完整的树层次结构检索到具有正确嵌套子节点的普通 Collection 对象中非常简单
$tree = Category::where('name', '=', 'Books')->first()->getDescendantsAndSelf()->toHierarchy();
模型事件:moving 和 moved
Baum 模型在每次节点在嵌套集合树中移动时都会触发以下事件:moving 和 moved。这允许您在节点移动过程中挂钩到这些点。与正常的 Eloquent 模型事件一样,如果从 moving 事件中返回 false,则移动操作将被取消。
挂钩到这些事件的推荐方法是使用模型的 boot 方法
class Category extends Baum\Node { public static function boot() { parent::boot(); static::moving(function($node) { // Before moving the node this function will be called. }); static::moved(function($node) { // After the move operation is processed this function will be // called. }); } }
作用域支持
Baum提供了一种简单的方法来提供嵌套集“作用域”,这限制了我们认为嵌套集树的一部分。这应该允许在同一个数据库表中存在多个嵌套集树。
要使用作用域功能,您可以在您的子类中重写scoped模型属性。该属性应包含一个列名(数据库字段)数组,用于限制嵌套集查询。
class Category extends Baum\Node { ... protected $scoped = array('company_id'); ... }
在上一个示例中,company_id有效地限制了(或“作用域”)一个嵌套集树。因此,对于该字段的每个值,我们可能能够构建一个完全不同的树。
$root1 = Category::create(['name' => 'R1', 'company_id' => 1]); $root2 = Category::create(['name' => 'R2', 'company_id' => 2]); $child1 = Category::create(['name' => 'C1', 'company_id' => 1]); $child2 = Category::create(['name' => 'C2', 'company_id' => 2]); $child1->makeChildOf($root1); $child2->makeChildOf($root2); $root1->children()->get(); // <- returns $child1 $root2->children()->get(); // <- returns $child2
所有请求或遍历嵌套集树的函数都将使用scoped属性(如果提供)。
请注意,目前不支持在作用域之间移动节点。
验证
::isValidNestedSet()静态方法允许您检查您的底层树结构是否正确。它主要检查以下三件事
- 检查边界索引
lft、rgt是否不为空,rgt值大于lft,并在父节点(如果设置)的范围内。 lft和rgt列值没有重复。- 由于第一个检查实际上并没有检查根节点,因此请检查每个根节点是否有在子节点范围内的
lft和rgt索引。
所有检查都是作用域意识的,如果需要,将单独检查每个作用域。
示例使用,给定一个Category节点类
Category::isValidNestedSet() => true
树重建
Baum支持通过::rebuild()静态方法完成树结构的完全重建(或重新索引)。
此方法将重新索引所有lft、rgt和depth列值,仅从父 <-> 子关系角度检查您的树。这意味着您只需要一个正确填充的parent_id列,Baum将尽力重新计算其余部分。
当索引值出现严重错误时,这非常有用,或者当需要转换到另一个实现(该实现可能有一个parent_id列)时,这可能非常有用。
此操作也是作用域意识的,如果定义了作用域,将分别重建所有作用域。
简单示例使用,给定一个Category节点类
Category::rebuild()
有效的树(根据isValidNestedSet方法)不会重建。要强制索引重建过程,只需将重建方法的第一参数设置为true。
Category::rebuild(true);
软删除
Baum对软删除操作提供有限支持。我所说的有限是指测试仍然是有限的,并且软删除功能将在框架的即将到来的4.2版本中发生变化,因此请谨慎使用此功能。
目前,您可以考虑将安全的restore()操作视为以下之一
- 恢复一个叶节点
- 恢复整个子树,其中父节点未被软删除
播种/大量分配
因为嵌套集结构通常涉及许多方法调用来构建层次结构(这会导致多个数据库查询),Baum提供了两个方便的方法,将提供的节点属性数组映射到数据库中
buildTree($nodeList):(静态方法) 将提供的节点属性数组映射到数据库中。makeTree($nodeList):(实例方法) 使用当前节点实例作为提供的子树的父节点,将提供的节点属性数组映射到数据库中。
这两个方法将在主键未提供时创建新节点,如果提供了,则更新或创建,并删除在影响作用域中不存在的所有节点。请理解,对于buildTree静态方法,影响作用域是整个嵌套集树,对于makeTree实例方法,是当前节点所有后代。
例如,假设我们想要将以下类别层次结构映射到我们的数据库中
- 电视 & 家庭影院
- 平板电脑和电子阅读器
- 电脑
- 笔记本电脑
- 个人电脑笔记本电脑
- Macbook(Air/Pro)
- 台式机
- 显示器
- 笔记本电脑
- 手机
以下代码可以轻松实现这一点
$categories = [ ['id' => 1, 'name' => 'TV & Home Theather'], ['id' => 2, 'name' => 'Tablets & E-Readers'], ['id' => 3, 'name' => 'Computers', 'children' => [ ['id' => 4, 'name' => 'Laptops', 'children' => [ ['id' => 5, 'name' => 'PC Laptops'], ['id' => 6, 'name' => 'Macbooks (Air/Pro)'] ]], ['id' => 7, 'name' => 'Desktops'], ['id' => 8, 'name' => 'Monitors'] ]], ['id' => 9, 'name' => 'Cell Phones'] ]; Category::buildTree($categories) // => true
之后,我们可以根据需要更新层次结构
$categories = [ ['id' => 1, 'name' => 'TV & Home Theather'], ['id' => 2, 'name' => 'Tablets & E-Readers'], ['id' => 3, 'name' => 'Computers', 'children' => [ ['id' => 4, 'name' => 'Laptops', 'children' => [ ['id' => 5, 'name' => 'PC Laptops'], ['id' => 6, 'name' => 'Macbooks (Air/Pro)'] ]], ['id' => 7, 'name' => 'Desktops', 'children' => [ // These will be created ['name' => 'Towers Only'], ['name' => 'Desktop Packages'], ['name' => 'All-in-One Computers'], ['name' => 'Gaming Desktops'] ]] // This one, as it's not present, will be deleted // ['id' => 8, 'name' => 'Monitors'], ]], ['id' => 9, 'name' => 'Cell Phones'] ]; Category::buildTree($categories); // => true
makeTree 实例方法以类似的方式工作。唯一的区别是它将只对调用节点实例的 后代 执行操作。
现在假设我们已经在数据库中有了以下层次结构
- 电子产品
- 健康健身与美容
- 小家电
- 大家电
如果我们执行以下代码
$children = [ ['name' => 'TV & Home Theather'], ['name' => 'Tablets & E-Readers'], ['name' => 'Computers', 'children' => [ ['name' => 'Laptops', 'children' => [ ['name' => 'PC Laptops'], ['name' => 'Macbooks (Air/Pro)'] ]], ['name' => 'Desktops'], ['name' => 'Monitors'] ]], ['name' => 'Cell Phones'] ]; $electronics = Category::where('name', '=', 'Electronics')->first(); $electronics->makeTree($children); // => true
将得到以下结果
- 电子产品
- 电视 & 家庭影院
- 平板电脑和电子阅读器
- 电脑
- 笔记本电脑
- 个人电脑笔记本电脑
- Macbook(Air/Pro)
- 台式机
- 显示器
- 笔记本电脑
- 手机
- 健康健身与美容
- 小家电
- 大家电
更新和删除子树中的节点的方式相同。
其他/实用函数
节点提取查询范围
Baum 提供了一些查询范围,可以用于从当前结果集中提取(删除)选定的节点。
withoutNode(node):从当前结果集中提取指定的节点。withoutSelf():从当前结果集中提取自身。withoutRoot():从结果集中提取当前根节点。
$node = Category::where('name', '=', 'Some category I do not want to see.')->first(); $root = Category::where('name', '=', 'Old boooks')->first(); var_dump($root->descendantsAndSelf()->withoutNode($node)->get()); ... // <- This result set will not contain $node
获取嵌套列表的列值
::getNestedList() 静态方法返回一个键值对数组,指示节点的深度。这对于设置 select 元素等非常有用。
它期望返回的列名,以及可选的:用作数组键的列(如果没有提供,将使用 id)和/或分隔符
public static function getNestedList($column, $key = null, $seperator = ' ');
一个示例用例
$nestedList = Category::getNestedList('name'); // $nestedList will contain an array like the following: // array( // 1 => 'Root 1', // 2 => ' Child 1', // 3 => ' Child 2', // 4 => ' Child 2.1', // 5 => ' Child 3', // 6 => 'Root 2' // );
更多信息
您可以在wiki中找到有关Baum的更多信息、使用示例和/或常见问题。
在完成本README后,请随意浏览wiki
https://github.com/etrepat/baum/wiki
贡献
想要贡献吗?也许你发现了一些讨厌的bug?这是个好消息!
- 分支并克隆项目:
git clone git@github.com:your-username/baum.git。 - 运行测试并确保它们通过您的设置:
phpunit。 - 创建您的bug修复/特性分支并编写您的更改。为您的更改添加测试。
- 确保所有测试仍然通过:
phpunit。 - 将更改推送到您的分支并提交新的pull请求。
请参阅CONTRIBUTING.md文件以获取更详细的指南和建议。
许可证
Baum根据MIT许可证的条款进行许可(有关详细信息,请参阅LICENSE文件)。
由Estanislau Trepat (etrepat)编写。我还在twitter上@etrepat。