BlockAdminController.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. <?php
  2. namespace App\Http\Controllers\Admin;
  3. /*
  4. * BlockAdminController — управление блоками контента.
  5. *
  6. * Каждый блок привязан к макету (BlockLayoutRegistry), пользователь заполняет
  7. * структурированные поля — никакой вёрстки в руках редактора.
  8. * Данные хранятся в JSON-поле data, рендеринг — через Blade-шаблон blocks/{layout}.
  9. *
  10. * Изображения: загружаются через ImageUploadService, хранятся в storage/public/blocks/,
  11. * ресайзятся по размерам из определения поля макета, конвертируются в WebP.
  12. */
  13. use App\Http\Controllers\Controller;
  14. use App\Models\Block;
  15. use App\Services\ImageUploadService;
  16. use App\Support\BlockLayoutRegistry;
  17. use Illuminate\Http\RedirectResponse;
  18. use Illuminate\Http\Request;
  19. use Illuminate\Support\Facades\Cache;
  20. use Illuminate\View\View;
  21. class BlockAdminController extends Controller
  22. {
  23. public function __construct(private ImageUploadService $images) {}
  24. public function index(): View
  25. {
  26. $blocks = Block::orderBy('title')->get();
  27. return view('admin.blocks.index', compact('blocks'));
  28. }
  29. public function create(): View
  30. {
  31. return view('admin.blocks.form', [
  32. 'block' => new Block,
  33. 'layouts' => BlockLayoutRegistry::all(),
  34. 'layoutDef' => null,
  35. ]);
  36. }
  37. public function store(Request $request): RedirectResponse
  38. {
  39. $validated = $request->validate([
  40. 'name' => 'required|string|max:100|regex:/^[a-z0-9_]+$/|unique:blocks,name',
  41. 'title' => 'required|string|max:255',
  42. 'layout' => 'required|string|in:'.implode(',', BlockLayoutRegistry::keys()),
  43. 'is_active' => 'nullable|boolean',
  44. ]);
  45. $validated['is_active'] = $request->boolean('is_active');
  46. $validated['data'] = $this->buildData($request, $validated['layout'], null);
  47. Block::create($validated);
  48. return redirect()->route('admin.blocks.index')
  49. ->with('success', 'Блок «'.$validated['title'].'» создан.');
  50. }
  51. public function edit(Block $block): View
  52. {
  53. return view('admin.blocks.form', [
  54. 'block' => $block,
  55. 'layouts' => BlockLayoutRegistry::all(),
  56. 'layoutDef' => BlockLayoutRegistry::get($block->layout ?? ''),
  57. ]);
  58. }
  59. public function update(Request $request, Block $block): RedirectResponse
  60. {
  61. $validated = $request->validate([
  62. 'title' => 'required|string|max:255',
  63. 'is_active' => 'nullable|boolean',
  64. ]);
  65. $validated['is_active'] = $request->boolean('is_active');
  66. $validated['data'] = $this->buildData($request, $block->layout, $block->data ?? []);
  67. $block->update($validated);
  68. Cache::forget('block.'.$block->name);
  69. // Cache::tags() не поддерживается database-драйвером — сбрасываем секции всех страниц явно
  70. foreach (['home', 'services', 'contacts', 'privacy', 'offer'] as $slug) {
  71. Cache::forget('page_sections.'.$slug);
  72. Cache::forget('page.'.$slug);
  73. }
  74. return redirect()->route('admin.blocks.index')
  75. ->with('success', 'Блок «'.$block->title.'» обновлён.');
  76. }
  77. public function destroy(Block $block): RedirectResponse
  78. {
  79. Cache::forget('block.'.$block->name);
  80. $title = $block->title;
  81. $block->delete();
  82. return redirect()->route('admin.blocks.index')
  83. ->with('success', 'Блок «'.$title.'» удалён.');
  84. }
  85. // ── Сборка data: текстовые поля + загруженные изображения ────────────
  86. private function buildData(Request $request, ?string $layoutKey, ?array $oldData): array
  87. {
  88. $layoutDef = $layoutKey ? BlockLayoutRegistry::get($layoutKey) : null;
  89. if (! $layoutDef) {
  90. return [];
  91. }
  92. $raw = $request->input('data', []);
  93. $uploads = $request->file('uploads', []);
  94. $result = [];
  95. foreach ($layoutDef['fields'] as $field) {
  96. $name = $field['name'];
  97. if ($field['type'] === 'repeater') {
  98. $rows = array_values($raw[$name] ?? []);
  99. $oldRows = $oldData[$name] ?? [];
  100. $result[$name] = array_map(function (array $row, int $i) use ($field, $uploads, $name, $oldRows, $layoutKey) {
  101. $clean = [];
  102. foreach ($field['sub_fields'] as $sub) {
  103. $subName = $sub['name'];
  104. $oldVal = $oldRows[$i][$subName] ?? '';
  105. $uploadFile = data_get($uploads, "{$name}.{$i}.{$subName}");
  106. if ($sub['type'] === 'image' && $uploadFile?->isValid()) {
  107. $this->images->delete($oldVal);
  108. $clean[$subName] = $this->images->store($uploadFile, $layoutKey, $sub);
  109. } else {
  110. $clean[$subName] = $sub['type'] === 'image'
  111. ? ($row[$subName] ?? $oldVal) // hidden input сохраняет старый путь
  112. : trim($row[$subName] ?? '');
  113. }
  114. }
  115. return $clean;
  116. }, $rows, array_keys($rows));
  117. } elseif ($field['type'] === 'checkbox') {
  118. // Булев флаг: hidden-поле даёт "0", чекбокс при отмеченном даёт "1"
  119. $result[$name] = (bool) ($raw[$name] ?? false);
  120. } elseif (in_array($field['type'], ['cars_picker', 'makes_picker'])) {
  121. // Мультиселект — хранится как plain array значений
  122. $result[$name] = array_values(array_filter((array) ($raw[$name] ?? [])));
  123. } elseif ($field['type'] === 'reviews_picker') {
  124. // Мультиселект отзывов — хранится как plain array целочисленных ID
  125. $result[$name] = array_values(array_map('intval', array_filter((array) ($raw[$name] ?? []))));
  126. } elseif ($field['type'] === 'services_picker') {
  127. // Мультиселект услуг — plain array целочисленных ID
  128. $result[$name] = array_values(array_map('intval', array_filter((array) ($raw[$name] ?? []))));
  129. } elseif ($field['type'] === 'image') {
  130. $oldVal = $oldData[$name] ?? '';
  131. $uploadFile = data_get($uploads, $name);
  132. if ($uploadFile?->isValid()) {
  133. $this->images->delete($oldVal);
  134. $result[$name] = $this->images->store($uploadFile, $layoutKey, $field);
  135. } else {
  136. // Сохраняем путь из hidden-input (не трогаем старый файл)
  137. $result[$name] = $raw[$name] ?? $oldVal;
  138. }
  139. } else {
  140. $result[$name] = trim($raw[$name] ?? '');
  141. }
  142. }
  143. return $result;
  144. }
  145. }