Laravel Baum 巢狀集合模型中文文件翻譯-部分

leienshu發表於2019-01-08

翻譯地址,歡迎大家一起來:
https://github.com/leienshu/baum

Baum是Laravel 5's的EloquentORM的巢狀集模式的實現。

For Laravel 4.2.x compatibility, check the 1.0.x branch branch or use the latest 1.0.x tagged release.

文件

關於巢狀集

巢狀集是實現有序樹的智慧方法,允許快速,非遞迴查詢。 例如,無論樹有多深,您都可以在單個查詢中獲取節點的所有後代。 缺點是插入/移動/刪除需要複雜的SQL,但這是由這個包在幕後處理的!

巢狀集適用於有序樹(例如選單,電商分類)和必須有效查詢的大型樹(例如,threaded posts)。

有關詳細資訊,請參閱巢狀集的維基百科條目。 此外,這是一個很好的入門教程:https://leijingwei.com/archives/41.html

背後的原理,TL; DR版本

視覺化巢狀集如何工作的一種簡單方法是考慮圍繞其所有子節點的父實體及其周圍的父節點等。所以這個樹:

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 *service provider*。 進入 config/app.php 檔案 新增下面一行到providers 陣列中:

'Baum\Providers\BaumServiceProvider',

入門

正確安裝軟體包後,最簡單的方法是執行提供的生成器:

php artisan baum:install MODEL

使用您計劃用於巢狀集模型的類名替換上面的MODEL。

生成器將在您的應用程式中安裝一個遷移和一個模型檔案,該檔案被配置為使用Baum提供的巢狀集行為。 您應該看看這些檔案,因為每個檔案都描述瞭如何自定義它們。

接下來,您可能會執行php 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)

這是一個遷移檔案(migrations file)的例子:

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

或者你也可以取消parent_id列,來實現同樣的功能:

// 此方法與$node->makeRoot()同效果
$node->parent_id = null;
$node->save();

插入節點

// 直接使用關係
$child1 = $root->children()->create(['name' => 'Child 1']);

// 使用`makeChildOf`方法
$child2 = Category::create(['name' => 'Child 2']);
$child2->makeChildOf($root);

刪除節點

$child1->delete();

已刪除節點的後代也將被刪除,並且將重新計算所有lft和rgt界限。 請注意,目前,不會觸發刪除和刪除後代的模型事件。

獲取節點的巢狀級別

getLevel() 方法將返回節點的當前巢狀級別或深度。

$node->getLevel() // 節點是root的時候為0 

移動節點

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

判定節點請求

你可以使用下面的方法來詢問節點的一些屬性:

  • 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提供了一些非常基本的查詢範圍來訪問根節點和葉節點:

// 查詢範圍以所有根節點為目標
Category::roots()

// 查詢範圍以所有葉節點(所有分支的末尾)為目標
Category:allLeaves()

您也可能只對第一個根節點感興趣:

$firstRootNode = Category::root();

訪問祖先和後代鏈

There are several methods which Baum offers to access the ancestry/descendancy
chain of a node in the Nested Set tree. The main thing to keep in mind is that
they are provided in two ways:

First as query scopes, returning an Illuminate\Database\Eloquent\Builder
instance to continue to query further. To get actual results from these,
remember to call get() or first().

  • ancestorsAndSelf(): Targets all the ancestor chain nodes including the current one.
  • ancestors(): Query the ancestor chain nodes excluding the current one.
  • siblingsAndSelf(): Instance scope which targets all children of the parent, including self.
  • siblings(): Instance scope targeting all children of the parent, except self.
  • leaves(): Instance scope targeting all of its nested children which do not have children.
  • descendantsAndSelf(): Scope targeting itself and all of its nested children.
  • descendants(): Set of all children & nested children.
  • immediateDescendants(): Set of all children nodes (non-recursive).

