BlockLayoutWizardController.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. <?php
  2. namespace App\Http\Controllers\Admin;
  3. /*
  4. * BlockLayoutWizardController — мастер генерации макетов блоков.
  5. *
  6. * Шаг 1: пользователь вставляет HTML-фрагмент секции.
  7. * Шаг 2: парсер определяет поля, показывает черновик Blade и таблицу полей для правки.
  8. * Шаг 3: сохраняем два файла и редиректим на создание блока.
  9. *
  10. * Генерируемые файлы:
  11. * app/Support/layouts/{name}.php — определение полей для реестра
  12. * resources/views/blocks/{name}.blade.php — Blade-шаблон для рендера
  13. */
  14. use App\Http\Controllers\Controller;
  15. use App\Support\BlockLayoutParser;
  16. use App\Support\BlockLayoutRegistry;
  17. use Illuminate\Http\RedirectResponse;
  18. use Illuminate\Http\Request;
  19. use Illuminate\View\View;
  20. class BlockLayoutWizardController extends Controller
  21. {
  22. // Шаг 1: форма ввода HTML
  23. public function index(): View
  24. {
  25. return view('admin.blocks.wizard', ['parsed' => null]);
  26. }
  27. // Шаг 2: парсим HTML, показываем обнаруженные поля и черновик Blade
  28. public function analyze(Request $request): View
  29. {
  30. $request->validate([
  31. 'layout_name' => 'required|string|max:100|regex:/^[a-z0-9_]+$/',
  32. 'layout_title' => 'required|string|max:255',
  33. 'html' => 'required|string',
  34. ]);
  35. $parsed = (new BlockLayoutParser)->parse($request->input('html'));
  36. return view('admin.blocks.wizard', [
  37. 'parsed' => $parsed,
  38. 'layout_name' => $request->input('layout_name'),
  39. 'layout_title' => $request->input('layout_title'),
  40. 'original_html' => $request->input('html'),
  41. ]);
  42. }
  43. // Шаг 3: сохраняем файлы макета и Blade-шаблона
  44. public function generate(Request $request): RedirectResponse
  45. {
  46. $request->validate([
  47. 'layout_name' => 'required|string|max:100|regex:/^[a-z0-9_]+$/',
  48. 'layout_title' => 'required|string|max:255',
  49. 'blade' => 'required|string',
  50. ]);
  51. $name = $request->input('layout_name');
  52. $title = $request->input('layout_title');
  53. // Защита: не перезаписываем встроенные макеты
  54. if (isset(BlockLayoutRegistry::all()[$name]) && !file_exists(app_path("Support/layouts/{$name}.php"))) {
  55. return back()->withInput()->withErrors(['layout_name' => 'Такое имя уже занято встроенным макетом.']);
  56. }
  57. // Собираем определение полей из submitted-данных формы
  58. $fields = $this->buildFieldsFromRequest($request);
  59. // Сохраняем app/Support/layouts/{name}.php
  60. $layoutPhp = $this->renderLayoutPhp($title, $fields);
  61. file_put_contents(app_path("Support/layouts/{$name}.php"), $layoutPhp);
  62. // Сохраняем resources/views/blocks/{name}.blade.php
  63. $bladeContent = "{{-- Blade-шаблон блока «{$title}». Сгенерирован мастером макетов. --}}\n"
  64. . trim($request->input('blade')) . "\n";
  65. file_put_contents(resource_path("views/blocks/{$name}.blade.php"), $bladeContent);
  66. // Сбрасываем кеш скомпилированных вьюх
  67. array_map('unlink', glob(storage_path('framework/views/*.php')));
  68. return redirect()->route('admin.blocks.create')
  69. ->with('success', "Макет «{$title}» создан. Теперь создайте блок на его основе.");
  70. }
  71. // Собирает поля из multipart-данных формы шага 2
  72. private function buildFieldsFromRequest(Request $request): array
  73. {
  74. $isRepeater = (bool) $request->input('is_repeater', false);
  75. $rawFlatFields = $request->input('flat_fields', []);
  76. $flatFields = array_values(array_filter(
  77. array_map(fn($f) => [
  78. 'name' => trim($f['name'] ?? ''),
  79. 'label' => trim($f['label'] ?? ''),
  80. 'type' => $f['type'] ?? 'text',
  81. ], $rawFlatFields),
  82. fn($f) => $f['name'] !== ''
  83. ));
  84. if (!$isRepeater) {
  85. return $flatFields;
  86. }
  87. // Repeater: плоские поля перед/после + repeater-поле
  88. $subRaw = $request->input('sub_fields', []);
  89. $subFields = array_values(array_filter(
  90. array_map(fn($f) => [
  91. 'name' => trim($f['name'] ?? ''),
  92. 'label' => trim($f['label'] ?? ''),
  93. 'type' => $f['type'] ?? 'text',
  94. ], $subRaw),
  95. fn($f) => $f['name'] !== ''
  96. ));
  97. $repeaterField = [
  98. 'name' => trim($request->input('repeater_name', 'items')),
  99. 'label' => trim($request->input('repeater_label', 'Элементы')),
  100. 'type' => 'repeater',
  101. 'sub_fields' => $subFields,
  102. ];
  103. // Плоские поля (если есть) идут перед repeater
  104. return array_merge($flatFields, [$repeaterField]);
  105. }
  106. // Рендерит PHP-файл определения макета
  107. private function renderLayoutPhp(string $title, array $fields): string
  108. {
  109. $fieldsExport = $this->varExportPretty($fields, 1);
  110. return <<<PHP
  111. <?php
  112. // Сгенерирован мастером макетов tocha_app
  113. return [
  114. 'title' => {$this->exportStr($title)},
  115. 'fields' => {$fieldsExport},
  116. ];
  117. PHP;
  118. }
  119. // Аккуратный var_export с отступами вместо стандартного
  120. private function varExportPretty(mixed $value, int $depth = 0): string
  121. {
  122. $pad = str_repeat(' ', $depth);
  123. $pad1 = str_repeat(' ', $depth + 1);
  124. if (is_string($value)) {
  125. return $this->exportStr($value);
  126. }
  127. if (!is_array($value)) {
  128. return var_export($value, true);
  129. }
  130. if (empty($value)) {
  131. return '[]';
  132. }
  133. $isList = array_keys($value) === range(0, count($value) - 1);
  134. $lines = [];
  135. foreach ($value as $k => $v) {
  136. $key = $isList ? '' : $this->exportStr((string) $k) . ' => ';
  137. $lines[] = $pad1 . $key . $this->varExportPretty($v, $depth + 1);
  138. }
  139. return "[\n" . implode(",\n", $lines) . ",\n{$pad}]";
  140. }
  141. private function exportStr(string $s): string
  142. {
  143. return "'" . addcslashes($s, "'\\") . "'";
  144. }
  145. }