imajkumar/laravel-binary-tree
Composer 安装命令:
composer require imajkumar/laravel-binary-tree
包简介
Ayra is an implementation of the Nested Set pattern for Eloquent models.
README 文档
README
for laravel 5.8
An easy way to visualize how a nested set works is to think of a parent entity surrounding all of its children, and its parent surrounding it, etc. So this tree:
root
|_ Child 1
|_ Child 1.1
|_ Child 1.2
|_ Child 2
|_ Child 2.1
|_ Child 2.2
Could be visualized like this:
___________________________________________________________________
| 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
| |___________________________| |___________________________| |
|___________________________________________________________________|
The numbers represent the left and right boundaries. The table then might look like this:
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
To get all children of a parent node, you
SELECT * WHERE lft IS BETWEEN parent.lft AND parent.rgt
To get the number of children, it's
(right - left - 1)/2
To get a node and all its ancestors going back to the root, you
SELECT * WHERE node.lft IS BETWEEN lft AND rgt
As you can see, queries that would be recursive and prohibitively slow on ordinary trees are suddenly quite fast. Nifty, isn't it?
Creating a root node
By default, all nodes are created as roots:
$root = Category::create(['name' => 'Root category']);
Alternatively, you may find yourself in the need of converting an existing node into a root node:
$node->makeRoot();
You may also nullify it's parent_id column to accomplish the same behaviour:
// This works the same as makeRoot() $node->parent_id = null; $node->save();
Inserting nodes
// Directly with a relation $child1 = $root->children()->create(['name' => 'Child 1']); // with the `makeChildOf` method $child2 = Category::create(['name' => 'Child 2']); $child2->makeChildOf($root);
Deleting nodes
$child1->delete();
Descendants of deleted nodes will also be deleted and all the lft and rgt
bound will be recalculated. Pleases note that, for now, deleting and deleted
model events for the descendants will not be fired.
Getting the nesting level of a node
The getLevel() method will return current nesting level, or depth, of a node.
$node->getLevel() // 0 when root
Moving nodes around
Baum provides several methods for moving nodes around:
moveLeft(): Find the left sibling and move to the left of it.moveRight(): Find the right sibling and move to the right of it.moveToLeftOf($otherNode): Move to the node to the left of ...moveToRightOf($otherNode): Move to the node to the right of ...makeNextSiblingOf($otherNode): Alias formoveToRightOf.makeSiblingOf($otherNode): Alias formakeNextSiblingOf.makePreviousSiblingOf($otherNode): Alias formoveToLeftOf.makeChildOf($otherNode): Make the node a child of ...makeFirstChildOf($otherNode): Make the node the first child of ...makeLastChildOf($otherNode): Alias formakeChildOf.makeRoot(): Make current node a root node.
For example:
$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);
Asking questions to your nodes
You can ask some questions to your Baum nodes:
isRoot(): Returns true if this is a root node.isLeaf(): Returns true if this is a leaf node (end of a branch).isChild(): Returns true if this is a child node.isDescendantOf($other): Returns true if node is a descendant of the other.isSelfOrDescendantOf($other): Returns true if node is self or a descendant.isAncestorOf($other): Returns true if node is an ancestor of the other.isSelfOrAncestorOf($other): Returns true if node is self or an ancestor.equals($node): current node instance equals the other.insideSubtree($node): Checks whether the given node is inside the subtree defined by the left and right indices.inSameScope($node): Returns true if the given node is in the same scope as the current one. That is, if every column in thescopedproperty has the same value in both nodes.
Using the nodes from the previous example:
$demons->isRoot(); // => false $demons->isDescendantOf($root) // => true
Relations
provides two self-referential Eloquent relations for your nodes: parent
and children.
$parent = $node->parent()->get(); $children = $node->children()->get();
Root and Leaf scopes
provides some very basic query scopes for accessing the root and leaf nodes:
// Query scope which targets all root nodes Category::roots() // All leaf nodes (nodes at the end of a branch) Category:allLeaves()
You may also be interested in only the first root:
$firstRootNode = Category::root();
Accessing the ancestry/descendancy chain
There are several methods which 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). -
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 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 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
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
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 Ayra\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
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 Ayra\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,rgtare not null,rgtvalues greater thanlftand within the bounds of the parent node (if set). - That there are no duplicates for the
lftandrgtcolumn values. - As the first check does not actually check root nodes, see if each root has
the
lftandrgtindexes 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
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 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
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), 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
- Laptops
- 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
- Laptops
- 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
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' // );
Contributing
Thinking of contributing? Maybe you've found some nasty bug? That's great news!
- Fork & clone the project:
git clone git@github.com:your-username/baum.git. - Run the tests and make sure that they pass with your setup:
phpunit. - Create your bugfix/feature branch and code away your changes. Add tests for your changes.
- Make sure all the tests still pass:
phpunit. - Push to your fork and submit new a pull request.
License
This fork from Baum laravel package i have just improved for laravel 5.8 . thanks goes to Baum Baum is licensed under the terms of the MIT License (See LICENSE file for details).
imajkumar/laravel-binary-tree 适用场景与选型建议
imajkumar/laravel-binary-tree 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 2.28k 次下载、GitHub Stars 达 15, 最近一次更新时间为 2019 年 04 月 23 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「database」 「laravel」 「nested set」 「eloquent」 「laravel 5.8」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 imajkumar/laravel-binary-tree 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 imajkumar/laravel-binary-tree 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 imajkumar/laravel-binary-tree 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Dibi is Database Abstraction Library for PHP
Store your language lines in the database, yaml or other sources
A package for automatically encrypting and decrypting Eloquent attributes in Laravel 5.5+, based on configuration settings.
Baum is an implementation of the Nested Set pattern for Eloquent models.
A PSR-7 compatible library for making CRUD API endpoints
Nested field validation for Yii2
统计信息
- 总下载量: 2.28k
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 15
- 点击次数: 23
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2019-04-23