235 lines
10 KiB
PHP
235 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Category;
|
|
use App\Models\Collection;
|
|
use App\Models\User;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class CollectionSearchService
|
|
{
|
|
public function publicSearch(array $filters, int $perPage = 18): LengthAwarePaginator
|
|
{
|
|
$query = Collection::query()
|
|
->publicEligible()
|
|
->with(['user:id,username,name', 'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at']);
|
|
|
|
$this->applySharedFilters($query, $filters, false);
|
|
$this->applyPublicSort($query, (string) ($filters['sort'] ?? 'trending'));
|
|
|
|
return $query->paginate(max(1, min($perPage, 24)))->withQueryString();
|
|
}
|
|
|
|
public function ownerSearch(User $user, array $filters, int $perPage = 20): LengthAwarePaginator
|
|
{
|
|
$query = Collection::query()
|
|
->with(['user:id,username,name', 'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at'])
|
|
->when(! $user->isAdmin() && ! $user->isModerator(), fn ($builder) => $builder->where('user_id', $user->id));
|
|
|
|
$this->applySharedFilters($query, $filters, true);
|
|
|
|
return $query
|
|
->orderByDesc('updated_at')
|
|
->paginate(max(1, min($perPage, 50)))
|
|
->withQueryString();
|
|
}
|
|
|
|
public function publicFilterOptions(): array
|
|
{
|
|
$themeOptions = Collection::query()
|
|
->publicEligible()
|
|
->whereNotNull('theme_token')
|
|
->where('theme_token', '!=', '')
|
|
->select('theme_token')
|
|
->distinct()
|
|
->orderBy('theme_token')
|
|
->limit(12)
|
|
->pluck('theme_token')
|
|
->map(fn ($token): array => [
|
|
'value' => (string) $token,
|
|
'label' => $this->humanizeToken((string) $token),
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$categoryOptions = Category::query()
|
|
->active()
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->limit(16)
|
|
->get(['slug', 'name'])
|
|
->map(fn (Category $category): array => [
|
|
'value' => (string) $category->slug,
|
|
'label' => (string) $category->name,
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$styleOptions = collect((array) config('collections.smart_rules.style_terms', []))
|
|
->map(fn ($term): array => [
|
|
'value' => (string) $term,
|
|
'label' => $this->humanizeToken((string) $term),
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$colorOptions = collect((array) config('collections.smart_rules.color_terms', []))
|
|
->map(fn ($term): array => [
|
|
'value' => (string) $term,
|
|
'label' => $this->humanizeToken((string) $term),
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
return [
|
|
'category' => $categoryOptions,
|
|
'style' => $styleOptions,
|
|
'theme' => $themeOptions,
|
|
'color' => $colorOptions,
|
|
'quality_tier' => [
|
|
['value' => 'editorial', 'label' => 'Editorial'],
|
|
['value' => 'high', 'label' => 'High'],
|
|
['value' => 'standard', 'label' => 'Standard'],
|
|
['value' => 'limited', 'label' => 'Limited'],
|
|
],
|
|
];
|
|
}
|
|
|
|
private function applySharedFilters($query, array $filters, bool $includeInternal): void
|
|
{
|
|
if (filled($filters['q'] ?? null)) {
|
|
$term = '%' . trim((string) $filters['q']) . '%';
|
|
$query->where(function ($builder) use ($term): void {
|
|
$builder->where('title', 'like', $term)
|
|
->orWhere('summary', 'like', $term)
|
|
->orWhere('description', 'like', $term)
|
|
->orWhere('campaign_label', 'like', $term)
|
|
->orWhere('series_title', 'like', $term)
|
|
->orWhereHas('user', function (Builder $userQuery) use ($term): void {
|
|
$userQuery->where('username', 'like', $term)
|
|
->orWhere('name', 'like', $term);
|
|
});
|
|
});
|
|
}
|
|
|
|
foreach (['type', 'visibility', 'lifecycle_state', 'mode', 'campaign_key', 'program_key', 'workflow_state', 'health_state'] as $field) {
|
|
if (filled($filters[$field] ?? null)) {
|
|
$query->where($field, (string) $filters[$field]);
|
|
}
|
|
}
|
|
|
|
if (filled($filters['quality_tier'] ?? null)) {
|
|
$query->where('trust_tier', (string) $filters['quality_tier']);
|
|
}
|
|
|
|
if (filled($filters['theme'] ?? null)) {
|
|
$theme = trim((string) $filters['theme']);
|
|
$themeLike = '%' . mb_strtolower($theme) . '%';
|
|
|
|
$query->where(function (Builder $builder) use ($theme, $themeLike): void {
|
|
$builder->whereRaw('LOWER(theme_token) = ?', [mb_strtolower($theme)])
|
|
->orWhereHas('entityLinks', function (Builder $linkQuery) use ($themeLike): void {
|
|
$linkQuery->where('linked_type', CollectionLinkService::TYPE_TAG)
|
|
->whereExists(function ($tagQuery) use ($themeLike): void {
|
|
$tagQuery->select(DB::raw('1'))
|
|
->from('tags')
|
|
->whereColumn('tags.id', 'collection_entity_links.linked_id')
|
|
->where(function ($tagBuilder) use ($themeLike): void {
|
|
$tagBuilder->whereRaw('LOWER(tags.slug) like ?', [$themeLike])
|
|
->orWhereRaw('LOWER(tags.name) like ?', [$themeLike]);
|
|
});
|
|
});
|
|
})
|
|
->orWhereHas('artworks.tags', function (Builder $tagQuery) use ($themeLike): void {
|
|
$tagQuery->whereRaw('LOWER(tags.slug) like ?', [$themeLike])
|
|
->orWhereRaw('LOWER(tags.name) like ?', [$themeLike]);
|
|
});
|
|
});
|
|
}
|
|
|
|
if (filled($filters['category'] ?? null)) {
|
|
$category = trim((string) $filters['category']);
|
|
$categoryLike = '%' . mb_strtolower($category) . '%';
|
|
|
|
$query->where(function (Builder $builder) use ($categoryLike): void {
|
|
$builder->whereHas('entityLinks', function (Builder $linkQuery) use ($categoryLike): void {
|
|
$linkQuery->where('linked_type', CollectionLinkService::TYPE_CATEGORY)
|
|
->whereExists(function ($categoryQuery) use ($categoryLike): void {
|
|
$categoryQuery->select(DB::raw('1'))
|
|
->from('categories')
|
|
->whereColumn('categories.id', 'collection_entity_links.linked_id')
|
|
->where(function ($inner) use ($categoryLike): void {
|
|
$inner->whereRaw('LOWER(categories.slug) like ?', [$categoryLike])
|
|
->orWhereRaw('LOWER(categories.name) like ?', [$categoryLike]);
|
|
});
|
|
});
|
|
})->orWhereHas('artworks.categories', function (Builder $categoryQuery) use ($categoryLike): void {
|
|
$categoryQuery->whereRaw('LOWER(categories.slug) like ?', [$categoryLike])
|
|
->orWhereRaw('LOWER(categories.name) like ?', [$categoryLike]);
|
|
});
|
|
});
|
|
}
|
|
|
|
if (filled($filters['style'] ?? null)) {
|
|
$style = trim((string) $filters['style']);
|
|
$styleLike = '%' . mb_strtolower($style) . '%';
|
|
|
|
$query->where(function (Builder $builder) use ($styleLike): void {
|
|
$builder->whereRaw('LOWER(spotlight_style) like ?', [$styleLike])
|
|
->orWhereRaw('LOWER(presentation_style) like ?', [$styleLike])
|
|
->orWhereHas('artworks.tags', function (Builder $tagQuery) use ($styleLike): void {
|
|
$tagQuery->whereRaw('LOWER(tags.slug) like ?', [$styleLike])
|
|
->orWhereRaw('LOWER(tags.name) like ?', [$styleLike]);
|
|
});
|
|
});
|
|
}
|
|
|
|
if (filled($filters['color'] ?? null)) {
|
|
$color = trim((string) $filters['color']);
|
|
$colorLike = '%' . mb_strtolower($color) . '%';
|
|
|
|
$query->where(function (Builder $builder) use ($colorLike): void {
|
|
$builder->whereRaw('LOWER(theme_token) like ?', [$colorLike])
|
|
->orWhereHas('artworks.tags', function (Builder $tagQuery) use ($colorLike): void {
|
|
$tagQuery->whereRaw('LOWER(tags.slug) like ?', [$colorLike])
|
|
->orWhereRaw('LOWER(tags.name) like ?', [$colorLike]);
|
|
});
|
|
});
|
|
}
|
|
|
|
if (filled($filters['placement_eligibility'] ?? null) && $includeInternal) {
|
|
$query->where('placement_eligibility', filter_var($filters['placement_eligibility'], FILTER_VALIDATE_BOOLEAN));
|
|
}
|
|
|
|
if (filled($filters['partner_key'] ?? null) && $includeInternal) {
|
|
$query->where('partner_key', (string) $filters['partner_key']);
|
|
}
|
|
|
|
if (filled($filters['experiment_key'] ?? null) && $includeInternal) {
|
|
$query->where('experiment_key', (string) $filters['experiment_key']);
|
|
}
|
|
}
|
|
|
|
private function applyPublicSort($query, string $sort): void
|
|
{
|
|
match ($sort) {
|
|
'recent' => $query->orderByDesc('published_at')->orderByDesc('updated_at'),
|
|
'quality' => $query->orderByDesc('health_score')->orderByDesc('quality_score'),
|
|
'evergreen' => $query->orderByDesc('quality_score')->orderByDesc('followers_count'),
|
|
default => $query->orderByDesc('ranking_score')->orderByDesc('health_score')->orderByDesc('updated_at'),
|
|
};
|
|
}
|
|
|
|
private function humanizeToken(string $value): string
|
|
{
|
|
return str($value)
|
|
->replace(['_', '-'], ' ')
|
|
->title()
|
|
->value();
|
|
}
|
|
} |