diff --git a/README.md b/README.md
index 5290490..a6a5cd0 100644
--- a/README.md
+++ b/README.md
@@ -349,7 +349,7 @@ class User extends Model
#### Nested Results
-Use the `toTree()` method on the result collection to generate a nested tree:
+Use the `toTree()` method on a result collection to generate a nested tree:
```php
$users = User::tree()->get();
@@ -696,6 +696,7 @@ Supports Laravel 9+.
- [Depth](#graphs-depth)
- [Path](#graphs-path)
- [Custom Paths](#graphs-custom-paths)
+- [Nested Results](#graphs-nested-results)
- [Recursive Query Constraints](#graphs-recursive-query-constraints)
- [Known Issues](#graphs-known-issues)
@@ -1013,6 +1014,46 @@ class Node extends Model
}
```
+#### Nested Results
+
+Use the `toTree()` method on a result collection to generate a nested tree:
+
+```php
+$nodes = Node::find($id)->descendants;
+
+$tree = $nodes->toTree();
+```
+
+This recursively sets `children` relationships:
+
+```json
+[
+ {
+ "id": 1,
+ "children": [
+ {
+ "id": 2,
+ "children": [
+ {
+ "id": 3,
+ "children": []
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "children": [
+ {
+ "id": 5,
+ "children": []
+ }
+ ]
+ }
+ ]
+ }
+]
+```
+
#### Recursive Query Constraints
You can add custom constraints to the CTE's recursive query. Consider a query where you want to traverse a node's
diff --git a/src/Eloquent/Graph/Collection.php b/src/Eloquent/Graph/Collection.php
new file mode 100644
index 0000000..bcc6d0f
--- /dev/null
+++ b/src/Eloquent/Graph/Collection.php
@@ -0,0 +1,50 @@
+
+ */
+class Collection extends Base
+{
+ /**
+ * Generate a nested tree.
+ *
+ * @param string $childrenRelation
+ * @return static
+ */
+ public function toTree(string $childrenRelation = 'children'): static
+ {
+ if ($this->isEmpty()) {
+ return $this;
+ }
+
+ $parentKeyName = $this->first->relationLoaded('pivot')
+ ? 'pivot.' . $this->first()->getParentKeyName()
+ : 'pivot_' . $this->first()->getParentKeyName();
+ $localKeyName = $this->first()->getLocalKeyName();
+ $depthName = $this->first()->getDepthName();
+
+ $depths = $this->pluck($depthName);
+
+ $graph = new static(
+ $this->where($depthName, $depths->min())->values()
+ );
+
+ $itemsByParentKey = $this->groupBy($parentKeyName);
+
+ foreach ($this->items as $item) {
+ $item->setRelation(
+ $childrenRelation,
+ $itemsByParentKey[$item->$localKeyName] ?? new static()
+ );
+ }
+
+ return $graph;
+ }
+}
diff --git a/src/Eloquent/Traits/HasGraphAdjacencyList.php b/src/Eloquent/Traits/HasGraphAdjacencyList.php
index a83c45f..907483c 100644
--- a/src/Eloquent/Traits/HasGraphAdjacencyList.php
+++ b/src/Eloquent/Traits/HasGraphAdjacencyList.php
@@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Staudenmeir\LaravelAdjacencyList\Eloquent\Graph\Collection;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Graph\Ancestors;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Graph\Descendants;
@@ -421,6 +422,17 @@ public function newEloquentBuilder($query)
return new \Staudenmeir\LaravelAdjacencyList\Eloquent\Builder($query);
}
+ /**
+ * Create a new Eloquent Collection instance.
+ *
+ * @param array $models
+ * @return \Staudenmeir\LaravelAdjacencyList\Eloquent\Graph\Collection
+ */
+ public function newCollection(array $models = [])
+ {
+ return new Collection($models);
+ }
+
/**
* Set an additional constraint for the recursive query.
*
diff --git a/src/IdeHelper/RecursiveRelationsHook.php b/src/IdeHelper/RecursiveRelationsHook.php
index 9e465b2..38a5eb0 100644
--- a/src/IdeHelper/RecursiveRelationsHook.php
+++ b/src/IdeHelper/RecursiveRelationsHook.php
@@ -4,10 +4,10 @@
use Barryvdh\LaravelIdeHelper\Console\ModelsCommand;
use Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface;
-use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
-use Staudenmeir\LaravelAdjacencyList\Eloquent\Collection;
+use Staudenmeir\LaravelAdjacencyList\Eloquent\Collection as TreeCollection;
+use Staudenmeir\LaravelAdjacencyList\Eloquent\Graph\Collection as GraphCollection;
use Staudenmeir\LaravelAdjacencyList\Eloquent\HasGraphRelationships;
use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
@@ -135,7 +135,7 @@ public function run(ModelsCommand $command, Model $model): void
if (in_array(HasRecursiveRelationships::class, $traits)) {
foreach (static::$treeRelationships as $relationship) {
$type = $relationship['manyRelation']
- ? '\\' . Collection::class . '|\\' . $model::class . '[]'
+ ? '\\' . TreeCollection::class . '|\\' . $model::class . '[]'
: '\\' . $model::class;
$this->addRelationship($command, $relationship, $type);
@@ -144,7 +144,7 @@ public function run(ModelsCommand $command, Model $model): void
if (in_array(HasGraphRelationships::class, $traits)) {
foreach (static::$graphRelationships as $relationship) {
- $type = '\\' . EloquentCollection::class . '|\\' . $model::class . '[]';
+ $type = '\\' . GraphCollection::class . '|\\' . $model::class . '[]';
$this->addRelationship($command, $relationship, $type);
}
diff --git a/tests/Graph/CollectionTest.php b/tests/Graph/CollectionTest.php
new file mode 100644
index 0000000..6c15013
--- /dev/null
+++ b/tests/Graph/CollectionTest.php
@@ -0,0 +1,96 @@
+database === 'sqlsrv') {
+ $this->markTestSkipped();
+ }
+
+ $constraint = fn (Builder $query) => $query->whereIn('id', [2, 3]);
+
+ $nodes = Node::subgraph($constraint)->orderBy('id')->get();
+
+ $graph = $nodes->toTree();
+
+ $this->assertEquals([2, 3], $graph->pluck('id')->all());
+ $this->assertEquals([5], $graph[0]->children->pluck('id')->all());
+ $this->assertEquals([7, 8], $graph[0]->children[0]->children->pluck('id')->all());
+ $this->assertEquals([8], $graph[0]->children[0]->children[0]->children->pluck('id')->all());
+ $this->assertEquals([6], $graph[1]->children->pluck('id')->all());
+ }
+
+ public function testToTreeWithRelationship()
+ {
+ $nodes = Node::find(2)->descendants()->orderBy('id')->get();
+
+ $graph = $nodes->toTree();
+
+ $this->assertEquals([5], $graph->pluck('id')->all());
+ $this->assertEquals([7, 8], $graph[0]->children->pluck('id')->all());
+ $this->assertEquals([8], $graph[0]->children[0]->children->pluck('id')->all());
+ }
+
+ public function testToTreeWithCycle()
+ {
+ if ($this->database === 'sqlsrv') {
+ $this->markTestSkipped();
+ }
+
+ $this->seedCycle();
+
+ $constraint = fn (Builder $query) => $query->where('id', 12);
+
+ $nodes = NodeWithCycleDetection::subgraph($constraint)->orderBy('id')->get();
+
+ $graph = $nodes->toTree();
+
+ $this->assertEquals([12], $graph->pluck('id')->all());
+ $this->assertEquals([13], $graph[0]->children->pluck('id')->all());
+ $this->assertEquals([14], $graph[0]->children[0]->children->pluck('id')->all());
+ $this->assertEmpty($graph[0]->children[0]->children[0]->children);
+ }
+
+ public function testToTreeWithCycleAndStart()
+ {
+ if ($this->database === 'sqlsrv') {
+ $this->markTestSkipped();
+ }
+
+ $this->seedCycle();
+
+ $constraint = fn (Builder $query) => $query->where('id', 12);
+
+ $nodes = NodeWithCycleDetectionAndStart::subgraph($constraint)->orderBy('id')->get();
+
+ $graph = $nodes->toTree();
+
+ $this->assertEquals([12], $graph->pluck('id')->all());
+ $this->assertEquals([13], $graph[0]->children->pluck('id')->all());
+ $this->assertEquals([14], $graph[0]->children[0]->children->pluck('id')->all());
+ $this->assertEquals([12], $graph[0]->children[0]->children[0]->children->pluck('id')->all());
+ }
+
+ public function testToTreeWithEmptyCollection()
+ {
+ if ($this->database === 'sqlsrv') {
+ $this->markTestSkipped();
+ }
+
+ $constraint = fn (Builder $query) => $query->where('id', 1);
+
+ $nodes = Node::subgraph($constraint)->where('id', 0)->orderBy('id')->get();
+
+ $graph = $nodes->toTree();
+
+ $this->assertEmpty($graph);
+ }
+}
diff --git a/tests/IdeHelper/RecursiveRelationsHookTest.php b/tests/IdeHelper/RecursiveRelationsHookTest.php
index 9da4fd4..d63fac8 100644
--- a/tests/IdeHelper/RecursiveRelationsHookTest.php
+++ b/tests/IdeHelper/RecursiveRelationsHookTest.php
@@ -55,7 +55,7 @@ public function testGraphRelations()
$command->shouldReceive('setProperty')->times(2);
$command->shouldReceive('setProperty')->once()->with(
'ancestorsAndSelf',
- '\Illuminate\Database\Eloquent\Collection|\Staudenmeir\LaravelAdjacencyList\Tests\IdeHelper\Models\Node[]',
+ '\Staudenmeir\LaravelAdjacencyList\Eloquent\Graph\Collection|\Staudenmeir\LaravelAdjacencyList\Tests\IdeHelper\Models\Node[]',
true,
false,
"The node's recursive parents and itself.",
diff --git a/tests/Tree/CollectionTest.php b/tests/Tree/CollectionTest.php
index 3bbd679..31dc6c7 100644
--- a/tests/Tree/CollectionTest.php
+++ b/tests/Tree/CollectionTest.php
@@ -19,7 +19,7 @@ public function testToTree()
$this->assertEquals([12], $tree[1]->children->pluck('id')->all());
}
- public function testToTreeWithDescendants()
+ public function testToTreeWithRelationship()
{
$users = User::find(1)->descendants()->orderBy('id')->get();