這是一個 Laravel 4-10 套件,用於處理關聯式資料庫中的樹。
自 v6.0.4 起支援Laravel 11.0
自 v6.0.2 起支援Laravel 10.0
自 v6.0.1 起支援Laravel 9.0
自 v6.0.0 起支援Laravel 8.0
自 v5 起支援Laravel 5.7、5.8、6.0、7.0
Laravel 5.5、5.6從 v4.3 開始支持
自 v4 起支援Laravel 5.2、5.3、5.4
Laravel 5.1 v3 受支持
Laravel 4 v2 受支持
內容:
理論
文件
插入節點
檢索節點
刪除節點
一致性檢查和修復
範圍界定
要求
安裝
嵌套集或嵌套集模型是一種在關係表中有效儲存分層資料的方法。來自維基百科:
嵌套集模型是根據樹的遍歷對節點進行編號,樹的遍歷會訪問每個節點兩次,並按訪問的順序分配編號,並在兩次訪問時分配編號。這為每個節點留下兩個數字,它們儲存為兩個屬性。查詢變得廉價:可以透過比較這些數字來測試層次結構成員資格。更新需要重新編號,因此成本昂貴。
當樹很少更新時,NSM 顯示出良好的效能。它被調整為快速獲取相關節點。它非常適合為商店建立多深度菜單或類別。
假設我們有一個模型Category
; $node
變數是該模型和我們正在操作的節點的實例。它可以是一個新模型或來自資料庫的模型。
節點具有以下功能齊全且可立即載入的關係:
節點屬於parent
節點有很多children
節點有很多ancestors
節點有很多descendants
移動和插入節點涉及多個資料庫查詢,因此強烈建議使用事務。
重要的!從 v4.2.0 開始,事務不會自動啟動
另一個重要的注意事項是,結構操作會被推遲,直到您在模型上按一下save
(某些方法隱式呼叫已save
並返回操作的布林結果)。
如果模型成功保存,並不表示節點已移動。如果您的應用程式取決於節點是否實際更改了其位置,請使用hasMoved
方法:
if ($node->save()) {$moved = $node->hasMoved(); }
當您簡單地建立一個節點時,它將被附加到樹的末尾:
類別::create($attributes); // 儲存為root
$node = 新類別($attributes);$node->save(); // 儲存為root
在這種情況下,該節點被視為根,這意味著它沒有父節點。
// #1 隱式 save$node->saveAsRoot();// #2 明確 save$node->makeRoot()->save();
該節點將被附加到樹的末尾。
如果想要讓節點成為其他節點的子節點,可以將其設為最後一個或第一個子節點。
在下列範例中, $parent
是某個現有節點。
追加節點的方法有以下幾種:
// #1 使用延遲插入$node->appendToNode($parent)->save();// #2 使用父節點$parent->appendNode($node);// #3 使用父節點的子節點關係$parent- >children()->create($attributes);// #5 使用節點的父關係$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
必須存在,目標節點可以是新鮮的。如果目標節點存在,它將被移動到新位置,並且如果需要,父節點將被更改。
# 明確save$node->afterNode($neighbor)->save();$node->beforeNode($neighbor)->save();# 隱含save$node->insertAfterNode($neighbor);$node -> insertBeforeNode($neighbor);
當在節點上使用靜態方法create
時,它會檢查屬性是否包含children
鍵。如果是,它會遞歸地創建更多節點。
$node = Category::create(['name' => 'Foo','children' => [ ['姓名' => '酒吧','兒童' => [ [ '名字' => '巴茲' ], ], ], ], ]);
$node->children
現在包含已建立的子節點的清單。
您可以輕鬆地重建一棵樹。這對於大規模改變樹的結構很有用。
類別::rebuildTree($data, $delete);
$data
是一個節點數組:
$數據 = [ [ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ], [ '名稱' => '酒吧' ], ];
為名為foo
的節點指定了一個 id,這表示現有節點將被填充並保存。如果節點不存在,則拋出ModelNotFoundException
。此外,該節點還指定了children
,這也是一個節點數組;它們將以相同的方式處理並保存為節點foo
的子節點。
節點bar
沒有指定主鍵,因此將建立它。
$delete
顯示是否刪除已存在但不存在於$data
中的節點。預設情況下,不會刪除節點。
從 4.2.8 開始,您可以重建子樹:
類別::rebuildSubtree($root, $data);
這限制了樹重建到$root
節點的後代。
在某些情況下,我們將使用$id
變量,它是目標節點的 id。
祖先為節點創建了一條父鏈。有助於顯示目前類別的麵包屑。
後代是子樹中的所有節點,即節點的子節點、子節點的子節點等。
祖先和後代都可以急切地加載。
// 訪問祖先$node->ancestors;// 訪問後代$node->descendants;
可以使用自訂查詢載入祖先和後代:
$結果 = 類別::ancestorsOf($id);$結果 = 類別::ancestorsAndSelf($id);$結果 = 類別::descendantsOf($id);$結果 = 類別::descendantsAndSelf($id);
在大多數情況下,你需要你的祖先按級別排序:
$result = Category::defaultOrder()->ancestorsOf($id);
可以急切地載入祖先集合:
$categories = Category::with('ancestors')->paginate(30);// 在麵包屑視圖:@foreach($categories as $i => $category) <小>{{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : '頂級' }}</small><br> {{ $類別->名稱 }} @endforeach
兄弟節點是具有相同父節點的節點。
$結果 = $node->getSiblings();$結果 = $node->siblings()->get();
只獲取下一個兄弟姊妹:
// 取得緊接在該節點之後的同級節點$result = $node->getNextSibling();// 取得該節點之後的所有同級節點$result = $node->getNextSiblings();// 使用a 取得所有同級節點查詢$結果 = $node->nextSiblings()->get();
要獲取以前的兄弟姊妹:
// 取得緊鄰該節點之前的同級節點$result = $node->getPrevSibling();// 取得該節點之前的所有同級節點$result = $node->getPrevSiblings();// 使用a 取得所有同級節點查詢$結果 = $node->prevSiblings()->get();
想像一下,每個類別has many
商品。即HasMany
關係成立。如何獲得$category
及其所有後代的所有商品?簡單的!
// 取得後代的 id$categories = $category->descendants()->pluck('id');// 包含類別本身的 id$categories[] = $category->getKey();// 取得商品$ goods = Goods::whereIn('category_id', $categories)->get();
如果需要知道節點處於哪個層級:
$結果 = 類別::withDepth()->find($id);$深度 = $結果->深度;
根節點的層級為 0。
若要取得指定層級的節點,您可以套用having
約束:
$結果 = 類別::withDepth()->having('深度', '=', 1)->get();
重要的!這在資料庫嚴格模式下不起作用
所有節點都在內部嚴格組織。預設情況下,不套用任何順序,因此節點可能會以隨機順序出現,這不會影響樹的顯示。您可以按字母表或其他索引對節點進行排序。
但在某些情況下,層級順序是至關重要的。它是檢索祖先所必需的,並且可用於訂購菜單項目。
若要套用樹狀順序,請使用defaultOrder
方法:
$結果 = 類別::defaultOrder()->get();
您可以按相反的順序取得節點:
$結果 = 類別::reversed()->get();
若要在父級內部向上或向下移動節點以影響預設順序:
$bool = $node->down();$bool = $node->up();// 將節點移動 3 個兄弟節點$bool = $node->down(3);
運算結果是節點是否改變位置的布林值。
可應用於查詢產生器的各種約束:
whereIsRoot()僅取得根節點;
hasParent()取得非根節點;
whereIsLeaf()只取得葉子;
hasChildren()取得非離開節點;
whereIsAfter($id)取得指定 id 的節點之後的每個節點(不只是兄弟節點);
whereIsBefore($id)取得在具有指定 id 的節點之前的每個節點。
後代約束:
$結果 = 類別::whereDescendantOf($node)->get();$結果 = 類別::whereNotDescendantOf($node)->get();$結果 = 類別::orWhereDescendantOf($node)->get() ;$result = Category::orWhereNotDescendantOf($node)->get();$result = Category::whereDescendantAndSelf($id)->get();// 將目標節點包含到結果集中$result = Category:: whereDescendantOrSelf ($node)->get();
祖先約束:
$結果 = 類別::whereAncestorOf($node)->get();$結果 = 類別::whereAncestorOrSelf($id)->get();
$node
可以是模型或模型實例的主鍵。
獲得一組節點後,可以將其轉換為樹。例如:
$tree = 類別::get()->toTree();
這將填充集合中每個節點上children
parent
關係,您可以使用遞歸演算法渲染樹:
$nodes = Category::get()->toTree();$traverse = function ($categories, $prefix = '-') use (&$traverse) {foreach ($categories as $category) {echo PHP_EOL.$前綴。 '.$category->name;$traverse($category->children, $prefix.'-'); } };$遍歷($節點);
這將輸出類似這樣的內容:
- 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);
刪除節點:
$節點->刪除();
重要的!該節點的任何後代也將被刪除!
重要的!節點需要作為模型刪除,不要嘗試使用以下查詢刪除它們:
類別::where('id', '=', $id)->delete();
這會破壞樹的!
在模型層級也支援SoftDeletes
特徵。
檢查節點是否為其他節點的後代:
$bool = $node->isDescendantOf($parent);
檢查節點是否為根節點:
$bool = $node->isRoot();
其他檢查:
$node->isChildOf($other);
$node->isAncestorOf($other);
$node->isSiblingOf($other);
$node->isLeaf()
您可以檢查樹是否損壞(即有一些結構錯誤):
$bool = 類別::isBroken();
可以獲得錯誤統計資訊:
$data = Category::countErrors();
它將傳回一個包含以下鍵的陣列:
oddness
-- 具有錯誤的lft
和rgt
值集的節點數
duplicates
-- 具有相同lft
或rgt
值的節點數
wrong_parent
-具有與lft
和rgt
值不對應的無效parent_id
值的節點數
missing_parent
parent_id
指向不存在的節點的節點數
由於 v3.1 樹現在可以修復。使用parent_id
列中的繼承訊息,為每個節點設定正確的_lft
和_rgt
值。
節點::fixTree();
假設您有Menu
模型和MenuItems
。這些模型之間建立了一對多的關係。 MenuItem
具有用於將模型連接在一起的menu_id
屬性。 MenuItem
包含巢狀集。顯然,您希望根據menu_id
屬性單獨處理每棵樹。為此,您需要將此屬性指定為範圍屬性:
受保護函式 getScopeAttributes() {返回['選單_id']; }
但現在,為了執行一些自訂查詢,您需要提供用於確定範圍的屬性:
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OKMenuItem::descendantsOf($id)->get(); // 錯誤:從其他scopeMenuItem::scoped([ 'menu_id' => 5 ])->fixTree()傳回節點; // 好的
使用模型實例請求節點時,會根據該模型的屬性自動套用範圍:
$node = MenuItem::findOrFail($id);$node->siblings()->withDepth()->get(); // 好的
若要使用實例取得範圍查詢產生器:
$node->newScopedQuery();
急切載入時始終使用範圍查詢:
MenuItem::scoped([ 'menu_id' => 5])->with('後代')->findOrFail($id); // OKMenuItem::with('descendants')->findOrFail($id); // 錯誤的
PHP >= 5.4
Laravel >= 4.1
強烈建議使用支援交易的資料庫(如 MySql 的 InnoDb)來保護樹免受可能的損壞。
若要安裝軟體包,請在終端機中:
composer require kalnoy/nestedset
對於 Laravel 5.5 及以上版本的使用者:
Schema::create('table', function (Blueprint $table) {...$table->nestedSet(); });// 刪除 columnsSchema::table('table', function (Blueprint $table) {$table->dropNestedSet(); });
對於之前的 Laravel 版本:
...使用 KalnoyNestedsetNestedSet; Schema::create('table', function (Blueprint $table) {...NestedSet::columns($table); });
刪除列:
...使用 KalnoyNestedsetNestedSet; Schema::table('table', function (Blueprint $table) { NestedSet::dropColumns($table); });
您的模型應使用KalnoyNestedsetNodeTrait
特徵來啟用巢狀集:
use KalnoyNestedsetNodeTrait;class Foo 擴充 Model {use NodeTrait; }
如果您先前的擴充功能使用了不同的列集,您只需重寫模型類別上的以下方法:
公用函數 getLftName() {返回'左'; }公用函數 getRgtName() {返回'正確'; }公用函數 getParentIdName() {返回'父級'; }//指定父id屬性變元public function setParentAttribute($value) {$this->setParentIdAttribute($value); }
如果您的樹包含parent_id
訊息,您需要在您的模式中新增兩列:
$table->unsignedInteger('_lft');$table->unsignedInteger('_rgt');
設定模型後,您只需修復樹即可填入_lft
和_rgt
列:
MyModel::fixTree();
版權所有 (c) 2017 亞歷山大‧卡爾諾伊
特此免費授予任何獲得本軟體和相關文件文件(「軟體」)副本的人不受限制地使用本軟體,包括但不限於使用、複製、修改、合併的權利、發布、分發、再授權和/或銷售軟體的副本,並允許向其提供軟體的人員這樣做,但須滿足以下條件:
上述版權聲明和本授權聲明應包含在本軟體的所有副本或主要部分中。
本軟體以「現況」提供,不提供任何明示或暗示的保證,包括但不限於適銷性、特定用途的適用性和不侵權的保證。 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE軟體.