From e8ff3c0374cf7db00b3d7599ab0964339e871d30 Mon Sep 17 00:00:00 2001 From: Wietse van Ginkel Date: Tue, 12 May 2026 11:40:42 +0200 Subject: [PATCH] refactor: extract HasResourceQuery trait from BuildoraResource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third decomposition step for #135 (after HasResourceActions and HasResourceNavigation). Moves the two static query entry points out of BuildoraResource into Resources\Concerns\HasResourceQuery: - query() — list-context query (no panel eager-loads) - queryWithRelations() — detail-context query (eager-loads panels) Both are thin forwards to QueryFactory; they could in principle stay on the resource class, but keeping them with the other resource concerns being extracted gives BuildoraResource a single ownership story: 'every overridable hook lives in a trait'. The list/detail split itself (the boolean second argument to QueryFactory::make()) is unchanged here — that's the subject of #129 / PR #166, which makes the entry points explicit (forList/forDetail). The two efforts compose cleanly: after both land, this trait calls QueryFactory::forList() and forDetail() instead of make($resource, bool). Tests (5): - query() returns a BuildoraQueryBuilder - query() does not stage panel eager-loads (list-context invariant) - queryWithRelations() stages declared panel relations (detail-context invariant) - BuildoraResource uses the trait (reflection) - Source grep: methods absent from BuildoraResource.php, present in HasResourceQuery.php — re-inlining fails the test. Refs #135 --- src/Resources/BuildoraResource.php | 25 +--- src/Resources/Concerns/HasResourceQuery.php | 38 ++++++ .../Concerns/HasResourceQueryTest.php | 113 ++++++++++++++++++ 3 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 src/Resources/Concerns/HasResourceQuery.php create mode 100644 tests/Unit/Resources/Concerns/HasResourceQueryTest.php diff --git a/src/Resources/BuildoraResource.php b/src/Resources/BuildoraResource.php index e7d6a2e..2f30bd4 100644 --- a/src/Resources/BuildoraResource.php +++ b/src/Resources/BuildoraResource.php @@ -6,6 +6,7 @@ use Ginkelsoft\Buildora\Exceptions\BuildoraException; use Ginkelsoft\Buildora\Resources\Concerns\HasResourceActions; use Ginkelsoft\Buildora\Resources\Concerns\HasResourceNavigation; +use Ginkelsoft\Buildora\Resources\Concerns\HasResourceQuery; use Illuminate\Database\Eloquent\Model; use Ginkelsoft\Buildora\Fields\Field; use Exception; @@ -20,6 +21,7 @@ abstract class BuildoraResource { use HasResourceActions; use HasResourceNavigation; + use HasResourceQuery; protected ?Model $parentModel = null; protected string $modelClass; @@ -196,27 +198,8 @@ public function getModelClass(): string // slug() lives in HasResourceNavigation. - /** - * Return the query builder for this resource WITHOUT eager-loading relations. - * Use this for index/list views for optimal performance. - * - * @return \Ginkelsoft\Buildora\BuildoraQueryBuilder - */ - public static function query(): \Ginkelsoft\Buildora\BuildoraQueryBuilder - { - return QueryFactory::forList(new static()); - } - - /** - * Return the query builder for this resource WITH eager-loading of panel relations. - * Use this for detail/show views where relations are needed. - * - * @return \Ginkelsoft\Buildora\BuildoraQueryBuilder - */ - public static function queryWithRelations(): \Ginkelsoft\Buildora\BuildoraQueryBuilder - { - return QueryFactory::forDetail(new static()); - } + // query() and queryWithRelations() live in HasResourceQuery + // (Resources\Concerns\HasResourceQuery). public function setDetailView(string $view): static { diff --git a/src/Resources/Concerns/HasResourceQuery.php b/src/Resources/Concerns/HasResourceQuery.php new file mode 100644 index 0000000..3c10d83 --- /dev/null +++ b/src/Resources/Concerns/HasResourceQuery.php @@ -0,0 +1,38 @@ + 'comments'], + ]; + } +} + +class HasResourceQueryTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + Schema::create('rq_articles', function ($t) { + $t->increments('id'); + $t->string('title')->nullable(); + }); + } + + #[Test] + public function query_returns_a_buildora_query_builder(): void + { + $this->assertInstanceOf(BuildoraQueryBuilder::class, RQArticleBuildora::query()); + } + + #[Test] + public function query_does_not_eager_load_panel_relations(): void + { + // Behavioural sanity: query() must skip the panel eager-load path + // (that's the whole point of the list/detail split). + $eagerLoads = RQArticleBuildora::query()->getEagerLoads(); + + $this->assertSame([], array_keys($eagerLoads)); + } + + #[Test] + public function query_with_relations_eager_loads_declared_panels(): void + { + $eagerLoads = RQArticleBuildora::queryWithRelations()->getEagerLoads(); + + $this->assertContains('comments', array_keys($eagerLoads)); + } + + #[Test] + public function buildora_resource_uses_the_query_trait(): void + { + $traits = (new ReflectionClass(BuildoraResource::class))->getTraitNames(); + + $this->assertContains(HasResourceQuery::class, $traits); + } + + #[Test] + public function query_methods_are_no_longer_inlined_in_buildora_resource(): void + { + $resourceSource = file_get_contents( + (new ReflectionClass(BuildoraResource::class))->getFileName() + ); + + foreach (['query', 'queryWithRelations'] as $movedMethod) { + $this->assertStringNotContainsString( + "public static function {$movedMethod}(", + $resourceSource, + "Method '{$movedMethod}' has been re-inlined into BuildoraResource.php — it should live in HasResourceQuery trait." + ); + } + + $traitSource = file_get_contents( + (new ReflectionClass(HasResourceQuery::class))->getFileName() + ); + + foreach (['query', 'queryWithRelations'] as $movedMethod) { + $this->assertStringContainsString( + "function {$movedMethod}(", + $traitSource, + ); + } + } +}