Skip to content

Commit 4c9b748

Browse files
committed
Fix #82: Fixing tree now handles case when nodes pointing non-existing parent
1 parent ecaed88 commit 4c9b748

File tree

6 files changed

+104
-37
lines changed

6 files changed

+104
-37
lines changed

CHANGELOG.markdown

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
### 4.1.0
22

3-
* Converted to trait
4-
* Methods were renamed:
3+
* #75: Converted to trait. Following methods were renamed:
54
- `appendTo` to `appendToNode`
65
- `prependTo` to `prependToNode`
76
- `insertBefore` to `insertBeforeNode`
87
- `insertAfter` to `insertAfterNode`
98
- `getNext` to `getNextNode`
109
- `getPrev` to `getPrevNode`
10+
11+
* #82: Fixing tree now handles case when nodes pointing non-existing parent
12+
* The number of missing parent is now returned when using `countErrors`
1113

1214
### 3.1.1
1315

README.markdown

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,12 @@ $data = Category::countErrors();
420420

421421
It will return an array with following keys:
422422

423-
- `oddness` -- the number of nodes that have wrong set of `lft` and `rgt` values;
424-
- `duplicates` -- the number of nodes that have same `lft` or `rgt` values;
423+
- `oddness` -- the number of nodes that have wrong set of `lft` and `rgt` values
424+
- `duplicates` -- the number of nodes that have same `lft` or `rgt` values
425425
- `wrong_parent` -- the number of nodes that have invalid `parent_id` value that
426-
doesn't correspond to `lft` and `rgt` values.
426+
doesn't correspond to `lft` and `rgt` values
427+
- `missing_parent` -- the number of nodes that have `parent_id` pointing to
428+
node that doesn't exists
427429

428430
#### Fixing tree
429431

