diff --git a/src/Eloquent/Collection.php b/src/Eloquent/Collection.php index 41a9aed..f91ca8d 100644 --- a/src/Eloquent/Collection.php +++ b/src/Eloquent/Collection.php @@ -50,4 +50,61 @@ public function toTree($childrenRelation = 'children') return $tree; } + + /** + * Load parent/ancestor relations already present in the tree. + * + * @return static + */ + public function loadTreePathRelations(): static + { + if ($this->isEmpty()) { + return $this; + } + + /** @var TModel $instance */ + $instance = $this->first(); + + $keyName = $instance->getKeyName(); + $pathName = $instance->getPathName(); + $pathSeparator = $instance->getPathSeparator(); + + /** @var static $lookup */ + $lookup = $this->keyBy($keyName); + + /** @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() + ); + + $lookup = $lookup->union( + $instance->newQuery()->findMany($missing)->keyBy($keyName) + ); + + foreach ($this as $model) { + $pathSegments = array_reverse( + explode($pathSeparator, $model->$pathName) + ); + + $ancestorsAndSelf = array_reduce( + $pathSegments, + fn ($collection, string $key) => $collection->push($lookup[$key]), + $instance->newCollection(), + ); + + $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 31dc6c7..dd19c86 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 @@ -38,4 +39,51 @@ public function testToTreeWithEmptyCollection() $this->assertEmpty($tree); } + + public function testLoadTreePathRelations(): void + { + DB::enableQueryLog(); + + $tree = User::tree()->get()->loadTreePathRelations(); + + $this->assertCount(1, DB::getQueryLog()); + + foreach ($tree as $user) { + $this->assertTrue($user->relationLoaded('ancestors')); + $this->assertTrue($user->relationLoaded('ancestorsAndSelf')); + $this->assertTrue($user->relationLoaded('parent')); + + $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); + } + } + + public function testLoadTreePathRelationsWithMissingModels(): void + { + DB::enableQueryLog(); + + $tree = User::tree()->where('id', '>', 5)->get()->loadTreePathRelations(); + + $this->assertCount(2, DB::getQueryLog()); + + foreach ($tree as $user) { + $this->assertTrue($user->relationLoaded('ancestors')); + $this->assertTrue($user->relationLoaded('ancestorsAndSelf')); + $this->assertTrue($user->relationLoaded('parent')); + + $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); + } + } + + public function testLoadTreePathRelationsWithEmptyCollection(): void + { + $users = User::tree(1)->where('id', 0)->get(); + + $tree = $users->toTree()->loadTreePathRelations(); + + $this->assertEmpty($tree); + } }