Skip to content

Commit f21f2a1

Browse files
committed
Refactor
Added QueryBuilder to get rid of "scope" methods. Now query is ordered by lft only when no other orders is set and when either limit nor offset is set. Added few new methods. Updated readme
1 parent 259dc55 commit f21f2a1

File tree

6 files changed

+395
-154
lines changed

6 files changed

+395
-154
lines changed

README.markdown

Lines changed: 96 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
1-
This is Laravel 4 package that simplifies creating, managing and retrieving trees
2-
in database. Using [Nested Set](http://en.wikipedia.org/wiki/Nested_set_model)
3-
technique high performance descendants retrieval and path-to-node queries can be done.
1+
Say hi to Laravel 4 extension that will allow to create and manage hierarchies in
2+
your database out-of-box. You can:
43

5-
__IMPORTANT!__ To keep realization of Nested Set Model simple, it's made to work
6-
within single HTTP requests. Don't build trees in your code. [But, it's possible](#multiple-node-insertion).
4+
* Create multi-level menus and select items of specific level;
5+
* Create categories for the store with no limit of nesting level, query for
6+
descendants and ancestors;
7+
* Forget about performance issues!
78

89
## Installation
910

10-
The package can be installed as Composer package, just include it into
11-
`required` section of your `composer.json` file:
11+
The package can be installed using Composer, just include it into `required`
12+
section of your `composer.json` file:
1213

13-
"required": {
14-
"kalnoy/nestedset": "dev-master"
15-
}
14+
```json
15+
"required": {
16+
"kalnoy/nestedset": "dev-master"
17+
}
18+
```
1619

17-
And then hit `composer update` in the terminal. That's it - you are ready to go next.
20+
Hit `composer update` in the terminal, and you are ready to go next!
1821

1922
## Basic usage
2023

2124
### Schema
2225

23-
Storing trees in database requires additional columns for the table, so these
24-
fields need to be included in table schema. We use `NestedSet::columns($table)`
25-
inside table schema creation function, like so:
26+
Storing hierarchies in a database requires additional columns for the table, so these
27+
fields need to be included in the migration. There is a helper for this:
2628

2729
```php
2830
<?php
@@ -48,8 +50,9 @@ class CreateCategoriesTable extends Migration {
4850
NestedSet::columns($table);
4951
});
5052

53+
// The root node is required
5154
NestedSet::createRoot('categories', array(
52-
'title' => 'Root',
55+
'title' => 'Store',
5356
));
5457
}
5558

@@ -65,12 +68,13 @@ class CreateCategoriesTable extends Migration {
6568
}
6669
```
6770

68-
To simplify things root node is required. `NestedSet::createRoot` creates it for us.
69-
7071
### The model
7172

72-
The next step is to create `Eloquent` model. Do it whatever way you like, but
73-
make shure that model is extended from `\Kalnoy\Nestedset\Node`, like here:
73+
The next step is to create `Eloquent` model. I prefer [Jeffrey Way's generators][1],
74+
but you can stick to whatever you prefer, just make shure that model is extended
75+
from `\Kalnoy\Nestedset\Node`, like here:
76+
77+
[1]: https://github.com/JeffreyWay/Laravel-4-Generators
7478

7579
```php
7680
<?php
@@ -80,16 +84,20 @@ class Category extends \Kalnoy\Nestedset\Node {}
8084

8185
### Queries
8286

83-
You can create nodes like so:
87+
You can insert nodes using several methods:
8488

8589
```php
8690
$node = new Category(array('title' => 'TV\'s'));
87-
$node->appendTo(Category::root())->save();
91+
$target = Category::root();
92+
93+
$node->appendTo($target)->save();
94+
$node->prependTo($target)->save();
8895
```
8996

90-
the same thing can be done differently (to allow changing parent via mass assignment):
97+
The parent can be changed via mass asignment:
9198

9299
```php
100+
// The equivalent of $node->appendTo(Category::find($parent_id))
93101
$node->parent_id = $parent_id;
94102
$node->save();
95103
```
@@ -104,7 +112,7 @@ $srcNode->after($targetNode)->save();
104112
$srcNode->before($targetNode)->save();
105113
```
106114

107-
Path to the node can be obtained in two ways:
115+
_Ancestors_ can be obtained in two ways:
108116

109117
```php
110118
// Target node will not be included into result since it is already available
@@ -118,27 +126,42 @@ or using the scope:
118126
$path = Category::pathTo($nodeId)->get();
119127
```
120128

121-
Descendant nodes can easily be gotten this way:
129+
_Descendants_ can easily be retrieved in this way:
122130

123131
```php
124132
$descendants = $node->descendants()->get();
125133
```
126134

127-
Nodes can be provided with depth level if scope `withDepth` is applied:
135+
This method returns query builder, so you can apply any constraints or eager load
136+
some relations.
137+
138+
There are few more methods:
139+
140+
* `siblings()` for querying siblings of the node;
141+
* `nextSiblings()` and `prevSiblings()` to query nodes after and before the node
142+
respectively.
143+
144+
Nodes can be provided with _nesting level_ if scope `withDepth` is applied:
128145

129146
```php
130147
// Each node instance will recieve 'depth' attribute with depth level starting at
131148
// zero for the root node.
132149
$nodes = Category::withDepth()->get();
133150
```
134151

135-
Query can be filtered out from the root node using scope `withoutRoot`:
152+
Using `depth` attribute it is possible to get nodes with maximum level of nesting:
153+
154+
```php
155+
$menu = Menu::withDepth()->having('depth', '<=', 2)->get();
156+
```
157+
158+
The root node can be filtered out using scope `withoutRoot`:
136159

137160
```php
138161
$nodes = Category::withoutRoot()->get();
139162
```
140163

141-
Deleting nodes is as simple as before:
164+
Nothing changes when you need to remove the node:
142165

143166
```php
144167
$node->delete();
@@ -150,16 +173,48 @@ There are two relations provided by `Node`: _children_ and _parent_.
150173

151174
### Insertion, re-insertion and deletion of nodes
152175

153-
Operations such as insertion and deletion of nodes imply several independent queries
176+
Operations such as insertion and deletion of nodes imply extra queries
154177
before node is actually saved. That is why if something goes wrong, the whole tree
155-
might be broken. To avoid such situations each call to `save()` must be enclosed
156-
into transaction.
178+
might be broken. To avoid such situations, each call of `save()` has to be enclosed
179+
in the transaction.
180+
181+
## How-tos
182+
183+
### Move node up or down
184+
185+
Sometimes there is need to move nodes around while remaining in boundaries of
186+
the parent.
187+
188+
To move node down, this snippet can be used:
189+
190+
```php
191+
if ($sibling = $node->nextSiblings()->first())
192+
{
193+
$node->after($sibling)->save();
194+
}
195+
```
157196

158-
Also, experimentally was noticed that using transaction drastically improves
159-
performance when tree gets update.
197+
Moving up is a little bit trickier:
198+
199+
```php
200+
if ($sibling = $node->prevSiblings()->reversed()->first())
201+
{
202+
$node->before($sibling)->save();
203+
}
204+
```
205+
206+
To move node up we need to insert it before node that is right at the top of it.
207+
If we use `$node->prevSiblings()->first()` we'll get the first child of the parent
208+
since all nodes are ordered by fixed values. We apply `reversed()` scope to reverse
209+
default order.
160210

161211
## Advanced usage
162212

213+
### Default order
214+
215+
Nodes are ordered by lft column unless there is `limit` or `offset` is provided,
216+
or when user uses `orderBy`.
217+
163218
### Custom collection
164219

165220
This package also provides custom collection, which has two additional functions:
@@ -205,9 +260,10 @@ This is what we are going to get:
205260
}];
206261
```
207262

208-
Even though the query returned all nodes but _Netbooks_, the resulting tree does not contain any
209-
child from that node. This is very helpful when nodes are soft deleted. Active children of soft
210-
deleted nodes will inevitably show up in query results, which is not desired in most situations.
263+
Even though the query returned all nodes but _Netbooks_, the resulting tree does
264+
not contain any child from that node. This is very helpful when nodes are soft deleted.
265+
Active children of soft deleted nodes will inevitably show up in query results,
266+
which is not desired in most situations.
211267

212268
### Multiple node insertion
213269

@@ -244,48 +300,6 @@ work just fine.
244300
_THIS IS THE ONLY CASE WHEN MULTIPLE NODES CAN BE INSERTED AND/OR RE-INSERTED
245301
DURING SINGLE HTTP REQUEST WITHOUT REFRESHING DATA_
246302

247-
#### If you still need this
248-
249-
If you are up to create your tree structure in your code, make shure that target node
250-
is always updated. Here is the description of what nodes are target when using insertion
251-
functions:
252-
253-
```php
254-
/**
255-
* @var Category $node The node being inserted
256-
* @var Category $target The target node
257-
*/
258-
259-
$node->appendTo($target);
260-
$node->prependTo($target);
261-
$node->before($target);
262-
$node->after($target);
263-
$target->append($node);
264-
$target->prepend($node);
265-
```
266-
267-
When doing multiple insertions, just call `$target->refresh()` each time before calling
268-
any of the above functions.
269-
270-
```php
271-
DB::transaction(function () {
272-
$node = new Category(...);
273-
$root = Category::root();
274-
275-
// The root here is updated automatically
276-
$node->appendTo($root)->save();
277-
278-
$nodeSubNode = new Category(...);
279-
// No need to update $node since it is just saved
280-
// Also, $node gets update since it is new parent for $nodeSubNode
281-
$nodeSubNode->appendTo($node)->save();
282-
283-
$nodeSibling = new Category(...);
284-
// We refresh $root because it is not updated since last operation
285-
$nodeSibling->appendTo($root->refresh())->save();
286-
});
287-
```
288-
289303
### Deleting nodes
290304

291305
To delete a node, you just call `$node->delete()` as usual. If node is soft deleted,
@@ -296,4 +310,8 @@ When you create your table's schema and use `NestedSet::columns`, it creates for
296310
key for you, since nodes are connected by `parent_id` attribute. When you hard delete
297311
the node, all of descendants are cascaded.
298312

299-
In case when DBMS doesn't support foreign keys, descendants are removed manually.
313+
In case when DBMS doesn't support foreign keys, descendants are still removed.
314+
315+
## TODO
316+
317+
[*] Build up hierarchy from array;

src/Kalnoy/Nestedset/Collection.php

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public function toDictionary($key = null)
3737
*
3838
* To succesfully build tree "id" and "parent_id" keys must present.
3939
*
40+
* If {@link rootNodeId} is provided, the tree will contain only descendants
41+
* of the node with such primary key value.
42+
*
4043
* @param integer $rootNodeId
4144
*
4245
* @return Collection
@@ -46,19 +49,30 @@ public function toTree($rootNodeId = null)
4649
$dictionary = $this->toDictionary();
4750
$result = new static();
4851

49-
// If root node is not specified we take first node's parent.
50-
// This works since nodes are sorted by lft and first node has least depth.
51-
if ($rootNodeId === null) {
52-
$rootNodeId = $this->first()->getParentId();
52+
// If root node is not specified we take parent id from node with
53+
// least lft value as root node id.
54+
if ($rootNodeId === null)
55+
{
56+
$leastValue = null;
57+
58+
foreach ($this->items as $item) {
59+
if ($leastValue === null || $item->getLft() < $leastValue)
60+
{
61+
$leastValue = $item->getLft();
62+
$rootNodeId = $item->getParentId();
63+
}
64+
}
5365
}
5466

5567
$result->items = isset($dictionary[$rootNodeId]) ? $dictionary[$rootNodeId] : array();
5668

57-
if (empty($result->items)) {
69+
if (empty($result->items))
70+
{
5871
return $result;
5972
}
6073

61-
foreach ($this->items as $item) {
74+
foreach ($this->items as $item)
75+
{
6276
$key = $item->getKey();
6377

6478
$children = new BaseCollection(isset($dictionary[$key]) ? $dictionary[$key] : array());

0 commit comments

Comments
 (0)