From c43c6a478f4d43ba36bbf2c8a2626cd305b39a0e Mon Sep 17 00:00:00 2001 From: Ollie Date: Sat, 11 Apr 2026 00:13:12 +0100 Subject: [PATCH 01/19] PoC: Add `reuse_prototype` field to `/wiki` API endpoint Bug: T421877 --- app/Http/Controllers/PublicWikiController.php | 2 +- app/Http/Resources/PublicWikiResource.php | 15 +++++++++++++++ app/Providers/AppServiceProvider.php | 12 ++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/PublicWikiController.php b/app/Http/Controllers/PublicWikiController.php index ca4d3e7da..c4f8fc155 100644 --- a/app/Http/Controllers/PublicWikiController.php +++ b/app/Http/Controllers/PublicWikiController.php @@ -29,7 +29,7 @@ public function index(Request $request) { ]); $params = array_merge(self::$defaultParams, $request->input()); - $query = Wiki::query(); + $query = Wiki::query()->with('wikiLatestProfile'); if (array_key_exists('is_featured', $params)) { $query = $query->where([ diff --git a/app/Http/Resources/PublicWikiResource.php b/app/Http/Resources/PublicWikiResource.php index 37e1e92ef..423f37c46 100644 --- a/app/Http/Resources/PublicWikiResource.php +++ b/app/Http/Resources/PublicWikiResource.php @@ -16,6 +16,21 @@ public function toArray($request): array { 'sitename' => $this->sitename, 'wiki_site_stats' => $this->wikiSiteStats, 'logo_url' => $logoSetting ? $logoSetting->value : null, + + // TODO: delete these three fields before merging; here to easily prove the `reuse_prototype` logic works + 'test_purpose' => $this->wikiLatestProfile ? $this->wikiLatestProfile->purpose : null, + 'test_temporality' => $this->wikiLatestProfile ? $this->wikiLatestProfile->temporality : null, + 'test_audience' => $this->wikiLatestProfile ? $this->wikiLatestProfile->audience : null, + + // TODO: As the `$this->wikiLatestProfile` property can be accessed regardless of if + // `->with('wikiLatestProfile')` is used in the controller, we are unable to return null if + // `$this->wikiLatestProfile` isn't set. We should either look into addressing this, or remove the + // `$this->wikiLatestProfile ? ... : null` conditional. + 'reuse_prototype' => $this->wikiLatestProfile + ? $this->wikiLatestProfile->purpose === 'data_hub' + && $this->wikiLatestProfile->temporality === 'permanent' + && $this->wikiLatestProfile->audience === 'wide' + : null, ]; } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 38f07d753..99eb8956d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ use App\Http\Curl\CurlRequest; use App\Http\Curl\HttpRequest; +use Illuminate\Database\Events\QueryExecuted; use Illuminate\Queue\Events\JobFailed; use Illuminate\Support\Facades\Queue; use Illuminate\Support\ServiceProvider; @@ -25,5 +26,16 @@ public function boot(): void { $wrappedException = new \Exception("Executing Job '$name' failed.", 1, $event->exception); report($wrappedException); }); + + // TODO: delete this listener before merging or is it useful to keep in the local environment? + if ($this->app->environment('local')) { + \Event::listen(QueryExecuted::class, function (QueryExecuted $query) { + \Log::debug('Query Executed: ', [ + 'sql' => $query->sql, + 'bindings' => $query->bindings, + 'connection' => $query->connectionName, + ]); + }); + } } } From 73a92e7be2daea207de35d6b7b3f0b40cf177b3a Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 22 May 2026 17:05:09 +0200 Subject: [PATCH 02/19] add test for reuse_prototype --- tests/Routes/Wiki/PublicWikiTest.php | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/Routes/Wiki/PublicWikiTest.php b/tests/Routes/Wiki/PublicWikiTest.php index c7ed46fbb..90d830c79 100644 --- a/tests/Routes/Wiki/PublicWikiTest.php +++ b/tests/Routes/Wiki/PublicWikiTest.php @@ -3,6 +3,7 @@ namespace Tests\Routes\Wiki; use App\Wiki; +use App\WikiProfile; use App\WikiSetting; use App\WikiSiteStats; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -18,12 +19,14 @@ class PublicWikiTest extends TestCase { protected function setUp(): void { parent::setUp(); Wiki::query()->delete(); + WikiProfile::query()->delete(); WikiSiteStats::query()->delete(); WikiSetting::query()->delete(); } protected function tearDown(): void { Wiki::query()->delete(); + WikiProfile::query()->delete(); WikiSiteStats::query()->delete(); WikiSetting::query()->delete(); parent::tearDown(); @@ -279,4 +282,48 @@ public function testLogoUrl() { ) ->assertJsonPath('data.1.logo_url', null); } + + public function testReusePrototype() { + $reusableWiki = Wiki::factory()->create([ + 'domain' => 'reusable.wikibase.cloud', 'sitename' => 'asite', + ]); + WikiSiteStats::factory()->create([ + 'wiki_id' => $reusableWiki->id, + ]); + WikiProfile::create([ + 'wiki_id' => $reusableWiki->id, + 'purpose' => 'data_hub', + 'temporality' => 'permanent', + 'audience' => 'wide', + ]); + + $nonReusableWiki = Wiki::factory()->create([ + 'domain' => 'non-reusable.wikibase.cloud', 'sitename' => 'bsite', + ]); + WikiSiteStats::factory()->create([ + 'wiki_id' => $nonReusableWiki->id, + ]); + WikiProfile::create([ + 'wiki_id' => $nonReusableWiki->id, + 'purpose' => 'other', + 'temporality' => 'other', + 'audience' => 'other', + ]); + + $noProfileWiki = Wiki::factory()->create([ + 'domain' => 'no-profile.wikibase.cloud', 'sitename' => 'csite', + ]); + WikiSiteStats::factory()->create([ + 'wiki_id' => $noProfileWiki->id, + ]); + + $this->json('GET', $this->route) + ->assertStatus(200) + ->assertJsonPath('data.0.domain', 'reusable.wikibase.cloud') + ->assertJsonPath('data.0.reuse_prototype', true) + ->assertJsonPath('data.1.domain', 'non-reusable.wikibase.cloud') + ->assertJsonPath('data.1.reuse_prototype', false) + ->assertJsonPath('data.2.domain', 'no-profile.wikibase.cloud') + ->assertJsonPath('data.2.reuse_prototype', null); + } } From 07bb19b4b54cce6e2c7c8218c22a8c1a85e25643 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 22 May 2026 17:16:01 +0200 Subject: [PATCH 03/19] Add load-state-aware reuse_prototype resource logic --- app/Http/Resources/PublicWikiResource.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Http/Resources/PublicWikiResource.php b/app/Http/Resources/PublicWikiResource.php index 423f37c46..c9d38f9c6 100644 --- a/app/Http/Resources/PublicWikiResource.php +++ b/app/Http/Resources/PublicWikiResource.php @@ -22,14 +22,14 @@ public function toArray($request): array { 'test_temporality' => $this->wikiLatestProfile ? $this->wikiLatestProfile->temporality : null, 'test_audience' => $this->wikiLatestProfile ? $this->wikiLatestProfile->audience : null, - // TODO: As the `$this->wikiLatestProfile` property can be accessed regardless of if - // `->with('wikiLatestProfile')` is used in the controller, we are unable to return null if - // `$this->wikiLatestProfile` isn't set. We should either look into addressing this, or remove the - // `$this->wikiLatestProfile ? ... : null` conditional. - 'reuse_prototype' => $this->wikiLatestProfile - ? $this->wikiLatestProfile->purpose === 'data_hub' - && $this->wikiLatestProfile->temporality === 'permanent' - && $this->wikiLatestProfile->audience === 'wide' + // Checking relation load state before reading it to avoid N+1 query + // This relies on the controller to eager load `wikiLatestProfile` relationship + 'reuse_prototype' => $this->relationLoaded('wikiLatestProfile') + ? ($this->wikiLatestProfile + ? $this->wikiLatestProfile->purpose === 'data_hub' + && $this->wikiLatestProfile->temporality === 'permanent' + && $this->wikiLatestProfile->audience === 'wide' + : null) : null, ]; } From 9233389f93ee95d2bb4663fa5a1b35e528675ff1 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 22 May 2026 17:18:06 +0200 Subject: [PATCH 04/19] Remove temporary reuse_prototype debug fields --- app/Http/Resources/PublicWikiResource.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/Http/Resources/PublicWikiResource.php b/app/Http/Resources/PublicWikiResource.php index c9d38f9c6..c01e34f58 100644 --- a/app/Http/Resources/PublicWikiResource.php +++ b/app/Http/Resources/PublicWikiResource.php @@ -17,11 +17,6 @@ public function toArray($request): array { 'wiki_site_stats' => $this->wikiSiteStats, 'logo_url' => $logoSetting ? $logoSetting->value : null, - // TODO: delete these three fields before merging; here to easily prove the `reuse_prototype` logic works - 'test_purpose' => $this->wikiLatestProfile ? $this->wikiLatestProfile->purpose : null, - 'test_temporality' => $this->wikiLatestProfile ? $this->wikiLatestProfile->temporality : null, - 'test_audience' => $this->wikiLatestProfile ? $this->wikiLatestProfile->audience : null, - // Checking relation load state before reading it to avoid N+1 query // This relies on the controller to eager load `wikiLatestProfile` relationship 'reuse_prototype' => $this->relationLoaded('wikiLatestProfile') From 69da736759a37b2897b7808d3db0718568b8ed63 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 27 May 2026 16:25:28 +0200 Subject: [PATCH 05/19] Replace TODO with a purpose-focus explaintion for the local listener --- app/Providers/AppServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 99eb8956d..44afac2ec 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -27,7 +27,7 @@ public function boot(): void { report($wrappedException); }); - // TODO: delete this listener before merging or is it useful to keep in the local environment? + // Local-only SQL query logging for debugging if ($this->app->environment('local')) { \Event::listen(QueryExecuted::class, function (QueryExecuted $query) { \Log::debug('Query Executed: ', [ From a06042ec260d33ad4704fe8540ee6bb2d522b404 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 27 May 2026 18:43:28 +0200 Subject: [PATCH 06/19] Add /reusePrototype endpoint --- routes/api.php | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/api.php b/routes/api.php index 0522a0491..1ad9ea23c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -57,5 +57,6 @@ }); $router->apiResource('wiki', 'PublicWikiController')->only(['index', 'show']); + $router->apiResource('reusePrototype', 'PublicWikiController')->only(['index', 'show']); $router->apiResource('wikiConversionData', 'ConversionMetricController')->only(['index']); }); From e7a36ef2fce3d0ff1cfb5e51a79bf09555728849 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 27 May 2026 19:00:38 +0200 Subject: [PATCH 07/19] Eager-load Wiki model and preloads WikiLatestProfile in show() --- app/Http/Controllers/PublicWikiController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/PublicWikiController.php b/app/Http/Controllers/PublicWikiController.php index c4f8fc155..e644cf4bf 100644 --- a/app/Http/Controllers/PublicWikiController.php +++ b/app/Http/Controllers/PublicWikiController.php @@ -72,6 +72,6 @@ public function index(Request $request) { * Display the specified resource. */ public function show($id) { - return new PublicWikiResource(Wiki::findOrFail($id)); + return new PublicWikiResource(Wiki::query()->with('wikiLatestProfile')->findOrFail($id)); } } From 84f1b518f04e8df2488ae02b915f6c1c02eb4db8 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 27 May 2026 19:11:23 +0200 Subject: [PATCH 08/19] Add PublicWikiControllerTest unit test for loaded relation --- .../Controllers/PublicWikiControllerTest.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/Http/Controllers/PublicWikiControllerTest.php diff --git a/tests/Http/Controllers/PublicWikiControllerTest.php b/tests/Http/Controllers/PublicWikiControllerTest.php new file mode 100644 index 000000000..fe07744d8 --- /dev/null +++ b/tests/Http/Controllers/PublicWikiControllerTest.php @@ -0,0 +1,34 @@ +create([ + 'domain' => 'controller-test.wikibase.cloud', + 'sitename' => 'controller-test', + ]); + + WikiProfile::create([ + 'wiki_id' => $wiki->id, + 'purpose' => 'data_hub', + 'temporality' => 'permanent', + 'audience' => 'wide', + ]); + + $controller = new PublicWikiController; + $resource = $controller->show($wiki->id); + + $this->assertInstanceOf(PublicWikiResource::class, $resource); + $this->assertSame(true, $resource->toArray(request())['reuse_prototype']); + } +} From a5e0f75c0b7f636048f28513bf7fe9e1b49ca359 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 27 May 2026 19:25:24 +0200 Subject: [PATCH 09/19] Change endpoint to reusePrototype to explicitly verify that route --- tests/Routes/Wiki/PublicWikiTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Routes/Wiki/PublicWikiTest.php b/tests/Routes/Wiki/PublicWikiTest.php index 90d830c79..246a6fec1 100644 --- a/tests/Routes/Wiki/PublicWikiTest.php +++ b/tests/Routes/Wiki/PublicWikiTest.php @@ -317,7 +317,7 @@ public function testReusePrototype() { 'wiki_id' => $noProfileWiki->id, ]); - $this->json('GET', $this->route) + $this->json('GET', 'reusePrototype') ->assertStatus(200) ->assertJsonPath('data.0.domain', 'reusable.wikibase.cloud') ->assertJsonPath('data.0.reuse_prototype', true) From 5c931e2207d95d4c8ba6f7911a57e95e613a56b2 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 27 May 2026 19:38:06 +0200 Subject: [PATCH 10/19] extend testReusePrototype to also call GET /reusePrototype/{id} --- tests/Routes/Wiki/PublicWikiTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Routes/Wiki/PublicWikiTest.php b/tests/Routes/Wiki/PublicWikiTest.php index 246a6fec1..4e9752cbe 100644 --- a/tests/Routes/Wiki/PublicWikiTest.php +++ b/tests/Routes/Wiki/PublicWikiTest.php @@ -325,5 +325,9 @@ public function testReusePrototype() { ->assertJsonPath('data.1.reuse_prototype', false) ->assertJsonPath('data.2.domain', 'no-profile.wikibase.cloud') ->assertJsonPath('data.2.reuse_prototype', null); + + $this->json('GET', 'reusePrototype/' . $reusableWiki->id) + ->assertStatus(200) + ->assertJsonPath('data.reuse_prototype', true); } } From bdd52e881a14713033fdf312b16ffa67994a223d Mon Sep 17 00:00:00 2001 From: Dat Date: Thu, 28 May 2026 18:00:46 +0200 Subject: [PATCH 11/19] Check the model has the relation loaded --- tests/Http/Controllers/PublicWikiControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Http/Controllers/PublicWikiControllerTest.php b/tests/Http/Controllers/PublicWikiControllerTest.php index fe07744d8..bd63d076f 100644 --- a/tests/Http/Controllers/PublicWikiControllerTest.php +++ b/tests/Http/Controllers/PublicWikiControllerTest.php @@ -29,6 +29,6 @@ public function testShowEagerLoadsWikiLatestProfileForResource(): void { $resource = $controller->show($wiki->id); $this->assertInstanceOf(PublicWikiResource::class, $resource); - $this->assertSame(true, $resource->toArray(request())['reuse_prototype']); + $this->assertTrue($resource->resource->relationLoaded('wikiLatestProfile')); } } From 065773132e6671a89539f5ef4da188c78af723fa Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 3 Jun 2026 23:19:04 +0200 Subject: [PATCH 12/19] add unit test and query count for index() --- .../Controllers/PublicWikiControllerTest.php | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/Http/Controllers/PublicWikiControllerTest.php b/tests/Http/Controllers/PublicWikiControllerTest.php index bd63d076f..ea03316d6 100644 --- a/tests/Http/Controllers/PublicWikiControllerTest.php +++ b/tests/Http/Controllers/PublicWikiControllerTest.php @@ -6,7 +6,9 @@ use App\Http\Resources\PublicWikiResource; use App\Wiki; use App\WikiProfile; +use Illuminate\Database\Events\QueryExecuted; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class PublicWikiControllerTest extends TestCase { @@ -31,4 +33,35 @@ public function testShowEagerLoadsWikiLatestProfileForResource(): void { $this->assertInstanceOf(PublicWikiResource::class, $resource); $this->assertTrue($resource->resource->relationLoaded('wikiLatestProfile')); } + + public function testIndexEagerLoadsWikiLatestProfileOnceForCollection(): void { + $n = rand(3, 100); + echo $n; + for ($i = 1; $i <= $n; $i++) { + $wiki = Wiki::factory()->create([ + 'domain' => 'index-eager-load-test-' . $i . '.wikibase.cloud', + 'sitename' => 'Index Eager Load Test Site ' . $i, + ]); + + WikiProfile::create([ + 'wiki_id' => $wiki->id, + 'purpose' => 'data_hub', + 'temporality' => 'permanent', + 'audience' => 'wide', + ]); + } + + $profileQueryCount = 0; + DB::listen(function (QueryExecuted $query) use (&$profileQueryCount): void { + if (str_contains($query->sql, 'wiki_profiles')) { + $profileQueryCount++; + } + }); + + $controller = new PublicWikiController; + $resourceCollection = $controller->index(request()); + + $this->assertSame(1, $profileQueryCount); + $this->assertTrue($resourceCollection->collection[0]->resource->relationLoaded('wikiLatestProfile')); + } } From a993cbafa23cbe148529e69ca51743ad7c62bc75 Mon Sep 17 00:00:00 2001 From: Dat Date: Thu, 4 Jun 2026 14:46:48 +0200 Subject: [PATCH 13/19] remove debugging "echo" --- tests/Http/Controllers/PublicWikiControllerTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Http/Controllers/PublicWikiControllerTest.php b/tests/Http/Controllers/PublicWikiControllerTest.php index ea03316d6..05cb8ab89 100644 --- a/tests/Http/Controllers/PublicWikiControllerTest.php +++ b/tests/Http/Controllers/PublicWikiControllerTest.php @@ -35,9 +35,7 @@ public function testShowEagerLoadsWikiLatestProfileForResource(): void { } public function testIndexEagerLoadsWikiLatestProfileOnceForCollection(): void { - $n = rand(3, 100); - echo $n; - for ($i = 1; $i <= $n; $i++) { + for ($i = 1; $i <= rand(3, 100); $i++) { $wiki = Wiki::factory()->create([ 'domain' => 'index-eager-load-test-' . $i . '.wikibase.cloud', 'sitename' => 'Index Eager Load Test Site ' . $i, From e48435f1c7aca2d30f557fc17e1f30b67ae035cd Mon Sep 17 00:00:00 2001 From: Dat Date: Thu, 4 Jun 2026 15:49:46 +0200 Subject: [PATCH 14/19] Apply suggestions from reviews --- app/Http/Controllers/PublicWikiController.php | 2 +- app/Http/Resources/PublicWikiResource.php | 10 ++++------ .../Http/Controllers/PublicWikiControllerTest.php | 15 ++++++++------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/PublicWikiController.php b/app/Http/Controllers/PublicWikiController.php index e644cf4bf..c4f8fc155 100644 --- a/app/Http/Controllers/PublicWikiController.php +++ b/app/Http/Controllers/PublicWikiController.php @@ -72,6 +72,6 @@ public function index(Request $request) { * Display the specified resource. */ public function show($id) { - return new PublicWikiResource(Wiki::query()->with('wikiLatestProfile')->findOrFail($id)); + return new PublicWikiResource(Wiki::findOrFail($id)); } } diff --git a/app/Http/Resources/PublicWikiResource.php b/app/Http/Resources/PublicWikiResource.php index c01e34f58..a96899a1a 100644 --- a/app/Http/Resources/PublicWikiResource.php +++ b/app/Http/Resources/PublicWikiResource.php @@ -19,12 +19,10 @@ public function toArray($request): array { // Checking relation load state before reading it to avoid N+1 query // This relies on the controller to eager load `wikiLatestProfile` relationship - 'reuse_prototype' => $this->relationLoaded('wikiLatestProfile') - ? ($this->wikiLatestProfile - ? $this->wikiLatestProfile->purpose === 'data_hub' - && $this->wikiLatestProfile->temporality === 'permanent' - && $this->wikiLatestProfile->audience === 'wide' - : null) + 'reuse_prototype' => $this->wikiLatestProfile + ? $this->wikiLatestProfile->purpose === 'data_hub' + && $this->wikiLatestProfile->temporality === 'permanent' + && $this->wikiLatestProfile->audience === 'wide' : null, ]; } diff --git a/tests/Http/Controllers/PublicWikiControllerTest.php b/tests/Http/Controllers/PublicWikiControllerTest.php index 05cb8ab89..f5daedb2f 100644 --- a/tests/Http/Controllers/PublicWikiControllerTest.php +++ b/tests/Http/Controllers/PublicWikiControllerTest.php @@ -8,13 +8,14 @@ use App\WikiProfile; use Illuminate\Database\Events\QueryExecuted; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Tests\TestCase; class PublicWikiControllerTest extends TestCase { use DatabaseTransactions; - public function testShowEagerLoadsWikiLatestProfileForResource(): void { + public function testShowLoadsWikiLatestProfileForResource(): void { $wiki = Wiki::factory()->create([ 'domain' => 'controller-test.wikibase.cloud', 'sitename' => 'controller-test', @@ -31,14 +32,14 @@ public function testShowEagerLoadsWikiLatestProfileForResource(): void { $resource = $controller->show($wiki->id); $this->assertInstanceOf(PublicWikiResource::class, $resource); - $this->assertTrue($resource->resource->relationLoaded('wikiLatestProfile')); + $this->assertSame(true, $resource->toArray(new Request)['reuse_prototype']); } public function testIndexEagerLoadsWikiLatestProfileOnceForCollection(): void { - for ($i = 1; $i <= rand(3, 100); $i++) { + for ($i = 1; $i <= 3; $i++) { $wiki = Wiki::factory()->create([ - 'domain' => 'index-eager-load-test-' . $i . '.wikibase.cloud', - 'sitename' => 'Index Eager Load Test Site ' . $i, + 'domain' => "index-eager-load-test-{$i}.wikibase.cloud", + 'sitename' => "Index Eager Load Test Site {$i}", ]); WikiProfile::create([ @@ -57,9 +58,9 @@ public function testIndexEagerLoadsWikiLatestProfileOnceForCollection(): void { }); $controller = new PublicWikiController; - $resourceCollection = $controller->index(request()); + $resourceCollection = $controller->index(new Request); $this->assertSame(1, $profileQueryCount); - $this->assertTrue($resourceCollection->collection[0]->resource->relationLoaded('wikiLatestProfile')); + $this->assertTrue($resourceCollection->first()->relationLoaded('wikiLatestProfile')); } } From 0d93d7fe485cd2b902e0a638ee89ff6ceeb7de3c Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 5 Jun 2026 13:35:53 +0200 Subject: [PATCH 15/19] apply suggestion from review --- app/Http/Resources/PublicWikiResource.php | 5 +---- app/Providers/AppServiceProvider.php | 11 ----------- routes/api.php | 2 +- tests/Http/Controllers/PublicWikiControllerTest.php | 8 ++++---- 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/app/Http/Resources/PublicWikiResource.php b/app/Http/Resources/PublicWikiResource.php index a96899a1a..3deb10d7f 100644 --- a/app/Http/Resources/PublicWikiResource.php +++ b/app/Http/Resources/PublicWikiResource.php @@ -16,14 +16,11 @@ public function toArray($request): array { 'sitename' => $this->sitename, 'wiki_site_stats' => $this->wikiSiteStats, 'logo_url' => $logoSetting ? $logoSetting->value : null, - - // Checking relation load state before reading it to avoid N+1 query - // This relies on the controller to eager load `wikiLatestProfile` relationship 'reuse_prototype' => $this->wikiLatestProfile ? $this->wikiLatestProfile->purpose === 'data_hub' && $this->wikiLatestProfile->temporality === 'permanent' && $this->wikiLatestProfile->audience === 'wide' - : null, + : false, ]; } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 44afac2ec..1e3beb268 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -26,16 +26,5 @@ public function boot(): void { $wrappedException = new \Exception("Executing Job '$name' failed.", 1, $event->exception); report($wrappedException); }); - - // Local-only SQL query logging for debugging - if ($this->app->environment('local')) { - \Event::listen(QueryExecuted::class, function (QueryExecuted $query) { - \Log::debug('Query Executed: ', [ - 'sql' => $query->sql, - 'bindings' => $query->bindings, - 'connection' => $query->connectionName, - ]); - }); - } } } diff --git a/routes/api.php b/routes/api.php index 1ad9ea23c..f014ffbcb 100644 --- a/routes/api.php +++ b/routes/api.php @@ -57,6 +57,6 @@ }); $router->apiResource('wiki', 'PublicWikiController')->only(['index', 'show']); - $router->apiResource('reusePrototype', 'PublicWikiController')->only(['index', 'show']); + $router->apiResource('reusePrototype', 'PublicWikiController')->only(['index']); $router->apiResource('wikiConversionData', 'ConversionMetricController')->only(['index']); }); diff --git a/tests/Http/Controllers/PublicWikiControllerTest.php b/tests/Http/Controllers/PublicWikiControllerTest.php index f5daedb2f..8d82c3d13 100644 --- a/tests/Http/Controllers/PublicWikiControllerTest.php +++ b/tests/Http/Controllers/PublicWikiControllerTest.php @@ -50,17 +50,17 @@ public function testIndexEagerLoadsWikiLatestProfileOnceForCollection(): void { ]); } - $profileQueryCount = 0; - DB::listen(function (QueryExecuted $query) use (&$profileQueryCount): void { + $wikiProfileQueryCount = 0; + DB::listen(function (QueryExecuted $query) use (&$wikiProfileQueryCount): void { if (str_contains($query->sql, 'wiki_profiles')) { - $profileQueryCount++; + $wikiProfileQueryCount++; } }); $controller = new PublicWikiController; $resourceCollection = $controller->index(new Request); - $this->assertSame(1, $profileQueryCount); + $this->assertSame(1, $wikiProfileQueryCount); $this->assertTrue($resourceCollection->first()->relationLoaded('wikiLatestProfile')); } } From e937b5dec34800171f1e9305d8da4140de72132d Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 5 Jun 2026 14:33:38 +0200 Subject: [PATCH 16/19] change null to false in testReusePrototype --- tests/Routes/Wiki/PublicWikiTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Routes/Wiki/PublicWikiTest.php b/tests/Routes/Wiki/PublicWikiTest.php index 4e9752cbe..15e6f6cb9 100644 --- a/tests/Routes/Wiki/PublicWikiTest.php +++ b/tests/Routes/Wiki/PublicWikiTest.php @@ -324,7 +324,7 @@ public function testReusePrototype() { ->assertJsonPath('data.1.domain', 'non-reusable.wikibase.cloud') ->assertJsonPath('data.1.reuse_prototype', false) ->assertJsonPath('data.2.domain', 'no-profile.wikibase.cloud') - ->assertJsonPath('data.2.reuse_prototype', null); + ->assertJsonPath('data.2.reuse_prototype', false); $this->json('GET', 'reusePrototype/' . $reusableWiki->id) ->assertStatus(200) From 2eaab8b53ad4e3a846ebf0b72af1035ae07c0cab Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 5 Jun 2026 14:37:23 +0200 Subject: [PATCH 17/19] Add tests index() returning false --- .../Controllers/PublicWikiControllerTest.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/Http/Controllers/PublicWikiControllerTest.php b/tests/Http/Controllers/PublicWikiControllerTest.php index 8d82c3d13..7faf1eeae 100644 --- a/tests/Http/Controllers/PublicWikiControllerTest.php +++ b/tests/Http/Controllers/PublicWikiControllerTest.php @@ -63,4 +63,42 @@ public function testIndexEagerLoadsWikiLatestProfileOnceForCollection(): void { $this->assertSame(1, $wikiProfileQueryCount); $this->assertTrue($resourceCollection->first()->relationLoaded('wikiLatestProfile')); } + + public function testIndexReturnsFalseWhenWikiHasNoLatestProfile(): void { + $wikiWithoutProfile = Wiki::factory()->create([ + 'domain' => 'no-profile.wikibase.cloud', + 'sitename' => 'No Profile Test Site', + ]); + + $controller = new PublicWikiController; + $request = new Request; + $resourceCollection = $controller->index($request); + + $resource = $resourceCollection->firstWhere('id', $wikiWithoutProfile->id); + + $this->assertNotNull($resource); + $this->assertFalse($resource->toArray($request)['reuse_prototype']); + } + + public function testIndexReturnsFalseWhenWikiLatestProfileDoesNotMatchReusePrototype(): void { + $incompleteProfileWiki = Wiki::factory()->create([ + 'domain' => 'incomplete-profile.wikibase.cloud', + 'sitename' => 'Incomplete Profile Test Site', + ]); + WikiProfile::create([ + 'wiki_id' => $incompleteProfileWiki->id, + 'purpose' => 'other', + 'temporality' => 'temporary', + 'audience' => 'other', + ]); + + $controller = new PublicWikiController; + $request = new Request; + $resourceCollection = $controller->index($request); + + $resource = $resourceCollection->firstWhere('id', $incompleteProfileWiki->id); + + $this->assertNotNull($resource); + $this->assertFalse($resource->toArray($request)['reuse_prototype']); + } } From 16c8b670d834dbd465637f13b6d71c4322b65975 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 5 Jun 2026 14:38:33 +0200 Subject: [PATCH 18/19] fix linting --- app/Providers/AppServiceProvider.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1e3beb268..38f07d753 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,7 +4,6 @@ use App\Http\Curl\CurlRequest; use App\Http\Curl\HttpRequest; -use Illuminate\Database\Events\QueryExecuted; use Illuminate\Queue\Events\JobFailed; use Illuminate\Support\Facades\Queue; use Illuminate\Support\ServiceProvider; From fd0f8a4b72507f034b403f1c349546e51e2f5de1 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 5 Jun 2026 18:04:50 +0200 Subject: [PATCH 19/19] update the assertion call to use valid route --- tests/Routes/Wiki/PublicWikiTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Routes/Wiki/PublicWikiTest.php b/tests/Routes/Wiki/PublicWikiTest.php index 15e6f6cb9..f3269088e 100644 --- a/tests/Routes/Wiki/PublicWikiTest.php +++ b/tests/Routes/Wiki/PublicWikiTest.php @@ -326,7 +326,7 @@ public function testReusePrototype() { ->assertJsonPath('data.2.domain', 'no-profile.wikibase.cloud') ->assertJsonPath('data.2.reuse_prototype', false); - $this->json('GET', 'reusePrototype/' . $reusableWiki->id) + $this->json('GET', $this->route . '/' . $reusableWiki->id) ->assertStatus(200) ->assertJsonPath('data.reuse_prototype', true); }