Skip to content

Commit 5481254

Browse files
committed
Initial commit
0 parents  commit 5481254

File tree

9 files changed

+1089
-0
lines changed

9 files changed

+1089
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/vendor
2+
composer.phar
3+
composer.lock
4+
.DS_Store

.travis.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
language: php
2+
3+
php:
4+
- 5.3
5+
- 5.4
6+
- 5.5
7+
8+
before_script:
9+
- curl -s http://getcomposer.org/installer | php
10+
- php composer.phar install --dev
11+
12+
script: phpunit

README.markdown

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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.
4+
5+
## Installation
6+
7+
The package can be installed as Composer package, just include it into
8+
`required` section of your `composer.json` file:
9+
10+
"required": {
11+
"kalnoy/nestedset": "dev-master"
12+
}
13+
14+
## Basic usage
15+
16+
Storing trees in database requires additional columns for the table, so these
17+
fields need to be included in table schema. We use `NestedSet::columns($table)`
18+
inside table schema creation function, like so:
19+
20+
```php
21+
<?php
22+
23+
use Illuminate\Database\Migrations\Migration;
24+
use Illuminate\Database\Schema\Blueprint;
25+
use Kalnoy\Nestedset\NestedSet;
26+
27+
class CreateCategoriesTable extends Migration {
28+
29+
/**
30+
* Run the migrations.
31+
*
32+
* @return void
33+
*/
34+
public function up()
35+
{
36+
Schema::create('categories', function(Blueprint $table) {
37+
$table->increments('id');
38+
$table->string('title');
39+
$table->timestamps();
40+
41+
NestedSet::columns($table);
42+
});
43+
44+
NestedSet::createRoot('categories', array(
45+
'title' => 'Root',
46+
));
47+
}
48+
49+
/**
50+
* Reverse the migrations.
51+
*
52+
* @return void
53+
*/
54+
public function down()
55+
{
56+
Schema::drop('categories');
57+
}
58+
}
59+
```
60+
61+
To simplify things root node is required. `NestedSet::createRoot` creates it for us.
62+
63+
The next step is to create `Eloquent` model. Do it whatever way you like, but
64+
make shure that node is extended from `\Kalnoy\Nestedset\Node`, like here:
65+
66+
```php
67+
<?php
68+
69+
class Category extends \Kalnoy\Nestedset\Node {}
70+
```
71+
Now you can create nodes like so:
72+
73+
```php
74+
$node = new Category(array('title' => 'TV\'s'));
75+
$node->appendTo(Category::root())->save();
76+
```
77+
78+
the same thing can be done differently (to allow changing parent via mass assignment):
79+
80+
```php
81+
$node->parent_id = $parent_id;
82+
$node->save();
83+
```
84+
85+
You can insert the node right next to or before the other node:
86+
87+
```php
88+
$srcNode = Category::find($src_id);
89+
$targetNode = Category::find($target_id);
90+
91+
$srcNode->after($targetNode)->save();
92+
$srcNode->before($targetNode)->save();
93+
```
94+
95+
Path to the node can be obtained in two ways:
96+
97+
```php
98+
// Target node will not be included into result since it is already available
99+
$path = $node->path()->get();
100+
```
101+
102+
or using the scope:
103+
104+
```php
105+
// Target node will be included into result
106+
$path = Category::pathTo($nodeId)->get();
107+
```
108+
109+
Descendant nodes can easily be gotten this way:
110+
111+
```php
112+
$descendants = $node->descendants()->get();
113+
```
114+
115+
Nodes can be provided with depth level if scope `withDepth` is applied:
116+
117+
```php
118+
// Each node instance will recieve 'depth' attribute with depth level starting at
119+
// zero for the root node.
120+
$nodes = Category::withDepth()->get();
121+
```
122+
123+
Query can be filtered out from the root node using scope `withoutRoot`.
124+
125+
## Insertion, re-insertion and deletion of nodes
126+
127+
Operations such as insertion and deletion of nodes imply several independent queries
128+
before node is actually saved. That is why if something goes wrong, the whole tree
129+
might be broken. To avoid such situations each call to `save()` must be enclosed
130+
into transaction.
131+
132+
Also, experimentally was noticed that using transaction drastically improves
133+
performance when tree gets update.
134+
135+
## Advanced usage
136+
137+
### Multiple node insertion
138+
139+
_DO NOT MAKE MULTIPLE INSERTIONS DURING SINGLE HTTP REQUEST_
140+
141+
Since when node is inserted or re-inserted tree is changed in database, nodes
142+
that are already loaded might also have changed and need to be refreshed. This
143+
doesn't happen automatically with exception of one scenario.
144+
145+
Consider this example:
146+
147+
```php
148+
$nodes = Category::whereIn('id', Input::get('selected_ids'))->get();
149+
$target = Category::find(Input::get('target_id'));
150+
151+
foreach ($nodes as $node) {
152+
$node->appendTo($target)->save();
153+
}
154+
```
155+
156+
This is the example of situation when user picks up several nodes and moves them
157+
into new parent. When we call `appendTo` nothing is really changed but internal
158+
variables. Actual transformations are performed when `save` is called. When that
159+
happens, values of internal variables are definately changed for `$target` and
160+
might change for some nodes in `$nodes` list. But this changes happen in database
161+
and do not reflect into memory for loaded nodes. Calling `appendTo` with outdated
162+
values brakes the tree.
163+
164+
In this case only values of `$target` are crucial. The system always updates crucial
165+
attributes of parent of node being saved. Since `$target` becomes new parent for
166+
every node, the data of that node will always be up to date and this example will
167+
work just fine.
168+
169+
_THIS IS THE ONLY CASE WHEN MULTIPLE NODES CAN BE INSERTED AND/OR RE-INSERTED
170+
DURING SINGLE HTTP REQUEST WITHOUT REFRESHING DATA_

composer.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "kalnoy/nestedset",
3+
"description": "",
4+
"authors": [
5+
{
6+
"name": "Aleksander Kalnoy",
7+
"email": "[email protected]"
8+
}
9+
],
10+
"require": {
11+
"php": ">=5.3.0",
12+
"illuminate/support": "4.0.x",
13+
"illuminate/database": "4.0.x"
14+
},
15+
"autoload": {
16+
"psr-0": {
17+
"Kalnoy\\Nestedset": "src/"
18+
}
19+
},
20+
"minimum-stability": "dev"
21+
}

phpunit.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit backupGlobals="false"
3+
backupStaticAttributes="false"
4+
bootstrap="vendor/autoload.php"
5+
colors="true"
6+
convertErrorsToExceptions="true"
7+
convertNoticesToExceptions="true"
8+
convertWarningsToExceptions="true"
9+
processIsolation="false"
10+
stopOnFailure="false"
11+
syntaxCheck="false"
12+
>
13+
<testsuites>
14+
<testsuite name="Package Test Suite">
15+
<directory suffix=".php">./tests/</directory>
16+
</testsuite>
17+
</testsuites>
18+
</phpunit>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php namespace Kalnoy\Nestedset;
2+
3+
use \Illuminate\Database\Eloquent\Collection as BaseCollection;
4+
5+
class Collection extends BaseCollection {
6+
7+
/**
8+
* Convert list of nodes to dictionary with specified key.
9+
*
10+
* If no key is specified then "parent_id" is used.
11+
*
12+
* @param string $key
13+
*
14+
* @return array
15+
*/
16+
public function toDictionary($key = null)
17+
{
18+
if (empty($this->items)) {
19+
return array();
20+
}
21+
22+
if ($key === null) {
23+
$key = $this->first()->getParentIdName();
24+
}
25+
26+
$result = array();
27+
28+
foreach ($this->items as $item) {
29+
$result[$item->$key][] = $item;
30+
}
31+
32+
return $result;
33+
}
34+
35+
/**
36+
* Build tree from node list.
37+
*
38+
* To succesfully build tree "id" and "parent_id" keys must present.
39+
*
40+
* @param integer $rootNodeId
41+
*
42+
* @return Collection
43+
*/
44+
public function toTree($rootNodeId = null)
45+
{
46+
$dictionary = $this->toDictionary();
47+
$result = new static();
48+
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();
53+
}
54+
55+
$result->items = isset($dictionary[$rootNodeId]) ? $dictionary[$rootNodeId] : array();
56+
57+
if (empty($result->items)) {
58+
return $result;
59+
}
60+
61+
foreach ($this->items as $item) {
62+
$key = $item->getKey();
63+
64+
$children = new BaseCollection(isset($dictionary[$key]) ? $dictionary[$key] : array());
65+
$item->setRelation('children', $children);
66+
}
67+
68+
return $result;
69+
}
70+
}

src/Kalnoy/Nestedset/NestedSet.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php namespace Kalnoy\Nestedset;
2+
3+
use \Illuminate\Database\Connection;
4+
use \Illuminate\Database\Schema\Blueprint;
5+
6+
class NestedSet {
7+
8+
/**
9+
* Add NestedSet columns to the table. Also create index and foreign key.
10+
*
11+
* @param Blueprint $table
12+
*
13+
* @return void
14+
*/
15+
static public function columns(Blueprint $table, $primaryKey = 'id')
16+
{
17+
$table->integer(Node::LFT);
18+
$table->integer(Node::RGT);
19+
$table->unsignedInteger(Node::PARENT_ID)->nullable();
20+
21+
$table->index([ Node::LFT, Node::RGT, Node::PARENT_ID ], 'nested_set_index');
22+
23+
$table
24+
->foreign(Node::PARENT_ID, 'nested_set_foreign')
25+
->references($primaryKey)
26+
->on($table->getTable())
27+
->onDelete('cascade');
28+
}
29+
30+
/**
31+
* Drop NestedSet columns.
32+
*
33+
* @param Blueprint $table
34+
*
35+
* @return void
36+
*/
37+
static public function dropColumns(Blueprint $table)
38+
{
39+
$table->dropForeign('nested_set_foreign');
40+
$table->dropIndex('nested_set_index');
41+
$table->dropColumn(Node::LFT, Node::RGT, Node::PARENT_ID);
42+
}
43+
44+
/**
45+
* Create root node.
46+
*
47+
* @param string $table
48+
* @param array $extra
49+
* @param string $connection
50+
*
51+
* @return boolean
52+
*/
53+
static function createRoot($table, array $extra = array(), $connection = null)
54+
{
55+
$extra = array_merge($extra, array(
56+
Node::LFT => 1,
57+
Node::RGT => 2,
58+
Node::PARENT_ID => NULL,
59+
));
60+
61+
return \DB::connection($connection)->table($table)->insert($extra);
62+
}
63+
}

0 commit comments

Comments
 (0)