Cache Schema::hasTable lookups on artwork JSON.
Artwork detail was paying repeated information_schema hits per request. Keep public detail edge-case behavior covered by regression tests.
This commit is contained in:
@@ -15,6 +15,9 @@ use Illuminate\Support\Facades\Schema;
|
|||||||
|
|
||||||
class ArtworkResource extends JsonResource
|
class ArtworkResource extends JsonResource
|
||||||
{
|
{
|
||||||
|
/** @var array<string, bool> */
|
||||||
|
private array $tablePresence = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform the resource into an array.
|
* Transform the resource into an array.
|
||||||
*/
|
*/
|
||||||
@@ -35,14 +38,14 @@ class ArtworkResource extends JsonResource
|
|||||||
|
|
||||||
$followerCount = 0;
|
$followerCount = 0;
|
||||||
if (!empty($this->user?->id)) {
|
if (!empty($this->user?->id)) {
|
||||||
if (Schema::hasTable('user_statistics')) {
|
if ($this->hasTable('user_statistics')) {
|
||||||
$followerCount = (int) DB::table('user_statistics')
|
$followerCount = (int) DB::table('user_statistics')
|
||||||
->where('user_id', (int) $this->user->id)
|
->where('user_id', (int) $this->user->id)
|
||||||
->value('followers_count');
|
->value('followers_count');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy fallback for environments where new tables are unavailable.
|
// Legacy fallback for environments where new tables are unavailable.
|
||||||
if (($followerCount <= 0) && Schema::hasTable('friends_list')) {
|
if (($followerCount <= 0) && $this->hasTable('friends_list')) {
|
||||||
$followerCount = (int) DB::table('friends_list')
|
$followerCount = (int) DB::table('friends_list')
|
||||||
->where('friend_id', (int) $this->user->id)
|
->where('friend_id', (int) $this->user->id)
|
||||||
->count();
|
->count();
|
||||||
@@ -58,19 +61,19 @@ class ArtworkResource extends JsonResource
|
|||||||
$viewerAward = null;
|
$viewerAward = null;
|
||||||
$isOwner = $viewerId > 0 && $viewerId === (int) ($this->user?->id ?? 0);
|
$isOwner = $viewerId > 0 && $viewerId === (int) ($this->user?->id ?? 0);
|
||||||
|
|
||||||
$bookmarksCount = Schema::hasTable('artwork_bookmarks')
|
$bookmarksCount = $this->hasTable('artwork_bookmarks')
|
||||||
? (int) DB::table('artwork_bookmarks')->where('artwork_id', (int) $this->id)->count()
|
? (int) DB::table('artwork_bookmarks')->where('artwork_id', (int) $this->id)->count()
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
if ($viewerId > 0) {
|
if ($viewerId > 0) {
|
||||||
if (Schema::hasTable('artwork_likes')) {
|
if ($this->hasTable('artwork_likes')) {
|
||||||
$isLiked = DB::table('artwork_likes')
|
$isLiked = DB::table('artwork_likes')
|
||||||
->where('user_id', $viewerId)
|
->where('user_id', $viewerId)
|
||||||
->where('artwork_id', (int) $this->id)
|
->where('artwork_id', (int) $this->id)
|
||||||
->exists();
|
->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Schema::hasTable('artwork_bookmarks')) {
|
if ($this->hasTable('artwork_bookmarks')) {
|
||||||
$isBookmarked = DB::table('artwork_bookmarks')
|
$isBookmarked = DB::table('artwork_bookmarks')
|
||||||
->where('user_id', $viewerId)
|
->where('user_id', $viewerId)
|
||||||
->where('artwork_id', (int) $this->id)
|
->where('artwork_id', (int) $this->id)
|
||||||
@@ -83,12 +86,12 @@ class ArtworkResource extends JsonResource
|
|||||||
->exists();
|
->exists();
|
||||||
|
|
||||||
if (!empty($this->user?->id)) {
|
if (!empty($this->user?->id)) {
|
||||||
if (Schema::hasTable('user_followers')) {
|
if ($this->hasTable('user_followers')) {
|
||||||
$isFollowing = DB::table('user_followers')
|
$isFollowing = DB::table('user_followers')
|
||||||
->where('user_id', (int) $this->user->id)
|
->where('user_id', (int) $this->user->id)
|
||||||
->where('follower_id', $viewerId)
|
->where('follower_id', $viewerId)
|
||||||
->exists();
|
->exists();
|
||||||
} elseif (Schema::hasTable('friends_list')) {
|
} elseif ($this->hasTable('friends_list')) {
|
||||||
// Legacy fallback only.
|
// Legacy fallback only.
|
||||||
$isFollowing = DB::table('friends_list')
|
$isFollowing = DB::table('friends_list')
|
||||||
->where('user_id', $viewerId)
|
->where('user_id', $viewerId)
|
||||||
@@ -97,14 +100,14 @@ class ArtworkResource extends JsonResource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($this->group?->id) && Schema::hasTable('group_follows')) {
|
if (!empty($this->group?->id) && $this->hasTable('group_follows')) {
|
||||||
$isFollowingGroup = DB::table('group_follows')
|
$isFollowingGroup = DB::table('group_follows')
|
||||||
->where('group_id', (int) $this->group->id)
|
->where('group_id', (int) $this->group->id)
|
||||||
->where('user_id', $viewerId)
|
->where('user_id', $viewerId)
|
||||||
->exists();
|
->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Schema::hasTable('artwork_medals')) {
|
if ($this->hasTable('artwork_medals')) {
|
||||||
$viewerAward = DB::table('artwork_medals')
|
$viewerAward = DB::table('artwork_medals')
|
||||||
->where('user_id', $viewerId)
|
->where('user_id', $viewerId)
|
||||||
->where('artwork_id', (int) $this->id)
|
->where('artwork_id', (int) $this->id)
|
||||||
@@ -279,7 +282,7 @@ class ArtworkResource extends JsonResource
|
|||||||
|
|
||||||
private function resolveScreenshotAssets(): array
|
private function resolveScreenshotAssets(): array
|
||||||
{
|
{
|
||||||
if (! Schema::hasTable('artwork_files')) {
|
if (! $this->hasTable('artwork_files')) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +342,7 @@ class ArtworkResource extends JsonResource
|
|||||||
$items = collect();
|
$items = collect();
|
||||||
$participationWorlds = collect();
|
$participationWorlds = collect();
|
||||||
|
|
||||||
if (Schema::hasTable('world_relations') && Schema::hasTable('worlds')) {
|
if ($this->hasTable('world_relations') && $this->hasTable('worlds')) {
|
||||||
$relations = WorldRelation::query()
|
$relations = WorldRelation::query()
|
||||||
->with('world')
|
->with('world')
|
||||||
->where('related_type', WorldRelation::TYPE_ARTWORK)
|
->where('related_type', WorldRelation::TYPE_ARTWORK)
|
||||||
@@ -369,7 +372,7 @@ class ArtworkResource extends JsonResource
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Schema::hasTable('world_submissions')) {
|
if ($this->hasTable('world_submissions')) {
|
||||||
$liveSubmissions = $this->worldSubmissions
|
$liveSubmissions = $this->worldSubmissions
|
||||||
->filter(function (WorldSubmission $submission): bool {
|
->filter(function (WorldSubmission $submission): bool {
|
||||||
return (string) $submission->status === WorldSubmission::STATUS_LIVE
|
return (string) $submission->status === WorldSubmission::STATUS_LIVE
|
||||||
@@ -403,7 +406,7 @@ class ArtworkResource extends JsonResource
|
|||||||
World::primeCanonicalEditionIds($participationWorlds->pluck('recurrence_key')->all());
|
World::primeCanonicalEditionIds($participationWorlds->pluck('recurrence_key')->all());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Schema::hasTable('world_reward_grants')) {
|
if ($this->hasTable('world_reward_grants')) {
|
||||||
$items = $items->concat(app(WorldRewardService::class)->artworkRewardBadges($this->resource));
|
$items = $items->concat(app(WorldRewardService::class)->artworkRewardBadges($this->resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,6 +424,11 @@ class ArtworkResource extends JsonResource
|
|||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function hasTable(string $table): bool
|
||||||
|
{
|
||||||
|
return $this->tablePresence[$table] ??= Schema::hasTable($table);
|
||||||
|
}
|
||||||
|
|
||||||
private function authorCanPublishLinks(): bool
|
private function authorCanPublishLinks(): bool
|
||||||
{
|
{
|
||||||
$level = (int) ($this->user?->level ?? 1);
|
$level = (int) ($this->user?->level ?? 1);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Artwork;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('keeps the artwork detail edge-case responses intact', function (): void {
|
||||||
|
$artwork = Artwork::factory()->create([
|
||||||
|
'title' => 'M18B2 Artwork',
|
||||||
|
'slug' => 'm18b2-artwork',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->get(route('art.show', ['id' => $artwork->id, 'slug' => 'm18b2-artwork']))
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$this->get(route('art.show', ['id' => $artwork->id, 'slug' => 'wrong-slug']))
|
||||||
|
->assertRedirect(route('art.show', ['id' => $artwork->id, 'slug' => 'm18b2-artwork']))
|
||||||
|
->assertHeader('Location', route('art.show', ['id' => $artwork->id, 'slug' => 'm18b2-artwork']));
|
||||||
|
|
||||||
|
$deleted = Artwork::factory()->create();
|
||||||
|
$deleted->delete();
|
||||||
|
|
||||||
|
$this->get(route('art.show', ['id' => $deleted->id, 'slug' => $deleted->slug]))
|
||||||
|
->assertStatus(410);
|
||||||
|
|
||||||
|
$private = Artwork::factory()->private()->create();
|
||||||
|
$this->get(route('art.show', ['id' => $private->id, 'slug' => $private->slug]))
|
||||||
|
->assertForbidden();
|
||||||
|
|
||||||
|
$this->get(route('art.show', ['id' => 999999, 'slug' => 'missing']))
|
||||||
|
->assertNotFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('performs one primary artwork lookup on the public detail path', function (): void {
|
||||||
|
$artwork = Artwork::factory()->create([
|
||||||
|
'slug' => 'm18b2-query-regression',
|
||||||
|
]);
|
||||||
|
$primaryLookups = 0;
|
||||||
|
|
||||||
|
DB::listen(function ($query) use (&$primaryLookups): void {
|
||||||
|
$sql = strtolower(preg_replace('/\s+/', ' ', $query->sql));
|
||||||
|
|
||||||
|
if (str_contains($sql, 'from "artworks"')
|
||||||
|
&& str_contains($sql, 'where "id" = ?')
|
||||||
|
&& str_contains($sql, 'limit 1')) {
|
||||||
|
$primaryLookups++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->get(route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]))
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
expect($primaryLookups)->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the authenticated artwork detail query count bounded', function (): void {
|
||||||
|
$artwork = Artwork::factory()->create([
|
||||||
|
'slug' => 'm18b2-auth-query-regression',
|
||||||
|
]);
|
||||||
|
$viewer = User::factory()->create();
|
||||||
|
$queryCount = 0;
|
||||||
|
|
||||||
|
DB::listen(function () use (&$queryCount): void {
|
||||||
|
$queryCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->actingAs($viewer)
|
||||||
|
->get(route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]))
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
expect($queryCount)->toBeLessThanOrEqual(50);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user