| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181 |
- <?php
- namespace App\Http\Controllers\Admin;
- /*
- * BlockAdminController — управление блоками контента.
- *
- * Каждый блок привязан к макету (BlockLayoutRegistry), пользователь заполняет
- * структурированные поля — никакой вёрстки в руках редактора.
- * Данные хранятся в JSON-поле data, рендеринг — через Blade-шаблон blocks/{layout}.
- *
- * Изображения: загружаются через ImageUploadService, хранятся в storage/public/blocks/,
- * ресайзятся по размерам из определения поля макета, конвертируются в WebP.
- */
- use App\Http\Controllers\Controller;
- use App\Models\Block;
- use App\Services\ImageUploadService;
- use App\Support\BlockLayoutRegistry;
- use Illuminate\Http\RedirectResponse;
- use Illuminate\Http\Request;
- use Illuminate\Support\Facades\Cache;
- use Illuminate\View\View;
- class BlockAdminController extends Controller
- {
- public function __construct(private ImageUploadService $images) {}
- public function index(): View
- {
- $blocks = Block::orderBy('title')->get();
- return view('admin.blocks.index', compact('blocks'));
- }
- public function create(): View
- {
- return view('admin.blocks.form', [
- 'block' => new Block,
- 'layouts' => BlockLayoutRegistry::all(),
- 'layoutDef' => null,
- ]);
- }
- public function store(Request $request): RedirectResponse
- {
- $validated = $request->validate([
- 'name' => 'required|string|max:100|regex:/^[a-z0-9_]+$/|unique:blocks,name',
- 'title' => 'required|string|max:255',
- 'layout' => 'required|string|in:'.implode(',', BlockLayoutRegistry::keys()),
- 'is_active' => 'nullable|boolean',
- ]);
- $validated['is_active'] = $request->boolean('is_active');
- $validated['data'] = $this->buildData($request, $validated['layout'], null);
- Block::create($validated);
- return redirect()->route('admin.blocks.index')
- ->with('success', 'Блок «'.$validated['title'].'» создан.');
- }
- public function edit(Block $block): View
- {
- return view('admin.blocks.form', [
- 'block' => $block,
- 'layouts' => BlockLayoutRegistry::all(),
- 'layoutDef' => BlockLayoutRegistry::get($block->layout ?? ''),
- ]);
- }
- public function update(Request $request, Block $block): RedirectResponse
- {
- $validated = $request->validate([
- 'title' => 'required|string|max:255',
- 'is_active' => 'nullable|boolean',
- ]);
- $validated['is_active'] = $request->boolean('is_active');
- $validated['data'] = $this->buildData($request, $block->layout, $block->data ?? []);
- $block->update($validated);
- Cache::forget('block.'.$block->name);
- // Cache::tags() не поддерживается database-драйвером — сбрасываем секции всех страниц явно
- foreach (['home', 'services', 'contacts', 'privacy', 'offer'] as $slug) {
- Cache::forget('page_sections.'.$slug);
- Cache::forget('page.'.$slug);
- }
- return redirect()->route('admin.blocks.index')
- ->with('success', 'Блок «'.$block->title.'» обновлён.');
- }
- public function destroy(Block $block): RedirectResponse
- {
- Cache::forget('block.'.$block->name);
- $title = $block->title;
- $block->delete();
- return redirect()->route('admin.blocks.index')
- ->with('success', 'Блок «'.$title.'» удалён.');
- }
- // ── Сборка data: текстовые поля + загруженные изображения ────────────
- private function buildData(Request $request, ?string $layoutKey, ?array $oldData): array
- {
- $layoutDef = $layoutKey ? BlockLayoutRegistry::get($layoutKey) : null;
- if (! $layoutDef) {
- return [];
- }
- $raw = $request->input('data', []);
- $uploads = $request->file('uploads', []);
- $result = [];
- foreach ($layoutDef['fields'] as $field) {
- $name = $field['name'];
- if ($field['type'] === 'repeater') {
- $rows = array_values($raw[$name] ?? []);
- $oldRows = $oldData[$name] ?? [];
- $result[$name] = array_map(function (array $row, int $i) use ($field, $uploads, $name, $oldRows, $layoutKey) {
- $clean = [];
- foreach ($field['sub_fields'] as $sub) {
- $subName = $sub['name'];
- $oldVal = $oldRows[$i][$subName] ?? '';
- $uploadFile = data_get($uploads, "{$name}.{$i}.{$subName}");
- if ($sub['type'] === 'image' && $uploadFile?->isValid()) {
- $this->images->delete($oldVal);
- $clean[$subName] = $this->images->store($uploadFile, $layoutKey, $sub);
- } else {
- $clean[$subName] = $sub['type'] === 'image'
- ? ($row[$subName] ?? $oldVal) // hidden input сохраняет старый путь
- : trim($row[$subName] ?? '');
- }
- }
- return $clean;
- }, $rows, array_keys($rows));
- } elseif ($field['type'] === 'checkbox') {
- // Булев флаг: hidden-поле даёт "0", чекбокс при отмеченном даёт "1"
- $result[$name] = (bool) ($raw[$name] ?? false);
- } elseif (in_array($field['type'], ['cars_picker', 'makes_picker'])) {
- // Мультиселект — хранится как plain array значений
- $result[$name] = array_values(array_filter((array) ($raw[$name] ?? [])));
- } elseif ($field['type'] === 'reviews_picker') {
- // Мультиселект отзывов — хранится как plain array целочисленных ID
- $result[$name] = array_values(array_map('intval', array_filter((array) ($raw[$name] ?? []))));
- } elseif ($field['type'] === 'services_picker') {
- // Мультиселект услуг — plain array целочисленных ID
- $result[$name] = array_values(array_map('intval', array_filter((array) ($raw[$name] ?? []))));
- } elseif ($field['type'] === 'image') {
- $oldVal = $oldData[$name] ?? '';
- $uploadFile = data_get($uploads, $name);
- if ($uploadFile?->isValid()) {
- $this->images->delete($oldVal);
- $result[$name] = $this->images->store($uploadFile, $layoutKey, $field);
- } else {
- // Сохраняем путь из hidden-input (не трогаем старый файл)
- $result[$name] = $raw[$name] ?? $oldVal;
- }
- } else {
- $result[$name] = trim($raw[$name] ?? '');
- }
- }
- return $result;
- }
- }
|