From 69e2ba9dbca7d9edfa5e13a549e028801830ceb9 Mon Sep 17 00:00:00 2001 From: Tyler Date: Thu, 5 Sep 2024 14:34:03 -0500 Subject: [PATCH 01/20] #259: Added method to load in relations. --- src/Eloquent/Collection.php | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php index 06dc76d..156ebb0 100644 --- a/src/Eloquent/Collection.php +++ b/src/Eloquent/Collection.php @@ -49,4 +49,59 @@ public function toTree($childrenRelation = 'children') return $tree; } + + /** + * Load parent/anscestor relations already present in the tree. + * + * @return static + */ + public function loadTreePathRelations() + { + $instance = $this->first(); + + if (is_null($instance)) { + return $this; + } + + if (! method_exists($instance, 'getPathName') || ! method_exists($instance, 'getPathSeparator')) { + throw new RuntimeException(sprintf( + 'Model [%s] is not have recusive relations.', + $instance::class, + )); + } + + $keyName = $instance->getKeyName(); + $pathName = $instance->getPathName(); + $pathSeparator = $instance->getPathSeparator(); + + $lookup = $this->keyBy($keyName); + + $keys = $this + ->pluck($pathName) + ->flatMap(fn (string $path): array => explode($pathSeparator, $path)) + ->unique() + ->values(); + + $missing = $keys->diff($lookup->modelKeys()); + + if ($missing->isNotEmpty()) { + $lookup->merge($instance->newQuery()->findMany($missing)->keyBy($keyName)); + } + + foreach ($this->all() as $model) { + $path = array_reverse(explode($pathSeparator, $model->getAttribute($pathName))); + + $ancestorsAndSelf = array_reduce( + $path, + fn ($collection, $step) => $collection->push($lookup[$step] ?? null), + $instance->newCollection(), + ); + + $model->setRelation('parent', count($path) > 1 ? $lookup[$path[1]] : null); + $model->setRelation('ancestorsAndSelf', $ancestorsAndSelf); + $model->setRelation('ancestors', $ancestorsAndSelf->slice(0, -1)); + } + + return $this; + } } From f8b6ce02d1e94517f74b92340792f3eeefe111dc Mon Sep 17 00:00:00 2001 From: Tyler Date: Thu, 5 Sep 2024 14:37:42 -0500 Subject: [PATCH 02/20] #259: Fixed typo --- src/Eloquent/Collection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php index 156ebb0..41dbebe 100644 --- a/src/Eloquent/Collection.php +++ b/src/Eloquent/Collection.php @@ -65,7 +65,7 @@ public function loadTreePathRelations() if (! method_exists($instance, 'getPathName') || ! method_exists($instance, 'getPathSeparator')) { throw new RuntimeException(sprintf( - 'Model [%s] is not have recusive relations.', + 'Model [%s] does not have recusive relations.', $instance::class, )); } From 29c4de49284a23e6ec366ba8f509bb05f1f3641e Mon Sep 17 00:00:00 2001 From: Tyler Date: Thu, 5 Sep 2024 15:24:42 -0500 Subject: [PATCH 03/20] #259: Added missing include --- src/Eloquent/Collection.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php index 41dbebe..c31a359 100644 --- a/src/Eloquent/Collection.php +++ b/src/Eloquent/Collection.php @@ -3,6 +3,7 @@ namespace Staudenmeir\LaravelAdjacencyList\Eloquent; use Illuminate\Database\Eloquent\Collection as Base; +use RuntimeException; /** * @template TKey of array-key From d1090c5e07f79361cc4c4e74192a36e1ed71aa54 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 6 Sep 2024 08:23:37 -0500 Subject: [PATCH 04/20] #259: Added attributes to be used with test --- tests/Tree/Models/User.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Tree/Models/User.php b/tests/Tree/Models/User.php index 96a9f13..2df212b 100644 --- a/tests/Tree/Models/User.php +++ b/tests/Tree/Models/User.php @@ -214,4 +214,31 @@ public function videosAndSelf(): MorphToManyOfDescendants { return $this->morphedByManyOfDescendantsAndSelf(Video::class, 'authorable'); } + + /** + * @return Attribute + */ + protected function displayPath(): Attribute + { + return Attribute::get( + fn (): string => (string) $this->ancestorsAndSelf + ->reverse() + ->reduce(function ($carry, $item) { + return $carry ? "{$carry} > {$item->name}" : $item->name; + }), + ); + } + + /** + * @return Attribute + */ + protected function displayPathReverse(): Attribute + { + return Attribute::get( + fn (): string => (string) $this->ancestorsAndSelf + ->reduce(function ($carry, $item) { + return $carry ? "{$carry} < {$item->name}" : $item->name; + }), + ); + } } From 75dac506bafc579b35efff6aeb3350a5b986a377 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 6 Sep 2024 08:35:10 -0500 Subject: [PATCH 05/20] #259: Updated to match schema --- tests/Tree/Models/User.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Tree/Models/User.php b/tests/Tree/Models/User.php index 2df212b..6b45d19 100644 --- a/tests/Tree/Models/User.php +++ b/tests/Tree/Models/User.php @@ -216,28 +216,28 @@ public function videosAndSelf(): MorphToManyOfDescendants } /** - * @return Attribute + * @return Attribute */ - protected function displayPath(): Attribute + protected function slugPath(): Attribute { return Attribute::get( fn (): string => (string) $this->ancestorsAndSelf ->reverse() - ->reduce(function ($carry, $item) { - return $carry ? "{$carry} > {$item->name}" : $item->name; + ->reduce(function ($carry, $user) { + return $carry ? "{$carry} > {$user->slug}" : $user->slug; }), ); } /** - * @return Attribute + * @return Attribute */ - protected function displayPathReverse(): Attribute + protected function reverseSlugPath(): Attribute { return Attribute::get( fn (): string => (string) $this->ancestorsAndSelf - ->reduce(function ($carry, $item) { - return $carry ? "{$carry} < {$item->name}" : $item->name; + ->reduce(function ($carry, $user) { + return $carry ? "{$carry} < {$user->slug}" : $user->slug; }), ); } From 5c0ba5c5719e2fac7995e3694dda7cb864691185 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 6 Sep 2024 08:39:07 -0500 Subject: [PATCH 06/20] #259: Added initial test --- tests/Tree/CollectionTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/Tree/CollectionTest.php b/tests/Tree/CollectionTest.php index 31dc6c7..e50e5f0 100644 --- a/tests/Tree/CollectionTest.php +++ b/tests/Tree/CollectionTest.php @@ -38,4 +38,33 @@ public function testToTreeWithEmptyCollection() $this->assertEmpty($tree); } + + public function testLoadTreePathRelations() + { + $limit = User::count(); + + $loaded = 0; + + User::retrieved(function () use (&$count) { + $count++; + }); + + $tree = User::query() + ->tree() + ->get() + ->loadTreePathRelations() + ->each(fn ($s) => $s->setAppends(['slug_path', 'reverse_slug_path'])) + ->toTree(); + + $this->assertLessThanOrEqual($limit, $loaded); + + $this->assertEquals('user-1', $tree[0]->slug_path); + $this->assertEquals('user-11', $tree[1]->slug_path); + $this->assertEquals('user-1 > user-2', $tree[0]->children[0]->slug_path); + $this->assertEquals('user-1 > user-3', $tree[0]->children[1]->slug_path); + $this->assertEquals('user-1 > user-4', $tree[0]->children[2]->slug_path); + $this->assertEquals('user-1 > user-2 > user-5', $tree[0]->children[0]->children[0]->slug_path); + $this->assertEquals('user-1 > user-2 > user-5 > user-8', $tree[0]->children[0]->children[0]->children[0]->slug_path); + $this->assertEquals('user-11 > user-12', $tree[1]->children[0]->slug_path); + } } From 5ab4447158c9e83618c8f44f0aabf829c3ca8627 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 6 Sep 2024 09:07:28 -0500 Subject: [PATCH 07/20] #259: Updated attribute names --- tests/Tree/Models/User.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Tree/Models/User.php b/tests/Tree/Models/User.php index 6b45d19..5e6a888 100644 --- a/tests/Tree/Models/User.php +++ b/tests/Tree/Models/User.php @@ -218,7 +218,7 @@ public function videosAndSelf(): MorphToManyOfDescendants /** * @return Attribute */ - protected function slugPath(): Attribute + protected function displayPath(): Attribute { return Attribute::get( fn (): string => (string) $this->ancestorsAndSelf @@ -232,7 +232,7 @@ protected function slugPath(): Attribute /** * @return Attribute */ - protected function reverseSlugPath(): Attribute + protected function reverseDisplayPath(): Attribute { return Attribute::get( fn (): string => (string) $this->ancestorsAndSelf From f3d30bb7b71b0f9cd3e4cdbd848555c0c4c69807 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 6 Sep 2024 09:08:16 -0500 Subject: [PATCH 08/20] #259: Updated to use display path --- tests/Tree/CollectionTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/Tree/CollectionTest.php b/tests/Tree/CollectionTest.php index e50e5f0..ec4610d 100644 --- a/tests/Tree/CollectionTest.php +++ b/tests/Tree/CollectionTest.php @@ -53,18 +53,18 @@ public function testLoadTreePathRelations() ->tree() ->get() ->loadTreePathRelations() - ->each(fn ($s) => $s->setAppends(['slug_path', 'reverse_slug_path'])) + ->each(fn ($s) => $s->setAppends(['display_path', 'reverse_display_path'])) ->toTree(); $this->assertLessThanOrEqual($limit, $loaded); - $this->assertEquals('user-1', $tree[0]->slug_path); - $this->assertEquals('user-11', $tree[1]->slug_path); - $this->assertEquals('user-1 > user-2', $tree[0]->children[0]->slug_path); - $this->assertEquals('user-1 > user-3', $tree[0]->children[1]->slug_path); - $this->assertEquals('user-1 > user-4', $tree[0]->children[2]->slug_path); - $this->assertEquals('user-1 > user-2 > user-5', $tree[0]->children[0]->children[0]->slug_path); - $this->assertEquals('user-1 > user-2 > user-5 > user-8', $tree[0]->children[0]->children[0]->children[0]->slug_path); - $this->assertEquals('user-11 > user-12', $tree[1]->children[0]->slug_path); + $this->assertEquals('user-1', $tree[0]->display_path); + $this->assertEquals('user-11', $tree[1]->display_path); + $this->assertEquals('user-1 > user-2', $tree[0]->children[0]->display_path); + $this->assertEquals('user-1 > user-3', $tree[0]->children[1]->display_path); + $this->assertEquals('user-1 > user-4', $tree[0]->children[2]->display_path); + $this->assertEquals('user-1 > user-2 > user-5', $tree[0]->children[0]->children[0]->display_path); + $this->assertEquals('user-1 > user-2 > user-5 > user-8', $tree[0]->children[0]->children[0]->children[0]->display_path); + $this->assertEquals('user-11 > user-12', $tree[1]->children[0]->display_path); } } From 386c15661de15c4f31a7a39c2802a665ceaa4a72 Mon Sep 17 00:00:00 2001 From: Tyler Reed Date: Fri, 6 Sep 2024 09:20:29 -0500 Subject: [PATCH 09/20] #259: Added missing include --- tests/Tree/Models/User.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Tree/Models/User.php b/tests/Tree/Models/User.php index 5e6a888..a5e009d 100644 --- a/tests/Tree/Models/User.php +++ b/tests/Tree/Models/User.php @@ -2,6 +2,7 @@ namespace Staudenmeir\LaravelAdjacencyList\Tests\Tree\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Query\Expression; From 33d19dbe98d31e46865f3e23f1713284a8cd91aa Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 6 Sep 2024 09:25:33 -0500 Subject: [PATCH 10/20] #259: Sort results to ensure consistency across database platforms --- tests/Tree/CollectionTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Tree/CollectionTest.php b/tests/Tree/CollectionTest.php index ec4610d..a28b094 100644 --- a/tests/Tree/CollectionTest.php +++ b/tests/Tree/CollectionTest.php @@ -60,11 +60,11 @@ public function testLoadTreePathRelations() $this->assertEquals('user-1', $tree[0]->display_path); $this->assertEquals('user-11', $tree[1]->display_path); - $this->assertEquals('user-1 > user-2', $tree[0]->children[0]->display_path); - $this->assertEquals('user-1 > user-3', $tree[0]->children[1]->display_path); - $this->assertEquals('user-1 > user-4', $tree[0]->children[2]->display_path); - $this->assertEquals('user-1 > user-2 > user-5', $tree[0]->children[0]->children[0]->display_path); - $this->assertEquals('user-1 > user-2 > user-5 > user-8', $tree[0]->children[0]->children[0]->children[0]->display_path); + $this->assertEquals('user-1 > user-2', $tree[0]->children->sortBy('id')[0]->display_path); + $this->assertEquals('user-1 > user-3', $tree[0]->children->sortBy('id')[1]->display_path); + $this->assertEquals('user-1 > user-4', $tree[0]->children->sortBy('id')[2]->display_path); + $this->assertEquals('user-1 > user-2 > user-5', $tree[0]->children->sortBy('id')[0]->children[0]->display_path); + $this->assertEquals('user-1 > user-2 > user-5 > user-8', $tree[0]->children->sortBy('id')[0]->children[0]->children[0]->display_path); $this->assertEquals('user-11 > user-12', $tree[1]->children[0]->display_path); } } From 3a878037d35739df935660f4696ffdf9117ed420 Mon Sep 17 00:00:00 2001 From: Tyler Reed Date: Fri, 6 Sep 2024 09:39:14 -0500 Subject: [PATCH 11/20] #259: More sorting to ensure consistency across database platforms --- tests/Tree/CollectionTest.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/Tree/CollectionTest.php b/tests/Tree/CollectionTest.php index a28b094..c387ed8 100644 --- a/tests/Tree/CollectionTest.php +++ b/tests/Tree/CollectionTest.php @@ -54,17 +54,22 @@ public function testLoadTreePathRelations() ->get() ->loadTreePathRelations() ->each(fn ($s) => $s->setAppends(['display_path', 'reverse_display_path'])) - ->toTree(); + ->toTree() + ->sortBy('id') + ->values(); $this->assertLessThanOrEqual($limit, $loaded); $this->assertEquals('user-1', $tree[0]->display_path); - $this->assertEquals('user-11', $tree[1]->display_path); - $this->assertEquals('user-1 > user-2', $tree[0]->children->sortBy('id')[0]->display_path); - $this->assertEquals('user-1 > user-3', $tree[0]->children->sortBy('id')[1]->display_path); - $this->assertEquals('user-1 > user-4', $tree[0]->children->sortBy('id')[2]->display_path); - $this->assertEquals('user-1 > user-2 > user-5', $tree[0]->children->sortBy('id')[0]->children[0]->display_path); - $this->assertEquals('user-1 > user-2 > user-5 > user-8', $tree[0]->children->sortBy('id')[0]->children[0]->children[0]->display_path); + $this->assertEquals('user-11', $tree[1]->display_path); + + $children = $tree[0]->children->sortBy('id')->values(); + $this->assertEquals('user-1 > user-2', $children[0]->display_path); + $this->assertEquals('user-1 > user-3', $children[1]->display_path); + $this->assertEquals('user-1 > user-4', $children[2]->display_path); + + $this->assertEquals('user-1 > user-2 > user-5', $children[0]->children[0]->display_path); + $this->assertEquals('user-1 > user-2 > user-5 > user-8', $children[0]->children[0]->children[0]->display_path); $this->assertEquals('user-11 > user-12', $tree[1]->children[0]->display_path); } } From ea6e47d654dbe2e9b1c0ecff8c42b17696abc492 Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sun, 6 Oct 2024 13:30:07 +0200 Subject: [PATCH 12/20] Update src/Eloquent/Collection.php Co-authored-by: Sander Muller --- src/Eloquent/Collection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php index bf3d524..0208b8a 100644 --- a/src/Eloquent/Collection.php +++ b/src/Eloquent/Collection.php @@ -57,7 +57,7 @@ public function toTree($childrenRelation = 'children') * * @return static */ - public function loadTreePathRelations() + public function loadTreePathRelations(): self { $instance = $this->first(); From 6ffb174ef71c8465f8efffad0b58636e4865934a Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sun, 6 Oct 2024 13:30:14 +0200 Subject: [PATCH 13/20] Update src/Eloquent/Collection.php Co-authored-by: Sander Muller --- src/Eloquent/Collection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php index 0208b8a..f3d54a5 100644 --- a/src/Eloquent/Collection.php +++ b/src/Eloquent/Collection.php @@ -67,7 +67,7 @@ public function loadTreePathRelations(): self if (! method_exists($instance, 'getPathName') || ! method_exists($instance, 'getPathSeparator')) { throw new RuntimeException(sprintf( - 'Model [%s] does not have recusive relations.', + 'Model [%s] does not have recursive relations.', $instance::class, )); } From ce2e0610ddb2945837a81886791e61d5cef8c682 Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sun, 6 Oct 2024 13:30:31 +0200 Subject: [PATCH 14/20] Update tests/Tree/Models/User.php Co-authored-by: Sander Muller --- tests/Tree/Models/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tree/Models/User.php b/tests/Tree/Models/User.php index a5e009d..d6333ed 100644 --- a/tests/Tree/Models/User.php +++ b/tests/Tree/Models/User.php @@ -224,7 +224,7 @@ protected function displayPath(): Attribute return Attribute::get( fn (): string => (string) $this->ancestorsAndSelf ->reverse() - ->reduce(function ($carry, $user) { + ->reduce(function (?string $carry, self $user): ?string { return $carry ? "{$carry} > {$user->slug}" : $user->slug; }), ); From 460509682ee600d90a50adbae51dac37a3b1a9dd Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sun, 6 Oct 2024 13:30:40 +0200 Subject: [PATCH 15/20] Update tests/Tree/Models/User.php Co-authored-by: Sander Muller --- tests/Tree/Models/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tree/Models/User.php b/tests/Tree/Models/User.php index d6333ed..a2b17c3 100644 --- a/tests/Tree/Models/User.php +++ b/tests/Tree/Models/User.php @@ -237,7 +237,7 @@ protected function reverseDisplayPath(): Attribute { return Attribute::get( fn (): string => (string) $this->ancestorsAndSelf - ->reduce(function ($carry, $user) { + ->reduce(function (?string $carry, self $user): ?string { return $carry ? "{$carry} < {$user->slug}" : $user->slug; }), ); From 896782fc6c1673b8e75996d74d698c03bfe85e90 Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sun, 6 Oct 2024 13:31:06 +0200 Subject: [PATCH 16/20] Update src/Eloquent/Collection.php Co-authored-by: Sander Muller --- src/Eloquent/Collection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php index f3d54a5..952f731 100644 --- a/src/Eloquent/Collection.php +++ b/src/Eloquent/Collection.php @@ -101,7 +101,7 @@ public function loadTreePathRelations(): self $model->setRelation('parent', count($path) > 1 ? $lookup[$path[1]] : null); $model->setRelation('ancestorsAndSelf', $ancestorsAndSelf); - $model->setRelation('ancestors', $ancestorsAndSelf->slice(0, -1)); + $model->setRelation('ancestors', $ancestorsAndSelf->slice(1)); } return $this; From ffad79c7ffc952491644ca48e849dc42e739ed2c Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sun, 6 Oct 2024 13:33:36 +0200 Subject: [PATCH 17/20] Update tests/Tree/CollectionTest.php Co-authored-by: Sander Muller --- tests/Tree/CollectionTest.php | 48 ++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/tests/Tree/CollectionTest.php b/tests/Tree/CollectionTest.php index c387ed8..26b05f6 100644 --- a/tests/Tree/CollectionTest.php +++ b/tests/Tree/CollectionTest.php @@ -41,35 +41,49 @@ public function testToTreeWithEmptyCollection() public function testLoadTreePathRelations() { - $limit = User::count(); + $totalUsers = User::count(); $loaded = 0; - User::retrieved(function () use (&$count) { - $count++; + User::retrieved(function () use (&$loaded) { + $loaded++; }); - + $tree = User::query() ->tree() ->get() ->loadTreePathRelations() - ->each(fn ($s) => $s->setAppends(['display_path', 'reverse_display_path'])) - ->toTree() - ->sortBy('id') + ->each(fn (User $treeUser) => $treeUser->setAppends(['display_path', 'reverse_display_path'])) + ->toTree() + ->sortBy('id') ->values(); - $this->assertLessThanOrEqual($limit, $loaded); + self::assertSame($totalUsers, $loaded); + + $tree->each(function (User $treeUser): void { + self::assertSame($treeUser->ancestorsAndSelf->count(), $treeUser->ancestors->count() + 1); + + $treeUser->children->each(function (User $childUser): void { + $childUser->ancestors->each(function (User $ancestor): void { + self::assertGreaterThanOrEqual(1, $ancestor->children->count()); + }); + + self::assertSame($childUser->ancestorsAndSelf->count(), $childUser->ancestors->count() + 1); + }); + }); + + self::assertSame($totalUsers, $loaded); - $this->assertEquals('user-1', $tree[0]->display_path); - $this->assertEquals('user-11', $tree[1]->display_path); + self::assertSame('user-1', $tree[0]->display_path); + self::assertSame('user-11', $tree[1]->display_path); - $children = $tree[0]->children->sortBy('id')->values(); - $this->assertEquals('user-1 > user-2', $children[0]->display_path); - $this->assertEquals('user-1 > user-3', $children[1]->display_path); - $this->assertEquals('user-1 > user-4', $children[2]->display_path); + $children = $tree[0]->children->sortBy('id')->values(); + self::assertSame('user-1 > user-2', $children[0]->display_path); + self::assertSame('user-1 > user-3', $children[1]->display_path); + self::assertSame('user-1 > user-4', $children[2]->display_path); - $this->assertEquals('user-1 > user-2 > user-5', $children[0]->children[0]->display_path); - $this->assertEquals('user-1 > user-2 > user-5 > user-8', $children[0]->children[0]->children[0]->display_path); - $this->assertEquals('user-11 > user-12', $tree[1]->children[0]->display_path); + self::assertSame('user-1 > user-2 > user-5', $children[0]->children[0]->display_path); + self::assertSame('user-1 > user-2 > user-5 > user-8', $children[0]->children[0]->children[0]->display_path); + self::assertSame('user-11 > user-12', $tree[1]->children[0]->display_path); } } From d310743c221feb38f4e437ec479b8d9f8d1f78b0 Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sun, 6 Oct 2024 16:02:26 +0200 Subject: [PATCH 18/20] Fix loading of missing models --- src/Eloquent/Collection.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php index 952f731..303d4ea 100644 --- a/src/Eloquent/Collection.php +++ b/src/Eloquent/Collection.php @@ -87,7 +87,9 @@ public function loadTreePathRelations(): self $missing = $keys->diff($lookup->modelKeys()); if ($missing->isNotEmpty()) { - $lookup->merge($instance->newQuery()->findMany($missing)->keyBy($keyName)); + $lookup = $lookup->union( + $instance->newQuery()->findMany($missing)->keyBy($keyName) + ); } foreach ($this->all() as $model) { From 531b6e82b9752da9b3eb09beb3c069555a1ecfbd Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sun, 6 Oct 2024 16:03:20 +0200 Subject: [PATCH 19/20] Refactoring --- src/Eloquent/Collection.php | 61 ++++++++++++++++--------------- tests/Tree/CollectionTest.php | 68 +++++++++++++++++------------------ tests/Tree/Models/User.php | 28 --------------- 3 files changed, 64 insertions(+), 93 deletions(-) diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php index 303d4ea..f91ca8d 100644 --- a/src/Eloquent/Collection.php +++ b/src/Eloquent/Collection.php @@ -3,7 +3,6 @@ namespace Staudenmeir\LaravelAdjacencyList\Eloquent; use Illuminate\Database\Eloquent\Collection as Base; -use RuntimeException; /** * @template TKey of array-key @@ -53,57 +52,57 @@ public function toTree($childrenRelation = 'children') } /** - * Load parent/anscestor relations already present in the tree. + * Load parent/ancestor relations already present in the tree. * - * @return static + * @return static */ - public function loadTreePathRelations(): self + public function loadTreePathRelations(): static { - $instance = $this->first(); - - if (is_null($instance)) { + if ($this->isEmpty()) { return $this; } - if (! method_exists($instance, 'getPathName') || ! method_exists($instance, 'getPathSeparator')) { - throw new RuntimeException(sprintf( - 'Model [%s] does not have recursive relations.', - $instance::class, - )); - } + /** @var TModel $instance */ + $instance = $this->first(); - $keyName = $instance->getKeyName(); - $pathName = $instance->getPathName(); + $keyName = $instance->getKeyName(); + $pathName = $instance->getPathName(); $pathSeparator = $instance->getPathSeparator(); + /** @var static $lookup */ $lookup = $this->keyBy($keyName); - $keys = $this - ->pluck($pathName) - ->flatMap(fn (string $path): array => explode($pathSeparator, $path)) - ->unique() + /** @var \Illuminate\Support\Collection $paths */ + $paths = $this->pluck($pathName); + + $keys = $paths + ->flatMap( + fn (string $path): array => explode($pathSeparator, $path) + )->unique() ->values(); - $missing = $keys->diff($lookup->modelKeys()); + $missing = $keys->diff( + $lookup->modelKeys() + ); - if ($missing->isNotEmpty()) { - $lookup = $lookup->union( - $instance->newQuery()->findMany($missing)->keyBy($keyName) - ); - } + $lookup = $lookup->union( + $instance->newQuery()->findMany($missing)->keyBy($keyName) + ); - foreach ($this->all() as $model) { - $path = array_reverse(explode($pathSeparator, $model->getAttribute($pathName))); + foreach ($this as $model) { + $pathSegments = array_reverse( + explode($pathSeparator, $model->$pathName) + ); $ancestorsAndSelf = array_reduce( - $path, - fn ($collection, $step) => $collection->push($lookup[$step] ?? null), + $pathSegments, + fn ($collection, string $key) => $collection->push($lookup[$key]), $instance->newCollection(), ); - $model->setRelation('parent', count($path) > 1 ? $lookup[$path[1]] : null); - $model->setRelation('ancestorsAndSelf', $ancestorsAndSelf); $model->setRelation('ancestors', $ancestorsAndSelf->slice(1)); + $model->setRelation('ancestorsAndSelf', $ancestorsAndSelf); + $model->setRelation('parent', count($pathSegments) > 1 ? $lookup[$pathSegments[1]] : null); } return $this; diff --git a/tests/Tree/CollectionTest.php b/tests/Tree/CollectionTest.php index 26b05f6..d858d81 100644 --- a/tests/Tree/CollectionTest.php +++ b/tests/Tree/CollectionTest.php @@ -2,6 +2,7 @@ namespace Staudenmeir\LaravelAdjacencyList\Tests\Tree; +use Illuminate\Support\Facades\DB; use Staudenmeir\LaravelAdjacencyList\Tests\Tree\Models\User; class CollectionTest extends TestCase @@ -39,51 +40,50 @@ public function testToTreeWithEmptyCollection() $this->assertEmpty($tree); } - public function testLoadTreePathRelations() + public function testLoadTreePathRelations(): void { - $totalUsers = User::count(); + DB::enableQueryLog(); - $loaded = 0; + $tree = User::tree()->get()->loadTreePathRelations(); - User::retrieved(function () use (&$loaded) { - $loaded++; - }); + $this->assertCount(1, DB::getQueryLog()); - $tree = User::query() - ->tree() - ->get() - ->loadTreePathRelations() - ->each(fn (User $treeUser) => $treeUser->setAppends(['display_path', 'reverse_display_path'])) - ->toTree() - ->sortBy('id') - ->values(); + foreach ($tree as $user) { + $this->assertTrue($user->relationLoaded('ancestors')); + $this->assertTrue($user->relationLoaded('ancestorsAndSelf')); + $this->assertTrue($user->relationLoaded('parent')); - self::assertSame($totalUsers, $loaded); + $this->assertEquals($user->ancestors()->pluck('id')->all(), $user->ancestors->pluck('id')->all()); + $this->assertEquals($user->ancestorsAndSelf()->pluck('id')->all(), $user->ancestorsAndSelf->pluck('id')->all()); + $this->assertEquals($user->parent()->first()?->id, $user->parent?->id); + } + } - $tree->each(function (User $treeUser): void { - self::assertSame($treeUser->ancestorsAndSelf->count(), $treeUser->ancestors->count() + 1); + public function testLoadTreePathRelationsWithMissingModels(): void + { + DB::enableQueryLog(); - $treeUser->children->each(function (User $childUser): void { - $childUser->ancestors->each(function (User $ancestor): void { - self::assertGreaterThanOrEqual(1, $ancestor->children->count()); - }); + $tree = User::tree()->where('id', '>', 5)->get()->loadTreePathRelations(); - self::assertSame($childUser->ancestorsAndSelf->count(), $childUser->ancestors->count() + 1); - }); - }); + $this->assertCount(2, DB::getQueryLog()); - self::assertSame($totalUsers, $loaded); + foreach ($tree as $user) { + $this->assertTrue($user->relationLoaded('ancestors')); + $this->assertTrue($user->relationLoaded('ancestorsAndSelf')); + $this->assertTrue($user->relationLoaded('parent')); - self::assertSame('user-1', $tree[0]->display_path); - self::assertSame('user-11', $tree[1]->display_path); + $this->assertEquals($user->ancestors()->pluck('id')->all(), $user->ancestors->pluck('id')->all()); + $this->assertEquals($user->ancestorsAndSelf()->pluck('id')->all(), $user->ancestorsAndSelf->pluck('id')->all()); + $this->assertEquals($user->parent()->first()?->id, $user->parent?->id); + } + } - $children = $tree[0]->children->sortBy('id')->values(); - self::assertSame('user-1 > user-2', $children[0]->display_path); - self::assertSame('user-1 > user-3', $children[1]->display_path); - self::assertSame('user-1 > user-4', $children[2]->display_path); + public function testLoadTreePathRelationsWithEmptyCollection(): void + { + $users = User::tree(1)->where('id', 0)->get(); + + $tree = $users->toTree()->loadTreePathRelations(); - self::assertSame('user-1 > user-2 > user-5', $children[0]->children[0]->display_path); - self::assertSame('user-1 > user-2 > user-5 > user-8', $children[0]->children[0]->children[0]->display_path); - self::assertSame('user-11 > user-12', $tree[1]->children[0]->display_path); + $this->assertEmpty($tree); } } diff --git a/tests/Tree/Models/User.php b/tests/Tree/Models/User.php index f690c1c..9e8d201 100644 --- a/tests/Tree/Models/User.php +++ b/tests/Tree/Models/User.php @@ -2,7 +2,6 @@ namespace Staudenmeir\LaravelAdjacencyList\Tests\Tree\Models; -use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Query\Expression; @@ -155,31 +154,4 @@ public function videosAndSelf(): MorphToManyOfDescendants { return $this->morphedByManyOfDescendantsAndSelf(Video::class, 'authorable'); } - - /** - * @return Attribute - */ - protected function displayPath(): Attribute - { - return Attribute::get( - fn (): string => (string) $this->ancestorsAndSelf - ->reverse() - ->reduce(function (?string $carry, self $user): ?string { - return $carry ? "{$carry} > {$user->slug}" : $user->slug; - }), - ); - } - - /** - * @return Attribute - */ - protected function reverseDisplayPath(): Attribute - { - return Attribute::get( - fn (): string => (string) $this->ancestorsAndSelf - ->reduce(function (?string $carry, self $user): ?string { - return $carry ? "{$carry} < {$user->slug}" : $user->slug; - }), - ); - } } From 5f98c3406067b015c2eb5b1942ff4e195d63cb9c Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Sun, 6 Oct 2024 16:38:31 +0200 Subject: [PATCH 20/20] Refactoring --- tests/Tree/CollectionTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Tree/CollectionTest.php b/tests/Tree/CollectionTest.php index d858d81..dd19c86 100644 --- a/tests/Tree/CollectionTest.php +++ b/tests/Tree/CollectionTest.php @@ -53,8 +53,8 @@ public function testLoadTreePathRelations(): void $this->assertTrue($user->relationLoaded('ancestorsAndSelf')); $this->assertTrue($user->relationLoaded('parent')); - $this->assertEquals($user->ancestors()->pluck('id')->all(), $user->ancestors->pluck('id')->all()); - $this->assertEquals($user->ancestorsAndSelf()->pluck('id')->all(), $user->ancestorsAndSelf->pluck('id')->all()); + $this->assertEquals($user->ancestors()->orderByDesc('depth')->pluck('id')->all(), $user->ancestors->pluck('id')->all()); + $this->assertEquals($user->ancestorsAndSelf()->orderByDesc('depth')->pluck('id')->all(), $user->ancestorsAndSelf->pluck('id')->all()); $this->assertEquals($user->parent()->first()?->id, $user->parent?->id); } } @@ -72,8 +72,8 @@ public function testLoadTreePathRelationsWithMissingModels(): void $this->assertTrue($user->relationLoaded('ancestorsAndSelf')); $this->assertTrue($user->relationLoaded('parent')); - $this->assertEquals($user->ancestors()->pluck('id')->all(), $user->ancestors->pluck('id')->all()); - $this->assertEquals($user->ancestorsAndSelf()->pluck('id')->all(), $user->ancestorsAndSelf->pluck('id')->all()); + $this->assertEquals($user->ancestors()->orderByDesc('depth')->pluck('id')->all(), $user->ancestors->pluck('id')->all()); + $this->assertEquals($user->ancestorsAndSelf()->orderByDesc('depth')->pluck('id')->all(), $user->ancestorsAndSelf->pluck('id')->all()); $this->assertEquals($user->parent()->first()?->id, $user->parent?->id); } }