Skip to content

Commit f46043a

Browse files
committed
Update
1 parent b470193 commit f46043a

File tree

4 files changed

+126
-32
lines changed

4 files changed

+126
-32
lines changed

CHANGELOG.markdown

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### 4.2.8
2+
* Support Laravel 5.5
3+
* Added `fixSubtree` and `rebuildSubtree` methods
4+
* Increased performance of tree rebuilding
5+
16
### 4.2.7
27

38
* #217: parent_id, lft and rgt are reset when replicating a node

README.markdown

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
This is a Laravel 4-5 package for working with trees in relational databases.
88

9-
* **Laravel 5.2, 5.3, 5.4** is supported since v4
9+
* **Laravel 5.2, 5.3, 5.4, 5.5** is supported since v4
1010
* **Laravel 5.1** is supported in v3
1111
* **Laravel 4** is supported in v2
1212

@@ -214,6 +214,16 @@ Node `bar` has no primary key specified, so it will be created.
214214
`$delete` shows whether to delete nodes that are already exists but not present
215215
in `$data`. By default, nodes aren't deleted.
216216

217+
##### Rebuilding a subtree
218+
219+
As of 4.2.8 you can rebuild a subtree:
220+
221+
```php
222+
Category::rebuildSubtree($root, $data);
223+
```
224+
225+
This constraints tree rebuilding to descendants of `$root` node.
226+
217227
### Retrieving nodes
218228

219229
*In some cases we will use an `$id` variable which is an id of the target node.*

src/QueryBuilder.php

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -841,9 +841,11 @@ public function isBroken()
841841
*
842842
* Nodes with invalid parent are saved as roots.
843843
*
844-
* @return int The number of fixed nodes
844+
* @param null|NodeTrait|Model $root
845+
*
846+
* @return int The number of changed nodes
845847
*/
846-
public function fixTree()
848+
public function fixTree($root = null)
847849
{
848850
$columns = [
849851
$this->model->getKeyName(),
@@ -852,48 +854,78 @@ public function fixTree()
852854
$this->model->getRgtName(),
853855
];
854856

855-
$dictionary = $this->model->newNestedSetQuery()
856-
->defaultOrder()
857-
->get($columns)
858-
->groupBy($this->model->getParentIdName())
859-
->all();
857+
$dictionary = $this->model
858+
->newNestedSetQuery()
859+
->when($root, function (self $query, $root) {
860+
$query->whereDescendantOf($root);
861+
})
862+
->defaultOrder()
863+
->get($columns)
864+
->groupBy($this->model->getParentIdName())
865+
->all();
866+
867+
return $this->fixNodes($dictionary, $root);
868+
}
860869

861-
return self::fixNodes($dictionary);
870+
/**
871+
* @param NodeTrait|Model $root
872+
*
873+
* @return int
874+
*/
875+
public function fixSubtree($root)
876+
{
877+
return $this->fixTree($root);
862878
}
863879

864880
/**
865881
* @param array $dictionary
882+
* @param NodeTrait|Model|null $parent
866883
*
867884
* @return int
868885
*/
869-
protected static function fixNodes(array &$dictionary)
886+
protected function fixNodes(array &$dictionary, $parent = null)
870887
{
871-
$fixed = 0;
888+
$parentId = $parent ? $parent->getKey() : null;
889+
$cut = $parent ? $parent->getLft() + 1 : 1;
890+
891+
$updated = [];
892+
$moved = 0;
872893

873-
$cut = self::reorderNodes($dictionary, $fixed);
894+
$cut = self::reorderNodes($dictionary, $updated, $parentId, $cut);
874895

875896
// Save nodes that have invalid parent as roots
876897
while ( ! empty($dictionary)) {
877898
$dictionary[null] = reset($dictionary);
878899

879900
unset($dictionary[key($dictionary)]);
880901

881-
$cut = self::reorderNodes($dictionary, $fixed, null, $cut);
902+
$cut = self::reorderNodes($dictionary, $updated, $parentId, $cut);
903+
}
904+
905+
if ($parent && ($grown = $cut - $parent->getRgt()) != 0) {
906+
$moved = $this->model->newScopedQuery()->makeGap($parent->getRgt() + 1, $grown);
907+
908+
$updated[] = $parent->rawNode($parent->getLft(), $cut, $parent->getParentId());
909+
}
910+
911+
foreach ($updated as $model) {
912+
$model->save();
882913
}
883914

884-
return $fixed;
915+
return count($updated) + $moved;
885916
}
886917

