Files
KantSites/src/Controller/MenuController.php
T
kantarciandClaude Opus 5 aaea385422 Die Verwaltung kann umziehen: /admin ist frei waehlbar
php bin/console cms:admin:path <neu> legt die Verwaltung auf eine
andere Adresse. Geprueft wird, dass der Name frei ist und keine Seite
verdeckt; der Wert steht in .env.local und fuellt einen Parameter, der
als Praefix in jeder Verwaltungsroute steht, in der Firewall, in den
Schutzkopfzeilen und in der Statistik.

Die robots.txt nennt einen geaenderten Namen nicht - sie ist oeffentlich
und waere sonst ein Wegweiser. Beim Standardnamen bleibt die Zeile,
weil diese Adresse ohnehin jeder kennt.

Die Auffangroute fuer Seiten steht jetzt zuletzt: sie kann einen frei
gewaehlten Namen nicht per Regex ausschliessen, und die Reihenfolge ist
ohnehin die richtige. Der Befehl raeumt den Zwischenspeicher auf, aber
nicht den, in dem er selbst laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:51:55 +02:00

132 lines
4.8 KiB
PHP

<?php
namespace App\Controller;
use App\Content\MenuRepository;
use App\Content\PageRepository;
use App\Domain\InvalidMenu;
use App\Domain\StaleWrite;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class MenuController extends AbstractController
{
public function __construct(private MenuRepository $menus, private PageRepository $pages)
{
}
#[Route('/%kant.admin_path%/menus', name: 'admin_menus', methods: ['GET'])]
public function index(): Response
{
$menus = $this->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<array{label: string, type: string, pageId: string, anchor: string, url: string, level: int}>
*/
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');
}
}