Avoid per-comment reaction queries on the deferred comments API by aggregating counts in one service call.
147 lines
5.0 KiB
PHP
147 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Enums\ReactionType;
|
|
use App\Models\Artwork;
|
|
use App\Models\ArtworkComment;
|
|
use App\Models\CommentReaction;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function m18b1ArtworkCommentTree(int $topLevelCount = 2): array
|
|
{
|
|
$artwork = Artwork::factory()->create();
|
|
$author = User::factory()->create();
|
|
$viewer = User::factory()->create();
|
|
$other = User::factory()->create();
|
|
$comments = [];
|
|
|
|
for ($i = 0; $i < $topLevelCount; $i++) {
|
|
$comment = ArtworkComment::factory()->create([
|
|
'artwork_id' => $artwork->id,
|
|
'user_id' => $author->id,
|
|
'parent_id' => null,
|
|
]);
|
|
$reply = ArtworkComment::factory()->create([
|
|
'artwork_id' => $artwork->id,
|
|
'user_id' => $other->id,
|
|
'parent_id' => $comment->id,
|
|
]);
|
|
|
|
CommentReaction::create([
|
|
'comment_id' => $comment->id,
|
|
'user_id' => $other->id,
|
|
'reaction' => ReactionType::Heart->value,
|
|
]);
|
|
CommentReaction::create([
|
|
'comment_id' => $reply->id,
|
|
'user_id' => $viewer->id,
|
|
'reaction' => ReactionType::Fire->value,
|
|
]);
|
|
CommentReaction::create([
|
|
'comment_id' => $reply->id,
|
|
'user_id' => $other->id,
|
|
'reaction' => ReactionType::Clap->value,
|
|
]);
|
|
|
|
$comments[] = [$comment, $reply];
|
|
}
|
|
|
|
return [$artwork, $viewer, $comments];
|
|
}
|
|
|
|
function m18b1ReactionQueryCount(array $queries): int
|
|
{
|
|
return collect($queries)
|
|
->filter(fn (array $query): bool => str_contains(strtolower((string) $query['query']), 'comment_reactions'))
|
|
->count();
|
|
}
|
|
|
|
it('includes bulk reaction totals for guests with one reaction query', function (): void {
|
|
[$artwork, , $comments] = m18b1ArtworkCommentTree();
|
|
|
|
DB::enableQueryLog();
|
|
DB::flushQueryLog();
|
|
|
|
$response = $this->getJson("/api/artworks/{$artwork->id}/comments")->assertOk();
|
|
$payload = $response->json('data');
|
|
$withoutReactions = json_decode(json_encode($payload, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR);
|
|
$stripReactions = function (array &$items) use (&$stripReactions): void {
|
|
foreach ($items as &$item) {
|
|
unset($item['reactions']);
|
|
if (isset($item['replies']) && is_array($item['replies'])) {
|
|
$stripReactions($item['replies']);
|
|
}
|
|
}
|
|
};
|
|
$stripReactions($withoutReactions);
|
|
$commentCount = 0;
|
|
$countComments = function (array $items) use (&$countComments, &$commentCount): void {
|
|
foreach ($items as $item) {
|
|
$commentCount++;
|
|
if (isset($item['replies']) && is_array($item['replies'])) {
|
|
$countComments($item['replies']);
|
|
}
|
|
}
|
|
};
|
|
$countComments($payload);
|
|
|
|
fwrite(STDERR, sprintf(
|
|
"M18B1 guest payload bytes: before=%d after=%d added=%d comments/replies=%d\n",
|
|
strlen(json_encode($withoutReactions, JSON_THROW_ON_ERROR)),
|
|
strlen(json_encode($payload, JSON_THROW_ON_ERROR)),
|
|
strlen(json_encode($payload, JSON_THROW_ON_ERROR)) - strlen(json_encode($withoutReactions, JSON_THROW_ON_ERROR)),
|
|
$commentCount,
|
|
));
|
|
|
|
expect(m18b1ReactionQueryCount(DB::getQueryLog()))->toBe(1)
|
|
->and($payload[0])->toHaveKey('reactions')
|
|
->and($payload[0]['reactions'][ReactionType::Heart->value]['count'])->toBe(1)
|
|
->and($payload[0]['reactions'][ReactionType::Heart->value]['mine'])->toBeFalse()
|
|
->and($payload[0]['replies'][0]['reactions'][ReactionType::Fire->value]['count'])->toBe(1)
|
|
->and(count($comments))->toBe(2);
|
|
|
|
DB::disableQueryLog();
|
|
});
|
|
|
|
it('includes viewer reaction state for comments and replies with one extra query', function (): void {
|
|
[$artwork, $viewer] = m18b1ArtworkCommentTree();
|
|
|
|
DB::enableQueryLog();
|
|
DB::flushQueryLog();
|
|
|
|
$response = $this->actingAs($viewer)
|
|
->getJson("/api/artworks/{$artwork->id}/comments")
|
|
->assertOk();
|
|
$payload = $response->json('data');
|
|
|
|
expect(m18b1ReactionQueryCount(DB::getQueryLog()))->toBe(2)
|
|
->and($payload[0]['reactions'][ReactionType::Heart->value]['mine'])->toBeFalse()
|
|
->and($payload[0]['replies'][0]['reactions'][ReactionType::Fire->value]['mine'])->toBeTrue();
|
|
|
|
DB::disableQueryLog();
|
|
});
|
|
|
|
it('keeps reaction query count constant as the comment tree grows', function (): void {
|
|
[$smallArtwork] = m18b1ArtworkCommentTree(2);
|
|
[$largeArtwork] = m18b1ArtworkCommentTree(20);
|
|
|
|
DB::enableQueryLog();
|
|
|
|
DB::flushQueryLog();
|
|
$this->getJson("/api/artworks/{$smallArtwork->id}/comments")->assertOk();
|
|
$smallCount = m18b1ReactionQueryCount(DB::getQueryLog());
|
|
|
|
DB::flushQueryLog();
|
|
$this->getJson("/api/artworks/{$largeArtwork->id}/comments")->assertOk();
|
|
$largeCount = m18b1ReactionQueryCount(DB::getQueryLog());
|
|
|
|
DB::disableQueryLog();
|
|
|
|
expect($smallCount)->toBe(1)->and($largeCount)->toBe(1);
|
|
});
|