CarController.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. <?php
  2. /*
  3. * CarController — CRUD-контроллер для управления автомобилями в административной панели.
  4. *
  5. * Создан: 2026-05-06
  6. * Маршруты: resource /admin/cars (index, create, store, edit, update, destroy)
  7. * Защита: middleware 'admin' → EnsureUserIsAdmin — только авторизованные администраторы
  8. * Зависимости: модель Car (app/Models/Car.php), DictSection (справочники для форм)
  9. */
  10. namespace App\Http\Controllers\Admin;
  11. use App\Http\Controllers\Controller;
  12. use App\Models\Car;
  13. use App\Models\DictSection;
  14. use Illuminate\Http\Request;
  15. use Illuminate\Support\Facades\Cache;
  16. use Illuminate\Support\Facades\Storage;
  17. class CarController extends Controller
  18. {
  19. // Список с фильтрацией: текст (марка/модель/VIN/заголовок) + поля (статус, марка и др.)
  20. // paginate(20)->withQueryString() — сохраняет фильтры в ссылках страниц
  21. // Вьюха: resources/views/admin/cars/index.blade.php
  22. public function index(Request $request)
  23. {
  24. $query = Car::query()->orderByDesc('id');
  25. if ($request->filled('q')) {
  26. $q = '%'.$request->q.'%';
  27. $query->where(function ($qb) use ($q) {
  28. $qb->where('make', 'like', $q)
  29. ->orWhere('model', 'like', $q)
  30. ->orWhere('title', 'like', $q)
  31. ->orWhere('vin', 'like', $q);
  32. });
  33. }
  34. foreach (['status', 'condition', 'make', 'engine_type', 'transmission', 'drive', 'platform'] as $field) {
  35. if ($request->filled($field)) {
  36. $query->where($field, $request->input($field));
  37. }
  38. }
  39. $cars = $query->paginate(20)->withQueryString();
  40. $makes = Car::select('make')->distinct()->orderBy('make')->pluck('make');
  41. // Площадки строго из справочника — чтобы отображались все, даже без привязанных авто
  42. $platformSection = DictSection::where('code', 'platforms')->with('values')->first();
  43. $platforms = $platformSection ? $platformSection->values->pluck('value') : collect();
  44. return view('admin.cars.index', compact('cars', 'makes', 'platforms'));
  45. }
  46. // Форма создания нового автомобиля
  47. // keyBy('code') — индексирует коллекцию по полю code для удобного доступа в шаблоне: $dictSections->get('makes')
  48. // Вьюха: resources/views/admin/cars/form.blade.php (общая для create и edit)
  49. public function create()
  50. {
  51. $dictSections = DictSection::with('values')->orderBy('sort_order')->get()->keyBy('code');
  52. return view('admin.cars.form', compact('dictSections'));
  53. }
  54. // Сохранение нового автомобиля: validated() → Car::create() → handlePhotos() → редирект
  55. public function store(Request $request)
  56. {
  57. $data = $this->validated($request);
  58. $car = Car::create($data);
  59. $this->handlePhotos($request, $car);
  60. Cache::flush(); // каталог с фильтрами — сбрасываем весь кеш при изменении авто
  61. return redirect()->route('admin.cars.index')->with('success', 'Автомобиль добавлен.');
  62. }
  63. // Форма редактирования: $car передаётся через route model binding по {car} в URL
  64. // Та же вьюха form.blade.php — внутри проверяется isset($car)
  65. public function edit(Car $car)
  66. {
  67. $dictSections = DictSection::with('values')->orderBy('sort_order')->get()->keyBy('code');
  68. return view('admin.cars.form', compact('car', 'dictSections'));
  69. }
  70. // Обновление данных автомобиля: логика аналогична store()
  71. public function update(Request $request, Car $car)
  72. {
  73. $data = $this->validated($request);
  74. $car->update($data);
  75. $this->handlePhotos($request, $car);
  76. Cache::flush(); // каталог с фильтрами — сбрасываем весь кеш при изменении авто
  77. return redirect()->route('admin.cars.index')->with('success', 'Автомобиль обновлён.');
  78. }
  79. // Удаление: сначала удаляет файлы с диска, затем запись из БД
  80. // photo_main и photos_gallery (JSON-массив) удаляются из Storage::disk('public')
  81. public function destroy(Car $car)
  82. {
  83. if ($car->photo_main) {
  84. Storage::disk('public')->delete($car->photo_main);
  85. }
  86. if ($car->photos_gallery) {
  87. foreach ($car->photos_gallery as $path) {
  88. Storage::disk('public')->delete($path);
  89. }
  90. }
  91. $car->delete();
  92. Cache::flush();
  93. return redirect()->route('admin.cars.index')->with('success', 'Автомобиль удалён.');
  94. }
  95. // Правила валидации для формы автомобиля — вынесены в приватный метод
  96. // чтобы не дублировать между store() и update(); фото-поля здесь не включены
  97. private function validated(Request $request): array
  98. {
  99. return $request->validate([
  100. 'status' => 'required|in:active,sold,draft',
  101. 'condition' => 'required|in:new,used',
  102. 'make' => 'required|string|max:64',
  103. 'model' => 'required|string|max:64',
  104. 'generation' => 'nullable|string|max:64',
  105. 'year' => 'required|integer|min:1900|max:2100',
  106. 'vin' => 'nullable|string|max:17',
  107. 'plate' => 'nullable|string|max:20',
  108. 'body_type' => 'nullable|string|max:32',
  109. 'doors' => 'nullable|integer|min:1|max:10',
  110. 'color_exterior' => 'nullable|string|max:32',
  111. 'color_interior' => 'nullable|string|max:32',
  112. 'engine_type' => 'nullable|in:petrol,diesel,hybrid,electric,gas,other',
  113. 'engine_volume' => 'nullable|numeric|min:0|max:99',
  114. 'engine_power_hp' => 'nullable|integer|min:0|max:5000',
  115. 'transmission' => 'nullable|in:manual,automatic,robot,variator,electric',
  116. 'drive' => 'nullable|in:FWD,RWD,AWD,4WD',
  117. 'mileage_km' => 'nullable|integer|min:0',
  118. 'steering' => 'required|in:left,right',
  119. 'owners_count' => 'nullable|integer|min:0',
  120. 'customs_cleared' => 'nullable|boolean',
  121. 'pts' => 'nullable|in:original,duplicate,electronic',
  122. 'accident_free' => 'nullable|boolean',
  123. 'price_usd' => 'nullable|integer|min:0',
  124. 'price_rub' => 'nullable|integer|min:0',
  125. 'price_vladivostok' => 'nullable|integer|min:0',
  126. 'price_moscow' => 'nullable|integer|min:0',
  127. 'price_negotiable' => 'nullable|boolean',
  128. 'country_origin' => 'nullable|string|max:64',
  129. 'city' => 'nullable|string|max:64',
  130. 'platform' => 'required|string|max:64',
  131. 'options' => 'nullable|array',
  132. 'title' => 'nullable|string|max:128',
  133. 'description' => 'nullable|string',
  134. ]);
  135. }
  136. // Загрузка и обновление фото: вызывается из store() и update() после сохранения записи
  137. // 1. photo_main — старый файл удаляется, новый → storage/app/public/cars/{id}/main/
  138. // 2. photos_gallery — новые фото добавляются к JSON-массиву → cars/{id}/gallery/
  139. // 3. delete_gallery — пути из формы → удаление файла + фильтрация массива + array_values()
  140. // Доступ через web: /storage/cars/... (требует: php artisan storage:link)
  141. private function handlePhotos(Request $request, Car $car): void
  142. {
  143. if ($request->hasFile('photo_main') && $request->file('photo_main')->isValid()) {
  144. if ($car->photo_main) {
  145. Storage::disk('public')->delete($car->photo_main);
  146. }
  147. $path = $request->file('photo_main')->store("cars/{$car->id}/main", 'public');
  148. $car->update(['photo_main' => $path]);
  149. }
  150. if ($request->hasFile('photos_gallery')) {
  151. $gallery = $car->photos_gallery ?? [];
  152. foreach ($request->file('photos_gallery') as $file) {
  153. if ($file->isValid()) {
  154. $gallery[] = $file->store("cars/{$car->id}/gallery", 'public');
  155. }
  156. }
  157. $car->update(['photos_gallery' => $gallery]);
  158. }
  159. if ($request->filled('delete_gallery')) {
  160. $toDelete = (array) $request->input('delete_gallery');
  161. $gallery = $car->photos_gallery ?? [];
  162. foreach ($toDelete as $path) {
  163. Storage::disk('public')->delete($path);
  164. $gallery = array_filter($gallery, fn ($p) => $p !== $path);
  165. }
  166. $car->update(['photos_gallery' => array_values($gallery)]);
  167. }
  168. }
  169. }