CatalogController.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. <?php
  2. namespace App\Http\Controllers;
  3. /*
  4. * CatalogController — публичный каталог и страница автомобиля.
  5. *
  6. * Кеш: plain array (не объекты) → без __PHP_Incomplete_Class при unserialize.
  7. * Ключ кеша включает все параметры фильтра + страницу пагинации.
  8. * filterOptions() кешируется отдельно — пересобирать при каждом запросе дорого.
  9. */
  10. use App\Models\Car;
  11. use Illuminate\Http\Request;
  12. use Illuminate\Pagination\LengthAwarePaginator;
  13. use Illuminate\Support\Facades\Cache;
  14. use Illuminate\View\View;
  15. class CatalogController extends Controller
  16. {
  17. private const CACHE_TTL = 86400; // 24 часа
  18. private const PER_PAGE = 12;
  19. public function index(Request $request): View
  20. {
  21. // ── Читаем фильтры из GET ──────────────────────────────────────
  22. $conditions = array_filter((array) $request->input('condition', []));
  23. $countries = array_filter((array) $request->input('country_origin', []));
  24. $fuels = array_filter((array) $request->input('engine_type', []));
  25. $make = trim((string) $request->input('make', ''));
  26. $bodyType = trim((string) $request->input('body_type', ''));
  27. $priceTo = $request->filled('price_to') ? (int) $request->input('price_to') : null;
  28. $priceFrom = null;
  29. // price_range из быстрого подбора: "1500000", "1500000-3000000", "5000000-"
  30. $priceRange = $request->input('price_range', '');
  31. if ($priceRange !== '') {
  32. if (str_ends_with($priceRange, '-')) {
  33. $priceFrom = (int) rtrim($priceRange, '-') ?: null;
  34. } elseif (str_contains($priceRange, '-')) {
  35. [$from, $to] = explode('-', $priceRange, 2);
  36. $priceFrom = $from !== '' ? (int) $from : null;
  37. $priceTo = $to !== '' ? (int) $to : $priceTo;
  38. } else {
  39. $priceTo = (int) $priceRange ?: $priceTo;
  40. }
  41. }
  42. $year = $request->filled('year') ? (int) $request->input('year') : null;
  43. $sort = $request->input('sort', '');
  44. $page = $request->integer('page', 1);
  45. // ── Строим ключ кеша ──────────────────────────────────────────
  46. $filterData = compact('conditions', 'countries', 'fuels', 'make', 'bodyType', 'priceFrom', 'priceTo', 'year', 'sort');
  47. ksort($filterData);
  48. $cacheKey = 'catalog.'.md5(serialize($filterData)).'.p'.$page;
  49. $cached = Cache::remember($cacheKey, self::CACHE_TTL, function () use (
  50. $conditions, $countries, $fuels, $make, $bodyType, $priceFrom, $priceTo, $year, $sort, $page
  51. ) {
  52. $query = Car::where('status', 'active');
  53. if ($conditions) {
  54. $query->whereIn('condition', $conditions);
  55. }
  56. if ($countries) {
  57. $query->whereIn('country_origin', $countries);
  58. }
  59. if ($fuels) {
  60. $query->whereIn('engine_type', $fuels);
  61. }
  62. if ($make) {
  63. $query->where('make', $make);
  64. }
  65. if ($bodyType) {
  66. $query->where('body_type', $bodyType);
  67. }
  68. if ($priceFrom) {
  69. $query->where('price_rub', '>=', $priceFrom);
  70. }
  71. if ($priceTo) {
  72. $query->where('price_rub', '<=', $priceTo);
  73. }
  74. if ($year) {
  75. $query->where('year', $year);
  76. }
  77. match ($sort) {
  78. 'price_asc' => $query->orderBy('price_rub'),
  79. 'price_desc' => $query->orderByDesc('price_rub'),
  80. 'year_desc' => $query->orderByDesc('year'),
  81. default => $query->orderByDesc('id'),
  82. };
  83. $paginator = $query->paginate(self::PER_PAGE, ['*'], 'page', $page);
  84. return [
  85. 'total' => $paginator->total(),
  86. 'items' => $paginator->getCollection()->map->getAttributes()->all(),
  87. ];
  88. });
  89. // Восстанавливаем Car из атрибутов (без unserialize объекта)
  90. $items = collect($cached['items'])->map(fn ($a) => (new Car)->setRawAttributes($a, true));
  91. $cars = new LengthAwarePaginator(
  92. $items, $cached['total'], self::PER_PAGE, $page,
  93. ['path' => $request->url(), 'query' => $request->query()]
  94. );
  95. $filterOptions = $this->filterOptions();
  96. $activeFilters = compact('conditions', 'countries', 'fuels', 'make', 'bodyType', 'priceFrom', 'priceTo', 'year', 'sort');
  97. return view('pages.catalog', compact('cars', 'filterOptions', 'activeFilters'));
  98. }
  99. public function show(int $id): View
  100. {
  101. // Детальная страница: без кеша — запросы редкие, зато данные всегда свежие
  102. $car = Car::where('status', 'active')->findOrFail($id);
  103. $similar = Car::where('status', 'active')
  104. ->where('id', '!=', $car->id)
  105. ->where(fn ($q) => $q->where('make', $car->make)->orWhere('body_type', $car->body_type))
  106. ->orderByDesc('id')
  107. ->limit(3)
  108. ->get();
  109. return view('pages.car', compact('car', 'similar'));
  110. }
  111. // Опции для фильтров — кешируются отдельно
  112. private function filterOptions(): array
  113. {
  114. return Cache::remember('catalog_filter_opts', self::CACHE_TTL, function () {
  115. $base = Car::where('status', 'active');
  116. return [
  117. 'makes' => (clone $base)->select('make')->distinct()->orderBy('make')->pluck('make')->toArray(),
  118. 'body_types' => (clone $base)->whereNotNull('body_type')->where('body_type', '!=', '')->select('body_type')->distinct()->orderBy('body_type')->pluck('body_type')->toArray(),
  119. 'countries' => (clone $base)->whereNotNull('country_origin')->select('country_origin')->distinct()->orderBy('country_origin')->pluck('country_origin')->toArray(),
  120. 'years' => (clone $base)->select('year')->distinct()->orderByDesc('year')->pluck('year')->toArray(),
  121. 'price_min' => (int) ((clone $base)->whereNotNull('price_rub')->min('price_rub') ?? 0),
  122. 'price_max' => (int) ((clone $base)->whereNotNull('price_rub')->max('price_rub') ?? 50000000),
  123. ];
  124. });
  125. }
  126. }