Baum 是 Eloquent 模型嵌套集合模式的实现。

1.0.9 2014-01-15 19:00 UTC

This package is auto-updated.

Last update: 2024-09-19 08:57:09 UTC


README

Build Status

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

被删除节点的后代也将被删除,并且所有的 lftrgt 边界将重新计算。请注意,目前,对于后代不会触发deletingdeleted模型事件。

获取节点的嵌套级别

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关系:parentchildren

$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();

模型事件:movingmoved

Baum 模型在每个节点在嵌套集树中移动时触发以下事件:movingmoved。这允许您在节点移动过程中钩入这些点。与正常的 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

贡献

想要贡献吗?也许你发现了一些讨厌的虫子?这是个好消息!

  1. 分支项目:
  2. 创建您的 bugfix/feature 分支。
  3. 编写您的更改,并尽可能提供一些测试。
  4. 提交您的更改并推送到分支。
  5. 创建一个新的拉取请求

许可证

Baum 根据 MIT 许可证 的条款许可(有关详细信息,请参阅 LICENSE 文件)。

Estanislau Trepat (etrepat) 编码。我还在推特上 @etrepat