Extract related artwork lookup into a dedicated service.
Reuse the same related-artwork ranking on similar-art pages and keep category and tag matches from duplicating results.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\User;
|
||||
use App\Services\Maturity\ArtworkMaturityService;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class ArtworkRelatedService
|
||||
{
|
||||
private const SPLIT_PIVOT_ROW_THRESHOLD = 1000;
|
||||
|
||||
private const RELATIONS = ['user', 'group', 'categories.contentType'];
|
||||
|
||||
public function __construct(private readonly ArtworkMaturityService $maturity) {}
|
||||
|
||||
/**
|
||||
* Return the related artwork models used by the artwork page.
|
||||
*
|
||||
* The fallback query is intentionally kept equivalent to the controller's
|
||||
* original OR/EXISTS query. The split path only changes how candidates are
|
||||
* found; the final models and their relations remain the same.
|
||||
*/
|
||||
public function related(Artwork $artwork, ?User $viewer, int $limit = 12): EloquentCollection
|
||||
{
|
||||
$categoryIds = $artwork->categories->pluck('id')->filter()->values();
|
||||
$tagIds = $artwork->tags->pluck('id')->filter()->values();
|
||||
|
||||
if ($categoryIds->isEmpty() || $this->matchingPivotRows($categoryIds) > self::SPLIT_PIVOT_ROW_THRESHOLD) {
|
||||
return $this->originalQuery($artwork, $viewer, $categoryIds, $tagIds, $limit)->get();
|
||||
}
|
||||
|
||||
$candidateRows = collect()
|
||||
->merge($this->topAuthorCandidates($artwork, $viewer, $limit))
|
||||
->merge($this->topGroupCandidates($artwork, $viewer, $limit))
|
||||
->merge($this->topCategoryCandidates($artwork, $viewer, $categoryIds, $limit))
|
||||
->merge($this->topTagCandidates($artwork, $viewer, $tagIds, $limit))
|
||||
->reject(fn (object $row): bool => (int) $row->id === (int) $artwork->id)
|
||||
->unique(fn (object $row): string => (string) $row->id)
|
||||
->sortByDesc(fn (object $row): mixed => $row->published_at)
|
||||
->take($limit)
|
||||
->values();
|
||||
|
||||
$ids = $candidateRows->pluck('id')->map(static fn ($id): int => (int) $id)->all();
|
||||
if ($ids === []) {
|
||||
return new EloquentCollection;
|
||||
}
|
||||
|
||||
$hydrated = Artwork::query()
|
||||
->with(self::RELATIONS)
|
||||
->whereIn('id', $ids)
|
||||
->tap(fn (Builder $builder) => $this->maturity->applyViewerFilter($builder, $viewer))
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
return new EloquentCollection(collect($ids)
|
||||
->map(fn (int $id): ?Artwork => $hydrated->get($id))
|
||||
->filter()
|
||||
->values()
|
||||
->all());
|
||||
}
|
||||
|
||||
private function matchingPivotRows(Collection $categoryIds): int
|
||||
{
|
||||
return (int) DB::table('artwork_category')
|
||||
->whereIn('category_id', $categoryIds->all())
|
||||
->count();
|
||||
}
|
||||
|
||||
private function candidateQuery(Artwork $artwork, ?User $viewer): Builder
|
||||
{
|
||||
return Artwork::query()
|
||||
->select(['artworks.id', 'artworks.published_at'])
|
||||
->whereKeyNot($artwork->id)
|
||||
->public()
|
||||
->published()
|
||||
->tap(fn (Builder $builder) => $this->maturity->applyViewerFilter($builder, $viewer));
|
||||
}
|
||||
|
||||
private function topAuthorCandidates(Artwork $artwork, ?User $viewer, int $limit): Collection
|
||||
{
|
||||
return $this->candidateQuery($artwork, $viewer)
|
||||
->where('user_id', $artwork->user_id)
|
||||
->latest('published_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function topGroupCandidates(Artwork $artwork, ?User $viewer, int $limit): Collection
|
||||
{
|
||||
if (! $artwork->group_id) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->candidateQuery($artwork, $viewer)
|
||||
->where('group_id', $artwork->group_id)
|
||||
->latest('published_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function topCategoryCandidates(Artwork $artwork, ?User $viewer, Collection $categoryIds, int $limit): Collection
|
||||
{
|
||||
return $this->candidateQuery($artwork, $viewer)
|
||||
->join('artwork_category', 'artwork_category.artwork_id', '=', 'artworks.id')
|
||||
->whereIn('artwork_category.category_id', $categoryIds->all())
|
||||
->select(['artworks.id', 'artworks.published_at'])
|
||||
->distinct()
|
||||
->orderByDesc('artworks.published_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function topTagCandidates(Artwork $artwork, ?User $viewer, Collection $tagIds, int $limit): Collection
|
||||
{
|
||||
if ($tagIds->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->candidateQuery($artwork, $viewer)
|
||||
->whereHas('tags', fn ($query) => $query->whereIn('tags.id', $tagIds->all()))
|
||||
->latest('published_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function originalQuery(Artwork $artwork, ?User $viewer, Collection $categoryIds, Collection $tagIds, int $limit): Builder
|
||||
{
|
||||
return Artwork::query()
|
||||
->with(self::RELATIONS)
|
||||
->whereKeyNot($artwork->id)
|
||||
->public()
|
||||
->published()
|
||||
->tap(fn (Builder $builder) => $this->maturity->applyViewerFilter($builder, $viewer))
|
||||
->where(function ($query) use ($artwork, $categoryIds, $tagIds): void {
|
||||
$query->where('user_id', $artwork->user_id);
|
||||
|
||||
if ($artwork->group_id) {
|
||||
$query->orWhere('group_id', $artwork->group_id);
|
||||
}
|
||||
|
||||
if ($categoryIds->isNotEmpty()) {
|
||||
$query->orWhereHas('categories', function ($categoryQuery) use ($categoryIds): void {
|
||||
$categoryQuery->whereIn('categories.id', $categoryIds->all());
|
||||
});
|
||||
}
|
||||
|
||||
if ($tagIds->isNotEmpty()) {
|
||||
$query->orWhereHas('tags', function ($tagQuery) use ($tagIds): void {
|
||||
$tagQuery->whereIn('tags.id', $tagIds->all());
|
||||
});
|
||||
}
|
||||
})
|
||||
->latest('published_at')
|
||||
->limit($limit);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user