Second, as methods which return actual Baum\Node instances (inside a Collection
object where appropiate):

  • getRoot(): Returns the root node starting at the current node.
  • getAncestorsAndSelf(): Retrieve all of the ancestor chain including the current node.
  • getAncestorsAndSelfWithoutRoot(): All ancestors (including the current node) except the root node.
  • getAncestors(): Get all of the ancestor chain from the database excluding the current node.
  • getAncestorsWithoutRoot(): All ancestors except the current node and the root node.
  • getSiblingsAndSelf(): Get all children of the parent, including self.
  • getSiblings(): Return all children of the parent, except self.
  • getLeaves(): Return all of its nested children which do not have children.
  • getDescendantsAndSelf(): Retrieve all nested children and self.
  • getDescendants(): Retrieve all of its children & nested children.
  • getImmediateDescendants(): Retrieve all of its children nodes (non-recursive).

Here's a simple example for iterating a node's descendants (provided a name
attribute is available):

$node = Category::where('name', '=', 'Books')->first();

foreach($node->getDescendantsAndSelf() as $descendant) {
  echo "{$descendant->name}";
}

Limiting the levels of children returned

In some situations where the hierarchy depth is huge it might be desirable to limit the number of levels of children returned (depth). You can do this in Baum by using the limitDepth query scope.

The following snippet will get the current node's descendants up to a maximum
of 5 depth levels below it:

$node->descendants()->limitDepth(5)->get();

Similarly, you can limit the descendancy levels with both the getDescendants and getDescendantsAndSelf methods by supplying the desired depth limit as the first argument:

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

Custom sorting column

By default in Baum all results are returned sorted by the lft index column
value for consistency.

If you wish to change this default behaviour you need to specify in your model
the name of the column you wish to use to sort your results like this:

protected $orderColumn = 'name';

Dumping the hierarchy tree

Baum extends the default Eloquent\Collection class and provides the
toHierarchy method to it which returns a nested collection representing the
queried tree.

Retrieving a complete tree hierarchy into a regular Collection object with
its children properly nested is as simple as:

$tree = Category::where('name', '=', 'Books')->first()->getDescendantsAndSelf()->toHierarchy();

Model events: moving and moved

Baum models fire the following events: moving and moved every time a node
is moved around the Nested Set tree. This allows you to hook into those points
in the node movement process. As with normal Eloquent model events, if false
is returned from the moving event, the movement operation will be cancelled.

The recommended way to hook into those events is by using the model's boot
method:

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.
    });
  }

}

Scope support

Baum provides a simple method to provide Nested Set "scoping" which restricts
what we consider part of a nested set tree. This should allow for multiple nested
set trees in the same database table.

To make use of the scoping funcionality you may override the scoped model
attribute in your subclass. This attribute should contain an array of the column
names (database fields) which shall be used to restrict Nested Set queries:

class Category extends Baum\Node {
  ...
  protected $scoped = array('company_id');
  ...
}

In the previous example, company_id effectively restricts (or "scopes") a
Nested Set tree. So, for each value of that field we may be able to construct
a full different tree.

$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

All methods which ask or traverse the Nested Set tree will use the scoped
attribute (if provided).

Please note that, for now, moving nodes between scopes is not supported.

Validation

The ::isValidNestedSet() static method allows you to check if your underlying tree structure is correct. It mainly checks for these 3 things:

  • Check that the bound indexes lft, rgt are not null, rgt values greater
    than lft and within the bounds of the parent node (if set).
  • That there are no duplicates for the lft and rgt column values.
  • As the first check does not actually check root nodes, see if each root has
    the lft and rgt indexes within the bounds of its children.

All of the checks are scope aware and will check each scope separately if needed.

Example usage, given a Category node class:

Category::isValidNestedSet()
=> true

Tree rebuilding

Baum supports for complete tree-structure rebuilding (or reindexing) via the
::rebuild() static method.

This method will re-index all your lft, rgt and depth column values,
inspecting your tree only from the parent <-> children relation
standpoint. Which means that you only need a correctly filled parent_id column
and Baum will try its best to recompute the rest.

This can prove quite useful when something has gone horribly wrong with the index
values or it may come quite handy when converting from another implementation
(which would probably have a parent_id column).

