首頁  >  文章  >  後端開發  >  laravel-nestedset多層無限分類詳解

laravel-nestedset多層無限分類詳解

小云云
小云云原創
2018-01-25 13:28:033215瀏覽
laravel-nestedset是一個關係型資料庫遍歷樹的larvel4-5的插件包,本文主要和大家分享laravel-nestedset多層無限分類,希望能幫助大家。

目錄:

  • Nested Sets Model簡介

  • 安裝需求

  • 安裝

  • 開始使用

    • #遷移檔案

    • 插入節點

    • 取得節點

    • 刪除節點

    • ##一致性檢查與修正

    • 作用域

Nested Sets Model簡介

Nested Set Model 是一種實作有序樹的高明的方法,它快速且不需要遞歸查詢,例如不管樹有多少層,你可以只使用一條查詢來獲取某個節點下的所有的後代,缺點是它的插入、移動、刪除需要執行複雜的sql語句,但是這些都在這個插件內處理了!

更多關於詳見維基百科! Nested set model  及它中文翻譯!嵌套集合模型

安裝需求

  • PHP>=5.4

  • laravel>=4.1

  • #v4.3版本以後支援Laravel-5.5

  • v4版本支援Laravel-5.2、5.3、5.4

  • #v3版本支援Laravel-5.1

  • v2版本支援Laravel-4

強烈建議使用支援事物功能的資料引擎(如MySql的innoDb)來防止可能的資料損壞。

安裝

composer.json檔案中加入下面程式碼:

"kalnoy/nestedset": "^4.3",
執行

composer install 來安裝它。

或直接在命令列輸入

composer require kalnoy/nestedset
如需安裝歷史版本請點選更多版本

開始使用

遷移檔案

你可以使用

NestedSet類別的columns方法來新增有預設名字的欄位:

...
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);
});
預設的欄位名為:

_lft_rgtparent_id,原始碼如下:

public static function columns(Blueprint $table)
    {
        $table->unsignedInteger(self::LFT)->default(0);
        $table->unsignedInteger(self::RGT)->default(0);
        $table->unsignedInteger(self::PARENT_ID)->nullable();

        $table->index(static::getDefaultColumns());
    }
模型

你的模型需要使用

Kalnoy\Nestedset\NodeTraittrait 來實作nested sets

use Kalnoy\Nestedset\NodeTrait;

class Foo extends Model {
    use NodeTrait;
}
遷移其他地方已有的資料

從其他的nested set 模型庫遷移

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

##Node具有以下功能,他們功能完全且被預先載入:

    Node belongs to parent
  • Node has many children
  • #Node has many ancestors
  • Node has many descendants
  • 假設我們有一個Category模型;變數$node是該模型的一個實例是我們操作的node(節點)。它可以為一個新建立的node或是從資料庫中取出的node

插入節點(node)

每次插入或移動一個節點都要執行好幾條資料庫操作,所有強烈推薦使用transaction.

注意!

對於v4.2.0版本不是自動開啟transaction的,另外node的結構化操作需要在模型上手動執行save,但是有些方法會隱性執行save並傳回操作後的布林類型的結果。 建立節點(node)

當你簡單的建立一個node,它會被加入到樹的末端。

Category::create($attributes); // 自动save为一个根节点(root)

$node = new Category($attributes);
$node->save(); // save为一个根节点(root)

在這裡node被設定為root,意味著它沒有父節點

將一個已存在的node設定為root

// #1 隐性 save
$node->saveAsRoot();

// #2 显性 save
$node->makeRoot()->save();

新增子節點到指定的父節點末端或前端

如果你想新增子節點,你可以新增為父節點的第一個子節點或最後一個子節點。

* 在下面的範例中,
$parent 為已存在的節點新增到父節點的末端的方法包括:

// #1 使用延迟插入
$node->appendToNode($parent)->save();

// #2 使用父节点
$parent->appendNode($node);

