326 lines
10 KiB
PHP
326 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Shop;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Category;
|
|
use App\Models\Product;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Http\Request;
|
|
|
|
class CatalogController extends Controller
|
|
{
|
|
private const PER_PAGE = 20;
|
|
|
|
public function search(Request $request)
|
|
{
|
|
$searchQuery = trim((string) $request->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),
|
|
];
|
|
}
|
|
}
|