Laravel-nestedset that base left and right values tree package
This is a Laravel 4-5 package for working with trees in relational databases.
- Laravel 5.5, 5.6, 5.7, 5.8 is supported since v4.3
- Laravel 5.2, 5.3, 5.4 is supported since v4
- Laravel 5.1 is supported in v3
- Laravel 4 is supported in v2
Although this project is completely free for use, I appreciate any support!
Contents:
What are nested sets?
Nested sets or Nested Set Model is a way to effectively store hierarchical data in a relational table. From wikipedia:
The nested set model is to number the nodes according to a tree traversal, which visits each node twice, assigning numbers in the order of visiting, and at both visits. This leaves two numbers for each node, which are stored as two attributes. Querying becomes inexpensive: hierarchy membership can be tested by comparing these numbers. Updating requires renumbering and is therefore expensive.
Applications
NSM shows good performance when tree is updated rarely. It is tuned to be fast for getting related nodes. It'is ideally suited for building multi-depth menu or categories for shop.
Documentation
Suppose that we have a model Category; a $node variable is an instance of that model and the node that we are manipulating. It can be a fresh model or one from database.
Relationships
Node has following relationships that are fully functional and can be eagerly loaded:
- Node belongs to
parent - Node has many
children - Node has many
ancestors - Node has many
descendants
Inserting nodes
Moving and inserting nodes includes several database queries, so it is highly recommended to use transactions.
IMPORTANT! As of v4.2.0 transaction is not automatically started
Another important note is that structural manipulations are deferred until you hit save on model (some methods implicitly call save and return boolean result of the operation).
If model is successfully saved it doesn't mean that node was moved. If your application depends on whether the node has actually changed its position, use hasMoved method:
if ($node->save()) {
$moved = $node->hasMoved();
}
Creating nodes
When you simply creating a node, it will be appended to the end of the tree:
Category::create($attributes); // Saved as root
$node = new Category($attributes);
$node->save(); // Saved as root
In this case the node is considered a root which means that it doesn't have a parent.
Making a root from existing node
// #1 Implicit save
$node->saveAsRoot(); // #2 Explicit save
$node->makeRoot()->save();
The node will be appended to the end of the tree.
Appending and prepending to the specified parent
If you want to make node a child of other node, you can make it last or first child.
In following examples, $parent is some existing node.
There are few ways to append a node:
// #1 Using deferred insert
$node->appendToNode($parent)->save(); // #2 Using parent node
$parent->appendNode($node); // #3 Using parent's children relationship
$parent->children()->create($attributes); // #5 Using node's parent relationship
$node->parent()->associate($parent)->save(); // #6 Using the parent attribute
$node->parent_id = $parent->id;
$node->save(); // #7 Using static method
Category::create($attributes, $parent);
And only a couple ways to prepend:
// #1
$node->prependToNode($parent)->save(); // #2
$parent->prependNode($node);
Inserting before or after specified node
You can make $node to be a neighbor of the $neighbor node using following methods:
$neighbor must exists, target node can be fresh. If target node exists, it will be moved to the new position and parent will be changed if it's required.
# Explicit save
$node->afterNode($neighbor)->save();
$node->beforeNode($neighbor)->save(); # Implicit save
$node->insertAfterNode($neighbor);
$node->insertBeforeNode($neighbor);
Building a tree from array
When using static method create on node, it checks whether attributes contains children key. If it does, it creates more nodes recursively.
$node = Category::create([
'name' => 'Foo', 'children' => [
[
'name' => 'Bar', 'children' => [
[ 'name' => 'Baz' ],
],
],
],
]);
$node->children now contains a list of created child nodes.
Rebuilding a tree from array
You can easily rebuild a tree. This is useful for mass-changing the structure of the tree.
Category::rebuildTree($data, $delete);
$data is an array of nodes:
$data = [
[ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ],
[ 'name' => 'bar' ],
];
There is an id specified for node with the name of foo which means that existing node will be filled and saved. If node is not exists ModelNotFoundException is thrown. Also, this node has children specified which is also an array of nodes; they will be processed in the same manner and saved as children of node foo.
Node bar has no primary key specified, so it will be created.
$delete shows whether to delete nodes that are already exists but not present in $data. By default, nodes aren't deleted.
Rebuilding a subtree
As of 4.2.8 you can rebuild a subtree:
Category::rebuildSubtree($root, $data);
This constraints tree rebuilding to descendants of $root node.
Retrieving nodes
In some cases we will use an $id variable which is an id of the target node.
Ancestors and descendants
Ancestors make a chain of parents to the node. Helpful for displaying breadcrumbs to the current category.
Descendants are all nodes in a sub tree, i.e. children of node, children of children, etc.
Both ancestors and descendants can be eagerly loaded.
// Accessing ancestors
$node->ancestors; // Accessing descendants
$node->descendants;
It is possible to load ancestors and descendants using custom query:
$result = Category::ancestorsOf($id);
$result = Category::ancestorsAndSelf($id);
$result = Category::descendantsOf($id);
$result = Category::descendantsAndSelf($id);
In most cases, you need your ancestors to be ordered by the level:
$result = Category::defaultOrder()->ancestorsOf($id);
A collection of ancestors can be eagerly loaded:
$categories = Category::with('ancestors')->paginate(30);
// in view for breadcrumbs:
@foreach($categories as $i => $category)
<small>{{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' }}</small><br>
{{ $category->name }}
@endforeach
Siblings
Siblings are nodes that have same parent.
$result = $node->getSiblings(); $result = $node->siblings()->get();
To get only next siblings:
// Get a sibling that is immediately after the node
$result = $node->getNextSibling(); // Get all siblings that are after the node
$result = $node->getNextSiblings(); // Get all siblings using a query
$result = $node->nextSiblings()->get();
To get previous siblings:
// Get a sibling that is immediately before the node
$result = $node->getPrevSibling(); // Get all siblings that are before the node
$result = $node->getPrevSiblings(); // Get all siblings using a query
$result = $node->prevSiblings()->get();
Getting related models from other table
Imagine that each category has many goods. I.e. HasMany relationship is established. How can you get all goods of $category and every its descendant? Easy!
// Get ids of descendants
$categories = $category->descendants()->pluck('id'); // Include the id of category itself
$categories[] = $category->getKey(); // Get goods
$goods = Goods::whereIn('category_id', $categories)->get();
Including node depth
If you need to know at which level the node is:
$result = Category::withDepth()->find($id); $depth = $result->depth;
Root node will be at level 0. Children of root nodes will have a level of 1, etc.
To get nodes of specified level, you can apply having constraint:
$result = Category::withDepth()->having('depth', '=', 1)->get();
IMPORTANT! This will not work in database strict mode
Default order
All nodes are strictly organized internally. By default, no order is applied, so nodes may appear in random order and this doesn't affect displaying a tree. You can order nodes by alphabet or other index.
But in some cases hierarchical order is essential. It is required for retrieving ancestors and can be used to order menu items.
To apply tree order defaultOrder method is used:
$result = Category::defaultOrder()->get();
You can get nodes in reversed order:
$result = Category::reversed()->get();
To shift node up or down inside parent to affect default order:
$bool = $node->down();
$bool = $node->up(); // Shift node by 3 siblings
$bool = $node->down(3);
The result of the operation is boolean value of whether the node has changed its position.
Constraints
Various constraints that can be applied to the query builder:
- whereIsRoot() to get only root nodes;
- hasParent() to get non-root nodes;
- whereIsLeaf() to get only leaves;
- hasChildren() to get non-leave nodes;
- whereIsAfter($id) to get every node (not just siblings) that are after a node with specified id;
- whereIsBefore($id) to get every node that is before a node with specified id.
Descendants constraints:
$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(); // Include target node into result set
$result = Category::whereDescendantOrSelf($node)->get();
Ancestor constraints:
$result = Category::whereAncestorOf($node)->get();
$result = Category::whereAncestorOrSelf($id)->get();
$node can be either a primary key of the model or model instance.
Building a tree
After getting a set of nodes, you can convert it to tree. For example:
$tree = Category::get()->toTree();
This will fill parent and children relationships on every node in the set and you can render a tree using recursive algorithm:
$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);
This will output something like this:
- Root
-- Child 1
--- Sub child 1
-- Child 2
- Another root
Building flat tree
Also, you can build a flat tree: a list of nodes where child nodes are immediately after parent node. This is helpful when you get nodes with custom order (i.e. alphabetically) and don't want to use recursion to iterate over your nodes.
$nodes = Category::get()->toFlatTree();
Previous example will output:
Root
Child 1
Sub child 1
Child 2
Another root
Getting a subtree
Sometimes you don't need whole tree to be loaded and just some subtree of specific node. It is show in following example:
$root = Category::descendantsAndSelf($rootId)->toTree()->first();
In a single query we are getting a root of a subtree and all of its descendants that are accessible via children relation.
If you don't need $root node itself, do following instead:
$tree = Category::descendantsOf($rootId)->toTree($rootId);
Deleting nodes
To delete a node:
$node->delete();
IMPORTANT! Any descendant that node has will also be deleted!
IMPORTANT! Nodes are required to be deleted as models, don't try do delete them using a query like so:
Category::where('id', '=', $id)->delete();
This will break the tree!
SoftDeletes trait is supported, also on model level.
Helper methods
To check if node is a descendant of other node:
$bool = $node->isDescendantOf($parent);
To check whether the node is a root:
$bool = $node->isRoot();
Other checks:
$node->isChildOf($other);$node->isAncestorOf($other);$node->isSiblingOf($other);$node->isLeaf()
Checking consistency
You can check whether a tree is broken (i.e. has some structural errors):
$bool = Category::isBroken();
It is possible to get error statistics:
$data = Category::countErrors();
It will return an array with following keys:
oddness-- the number of nodes that have wrong set oflftandrgtvaluesduplicates-- the number of nodes that have samelftorrgtvalueswrong_parent-- the number of nodes that have invalidparent_idvalue that doesn't correspond tolftandrgtvaluesmissing_parent-- the number of nodes that haveparent_idpointing to node that doesn't exists
Fixing tree
Since v3.1 tree can now be fixed. Using inheritance info from parent_id column, proper _lft and _rgt values are set for every node.
Node::fixTree();
Scoping
Imagine you have Menu model and MenuItems. There is a one-to-many relationship set up between these models. MenuItem has menu_id attribute for joining models together. MenuItem incorporates nested sets. It is obvious that you would want to process each tree separately based on menu_id attribute. In order to do so, you need to specify this attribute as scope attribute:
protected function getScopeAttributes()
{
return [ 'menu_id' ];
}
But now, in order to execute some custom query, you need to provide attributes that are used for scoping:
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OK
MenuItem::descendantsOf($id)->get(); // WRONG: returns nodes from other scope
MenuItem::scoped([ 'menu_id' => 5 ])->fixTree(); // OK
When requesting nodes using model instance, scopes applied automatically based on the attributes of that model:
$node = MenuItem::findOrFail($id); $node->siblings()->withDepth()->get(); // OK
To get scoped query builder using instance:
$node->newScopedQuery();
Scoping and eager loading
Always use scoped query when eager loading:
MenuItem::scoped([ 'menu_id' => 5])->with('descendants')->findOrFail($id); // OK
MenuItem::with('descendants')->findOrFail($id); // WRONG
Requirements
- PHP >= 5.4
- Laravel >= 4.1
It is highly suggested to use database that supports transactions (like MySql's InnoDb) to secure a tree from possible corruption.
Installation
To install the package, in terminal:
composer require kalnoy/nestedset
Setting up from scratch
The schema
For Laravel 5.5 and above users:
Schema::create('table', function (Blueprint $table) {
...
$table->nestedSet();
});
// To drop columns
Schema::table('table', function (Blueprint $table) {
$table->dropNestedSet();
});
For prior Laravel versions:
...
use Kalnoy\Nestedset\NestedSet; Schema::create('table', function (Blueprint $table) {
...
NestedSet::columns($table);
});
To drop columns:
...
use Kalnoy\Nestedset\NestedSet; Schema::table('table', function (Blueprint $table) {
NestedSet::dropColumns($table);
});
The model
Your model should use Kalnoy\Nestedset\NodeTrait trait to enable nested sets:
use Kalnoy\Nestedset\NodeTrait;
class Foo extends Model {
use NodeTrait;
}
Migrating existing data
Migrating from other nested set extension
If your previous extension used different set of columns, you just need to override following methods on your model class:
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);
}
Migrating from basic parentage info
If your tree contains parent_id info, you need to add two columns to your schema:
$table->unsignedInteger('_lft');
$table->unsignedInteger('_rgt');
After setting up your model you only need to fix the tree to fill _lft and _rgt columns:
MyModel::fixTree();
License
Copyright (c) 2017 Alexander Kalnoy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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 SOFTWARE.
Laravel-nestedset that base left and right values tree package的更多相关文章
- [NPM] Set default values for package.json using npm set
Npm by default uses global values when initializing a new package.json file. Learn how to set your o ...
- laravel 服务容器
服务容器,也叫IOC容器,其实包含了依赖注入(DI)和控制反转(IOC)两部分,是laravel的真正核心.其他的各种功能模块比如 Route(路由).Eloquent ORM(数据库 ORM 组件) ...
- laravel启动过程简单解析
:first-child{margin-top:0!important}img.plugin{box-shadow:0 1px 3px rgba(0,0,0,.1);border-radius:3px ...
- laravel的启动过程---摘自网络博客个人学习之用
如果没有使用过类似Yii之类的框架,直接去看laravel,会有点一脸迷糊的感觉,起码我是这样的.laravel的启动过程,也是laravel的核心,对这个过程有一个了解,有助于得心应手的使用框架,希 ...
- Laravel框架中Blade模板的用法
1. 继承.片段.占位.组件.插槽 1.1 继承 1.定义父模板 Laravel/resources/views/base.blade.php 2.子模板继承 @extends('base') 1.2 ...
- laravel的启动过程解析
laravel的启动过程,也是laravel的核心,对这个过程有一个了解,有助于得心应手的使用框架,希望能对大家有点帮助. 统一入口 laravel框架使用了统一入口,入口文件:/public/ind ...
- [笔记]Laravel TDD 胡乱记录
TDD: 测试驱动开发(Test-Driven Development),TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码. -- 载自TDD百度百科 参考 ...
- Android The layout "activity_main" in layout has no declaration in the base layout folder
报错: The layout "activity_main" in layout has no declaration in the base layout folder; thi ...
- 好久没玩laravel了,5.6玩下(一)
那么先到官方找到框架,然后安装下 composer安装的,composer怎么安装的 我就不说了 前置条件: PHP >= OpenSSL PHP Extension PDO PHP Exten ...
随机推荐
- caffe-windows环境配置(github上官方BVLC/caffe的推荐配置方法详解)
[转载来的文章:如有侵权,请联系我!我将马上删除!] 首先声明一下,如标题,本教程是caffe在windows系统上的配置方法,而且是github上官方BVLC/caffe目前推荐的配置方法,并不是使 ...
- android 获取对权限的选择
一般是第三方软件拦截,再次提示给用户,确认权限的,如360等.(PS 没有设置权限的app 是会崩溃的 ,而是不是弹出权限确认,因为你都没设置这个权限)看了网上很多,确切说没有一个适合我的. 其实用 ...
- python 的序列化和反序列化
什么是序列化?简单来说就是将数据存储到物理内存上的过程叫序列化. 什么是反序列化?将数据从物理内存存储到程序内存的过程叫做反序列化. 下面来看一下python中使用json进行序列化和反序列化的实例d ...
- python 处理 https链接 socket报错 链接https
// socket 链接 https 有问题 得去看看ssl文档 用法 import socketimport ssl def https_test(url): proto = "http& ...
- leetcode155
public class MinStack { Stack<int> S = new Stack<int>(); /** initialize your data struct ...
- delphi WebBrowser IPv6
We discovered one or more bugs in your app when reviewed on iPhone running iOS 11.4 on Wi-Fi connect ...
- 试了下阿里云的OPEN Api
逐渐理解open api的意义,试了下阿里云的续费接口,续费一个月 package com.test; /** * @author * @date 2018/12/05 */ import com.a ...
- python如何打开一个大文件?
with open('a.csv','r') as f: for i in f: print(i) while True: a = f.readline() if not a: break f.rea ...
- request设置cookies
mycookie = { "PHPSESSID":"56v9clgo1kdfo3q5q8ck0aaaaa" } request.get(url,cookies ...
- python re模块与正则表达式
首先要先继承re模块: import re re.findall() 方法 # 返回值为列表 \w 表示一个字符,为数字,字母,下滑线之一, \W匹配任意非数字,字母,下划线 print(re.fin ...