// #3 借助父节点的children关系
$parent->children()->create($attributes);

// #5 借助子节点的parent关系
$node->parent()->associate($parent)->save();

// #6 借助父节点属性
$node->parent_id = $parent->id;
$node->save();

// #7 使用静态方法
Category::create($attributes, $parent);

加入到父節點的前端的方法

// #1
$node->prependToNode($parent)->save();

// #2
$parent->prependNode($node);

插入節點到指定節點的前面或後面

你可以使用下面的方法將

$node

加入為指定節點$neighbor的相鄰節點

$neighbor

#必須存在,$node可以為新建立的節點,也可以為已存在的,如果$node為已存在的節點,它將移動到新的位置與$neighbor相鄰,必要時它的父級將改變。 <pre class="brush:php;toolbar:false"># 显性save $node-&gt;afterNode($neighbor)-&gt;save(); $node-&gt;beforeNode($neighbor)-&gt;save(); # 隐性 save $node-&gt;insertAfterNode($neighbor); $node-&gt;insertBeforeNode($neighbor);</pre>將陣列建構成樹

但使用

create

靜態方法時,它將檢查陣列是否包含children鍵,如果有的話,將遞歸創建更多的節點。 <pre class="brush:php;toolbar:false">$node = Category::create([     'name' =&gt; 'Foo',     'children' =&gt; [         [             'name' =&gt; 'Bar',             'children' =&gt; [                 [ 'name' =&gt; 'Baz' ],             ],         ],     ], ]);</pre>現在

$node->children

包含一組已建立的節點。 將陣列重建為樹

你可以輕鬆的重建一個樹,這對於大量的修改的樹結構的保存非常有用。

Category::rebuildTree($data, $delete);
#<p><code>$data为代表节点的数组

$data = [
    [ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ],
    [ 'name' => 'bar' ],
];

上面有一个namefoo的节点,它有指定的id,代表这个已存在的节点将被填充,如果这个节点不存在,就好抛出一个ModelNotFoundException ,另外,这个节点还有children数组,这个数组也会以相同的方式添加到foo节点内。
bar节点没有主键,就是不存在,它将会被创建。
$delete 代表是否删除数据库中已存在的但是$data 中不存在的数据,默认为不删除。

重建子树
对于4.3.8版本以后你可以重建子树

Category::rebuildSubtree($root, $data);

这将限制只重建$root子树

检索节点

在某些情况下我们需要使用变量$id代表目标节点的主键id

祖先和后代

Ancestors 创建一个节点的父级链,这对于展示当前种类的面包屑很有帮助。
Descendants 是一个父节点的所有子节点。
Ancestors和Descendants都可以预加载。

// Accessing ancestors
$node->ancestors;

// Accessing descendants
$node->descendants;

通过自定义的查询加载ancestors和descendants:

$result = Category::ancestorsOf($id);
$result = Category::ancestorsAndSelf($id);
$result = Category::descendantsOf($id);
$result = Category::descendantsAndSelf($id);

大多数情况下,你需要按层级排序:

$result = Category::defaultOrder()->ancestorsOf($id);

祖先集合可以被预加载:

$categories = Category::with('ancestors')->paginate(30);

// 视图模板中面包屑:
@foreach($categories as $i => $category)
    <small> $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' </small><br>
    $category->name
@endforeach

将祖先的name全部取出后转换为数组,在用>拼接为字符串输出。

兄弟节点

有相同父节点的节点互称为兄弟节点

$result = $node->getSiblings();

$result = $node->siblings()->get();

获取相邻的后面兄弟节点:

// 获取相邻的下一个兄弟节点
$result = $node->getNextSibling();

// 获取后面的所有兄弟节点
$result = $node->getNextSiblings();

// 使用查询获得所有兄弟节点
$result = $node->nextSiblings()->get();

获取相邻的前面兄弟节点:

// 获取相邻的前一个兄弟节点
$result = $node->getPrevSibling();

