Skip to content

Commit d2a95b3

Browse files
committed
#50: implemented tree rebuilding feature
1 parent 06237a6 commit d2a95b3

File tree

6 files changed

+224
-35
lines changed

6 files changed

+224
-35
lines changed

CHANGELOG.markdown

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
`has('parent')` instead
1717
* Default order is no longer applied for `siblings()`, `descendants()`,
1818
`prevNodes`, `nextNodes`
19+
* #50: implemented tree rebuilding feature
1920

2021
### 3.1.1
2122

README.markdown

+29-1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,34 @@ $node = Category::create([
184184

185185
`$node->children` now contains a list of created child nodes.
186186

187+
#### Rebuilding a tree from array
188+
189+
You can easily rebuild a tree. This is useful for mass-changing the structure of
190+
the tree.
191+
192+
```php
193+
Category::rebuildTree($data, $delete);
194+
```
195+
196+
`$data` is an array of nodes:
197+
198+
```php
199+
$data = [
200+
[ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ],
201+
[ 'name' => 'bar' ],
202+
];
203+
```
204+
205+
There is an id specified for node with the name of `foo` which means that existing
206+
node will be filled and saved. If node is not exists `ModelNotFoundException` is
207+
thrown. Also, this node has `children` specified which is also an array of nodes;
208+
they will be processed in the same manner and saved as children of node `foo`.
209+
210+
Node `bar` has no primary key specified, so it will be created.
211+
212+
`$delete` shows whether to delete nodes that are already exists but not present
213+
in `$data`. By default, nodes aren't deleted.
214+
187215
### Retrieving nodes
188216

189217
*In some cases we will use an `$id` variable which is an id of the target node.*
@@ -504,7 +532,7 @@ MenuItem::scoped([ 'menu_id' => 5 ])->fixTree();
504532
```
505533

506534
When requesting nodes using model instance, scopes applied automatically based
507-
on data of that model. See examples:
535+
on the attributes of that model. See examples:
508536

509537
```php
510538
$node = MenuItem::findOrFail($id);

src/Collection.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ public function linkNodes()
4646
*
4747
* If `$root` is provided, the tree will contain only descendants of that node.
4848
*
49-
* @param int|Model|null $root
49+
* @param mixed $root
5050
*
5151
* @return Collection
5252
*/
53-
public function toTree($root = null)
53+
public function toTree($root = false)
5454
{
5555
if ($this->isEmpty()) {
5656
return new static;
@@ -77,13 +77,13 @@ public function toTree($root = null)
7777
*
7878
* @return int
7979
*/
80-
protected function getRootNodeId($root = null)
80+
protected function getRootNodeId($root)
8181
{
8282
if (NestedSet::isNode($root)) {
8383
return $root->getKey();
8484
}
8585

86-
if ($root !== null) {
86+
if ($root !== false) {
8787
return $root;
8888
}
8989

src/NodeTrait.php

+26-4
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public static function bootNodeTrait()
9090
*
9191
* @return $this
9292
*/
93-
protected function setAction($action)
93+
protected function setNodeAction($action)
9494
{
9595
$this->pending = func_get_args();
9696

@@ -136,6 +136,14 @@ public static function usesSoftDelete()
136136
return $softDelete;
137137
}
138138

139+
/**
140+
* @return bool
141+
*/
142+
protected function actionRaw()
143+
{
144+
return true;
145+
}
146+
139147
/**
140148
* Make a root node.
141149
*/
@@ -342,7 +350,7 @@ public function ancestors()
342350
*/
343351
public function makeRoot()
344352
{
345-
return $this->setAction('root');
353+
return $this->setNodeAction('root');
346354
}
347355

348356
/**
@@ -420,7 +428,7 @@ public function appendOrPrependTo(self $parent, $prepend = false)
420428

421429
$this->setParent($parent)->dirtyBounds();
422430

423-
return $this->setAction('appendOrPrepend', $parent, $prepend);
431+
return $this->setNodeAction('appendOrPrepend', $parent, $prepend);
424432
}
425433

426434
/**
@@ -463,7 +471,7 @@ public function beforeOrAfterNode(self $node, $after = false)
463471

464472
$this->dirtyBounds();
465473

466-
return $this->setAction('beforeOrAfter', $node, $after);
474+
return $this->setNodeAction('beforeOrAfter', $node, $after);
467475
}
468476

469477
/**
@@ -495,6 +503,20 @@ public function insertBeforeNode(self $node)
495503
return true;
496504
}
497505

506+
/**
507+
* @param $lft
508+
* @param $rgt
509+
* @param $parentId
510+
*
511+
* @return $this
512+
*/
513+
public function rawNode($lft, $rgt, $parentId)
514+
{
515+
$this->setLft($lft)->setRgt($rgt)->setParentId($parentId);
516+
517+
return $this->setNodeAction('raw');
518+
}
519+
498520
/**
499521
* Move node up given amount of positions.
500522
*

src/QueryBuilder.php

+113-26
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Database\Eloquent\ModelNotFoundException;
88
use Illuminate\Database\Query\Builder as Query;
99
use Illuminate\Database\Query\Builder as BaseQueryBuilder;
10+
use Illuminate\Support\Arr;
1011
use LogicException;
1112
use Illuminate\Database\Query\Expression;
1213

@@ -726,9 +727,9 @@ public function isBroken()
726727
/**
727728
* Fixes the tree based on parentage info.
728729
*
729-
* Requires at least one root node. This will not update nodes with invalid parent.
730+
* Nodes with invalid parent are saved as roots.
730731
*
731-
* @return int The number of fixed nodes.
732+
* @return int The number of fixed nodes
732733
*/
733734
public function fixTree()
734735
{
@@ -739,54 +740,64 @@ public function fixTree()
739740
$this->model->getRgtName(),
740741
];
741742

742-
$nodes = $this->model
743-
->newNestedSetQuery()
744-
->defaultOrder()
745-
->get($columns)
746-
->groupBy($this->model->getParentIdName());
743+
$dictionary = $this->defaultOrder()
744+
->get($columns)
745+
->groupBy($this->model->getParentIdName())
746+
->all();
747747

748+
return self::fixNodes($dictionary);
749+
}
750+
751+
/**
752+
* @param array $dictionary
753+
*
754+
* @return int
755+
*/
756+
protected static function fixNodes(array &$dictionary)
757+
{
748758
$fixed = 0;
749759

750-
$cut = self::reorderNodes($nodes, $fixed);
760+
$cut = self::reorderNodes($dictionary, $fixed);
751761

752-
// Saved nodes that have invalid parent as roots
753-
while ( ! $nodes->isEmpty()) {
754-
$parentId = $nodes->keys()->first();
762+
// Save nodes that have invalid parent as roots
763+
while ( ! empty($dictionary)) {
764+
$dictionary[null] = reset($dictionary);
755765

756-
foreach ($nodes[$parentId] as $model) {
757-
$model->setParentId(null);
758-
}
766+
unset($dictionary[key($dictionary)]);
759767

760-
$cut = self::reorderNodes($nodes, $fixed, $parentId, $cut);
768+
$cut = self::reorderNodes($dictionary, $fixed, null, $cut);
761769
}
762770

763771
return $fixed;
764772
}
765773

766774
/**
767-
* @param Collection $models
775+
* @param array $dictionary
768776
* @param int $fixed
769777
* @param $parentId
770778
* @param int $cut
771779
*
772780
* @return int
773781
*/
774-
protected static function reorderNodes(Collection $models, &$fixed,
782+
protected static function reorderNodes(array &$dictionary, &$fixed,
775783
$parentId = null, $cut = 1
776784
) {
777-
if ( ! isset($models[$parentId])) {
785+
if ( ! isset($dictionary[$parentId])) {
778786
return $cut;
779787
}
780788

781-
/** @var Model|self $model */
782-
foreach ($models[$parentId] as $model) {
783-
$model->setLft($cut);
789+
/** @var Model|NodeTrait $model */
790+
foreach ($dictionary[$parentId] as $model) {
791+
$lft = $cut;
784792

785-
$cut = self::reorderNodes($models, $fixed, $model->getKey(), $cut + 1);
793+
$cut = self::reorderNodes($dictionary,
794+
$fixed,
795+
$model->getKey(),
796+
$cut + 1);
786797

787-
$model->setRgt($cut);
798+
$rgt = $cut;
788799

789-
if ($model->isDirty()) {
800+
if ($model->rawNode($lft, $rgt, $parentId)->isDirty()) {
790801
$model->save();
791802

792803
$fixed++;
@@ -795,13 +806,89 @@ protected static function reorderNodes(Collection $models, &$fixed,
795806
++$cut;
796807
}
797808

798-
unset($models[$parentId]);
809+
unset($dictionary[$parentId]);
799810

800811
return $cut;
801812
}
802813

803814
/**
804-
* @param null $table
815+
* Rebuild the tree based on raw data.
816+
*
817+
* If item data does not contain primary key, new node will be created.
818+
*
819+
* @param array $data
820+
* @param bool $delete Whether to delete nodes that exists but not in the data
821+
* array
822+
*
823+
* @return int
824+
*/
825+
public function rebuildTree(array $data, $delete = false)
826+
{
827+
$existing = $this->get()->getDictionary();
828+
$dictionary = [];
829+
830+
$this->buildRebuildDictionary($dictionary, $data, $existing);
831+
832+
if ( ! empty($existing)) {
833+
if ($delete) {
834+
$this->model
835+
->newScopedQuery()
836+
->whereIn($this->model->getKeyName(), array_keys($existing))
837+
->forceDelete();
838+
} else {
839+
foreach ($existing as $model) {
840+
$dictionary[$model->getParentId()][] = $model;
841+
}
842+
}
843+
}
844+
845+
return $this->fixNodes($dictionary);
846+
}
847+
848+
/**
849+
* @param array $dictionary
850+
* @param array $data
851+
* @param array $existing
852+
* @param mixed $parentId
853+
*/
854+
protected function buildRebuildDictionary(array &$dictionary,
855+
array $data,
856+
array &$existing,
857+
$parentId = null
858+
) {
859+
$keyName = $this->model->getKeyName();
860+
861+
foreach ($data as $itemData) {
862+
if ( ! isset($itemData[$keyName])) {
863+
$model = $this->model->newInstance();
864+
865+
// We will save it as raw node since tree will be fixed
866+
$model->rawNode(0, 0, $parentId);
867+
} else {
868+
if ( ! isset($existing[$key = $itemData[$keyName]])) {
869+
throw new ModelNotFoundException;
870+
}
871+
872+
$model = $existing[$key];
873+
874+
unset($existing[$key]);
875+
}
876+
877+
$model->fill($itemData)->save();
878+
879+
$dictionary[$parentId][] = $model;
880+
881+
if ( ! isset($itemData['children'])) continue;
882+
883+
$this->buildRebuildDictionary($dictionary,
884+
$itemData['children'],
885+
$existing,
886+
$model->getKey());
887+
}
888+
}
889+
890+
/**
891+
* @param string|null $table
805892
*
806893
* @return $this
807894
*/

0 commit comments

Comments
 (0)