1001menus / baum
Baum 是 Eloquent 模型嵌套集合模式的实现。
Requires
- php: >=5.3.0
- illuminate/console: 4.*
- illuminate/database: 4.*
- illuminate/filesystem: 4.*
- illuminate/support: 4.*
Requires (Dev)
- mockery/mockery: 0.7.2
- phpunit/phpunit: 3.7.*
This package is auto-updated.
Last update: 2024-09-19 08:57:09 UTC
README
Baum 是 Laravel 4 Eloquent ORM 嵌套集合模式的实现。
文档
关于嵌套集合
嵌套集合是一种智能的方式实现有序树,允许进行快速的非递归查询。例如,您可以在一个查询中获取一个节点的所有子节点,无论树有多深。缺点是插入/移动/删除需要复杂的 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 4 及更高版本兼容。您可以通过以下方式将其添加到您的 composer.json
文件中
"baum/baum": "~1.0"
运行 composer install
以安装它。
与大多数 Laravel 4 包一样,您接下来需要注册 Baum 服务提供者。为此,转到您的 app/config/app.php
文件,并在 providers
数组中添加以下行
'Baum\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
:对父级的引用(int)lft
:左索引边界(int)rgt
:右索引边界(int)depth
:深度或嵌套级别(int)
以下是一个示例迁移文件
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 与您的模型一起使用。以下是一些示例。
创建根节点
默认情况下,所有节点都创建为根节点
$root = Category::create(['name' => 'Root category']);
或者,您可能需要将现有的节点转换为根节点
$node->makeRoot();
插入节点
// 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)
:使节点成为指定节点的子节点。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提供了几种方法来访问嵌套集树中节点的祖先/后代链。需要记住的主要一点是,它们以两种方式提供
首先作为query scopes
,返回一个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 扩展了默认的 Eloquent\Collection
类,并为它提供了一个 toHierarchy
方法,该方法返回一个表示查询树的嵌套集合。
将完整的树层次结构检索到普通的 Collection
对象中,并正确嵌套其子节点,非常简单
$tree = Category::where('name', '=', Books)->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
属性(如果提供)。
其他/实用函数
节点提取查询范围
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
贡献
想要贡献吗?也许你发现了一些讨厌的虫子?这是个好消息!
- 分支项目:
- 创建您的 bugfix/feature 分支。
- 编写您的更改,并尽可能提供一些测试。
- 提交您的更改并推送到分支。
- 创建一个新的拉取请求
许可证
Baum 根据 MIT 许可证 的条款许可(有关详细信息,请参阅 LICENSE 文件)。
由 Estanislau Trepat (etrepat) 编码。我还在推特上 @etrepat。