menus->all(); $rows = []; foreach (MenuRepository::SLOTS as $slot => $label) { $rows[$slot] = $this->flatten($menus[$slot]['items'] ?? []); } $pages = []; foreach ($this->pages->listForEditor() as $page) { $document = json_decode($page['document'] ?? '{}', true, 512, JSON_THROW_ON_ERROR); $pages[] = ['id' => $page['id'], 'path' => $page['path'], 'title' => $document['title'] ?? $page['slug']]; } return $this->render('admin/menus.html.twig', [ 'slots' => MenuRepository::SLOTS, 'menus' => $menus, 'rows' => $rows, 'pages' => $pages, ]); } #[Route('/%kant.admin_path%/menus/{slot}', name: 'admin_menu_save', methods: ['POST'])] public function save(Request $request, string $slot): Response { $this->denyAccessUnlessGranted('ROLE_EDITOR'); if (!$this->isCsrfTokenValid('menus', $request->request->getString('_csrf_token'))) { return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.'); } try { $this->menus->save( $slot, $request->request->getString('name'), $this->tree($request->request->all('items')), $request->request->getInt('expectedVersion', -1), ); } catch (InvalidMenu|StaleWrite $e) { return $this->back($e->getMessage()); } return $this->back('Das MenĂ¼ wurde gespeichert.', 'success'); } /** * Turns the stored tree back into the flat rows the form edits. * * @return list */ private function flatten(array $items, int $level = 1): array { $rows = []; foreach ($items as $item) { $rows[] = [ 'label' => $item['label'], 'type' => $item['type'], 'level' => $level, 'pageId' => $item['pageId'] ?? '', 'anchor' => $item['anchor'] ?? '', 'url' => $item['url'] ?? '', ]; array_push($rows, ...$this->flatten($item['children'] ?? [], $level + 1)); } return $rows; } /** * The form posts a flat, numbered list so it works without JavaScript. Order comes from the * position field, nesting from the level field: a level-2 row joins the last level-1 row. * Rows without a label are treated as removed, which is what the empty "add" row is for. */ private function tree(array $rows): array { $flat = []; foreach ($rows as $row) { if (!is_array($row) || '' === trim((string) ($row['label'] ?? ''))) { continue; } $flat[] = [ 'label' => trim((string) $row['label']), 'type' => (string) ($row['type'] ?? 'page'), 'pageId' => (string) ($row['pageId'] ?? ''), 'anchor' => ltrim(trim((string) ($row['anchor'] ?? '')), '#'), 'url' => trim((string) ($row['url'] ?? '')), 'level' => max(1, min(MenuRepository::MAX_DEPTH, (int) ($row['level'] ?? 1))), 'position' => (int) ($row['position'] ?? 0), ]; } usort($flat, static fn (array $a, array $b): int => $a['position'] <=> $b['position']); $items = []; foreach ($flat as $row) { $item = ['label' => $row['label'], 'type' => $row['type'], 'children' => []]; if ('url' === $row['type']) { $item['url'] = $row['url']; } else { $item['pageId'] = $row['pageId']; $item['anchor'] = $row['anchor']; } if (1 === $row['level'] || [] === $items) { $items[] = $item; continue; } $items[array_key_last($items)]['children'][] = $item; } return $items; } private function back(string $message, string $type = 'error'): Response { $this->addFlash($type, $message); return $this->redirectToRoute('admin_menus'); } }