input('q', '')); $query = Product::query() ->where('is_active', true) ->with('category'); if ($searchQuery === '') { $query->whereRaw('1 = 0'); } else { $this->applyNameSearch($query, $searchQuery); } $sort = $request->string('sort')->toString(); match ($sort) { 'price_asc' => $query->orderBy('price'), 'price_desc' => $query->orderByDesc('price'), 'name_asc' => $query->orderBy('name'), default => $query->orderByDesc('id'), }; $products = $query->paginate(self::PER_PAGE)->withQueryString(); return view('shop.search', [ 'products' => $products, 'searchQuery' => $searchQuery, 'sort' => $sort ?: 'newest', ]); } public function index(Request $request) { $query = Product::query()->where('is_active', true)->with('category'); if ($request->filled('q')) { $this->applyNameSearch($query, (string) $request->input('q')); } if ($request->filled('category')) { $query->whereHas('category', function ($builder) use ($request) { $builder->where('slug', $request->string('category')); }); } $products = $query->orderByDesc('id')->paginate(self::PER_PAGE)->withQueryString(); $categories = Category::query() ->where('is_active', true) ->orderBy('name') ->get(); return view('shop.catalog', [ 'products' => $products, 'categories' => $categories, ]); } public function category(Request $request, Category $category) { $baseQuery = Product::query() ->where('category_id', $category->id) ->where('is_active', true); $query = (clone $baseQuery)->with('category'); $appliedFilters = collect((array) $request->input('filters', [])) ->map(function ($value) { if (!is_scalar($value)) { return ''; } return trim((string) $value); }) ->filter(fn (string $value) => $value !== '') ->all(); if ($request->filled('q')) { $searchTerm = (string) $request->input('q'); $this->applyNameSearch($baseQuery, $searchTerm); $this->applyNameSearch($query, $searchTerm); } $filters = $this->filtersForCategory($category->slug); $priceBounds = $this->priceBounds($baseQuery); $priceFrom = $this->parseFilterNumber($request->input('price_from')); if ($priceFrom !== null) { $query->where('price', '>=', $priceFrom); } $priceTo = $this->parseFilterNumber($request->input('price_to')); if ($priceTo !== null) { $query->where('price', '<=', $priceTo); } $rangeFilters = []; foreach ($filters as $filter) { if (!$this->isRangeFilter($filter)) { continue; } $key = (string) ($filter['key'] ?? ''); if ($key === '') { continue; } $bounds = $this->numericSpecBounds($baseQuery, $key); $fromParam = $key . '_from'; $toParam = $key . '_to'; $rangeFrom = $this->parseFilterNumber($request->input($fromParam)); if ($rangeFrom !== null) { $query->whereRaw( "NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric >= ?", [$key, $rangeFrom] ); } $rangeTo = $this->parseFilterNumber($request->input($toParam)); if ($rangeTo !== null) { $query->whereRaw( "NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric <= ?", [$key, $rangeTo] ); } $rangeFilters[$key] = [ 'from' => $request->filled($fromParam) ? trim((string) $request->input($fromParam)) : $this->formatFilterNumber($bounds['min']), 'to' => $request->filled($toParam) ? trim((string) $request->input($toParam)) : $this->formatFilterNumber($bounds['max']), 'min' => $this->formatFilterNumber($bounds['min']), 'max' => $this->formatFilterNumber($bounds['max']), ]; } foreach ($filters as $filter) { if ($this->isRangeFilter($filter)) { continue; } $key = (string) ($filter['key'] ?? ''); if ($key === '') { continue; } $value = $appliedFilters[$key] ?? null; if ($value !== null) { $query->whereRaw("specs->>? = ?", [$key, $value]); } } $sort = $request->string('sort')->toString(); match ($sort) { 'price_asc' => $query->orderBy('price'), 'price_desc' => $query->orderByDesc('price'), 'name_asc' => $query->orderBy('name'), default => $query->orderByDesc('id'), }; $products = $query->paginate(self::PER_PAGE)->withQueryString(); $filterOptions = $this->filterOptions($category, $filters); $priceFilter = [ 'from' => $request->filled('price_from') ? trim((string) $request->input('price_from')) : $this->formatFilterNumber($priceBounds['min']), 'to' => $request->filled('price_to') ? trim((string) $request->input('price_to')) : $this->formatFilterNumber($priceBounds['max']), 'min' => $this->formatFilterNumber($priceBounds['min']), 'max' => $this->formatFilterNumber($priceBounds['max']), ]; return view('shop.category', [ 'category' => $category, 'products' => $products, 'filters' => $filters, 'filterOptions' => $filterOptions, 'appliedFilters' => $appliedFilters, 'rangeFilters' => $rangeFilters, 'priceFilter' => $priceFilter, 'sort' => $sort ?: 'newest', ]); } private function filtersForCategory(string $slug): array { return config('product_specs.categories.' . $slug, []); } private function filterOptions(Category $category, array $filters): array { if (empty($filters)) { return []; } $specs = $category->products() ->where('is_active', true) ->pluck('specs'); $options = []; foreach ($filters as $filter) { if ($this->isRangeFilter($filter)) { continue; } $key = (string) ($filter['key'] ?? ''); if ($key === '') { continue; } $valuesFromProducts = $specs ->pluck($key) ->filter() ->unique() ->sort() ->values() ->all(); $presetValues = collect((array) ($filter['options'] ?? [])) ->filter(fn ($value) => $value !== null && $value !== '') ->map(fn ($value) => (string) $value) ->values() ->all(); $options[$key] = collect($presetValues) ->concat($valuesFromProducts) ->map(fn ($value) => (string) $value) ->filter() ->unique() ->values() ->all(); } return $options; } private function applyNameSearch(Builder $query, string $term): void { $normalizedTerm = mb_strtolower(trim($term)); if ($normalizedTerm === '') { return; } $query->whereRaw('LOWER(name) LIKE ?', ['%' . $normalizedTerm . '%']); } private function isRangeFilter(array $filter): bool { return (string) ($filter['filter'] ?? 'select') === 'range'; } private function parseFilterNumber(mixed $value): ?float { $string = trim((string) $value); if ($string === '') { return null; } $normalized = str_replace([' ', ','], ['', '.'], $string); if (!is_numeric($normalized)) { return null; } return (float) $normalized; } private function formatFilterNumber(?float $value): string { if ($value === null) { return ''; } if (abs($value - round($value)) < 0.000001) { return (string) (int) round($value); } return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.'); } private function priceBounds(Builder $query): array { $bounds = (clone $query) ->selectRaw('MIN(price) as min_value, MAX(price) as max_value') ->first(); return [ 'min' => $this->parseFilterNumber($bounds?->min_value), 'max' => $this->parseFilterNumber($bounds?->max_value), ]; } private function numericSpecBounds(Builder $query, string $key): array { if ($key === '') { return ['min' => null, 'max' => null]; } $bounds = (clone $query) ->selectRaw( "MIN(NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric) as min_value, MAX(NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric) as max_value", [$key, $key] ) ->first(); return [ 'min' => $this->parseFilterNumber($bounds?->min_value), 'max' => $this->parseFilterNumber($bounds?->max_value), ]; } }