A site can have further languages (Settings → Languages). A page is built once; each language adds only its texts, keyed by the stable block IDs, plus its own address under /en/… (ADR 111, document schema v27, migration 20261009000100 for page_translation). - Editor: DE | EN switch, a list of all texts with original, state (missing, translated, original changed) and filter; structure stays with the main language. Undo, drafts, publishing and revisions cover all languages at once. - Website: /en and translated slugs, lang, canonical, hreflang with x-default, og:locale, sitemap; links to translated pages follow the language; a language switch block. - Header, footer and linked sections translate the same way; menu entries carry labels per language; fixed template texts come from a small dictionary. - Forms: labels, help, options, button and success message per language; messages and the step script speak the language, the submission and the email stay in the main language. - Posts, archives, category names, consent text and the 404 page follow the language. - Single-language sites see none of it and never touch the new table. The forms administration becomes an overview with one row per form and its own edit page; fields are one line each with details collapsed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
7.9 KiB
PHP
147 lines
7.9 KiB
PHP
<?php
|
|
|
|
namespace App\Controller;
|
|
|
|
use App\Content\ButtonDesign;
|
|
use App\Content\FormRepository;
|
|
use App\Content\Languages;
|
|
use App\Content\MediaLibrary;
|
|
use App\Content\Theme;
|
|
use App\Domain\InvalidForm;
|
|
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 FormAdminController extends AbstractController
|
|
{
|
|
public function __construct(private FormRepository $forms, private Theme $theme, private MediaLibrary $media, private Languages $languages)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Die Übersicht: jedes Formular eine Zeile, dazu die Einsendungen. Bearbeitet wird ein
|
|
* Formular auf seiner eigenen Seite - mit mehreren Formularen war die eine lange Seite mit
|
|
* allen Editoren und Vorschauen nicht mehr zu überblicken (Umut, 09.10.2026).
|
|
*/
|
|
#[Route('/%kant.admin_path%/forms', name: 'admin_forms', methods: ['GET'])]
|
|
public function index(Request $request): Response
|
|
{
|
|
// Empfaenger, Felder und die Einsendungen selbst: nichts davon gehoert in die Hand einer
|
|
// Rolle, die nicht einmal eine Seite veroeffentlichen darf.
|
|
$this->denyAccessUnlessGranted('ROLE_PUBLISHER');
|
|
|
|
return $this->render('admin/forms.html.twig', [
|
|
'forms' => $this->forms->all(),
|
|
'counts' => $this->forms->submissionCounts(),
|
|
'submissions' => $this->forms->submissions($request->query->getInt('seite', 1)),
|
|
'languages' => array_map(fn (string $code): array => $this->languages->describe($code), $this->languages->extra()),
|
|
]);
|
|
}
|
|
|
|
/** Ein Formular bearbeiten, oder mit der Adresse „neu“ eines anlegen. */
|
|
#[Route('/%kant.admin_path%/forms/{slug}', name: 'admin_form_edit', requirements: ['slug' => '[a-z0-9-]{1,80}|neu'], methods: ['GET'])]
|
|
public function edit(string $slug): Response
|
|
{
|
|
$this->denyAccessUnlessGranted('ROLE_PUBLISHER');
|
|
$current = 'neu' === $slug ? null : $this->forms->findBySlug($slug);
|
|
if ('neu' !== $slug && null === $current) {
|
|
throw $this->createNotFoundException('Dieses Formular gibt es nicht.');
|
|
}
|
|
$theme = $this->theme->load();
|
|
|
|
return $this->render('admin/forms.html.twig', [
|
|
'editing' => true,
|
|
'current' => $current,
|
|
'types' => FormRepository::TYPES,
|
|
'colors' => FormRepository::COLORS,
|
|
// Die Bilder fuer eine Bildauswahl: die Mediathek als Liste, damit je Option gewaehlt statt getippt wird.
|
|
'images' => array_map(static fn (array $item): array => ['id' => $item['id'], 'title' => $item['title'], 'src' => $item['variants'][0]['src'] ?? ''], $this->media->page(1, false, 96)['items']),
|
|
'languages' => array_map(fn (string $code): array => $this->languages->describe($code), $this->languages->extra()),
|
|
// Die Vorschau zeichnet den Absendeknopf mit dem Standardbutton der Website und
|
|
// kennt die Palette, damit eine Farbe gewaehlt statt getippt wird.
|
|
'schema' => ['colors' => $theme['colors'], 'fonts' => $theme['fonts'], 'buttons' => $theme['buttons'] ?? ButtonDesign::defaults($theme['base']), 'base' => $theme['base']],
|
|
]);
|
|
}
|
|
|
|
#[Route('/%kant.admin_path%/forms/{slug}', name: 'admin_form_save', requirements: ['slug' => '[a-z0-9-]{1,80}|neu'], methods: ['POST'])]
|
|
public function save(Request $request, string $slug): Response
|
|
{
|
|
$this->denyAccessUnlessGranted('ROLE_PUBLISHER');
|
|
if (!$this->isCsrfTokenValid('forms', $request->request->getString('_csrf_token'))) {
|
|
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
|
}
|
|
$fields = [];
|
|
// Je weitere Sprache: Felder tr_<sprache>_label, _help, _unit, _options am Feld, dazu
|
|
// success_<sprache> und buttonLabel_<sprache> am Formular (ADR 111). Die Optionen stehen
|
|
// als Kommaliste in derselben Reihenfolge wie die der Hauptsprache.
|
|
$translations = [];
|
|
foreach ($this->languages->extra() as $code) {
|
|
$translations[$code] = ['success' => $request->request->getString('success_'.$code), 'buttonLabel' => $request->request->getString('buttonLabel_'.$code), 'fields' => []];
|
|
}
|
|
foreach ($request->request->all('fields') as $row) {
|
|
if (!is_array($row) || '' === trim((string) ($row['label'] ?? ''))) {
|
|
continue;
|
|
}
|
|
$originals = array_map(trim(...), explode(',', (string) ($row['options'] ?? '')));
|
|
foreach (array_keys($translations) as $code) {
|
|
$translatedOptions = array_map(trim(...), explode(',', (string) ($row['tr_'.$code.'_options'] ?? '')));
|
|
$translations[$code]['fields'][(string) ($row['name'] ?? '')] = [
|
|
'label' => (string) ($row['tr_'.$code.'_label'] ?? ''),
|
|
'help' => (string) ($row['tr_'.$code.'_help'] ?? ''),
|
|
'unit' => (string) ($row['tr_'.$code.'_unit'] ?? ''),
|
|
'options' => array_combine($originals, array_pad(array_slice($translatedOptions, 0, count($originals)), count($originals), '')),
|
|
];
|
|
}
|
|
$fields[] = [
|
|
'name' => (string) ($row['name'] ?? ''),
|
|
'type' => (string) ($row['type'] ?? 'text'),
|
|
'label' => (string) $row['label'],
|
|
'required' => '' !== (string) ($row['required'] ?? ''),
|
|
'help' => (string) ($row['help'] ?? ''),
|
|
'options' => array_map(trim(...), explode(',', (string) ($row['options'] ?? ''))),
|
|
'images' => (string) ($row['images'] ?? ''),
|
|
'min' => (string) ($row['min'] ?? ''),
|
|
'max' => (string) ($row['max'] ?? ''),
|
|
'unit' => (string) ($row['unit'] ?? ''),
|
|
// Die Bedingung: technischer Name eines früheren Auswahlfeldes und die Werte, bei
|
|
// denen dieses Feld gilt, als Kommaliste.
|
|
'showIfField' => (string) ($row['showIfField'] ?? ''),
|
|
'showIfValues' => (string) ($row['showIfValues'] ?? ''),
|
|
];
|
|
}
|
|
try {
|
|
$this->forms->save(
|
|
'neu' === $slug ? $request->request->getString('slug') : $slug,
|
|
$request->request->getString('name'),
|
|
$fields,
|
|
[
|
|
'recipient' => $request->request->getString('recipient'),
|
|
'sender' => $request->request->getString('sender'),
|
|
'subject' => $request->request->getString('subject'),
|
|
'success' => $request->request->getString('success'),
|
|
'replyToField' => $request->request->getString('replyToField'),
|
|
'buttonLabel' => $request->request->getString('buttonLabel'),
|
|
'summary' => '' !== $request->request->getString('summary'),
|
|
'translations' => $translations,
|
|
...array_map(fn (string $field): string => $request->request->getString($field), array_combine(array_keys(FormRepository::COLORS), array_keys(FormRepository::COLORS))),
|
|
],
|
|
$request->request->getInt('expectedVersion', -1),
|
|
);
|
|
} catch (InvalidForm|StaleWrite $e) {
|
|
return $this->back($e->getMessage(), 'error', $slug);
|
|
}
|
|
|
|
return $this->back('Das Formular wurde gespeichert.', 'success', 'neu' === $slug ? $request->request->getString('slug') : $slug);
|
|
}
|
|
|
|
private function back(string $message, string $type = 'error', ?string $slug = null): Response
|
|
{
|
|
$this->addFlash($type, $message);
|
|
|
|
// Zurück zum bearbeiteten Formular; ohne Adresse (abgelaufene Sitzung) zur Übersicht.
|
|
return null === $slug ? $this->redirectToRoute('admin_forms') : $this->redirectToRoute('admin_form_edit', ['slug' => $slug]);
|
|
}
|
|
}
|