From 55281f3c64773fc486a6d37ccc109846a84ab41e Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sun, 19 Nov 2023 16:26:27 +0100 Subject: [PATCH 1/2] Add phpdoc for generics in Collection (#171) Co-authored-by: R.Austin <110375847+raustin-m@users.noreply.github.com> --- src/Eloquent/Collection.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php index db46961..b7e8f73 100644 --- a/src/Eloquent/Collection.php +++ b/src/Eloquent/Collection.php @@ -4,6 +4,12 @@ use Illuminate\Database\Eloquent\Collection as Base; +/** + * @template TKey of array-key + * @template TModel of \Illuminate\Database\Eloquent\Model + * + * @extends \Illuminate\Database\Eloquent\Collection + */ class Collection extends Base { public function toTree($childrenRelation = 'children') From 48d0b841567249abfa7f5f845815508b4bb23af5 Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sat, 30 Dec 2023 16:40:26 +0100 Subject: [PATCH 2/2] Add toTree() method for graphs --- README.md | 43 ++++++++- src/Eloquent/Graph/Collection.php | 50 ++++++++++ src/Eloquent/Traits/HasGraphAdjacencyList.php | 12 +++ src/IdeHelper/RecursiveRelationsHook.php | 7 +- tests/Graph/CollectionTest.php | 96 +++++++++++++++++++ .../IdeHelper/RecursiveRelationsHookTest.php | 2 +- tests/Tree/CollectionTest.php | 2 +- 7 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 src/Eloquent/Graph/Collection.php create mode 100644 tests/Graph/CollectionTest.php diff --git a/README.md b/README.md index 03b6e83..955f9f6 100644 --- a/README.md +++ b/README.md @@ -325,7 +325,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(); @@ -662,6 +662,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) #### Getting Started @@ -985,6 +986,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 a3d5538..b1042fe 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 96dfb0a..a96e068 100644 --- a/src/IdeHelper/RecursiveRelationsHook.php +++ b/src/IdeHelper/RecursiveRelationsHook.php @@ -7,7 +7,8 @@ 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; @@ -148,7 +149,7 @@ protected function setTreeRelationProperties(ModelsCommand $command, Model $mode { foreach (static::$treeRelationMap as $relationDefinition) { $type = $relationDefinition['manyRelation'] - ? '\\' . Collection::class . '|' . class_basename($model) . '[]' + ? '\\' . TreeCollection::class . '|' . class_basename($model) . '[]' : class_basename($model); $command->setProperty( @@ -176,7 +177,7 @@ protected function setTreeRelationProperties(ModelsCommand $command, Model $mode protected function setGraphRelationProperties(ModelsCommand $command, Model $model): void { foreach (static::$graphRelationMap as $relationDefinition) { - $type = '\\' . EloquentCollection::class . '|' . class_basename($model) . '[]'; + $type = '\\' . GraphCollection::class . '|' . class_basename($model) . '[]'; $command->setProperty( $relationDefinition['name'], 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 b14d062..5b2e741 100644 --- a/tests/IdeHelper/RecursiveRelationsHookTest.php +++ b/tests/IdeHelper/RecursiveRelationsHookTest.php @@ -56,7 +56,7 @@ public function testGraphRelations() $command->shouldReceive('setProperty')->times(2); $command->shouldReceive('setProperty')->once()->with( 'ancestorsAndSelf', - '\Illuminate\Database\Eloquent\Collection|Node[]', + '\Staudenmeir\LaravelAdjacencyList\Eloquent\Graph\Collection|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();