This operation is also scope aware and will rebuild all of the scopes
separately if they are defined.

Simple example usage, given a Category node class:

Category::rebuild()

Valid trees (per the isValidNestedSet method) will not get rebuilt. To force the index rebuilding process simply call the rebuild method with true as the first parameter:

Category::rebuild(true);

Soft deletes

Baum comes with limited support for soft-delete operations. What I mean
by limited is that the testing is still limited and the soft delete
functionality is changing in the upcoming 4.2 version of the framework, so use
this feature wisely.

For now, you may consider a safe restore() operation to be one of:

  • Restoring a leaf node
  • Restoring a whole sub-tree in which the parent is not soft-deleted

Seeding/Mass-assignment

Because Nested Set structures usually involve a number of method calls to build a hierarchy structure (which result in several database queries), Baum provides two convenient methods which will map the supplied array of node attributes and create a hierarchy tree from them:

  • buildTree($nodeList): (static method) Maps the supplied array of node attributes into the database.
  • makeTree($nodeList): (instance method) Maps the supplied array of node attributes into the database using the current node instance as the parent for the provided subtree.

Both methods will create new nodes when the primary key is not supplied, update or create if it is, and delete all nodes which are not present in the affecting scope. Understand that the affecting scope for the buildTree static method is the whole nested set tree and for the makeTree instance method are all of the current node's descendants.

For example, imagine we wanted to map the following category hierarchy into our database:

  • TV & Home Theater
  • Tablets & E-Readers
  • Computers
    • Laptops
    • PC Laptops
    • Macbooks (Air/Pro)
    • Desktops
    • Monitors
  • Cell Phones

This could be easily accomplished with the following code:

$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

After that, we may just update the hierarchy as needed:

$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

The makeTree instance method works in a similar fashion. The only difference
is that it will only perform operations on the descendants of the calling node instance.

So now imagine we already have the following hierarchy in the database:

  • Electronics
  • Health Fitness & Beaty
  • Small Appliances
  • Major Appliances

If we execute the following code:

$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

Would result in:

  • Electronics
    • TV & Home Theater
    • Tablets & E-Readers
    • Computers
    • Laptops
      • PC Laptops
      • Macbooks (Air/Pro)
    • Desktops
    • Monitors
    • Cell Phones
  • Health Fitness & Beaty
  • Small Appliances
  • Major Appliances

Updating and deleting nodes from the subtree works the same way.

Misc/Utility functions

Node extraction query scopes

Baum provides some query scopes which may be used to extract (remove) selected nodes
from the current results set.

  • withoutNode(node): Extracts the specified node from the current results set.
  • withoutSelf(): Extracts itself from the current results set.
  • withoutRoot(): Extracts the current root node from the results set.
$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

Get a nested list of column values

The ::getNestedList() static method returns a key-value pair array indicating
a node's depth. Useful for silling select elements, etc.

It expects the column name to return, and optionally: the column
to use for array keys (will use id if none supplied) and/or a separator:

public static function getNestedList($column, $key = null, $seperator = ' ');

An example use case:

$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的其他資訊,用法示例和/或常見問題解答。

完成本自述檔案後,請隨意瀏覽wiki

貢獻

如果你也想貢獻自己的綿薄之力? 也許你發現了一些討厭的bug? 你可以按照下面的方法來做。

  1. Fork & clone 專案: git clone git@github.com:your-username/baum.git
  2. 執行測試,確定專案透過你的設定: phpunit
  3. 建立你的 bugfix/feature 分支並且把你的改動寫入。 把測試寫入改動當中。
  4. 確定所有測試都透過了: phpunit.
  5. 推送分支並且提交一個pull請求。

Please see the CONTRIBUTING.md file for extended guidelines and/or recommendations.

License

Baum根據MIT License許可證的條款獲得許可
(檢視LICENSE檔案看更多細節)。


最早由 Estanislau Trepat (etrepat) 編寫。 這是他的twitter
@etrepat

本作品採用《CC 協議》,轉載必須註明作者和本文連結
求知若飢,虛心若愚!

相關文章