210 lines
7.5 KiB
PHP
210 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Studio;
|
|
|
|
use App\Models\Artwork;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Handles artwork listing queries for Studio, using Meilisearch with DB fallback.
|
|
*/
|
|
final class StudioArtworkQueryService
|
|
{
|
|
/**
|
|
* List artworks for a creator with search, filter, and sort via Meilisearch.
|
|
*
|
|
* Supported $filters keys:
|
|
* q string — free-text search
|
|
* status string — published|draft|archived
|
|
* category string — category slug
|
|
* tags array — tag slugs
|
|
* date_from string — Y-m-d
|
|
* date_to string — Y-m-d
|
|
* performance string — rising|top|low
|
|
* sort string — created_at:desc (default), ranking_score:desc, heat_score:desc, etc.
|
|
*/
|
|
public function list(int $userId, array $filters = [], int $perPage = 24): LengthAwarePaginator
|
|
{
|
|
// Skip Meilisearch when driver is null (e.g. in tests)
|
|
$driver = config('scout.driver');
|
|
if (empty($driver) || $driver === 'null') {
|
|
return $this->listViaDatabase($userId, $filters, $perPage);
|
|
}
|
|
|
|
try {
|
|
return $this->listViaMeilisearch($userId, $filters, $perPage);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('Studio: Meilisearch unavailable, falling back to DB', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
return $this->listViaDatabase($userId, $filters, $perPage);
|
|
}
|
|
}
|
|
|
|
private function listViaMeilisearch(int $userId, array $filters, int $perPage): LengthAwarePaginator
|
|
{
|
|
$q = $filters['q'] ?? '';
|
|
$filterParts = ["author_id = {$userId}"];
|
|
$sort = [];
|
|
|
|
// Status filter
|
|
$status = $filters['status'] ?? null;
|
|
if ($status === 'published') {
|
|
$filterParts[] = 'is_public = true AND is_approved = true';
|
|
} elseif ($status === 'draft') {
|
|
$filterParts[] = 'is_public = false';
|
|
}
|
|
// archived handled at DB level since Meili doesn't see soft-deleted
|
|
|
|
// Category filter
|
|
if (!empty($filters['category'])) {
|
|
$filterParts[] = 'category = "' . addslashes((string) $filters['category']) . '"';
|
|
}
|
|
|
|
// Tag filter
|
|
if (!empty($filters['tags'])) {
|
|
foreach ((array) $filters['tags'] as $tag) {
|
|
$filterParts[] = 'tags = "' . addslashes((string) $tag) . '"';
|
|
}
|
|
}
|
|
|
|
// Date range
|
|
if (!empty($filters['date_from'])) {
|
|
$filterParts[] = 'created_at >= "' . $filters['date_from'] . '"';
|
|
}
|
|
if (!empty($filters['date_to'])) {
|
|
$filterParts[] = 'created_at <= "' . $filters['date_to'] . '"';
|
|
}
|
|
|
|
// Performance quick filters
|
|
if (!empty($filters['performance'])) {
|
|
match ($filters['performance']) {
|
|
'rising' => $filterParts[] = 'heat_score > 5',
|
|
'top' => $filterParts[] = 'ranking_score > 50',
|
|
'low' => $filterParts[] = 'views < 10',
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
// Sort
|
|
$sortParam = $filters['sort'] ?? 'created_at:desc';
|
|
$validSortFields = [
|
|
'created_at', 'ranking_score', 'heat_score',
|
|
'views', 'likes', 'shares_count',
|
|
'downloads', 'comments_count', 'favorites_count',
|
|
];
|
|
$parts = explode(':', $sortParam);
|
|
if (count($parts) === 2 && in_array($parts[0], $validSortFields, true)) {
|
|
$sort[] = $parts[0] . ':' . ($parts[1] === 'asc' ? 'asc' : 'desc');
|
|
}
|
|
|
|
$options = ['filter' => implode(' AND ', $filterParts)];
|
|
if ($sort !== []) {
|
|
$options['sort'] = $sort;
|
|
}
|
|
|
|
return Artwork::search($q ?: '')
|
|
->options($options)
|
|
->query(fn (Builder $query) => $query
|
|
->with(['stats', 'categories', 'tags'])
|
|
->withCount(['comments', 'downloads'])
|
|
)
|
|
->paginate($perPage);
|
|
}
|
|
|
|
private function listViaDatabase(int $userId, array $filters, int $perPage): LengthAwarePaginator
|
|
{
|
|
$query = Artwork::where('user_id', $userId)
|
|
->with(['stats', 'categories', 'tags'])
|
|
->withCount(['comments', 'downloads']);
|
|
|
|
$status = $filters['status'] ?? null;
|
|
if ($status === 'published') {
|
|
$query->where('is_public', true)->where('is_approved', true);
|
|
} elseif ($status === 'draft') {
|
|
$query->where('is_public', false);
|
|
} elseif ($status === 'archived') {
|
|
$query->onlyTrashed();
|
|
} else {
|
|
// Show all except archived by default
|
|
$query->whereNull('deleted_at');
|
|
}
|
|
|
|
// Free-text search
|
|
if (!empty($filters['q'])) {
|
|
$q = $filters['q'];
|
|
$query->where(function (Builder $w) use ($q) {
|
|
$w->where('title', 'LIKE', "%{$q}%")
|
|
->orWhereHas('tags', fn (Builder $t) => $t->where('slug', 'LIKE', "%{$q}%"));
|
|
});
|
|
}
|
|
|
|
// Category
|
|
if (!empty($filters['category'])) {
|
|
$query->whereHas('categories', fn (Builder $c) => $c->where('slug', $filters['category']));
|
|
}
|
|
|
|
// Tags
|
|
if (!empty($filters['tags'])) {
|
|
foreach ((array) $filters['tags'] as $tag) {
|
|
$query->whereHas('tags', fn (Builder $t) => $t->where('slug', $tag));
|
|
}
|
|
}
|
|
|
|
// Date range
|
|
if (!empty($filters['date_from'])) {
|
|
$query->where('created_at', '>=', $filters['date_from']);
|
|
}
|
|
if (!empty($filters['date_to'])) {
|
|
$query->where('created_at', '<=', $filters['date_to']);
|
|
}
|
|
|
|
// Performance
|
|
if (!empty($filters['performance'])) {
|
|
$query->whereHas('stats', function (Builder $s) use ($filters) {
|
|
match ($filters['performance']) {
|
|
'rising' => $s->where('heat_score', '>', 5),
|
|
'top' => $s->where('ranking_score', '>', 50),
|
|
'low' => $s->where('views', '<', 10),
|
|
default => null,
|
|
};
|
|
});
|
|
}
|
|
|
|
// Sort
|
|
$sortParam = $filters['sort'] ?? 'created_at:desc';
|
|
$parts = explode(':', $sortParam);
|
|
$sortField = $parts[0] ?? 'created_at';
|
|
$sortDir = ($parts[1] ?? 'desc') === 'asc' ? 'asc' : 'desc';
|
|
|
|
$dbSortMap = [
|
|
'created_at' => 'artworks.created_at',
|
|
'ranking_score' => 'ranking_score',
|
|
'heat_score' => 'heat_score',
|
|
'views' => 'views',
|
|
'likes' => 'favorites',
|
|
'shares_count' => 'shares_count',
|
|
'downloads' => 'downloads',
|
|
'comments_count' => 'comments_count',
|
|
'favorites_count' => 'favorites',
|
|
];
|
|
|
|
$statsSortFields = ['ranking_score', 'heat_score', 'views', 'likes', 'shares_count', 'downloads', 'comments_count', 'favorites_count'];
|
|
|
|
if (in_array($sortField, $statsSortFields, true)) {
|
|
$dbCol = $dbSortMap[$sortField] ?? $sortField;
|
|
$query->leftJoin('artwork_stats', 'artworks.id', '=', 'artwork_stats.artwork_id')
|
|
->orderBy("artwork_stats.{$dbCol}", $sortDir)
|
|
->select('artworks.*');
|
|
} else {
|
|
$query->orderBy($dbSortMap[$sortField] ?? 'artworks.created_at', $sortDir);
|
|
}
|
|
|
|
return $query->paginate($perPage);
|
|
}
|
|
}
|