// 获取前面的所有兄弟节点
$result = $node->getPrevSiblings();

// 使用查询获得所有兄弟节点
$result = $node->prevSiblings()->get();

获取表的相关model

假设每一个category has many goods, 并且 hasMany 关系已经建立,怎么样简单的获取$category 和它所有后代下所有的goods?

// 获取后代的id
$categories = $category->descendants()->pluck('id');

// 包含Category本身的id
$categories[] = $category->getKey();

// 获得goods
$goods = Goods::whereIn('category_id', $categories)->get();

包含node深度(depth)

如果你需要知道node的出入那一层级:

$result = Category::withDepth()->find($id);

$depth = $result->depth;

根节点(root)是第0层(level 0),root的子节点是第一层(level 1),以此类推
你可以使用having约束来获得特定的层级的节点

$result = Category::withDepth()->having('depth', '=', 1)->get();

注意 这在数据库严格模式下无效

默认排序

所有的节点都是在内部严格组织的,默认情况下没有顺序,所以节点是随机展现的,这部影响展现,你可以按字母和其他的顺序对节点排序。

但是在一些情况下按层级展示是必要的,它对获取祖先和用于菜单顺序有用。

使用deaultOrder运用树的排序:
$result = Category::defaultOrder()->get();

你也可以使用倒序排序:
$result = Category::reversed()->get();

让节点在父级内部上下移动来改变默认排序:

$bool = $node->down();
$bool = $node->up();

// 向下移动3个兄弟节点
$bool = $node->down(3);

操作返回根据操作的节点的位置是否改变的布尔值

约束

很多约束条件可以被用到这些查询构造器上:

  • whereIsRoot() 仅获取根节点;

  • whereIsAfter($id) 获取特定id的节点后面的所有节点(不仅是兄弟节点)。

  • whereIsBefore($id) 获取特定id的节点前面的所有节点(不仅是兄弟节点)。

祖先约束

$result = Category::whereAncestorOf($node)->get();
$result = Category::whereAncestorOrSelf($id)->get();

$node 可以为模型的主键或者模型实例

后代约束

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

//结果集合中包含目标node自身
$result = Category::whereDescendantOrSelf($node)->get();

构建树

在获取了node的结果集合后,我们就可以将它转化为树,例如:
$tree = Category::get()->toTree();

这将在每个node上添加parent 和 children 关系,且你可以使用递归算法来渲染树:

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

这将破坏树结构
支持SoftDeletestrait,且在模型层

helper 方法

检查节点是否为其他节点的子节点
$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 -- lft 和 rgt 值错误的节点的数量

  • duplicates -- lft 或者 rgt 值重复的节点的数量

  • wrong_parent -- left 和 rgt 值 与parent_id 不对应的造成无效parent_id 的节点的数量

  • missing_parent -- 含有parent_id对应的父节点不存在的节点的数量

修复树

从v3.1往后支持修复树,通过parent_id字段的继承信息,给每个node设置合适的lft 和 rgt值
Node::fixTree();

作用域(scope)

假设你有个Memu模型和MenuItems.他们之间是one-to-many 关系。MenuItems有menu_id属性并实现nested sets模型。显然你想基于menu_id属性来单独处理每个树,为了实现这样的功能,我们需要指定这个menu_id属性为scope属性。

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

但使用model实例查询node,scope自动基于设置的限制作用域属性来删选node。例如:

$node = MenuItem::findOrFail($id);

$node->siblings()->withDepth()->get(); // OK

使用实例来获取删选的查询:
$node->newScopedQuery();

注意,当通过主键获取模型时不需要使用scope

$node = MenuItem::findOrFail($id); // OK
$node = MenuItem::scoped([ 'menu_id' => 5 ])->findOrFail(); // OK, 但是多余

相关推荐:

php实现多级分类筛选程序代码

PHP 查询多级分类的实例程序代码

smarty实现多级分类的方法_php技巧

以上是laravel-nestedset多層無限分類詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn