Skip to content

Commit 6976a38

Browse files
committed
Add tests and fix some bugs
1 parent eeef19c commit 6976a38

File tree

9 files changed

+387
-62
lines changed

9 files changed

+387
-62
lines changed

README.markdown

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Query can be filtered out from the root node using scope `withoutRoot`:
136136
$nodes = Category::withoutRoot()->get();
137137
```
138138

139-
Deleting nodes as simple as before:
139+
Deleting nodes is as simple as before:
140140

141141
```php
142142
$node->delete();
@@ -293,3 +293,5 @@ children?
293293
When you create your table's schema and use `NestedSet::columns`, it creates foreign
294294
key for you, since nodes are connected by `parent_id` attribute. When you hard delete
295295
the node, all of descendants are cascaded.
296+
297+
In case when DBMS doesn't support foreign keys, descendants are removed manually.

composer.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
"illuminate/support": "4.0.x",
1515
"illuminate/database": "4.0.x"
1616
},
17+
18+
"require-dev": {
19+
"phpunit/phpunit": "3.7.*"
20+
},
21+
1722
"autoload": {
1823
"psr-0": {
1924
"Kalnoy\\Nestedset": "src/"
20-
}
25+
},
2126
},
2227
"minimum-stability": "dev"
2328
}

phpunit.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
include __DIR__.'/vendor/autoload.php';
4+
5+
$capsule = new \Illuminate\Database\Capsule\Manager;
6+
$capsule->addConnection(array('driver' => 'sqlite', 'database' => ':memory:'));
7+
$capsule->setEventDispatcher(new \Illuminate\Events\Dispatcher);
8+
$capsule->bootEloquent();
9+
$capsule->setAsGlobal();
10+
11+
include __DIR__.'/tests/models/Category.php';

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<phpunit backupGlobals="false"
33
backupStaticAttributes="false"
4-
bootstrap="vendor/autoload.php"
4+
bootstrap="phpunit.php"
55
colors="true"
66
convertErrorsToExceptions="true"
77
convertNoticesToExceptions="true"

src/Kalnoy/Nestedset/NestedSet.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ static public function columns(Blueprint $table, $primaryKey = 'id')
1818
$table->integer(Node::RGT);
1919
$table->unsignedInteger(Node::PARENT_ID)->nullable();
2020

21-
$table->index([ Node::LFT, Node::RGT, Node::PARENT_ID ], 'nested_set_index');
21+
$table->index(array(Node::LFT, Node::RGT, Node::PARENT_ID), 'nested_set_index');
2222

2323
$table
2424
->foreign(Node::PARENT_ID, 'nested_set_foreign')

src/Kalnoy/Nestedset/Node.php

Lines changed: 83 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
use \Illuminate\Database\Query\Builder;
55
use Exception;
66

7-
class Node extends Eloquent
8-
{
7+
class Node extends Eloquent {
98
/**
109
* The name of "lft" column.
1110
*
@@ -94,7 +93,7 @@ public function descendants()
9493
{
9594
$query = $this->newQuery();
9695

97-
return $query->whereBetween('lft', array($this->getLft() + 1, $this->getRgt()));
96+
return $query->whereBetween(static::LFT, array($this->getLft() + 1, $this->getRgt()));
9897
}
9998

10099
/**
@@ -162,7 +161,8 @@ public function scopeWithoutRoot($query)
162161
*/
163162
public function path()
164163
{
165-
if (!$this->exists) {
164+
if (!$this->exists)
165+
{
166166
throw new Exception("Cannot query for path to non-existing node.");
167167
}
168168

@@ -216,7 +216,7 @@ public function prepend(Node $node)
216216
*/
217217
public function appendTo(Node $parent)
218218
{
219-
return $this->insertAt($parent->attributes[static::RGT], $parent);
219+
return $this->checkTarget($parent)->insertAt($parent->attributes[static::RGT], $parent);
220220
}
221221

222222
/**
@@ -228,7 +228,7 @@ public function appendTo(Node $parent)
228228
*/
229229
public function prependTo(Node $parent)
230230
{
231-
return $this->insertAt($parent->attributes[static::LFT] + 1, $parent);
231+
return $this->checkTarget($parent)->insertAt($parent->attributes[static::LFT] + 1, $parent);
232232
}
233233

234234
/**
@@ -264,14 +264,15 @@ public function before(Node $node)
264264
* @param self::BEFORE|self::AFTER $dir
265265
*
266266
* @return Node
267+
* @throws Exception If $node is not a valid target.
268+
* @throws Exception If $node is the root.
267269
*/
268270
protected function beforeOrAfter(Node $node, $dir)
269271
{
270-
if (!$node->exists) {
271-
throw new Exception("Cannot insert node $dir node that does not exists.");
272-
}
272+
$this->checkTarget($node);
273273

274-
if ($node->isRoot()) {
274+
if ($node->isRoot())
275+
{
275276
throw new Exception("Cannot insert node $dir root node.");
276277
}
277278

@@ -280,18 +281,38 @@ protected function beforeOrAfter(Node $node, $dir)
280281
return $this->insertAt($position, $node->parent);
281282
}
282283

284+
/**
285+
* Check if target node is saved.
286+
*
287+
* @param Node $node
288+
*
289+
* @return Node
290+
* @throws Exception If target fails conditions.
291+
*/
292+
protected function checkTarget(Node $node)
293+
{
294+
if (!$node->exists || $node->isDirty(static::LFT))
295+
{
296+
throw new Exception("Target node is updated but not saved.");
297+
}
298+
299+
return $this;
300+
}
301+
283302
/**
284303
* Insert node at specific position and related parent.
285304
*
286305
* @param int $position
287306
* @param Node $parent
288307
*
289308
* @return Node
309+
* @throws Exceptino If node is inserted in one of it's descendants
290310
*/
291311
protected function insertAt($position, Node $parent)
292312
{
293-
if ($this->exists && $this->getLft() < $position && $position < $this->getRgt()) {
294-
throw new Exception("Cannot insert node into itself.");
313+
if ($this->exists && $this->getLft() < $position && $position < $this->getRgt())
314+
{
315+
throw new Exception("Trying to insert node into one of it's descendants.");
295316
}
296317

297318
$height = $this->getNodeHeight();
@@ -317,17 +338,23 @@ protected function insertAt($position, Node $parent)
317338
*/
318339
public function fireModelEvent($event, $halt = true)
319340
{
320-
if ($event === 'creating' or $event === 'updating' and $this->isDirty(static::LFT)) {
321-
if (!$this->updateTree()) {
341+
if ($event === 'creating')
342+
{
343+
if (!isset($this->attributes[static::LFT]) || !$this->updateTree())
344+
{
322345
return false;
323346
}
324347
}
325348

326-
if ($event === 'deleting' and $this->isRoot()) {
327-
return false;
349+
if ($event === 'updating' && $this->isDirty(static::LFT))
350+
{
351+
if (!$this->updateTree()) return false;
328352
}
329353

330-
if ($event === 'deleted' and !$this->softDelete) {
354+
if ($event === 'deleting' && $this->isRoot()) return false;
355+
356+
if ($event === 'deleted' && !$this->softDelete)
357+
{
331358
$this->processDeletedNode();
332359
}
333360

@@ -345,7 +372,8 @@ protected function updateTree()
345372
$rgt = $this->attributes[static::RGT];
346373
$height = $rgt - $lft + 1;
347374

348-
if ($this->exists) {
375+
if ($this->exists)
376+
{
349377
$oldLft = $this->original[static::LFT];
350378
$oldRgt = $this->original[static::RGT];
351379

@@ -358,7 +386,8 @@ protected function updateTree()
358386
// Node is going down.
359387
// Other nodes are going up on its place.
360388
static::shiftNodes(-$height, $oldRgt + 1, $lft - 1, $this);
361-
} else {
389+
} else
390+
{
362391
$shiftAmount = $lft - $oldLft;
363392

364393
// Node is going up.
@@ -382,11 +411,19 @@ protected function updateTree()
382411
*/
383412
protected function processDeletedNode()
384413
{
414+
$lft = $this->getLft();
415+
$rgt = $this->getRgt();
416+
417+
// DBMS with support of foreign keys will remove descendant nodes automatically
418+
$this->descendants()->delete();
419+
385420
// We cannot use getNodeHeight because it always return 2 for non-existing
386421
// nodes.
387-
$height = $this->attributes[static::RGT] - $this->attributes[static::LFT] + 1;
422+
$height = $rgt - $lft + 1;
423+
424+
$this->shiftNodes(-$height, $rgt + 1, null, $this);
388425

389-
$this->shiftNodes(-$height, $this->attributes[static::RGT] + 1, null, $this);
426+
unset($this->attributes[static::LFT], $this->attributes[static::RGT]);
390427
}
391428

392429
/**
@@ -438,13 +475,9 @@ protected function revealNodes($shiftAmount)
438475
*/
439476
static protected function shiftNodes($amount, $from, $to, $instance = null)
440477
{
441-
if ($amount == 0 || $from === null && $to === null) {
442-
return;
443-
}
478+
if ($amount == 0 || $from === null && $to === null) return;
444479

445-
if ($instance === null) {
446-
$instance = new static;
447-
}
480+
if ($instance === null) $instance = new static;
448481

449482
$query = $instance->newQueryWithDeleted()->getQuery();
450483
$method = $amount > 0 ? 'increment' : 'decrement';
@@ -453,13 +486,14 @@ static protected function shiftNodes($amount, $from, $to, $instance = null)
453486
static::shiftNodesColumn($query, static::LFT, $method, $amount, $from, $to);
454487
static::shiftNodesColumn($query, static::RGT, $method, $amount, $from, $to);
455488

456-
// Update parent of the instance to support inserting multiple nodes into
457-
// single parent.
458-
if (isset($instance->relations['parent'])) {
459-
$parent = $instance->relations['parent'];
489+
// Update all ancestors (if any) of the instance to support inserting
490+
// multiple nodes into single parent.
491+
while (isset($instance->relations['parent']))
492+
{
493+
$instance = $instance->relations['parent'];
460494

461-
$parent->shiftColumn(static::LFT, $amount, $from, $to);
462-
$parent->shiftColumn(static::RGT, $amount, $from, $to);
495+
$instance->shiftColumn(static::LFT, $amount, $from, $to);
496+
$instance->shiftColumn(static::RGT, $amount, $from, $to);
463497
}
464498
}
465499

@@ -480,13 +514,9 @@ static protected function shiftNodesColumn(Builder $query, $column, $method, $am
480514
$query->wheres = array();
481515
$query->setBindings(array());
482516

483-
if ($from === null) {
484-
$query->where($column, '<=', $to);
485-
} elseif ($to === null) {
486-
$query->where($column, '>=', $from);
487-
} else {
488-
$query->whereBetween($column, array($from, $to));
489-
}
517+
if ($from === null) $query->where($column, '<=', $to);
518+
elseif ($to === null) $query->where($column, '>=', $from);
519+
else $query->whereBetween($column, array($from, $to));
490520

491521
return $query->$method($column, $amount);
492522
}
@@ -502,9 +532,7 @@ static protected function shiftNodesColumn(Builder $query, $column, $method, $am
502532
*/
503533
protected function shift($amount, $from, $to)
504534
{
505-
if ($from === null && $to === null || $amount == 0) {
506-
return;
507-
}
535+
if ($from === null && $to === null || $amount == 0) return;
508536

509537
$this->shiftColumn(static::LFT, $amount, $from, $to);
510538
$this->shiftColumn(static::RGT, $amount, $from, $to);
@@ -545,7 +573,8 @@ protected function reveal($shiftAmount, $from, $to)
545573
{
546574
$lft = $this->getLft();
547575

548-
if ($from <= $lft && $lft <= $to) {
576+
if ($from <= $lft && $lft <= $to)
577+
{
549578
$this->attributes[static::LFT] += $shiftAmount;
550579
$this->attributes[static::RGT] += $shiftAmount;
551580
}
@@ -564,9 +593,7 @@ protected function reveal($shiftAmount, $from, $to)
564593
*/
565594
static protected function hideOrRevealNodes($from, $to, $shiftAmount = 0, $instance = null)
566595
{
567-
if ($instance === null) {
568-
$instance = new static;
569-
}
596+
if ($instance === null) $instance = new static;
570597

571598
$query = $instance->newQueryWithDeleted()->getQuery();
572599
$grammar = $query->getGrammar();
@@ -577,9 +604,11 @@ static protected function hideOrRevealNodes($from, $to, $shiftAmount = 0, $insta
577604
// and "revealing" is putting nodes back in database but in new position based on shiftAmount
578605
$updateValues = array();
579606

580-
if ($shiftAmount == 0) {
607+
if ($shiftAmount == 0)
608+
{
581609
$updateValues[static::LFT] = $query->raw('-'.$grammar->wrap(static::LFT));
582-
} else {
610+
} else
611+
{
583612
$shiftAmountStr = $shiftAmount > 0 ? '+'.$shiftAmount : $shiftAmount;
584613

585614
$updateValues[static::LFT] = $query->raw('-'.$grammar->wrap(static::LFT).$shiftAmountStr);
@@ -624,9 +653,7 @@ public function newCollection(array $models = array())
624653
*/
625654
public function getNodeHeight()
626655
{
627-
if (!$this->exists) {
628-
return 2;
629-
}
656+
if (!$this->exists) return 2;
630657

631658
return $this->attributes[static::RGT] - $this->attributes[static::LFT] + 1;
632659
}
@@ -647,14 +674,12 @@ public function getDescendantCount()
647674
* Behind the scenes node is appended to found parent node.
648675
*
649676
* @param int $value
677+
* @throws Exception If parent node doesn't exists
650678
*/
651-
public function setParentId($value)
679+
public function setParentIdAttribute($value)
652680
{
653-
if ($this->isRoot()) {
654-
throw new Exception("Cannot change parent of root node.");
655-
}
656-
657-
if ($this->attributes[static::PARENT_ID] != $value) {
681+
if (!isset($this->attributes[static::PARENT_ID]) || $this->attributes[static::PARENT_ID] != $value)
682+
{
658683
$this->appendTo(static::findOrFail($value));
659684
}
660685
}

0 commit comments

Comments
 (0)