optimizations
This commit is contained in:
235
app/Services/CollectionSearchService.php
Normal file
235
app/Services/CollectionSearchService.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user