887918
/**
888919
* @param array $dictionary
889-
* @param int $fixed
920+
* @param array $updated
890921
* @param $parentId
891922
* @param int $cut
892923
*
893924
* @return int
925+
* @internal param int $fixed
894926
*/
895-
protected static function reorderNodes(array &$dictionary, &$fixed,
896-
$parentId = null, $cut = 1
927+
protected static function reorderNodes(
928+
array &$dictionary, array &$updated, $parentId = null, $cut = 1
897929
) {
898930
if ( ! isset($dictionary[$parentId])) {
899931
return $cut;
@@ -903,17 +935,10 @@ protected static function reorderNodes(array &$dictionary, &$fixed,
903935
foreach ($dictionary[$parentId] as $model) {
904936
$lft = $cut;
905937

906-
$cut = self::reorderNodes($dictionary,
907-
$fixed,
908-
$model->getKey(),
909-
$cut + 1);
938+
$cut = self::reorderNodes($dictionary, $updated, $model->getKey(), $cut + 1);
910939

911-
$rgt = $cut;
912-
913-
if ($model->rawNode($lft, $rgt, $parentId)->isDirty()) {
914-
$model->save();
915-
916-
$fixed++;
940+
if ($model->rawNode($lft, $cut, $parentId)->isDirty()) {
941+
$updated[] = $model;
917942
}
918943

919944
++$cut;
@@ -932,20 +957,29 @@ protected static function reorderNodes(array &$dictionary, &$fixed,
932957
* @param array $data
933958
* @param bool $delete Whether to delete nodes that exists but not in the data
934959
* array
960+
* @param null $root
935961
*
936962
* @return int
937963
*/
938-
public function rebuildTree(array $data, $delete = false)
964+
public function rebuildTree(array $data, $delete = false, $root = null)
939965
{
940966
if ($this->model->usesSoftDelete()) {
941967
$this->withTrashed();
942968
}
943969

944-
$existing = $this->get()->getDictionary();
970+
$existing = $this
971+
->when($root, function (self $query, $root) {
972+
$query->whereDescendantOf($root);
973+
})
974+
->get()
975+
->getDictionary();
976+
945977
$dictionary = [];
978+
$parentId = $root ? $root->getKey() : null;
946979

947-
$this->buildRebuildDictionary($dictionary, $data, $existing);
980+
$this->buildRebuildDictionary($dictionary, $data, $existing, $parentId);
948981

982+
/** @var Model|NodeTrait $model */
949983
if ( ! empty($existing)) {
950984
if ($delete && ! $this->model->usesSoftDelete()) {
951985
$this->model
@@ -967,7 +1001,19 @@ public function rebuildTree(array $data, $delete = false)
9671001
}
9681002
}
9691003

970-
return $this->fixNodes($dictionary);
1004+
return $this->fixNodes($dictionary, $root);
1005+
}
1006+
1007+
/**
1008+
* @param $root
1009+
* @param array $data
1010+
* @param bool $delete
1011+
*
1012+
* @return int
1013+
*/
1014+
public function rebuildSubtree($root, array $data, $delete = false)
1015+
{
1016+
return $this->rebuildTree($data, $delete, $root);
9711017
}
9721018

9731019
/**
@@ -984,10 +1030,12 @@ protected function buildRebuildDictionary(array &$dictionary,
9841030
$keyName = $this->model->getKeyName();
9851031

9861032
foreach ($data as $itemData) {
1033+
/** @var NodeTrait|Model $model */
1034+
9871035
if ( ! isset($itemData[$keyName])) {
9881036
$model = $this->model->newInstance($this->model->getAttributes());
9891037

990-
// We will save it as raw node since tree will be fixed
1038+
// Set some values that will be fixed later
9911039
$model->rawNode(0, 0, $parentId);
9921040
} else {
9931041
if ( ! isset($existing[$key = $itemData[$keyName]])) {
@@ -996,6 +1044,9 @@ protected function buildRebuildDictionary(array &$dictionary,
9961044

9971045
$model = $existing[$key];
9981046

1047+
// Disable any tree actions
1048+
$model->rawNode($model->getLft(), $model->getRgt(), $parentId);
1049+
9991050
unset($existing[$key]);
10001051
}
10011052

tests/NodeTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,16 @@ public function testTreeIsFixed()
698698
$this->assertEquals(null, $node->getParentId());
699699
}
700700

701+
public function testSubtreeIsFixed()
702+
{
703+
Category::where('id', '=', 8)->update([ '_lft' => 11 ]);
704+
705+
$fixed = Category::fixSubtree(Category::find(5));
706+
$this->assertEquals($fixed, 1);
707+
$this->assertTreeNotBroken();
708+
$this->assertEquals(Category::find(8)->getLft(), 12);
709+
}
710+
701711
public function testParentIdDirtiness()
702712
{
703713
$node = $this->findCategory('apple');
@@ -810,6 +820,24 @@ public function testRebuildTree()
810820
$this->assertEquals(3, $node->getParentId());
811821
}
812822

823+
public function testRebuildSubtree()
824+
{
825+
$fixed = Category::rebuildSubtree(Category::find(7), [
826+
[ 'name' => 'new node' ],
827+
[ 'id' => '8' ],
828+
]);
829+
830+
echo PHP_EOL.$fixed.PHP_EOL;
831+
832+
$this->assertTrue($fixed > 0);
833+
$this->assertTreeNotBroken();
834+
835+
$node = $this->findCategory('new node');
836+
837+
$this->assertNotNull($node);
838+
$this->assertEquals($node->getLft(), 12);
839+
}
840+
813841
public function testRebuildTreeWithDeletion()
814842
{
815843
Category::rebuildTree([ [ 'name' => 'all deleted' ] ], true);

0 commit comments

Comments
 (0)