This commit is contained in:
325
app/Http/Controllers/Shop/CatalogController.php
Normal file
325
app/Http/Controllers/Shop/CatalogController.php
Normal file
@@ -0,0 +1,325 @@
|
||||
<?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),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user