src/NodeTrait.php

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,37 +45,37 @@ public static function bootNodeTrait()
4545
{
4646
static::saving(function (self $model) {
4747
$model->getConnection()->beginTransaction();
48-
48+
4949
return $model->callPendingAction();
5050
});
51-
51+
5252
static::saved(function (self $model) {
5353
$model->getConnection()->commit();
5454
});
5555

5656
static::deleting(function (self $model) {
5757
$model->getConnection()->beginTransaction();
58-
58+
5959
// We will need fresh data to delete node safely
6060
$model->refreshNode();
6161
});
6262

6363
static::deleted(function (self $model) {
6464
$model->deleteDescendants();
65-
65+
6666
$model->getConnection()->commit();
6767
});
6868

6969
if (static::usesSoftDelete()) {
7070
static::restoring(function (self $model) {
7171
$model->getConnection()->beginTransaction();
72-
72+
7373
static::$deletedAt = $model->{$model->getDeletedAtColumn()};
7474
});
7575

7676
static::restored(function (self $model) {
7777
$model->restoreDescendants(static::$deletedAt);
78-
78+
7979
$model->getConnection()->commit();
8080
});
8181
}
@@ -197,8 +197,8 @@ protected function actionAppendOrPrepend(self $parent, $prepend = false)
197197
*/
198198
protected function setParent($value)
199199
{
200-
$this->attributes[$this->getParentIdName()] = $value
201-
? $value->getKey()
200+
$this->attributes[$this->getParentIdName()] = $value
201+
? $value->getKey()
202202
: null;
203203

204204
$this->setRelation('parent', $value);
@@ -805,9 +805,7 @@ public function getParentIdName()
805805
*/
806806
public function getLft()
807807
{
808-
return isset($this->attributes[$this->getLftName()])
809-
? $this->attributes[$this->getLftName()]
810-
: null;
808+
return $this->getAttributeValue($this->getLftName());
811809
}
812810

813811
/**
@@ -817,9 +815,7 @@ public function getLft()
817815
*/
818816
public function getRgt()
819817
{
820-
return isset($this->attributes[$this->getRgtName()])
821-
? $this->attributes[$this->getRgtName()]
822-
: null;
818+
return $this->getAttributeValue($this->getRgtName());
823819
}
824820

825821
/**
@@ -829,14 +825,14 @@ public function getRgt()
829825
*/
830826
public function getParentId()
831827
{
832-
return $this->getAttribute($this->getParentIdName());
828+
return $this->getAttributeValue($this->getParentIdName());
833829
}
834830

835831
/**
836832
* Returns node that is next to current node without constraining to siblings.
837-
*
833+
*
838834
* This can be either a next sibling or a next sibling of the parent node.
839-
*
835+
*
840836
* @param array $columns
841837
*
842838
* @return self
@@ -850,7 +846,7 @@ public function getNextNode(array $columns = array( '*' ))
850846
* Returns node that is before current node without constraining to siblings.
851847
*
852848
* This can be either a prev sibling or parent node.
853-
*
849+
*
854850
* @param array $columns
855851
*
856852
* @return self
@@ -943,7 +939,7 @@ public function getPrevSibling(array $columns = array( '*' ))
943939
*/
944940
public function isDescendantOf(self $other)
945941
{
946-
return $this->getLft() > $other->getLft() &&
942+
return $this->getLft() > $other->getLft() &&
947943
$this->getLft() < $other->getRgt();
948944
}
949945

@@ -1029,15 +1025,23 @@ public function getBounds()
10291025
*/
10301026
public function setLft($value)
10311027
{
1032-
$this->setAttribute($this->getLftName(), $value);
1028+
$this->attributes[$this->getLftName()] = $value;
10331029
}
10341030

10351031
/**
10361032
* @param $value
10371033
*/
10381034
public function setRgt($value)
10391035
{
1040-
$this->setAttribute($this->getRgtName(), $value);
1036+
$this->attributes[$this->getRgtName()] = $value;
1037+
}
1038+
1039+
/**
1040+
* @param $value
1041+
*/
1042+
public function setParentId($value)
1043+
{
1044+
$this->attributes[$this->getParentIdName()] = $value;
10411045
}
10421046

10431047
}

src/QueryBuilder.php

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,10 @@ public function countErrors()
587587
// Check if parent_id is set correctly
588588
$checks['wrong_parent'] = $this->getWrongParentQuery();
589589

590-
$query = $this->toBase();
590+
// Check for nodes that have missing parent
591+
$checks['missing_parent' ] = $this->getMissingParentQuery();
592+
593+
$query = $this->query->newQuery();
591594

592595
foreach ($checks as $key => $inner) {
593596
$inner->selectRaw('count(1)');
@@ -625,7 +628,7 @@ protected function getDuplicatesQuery()
625628
->newServiceQuery()
626629
->toBase()
627630
->from($this->query->raw("{$table} c1, {$table} c2"))
628-
->whereRaw("c1.id <> c2.id")
631+
->whereRaw("c1.id < c2.id")
629632
->whereNested(function (BaseQueryBuilder $inner) {
630633
list($lft, $rgt) = $this->wrappedColumns();
631634

@@ -662,6 +665,32 @@ protected function getWrongParentQuery()
662665

663666
}
664667

668+
/**
669+
* @return $this
670+
*/
671+
protected function getMissingParentQuery()
672+
{
673+
return $this->model
674+
->newServiceQuery()
675+
->toBase()
676+
->whereNested(function (BaseQueryBuilder $inner) {
677+
$table = $this->wrappedTable();
678+
$keyName = $this->wrappedKey();
679+
$parentIdName = $this->query->raw($this->model->getParentIdName());
680+
681+
$existsCheck = $this->model
682+
->newServiceQuery()
683+
->toBase()
684+
->selectRaw('1')
685+
->from($this->query->raw("{$table} p"))
686+
->whereRaw("{$table}.{$parentIdName} = p.{$keyName}")
687+
->limit(1);
688+
689+
$inner->whereRaw("{$parentIdName} is not null")
690+
->addWhereExistsQuery($existsCheck, 'and', true);
691+
});
692+
}
693+
665694
/**
666695
* Get the number of total errors of the tree.
667696
*
@@ -710,7 +739,18 @@ public function fixTree()
710739

711740
$fixed = 0;
712741

713-
self::reorderNodes($nodes, $fixed);
742+
$cut = self::reorderNodes($nodes, $fixed);
743+
744+
// Saved nodes that have invalid parent as roots
745+
while ( ! $nodes->isEmpty()) {
746+
$parentId = $nodes->keys()->first();
747+
748+
foreach ($nodes[$parentId] as $model) {
749+
$model->setParentId(null);
750+
}
751+
752+
$cut = self::reorderNodes($nodes, $fixed, $parentId, $cut);
753+
}
714754

715755
return $fixed;
716756
}
@@ -726,8 +766,12 @@ public function fixTree()
726766
protected static function reorderNodes(Collection $models, &$fixed,
727767
$parentId = null, $cut = 1
728768
) {
769+
if ( ! isset($models[$parentId])) {
770+
return $cut;
771+
}
772+
729773
/** @var Model|self $model */
730-
foreach ($models->get($parentId, [ ]) as $model) {
774+
foreach ($models[$parentId] as $model) {
731775
$model->setLft($cut);
732776

733777
$cut = self::reorderNodes($models, $fixed, $model->getKey(), $cut + 1);
@@ -743,6 +787,8 @@ protected static function reorderNodes(Collection $models, &$fixed,
743787
++$cut;
744788
}
745789

790+
unset($models[$parentId]);
791+
746792
return $cut;
747793
}
748794
}

tests/NodeTest.php

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,8 @@ public function dumpTree($items = null)
7878
{
7979
if ( ! $items) $items = Category::withTrashed()->defaultOrder()->get();
8080

81-
foreach ($items as $item)
82-
{
83-
echo PHP_EOL.($item->trashed() ? '-' : '+').' '.$item->name." ".$item->getLft()." ".$item->getRgt();
81+
foreach ($items as $item) {
82+
echo PHP_EOL.($item->trashed() ? '-' : '+').' '.$item->name." ".$item->getKey().' '.$item->getLft()." ".$item->getRgt().' '.$item->getParentId();
8483
}
8584
}
8685

@@ -386,8 +385,6 @@ public function testSoftDeletedNodeisDeletedWhenParentIsDeleted()
386385

387386
$this->assertTreeNotBroken();
388387

389-
$this->dumpTree();
390-
391388
$this->assertNull($this->findCategory('samsung', true));
392389
$this->assertNull($this->findCategory('sony'));
393390
}
@@ -566,7 +563,21 @@ public function testCountsTreeErrors()
566563
{
567564
$errors = Category::countErrors();
568565

569-
$this->assertEquals([ 'oddness' => 0, 'duplicates' => 0, 'wrong_parent' => 0 ], $errors);
566+
$this->assertEquals([ 'oddness' => 0,
567+
'duplicates' => 0,
568+
'wrong_parent' => 0,
569+
'missing_parent' => 0 ], $errors);
570+
571+
Category::where('id', '=', 5)->update([ '_lft' => 14 ]);
572+
Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]);
573+
Category::where('id', '=', 11)->update([ '_lft' => 20 ]);
574+
Category::where('id', '=', 4)->update([ 'parent_id' => 24 ]);
575+
576+
$errors = Category::countErrors();
577+
578+
$this->assertEquals(1, $errors['oddness']);
579+
$this->assertEquals(2, $errors['duplicates']);
580+
$this->assertEquals(1, $errors['missing_parent']);
570581
}
571582

572583
public function testCreatesNode()
@@ -649,6 +660,8 @@ public function testTreeIsFixed()
649660
{
650661
Category::where('id', '=', 5)->update([ '_lft' => 14 ]);
651662
Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]);
663+
Category::where('id', '=', 11)->update([ '_lft' => 20 ]);
664+
Category::where('id', '=', 2)->update([ 'parent_id' => 24 ]);
652665

653666
$fixed = Category::fixTree();
654667

tests/data/categories.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@
1111
array('id' => 8, 'name' => 'galaxy', '_lft' => 12, '_rgt' => 13, 'parent_id' => 7),
1212
array('id' => 9, 'name' => 'sony', '_lft' => 15, '_rgt' => 16, 'parent_id' => 5),
1313
array('id' => 10, 'name' => 'lenovo', '_lft' => 17, '_rgt' => 18, 'parent_id' => 5),
14-
array('id' => 11, 'name' => 'store 2', '_lft' => 21, '_rgt' => 22, 'parent_id' => null),
14+
array('id' => 11, 'name' => 'store_2', '_lft' => 21, '_rgt' => 22, 'parent_id' => null),
1515
);

0 commit comments

Comments
 (0)