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>
132 lines
4.8 KiB
PHP
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');
|
|
}
|
|
}
|