
Contao 5 Symfony Forms: Professionelle Formularverarbeitung

Willkommen zu Teil 8 unserer Masterclass! Wenn es um professionelle Datenverarbeitung und komplexe Abläufe wie unser Strandkorb-Buchungssystem geht, sind Contao 5 Symfony Forms das ultimative Werkzeug für Entwickler. Der Kunde sieht unseren Strandkorb im Frontend, ist begeistert und möchte ihn buchen. Doch wie bekommen wir seine Daten sicher, validiert und strukturiert in unsere Datenbank?
Viele Entwickler greifen jetzt reflexartig zum klassischen Contao-Formulargenerator und versuchen, die Logik mühsam über den prepareFormData oder processFormData Hook abzufangen. Das funktioniert zwar, führt bei komplexer Business-Logik aber schnell zu unleserlichem und schwer testbarem Code.
Die moderne und professionelle Lösung in Contao 5 lautet: Contao 5 Symfony Forms. Da Contao 5 vollständig in das Symfony-Ökosystem integriert ist, können wir die mächtige Symfony Form Component nativ nutzen.
Warum Contao 5 Symfony Forms?
Objektorientierung: Ein Formular ist nicht länger ein Array aus POST-Daten, sondern ein echtes PHP-Objekt (Data Class).
Sicherheit & Validierung: CSRF-Schutz (Cross-Site Request Forgery) ist automatisch integriert. Fehlerhafte Eingaben werden durch strikte Validierungsregeln (Constraints) blockiert, bevor sie überhaupt in die Nähe der Datenbank kommen.
Trennung von Logik und Design: Die Definition der Felder (PHP), die Verarbeitung (Controller) und die Ausgabe (Twig) sind sauber getrennt.
Das ist unser Plan für dieses Modul:
Wir erstellen eine FormType-Klasse (
BookingType), die die Felder unseres Buchungsformulars definiert.Wir binden das Formular in unseren Fragment-Controller ein.
Wir rendern das Formular flexibel in unserem Twig-Template (mit Form Themes).
Wir verarbeiten den Submit, speichern die Daten über Doctrine in der Entity und versenden eine Bestätigungs-E-Mail via Symfony Mailer.
Wichtiges Vorwissen: Anders als beim Contao-Formulargenerator wird dieses Formular nicht im Backend zusammengeklickt. Es wird vollständig im Code definiert. Das macht es extrem robust und ideal für Applikationen, die nicht vom Redakteur verändert werden dürfen (ein Buchungsformular muss immer exakt diese Felder haben, sonst bricht die Datenbank-Logik zusammen).

Lass uns in die Code-Basis springen und unsere erste Formular-Klasse schreiben!
1. Die FormType-Klasse: Struktur und Logik vereint
In der klassischen Contao-Welt klickst du dir Formulare im Backend zusammen. Das ist toll für einfache Kontaktformulare. Aber für unser Strandkorb-Buchungssystem brauchen wir absolute Kontrolle. Wir wollen sicherstellen, dass die Formulardaten exakt zu unserer Booking-Entity (die wir in Teil 4 erstellt haben) passen.
Das Herzstück der Contao 5 Symfony Forms ist der sogenannte FormType. Dies ist eine PHP-Klasse, die als Bauplan fungiert. Sie definiert, welche Felder existieren, welche Typen sie haben (Text, Datum, Zahl) und welche Validierungsregeln gelten.
Schritt 1: Den FormType anlegen
Erstelle in deinem Bundle den Ordner src/Form und darin die Datei BookingType.php:
Pfad: /src/BeachsideBundle/src/Form/BookingType.php
1<?php
2
3namespace Acme\BeachsideBundle\Form;
4
5use Acme\BeachsideBundle\Entity\Booking;
6use Symfony\Component\Form\AbstractType;
7use Symfony\Component\Form\Extension\Core\Type\DateType;
8use Symfony\Component\Form\Extension\Core\Type\SubmitType;
9use Symfony\Component\Form\Extension\Core\Type\TextType;
10use Symfony\Component\Form\FormBuilderInterface;
11use Symfony\Component\OptionsResolver\OptionsResolver;
12
13class BookingType extends AbstractType
14{
15 public function buildForm(FormBuilderInterface $builder, array $options): void
16 {
17 $builder
18 ->add('customerName', TextType::class, [
19 'label' => 'Ihr vollständiger Name',
20 'attr' => [
21 'placeholder' => 'z.B. Max Mustermann',
22 'class' => 'form-control'
23 ],
24 // HTML5 required Attribut
25 'required' => true,
26 ])
27 ->add('dateStart', DateType::class, [
28 'label' => 'Anreisedatum',
29 'widget' => 'single_text', // Rendert ein modernes <input type="date">
30 // Da unsere Entity Integer-Timestamps erwartet (siehe Teil 4),
31 // transformiert Symfony das Datum hier automatisch, wenn konfiguriert,
32 // oder wir übergeben Datetime Objekte. Für dieses Beispiel nutzen wir den Standard.
33 'input' => 'datetime_immutable',
34 'required' => true,
35 ])
36 ->add('dateEnd', DateType::class, [
37 'label' => 'Abreisedatum',
38 'widget' => 'single_text',
39 'input' => 'datetime_immutable',
40 'required' => true,
41 ])
42 ->add('submit', SubmitType::class, [
43 'label' => 'Kostenpflichtig buchen',
44 'attr' => ['class' => 'btn btn-primary mt-3']
45 ])
46 ;
47 }
48
49 public function configureOptions(OptionsResolver $resolver): void
50 {
51 $resolver->setDefaults([
52 // Magic passiert hier: Wir binden das Formular an unsere Entity!
53 'data_class' => Booking::class,
54
55 // CSRF-Schutz ist standardmäßig aktiviert, aber hier machen wir es explizit
56 'csrf_protection' => true,
57 'csrf_field_name' => '_token',
58 'csrf_token_id' => 'booking_item',
59 ]);
60 }
61}
Analyse des Codes (Warum das besser ist als der Formulargenerator):
buildFormMethode: Hier nutzen wir denFormBuilder. Anstatt Tabellen in der Datenbank anzulegen, sagen wir Symfony im Code: "Ich brauche eincustomerNameFeld vom TypTextType".widget => 'single_text': Ein massiver Vorteil von Contao 5 Symfony Forms. BeiDateTyperendert Symfony standardmäßig drei hässliche Dropdowns (Tag, Monat, Jahr). Mitsingle_textgeneriert es ein natives HTML5<input type="date">, das auf mobilen Geräten den perfekten Datepicker des Betriebssystems öffnet.configureOptions(data_class): Das ist der absolute Gamechanger! Indem wir'data_class' => Booking::classsetzen, weiß Symfony: "Ah, dieses Formular füllt einBooking-Objekt." Wenn das Formular später abgeschickt wird, erstellt Symfony automatisch ein neues, fertigesBooking-Objekt für uns. Kein manuelles$obj->customerName = $_POST['customerName']mehr!CSRF-Schutz: Der Schutz vor Cross-Site Request Forgery ist direkt in die Architektur eingebaut. Das Token wird automatisch generiert und validiert.
Mit dieser Datei haben wir das komplette Fundament für unser Formular gelegt. Es ist versionskontrolliert (Git), wiederverwendbar und sicher.
2. Der Fragment Controller: Das Formular zum Leben erwecken
Unsere BookingType-Klasse ist bisher nur ein toter Bauplan. Um das Formular im Frontend anzuzeigen und abzusenden, brauchen wir einen Fragment Controller. Dieser Controller fungiert als Verkehrspolizist: Er nimmt den HTTP-Request entgegen, baut das Formular zusammen, prüft, ob es abgeschickt wurde, und leitet die Daten an die Datenbank weiter.
Wir erstellen ein neues Content-Element für unser Buchungsformular.
Pfad: /src/BeachsideBundle/src/Controller/ContentElement/BookingFormElementController.php
1<?php
2
3namespace Acme\BeachsideBundle\Controller\ContentElement;
4
5use Acme\BeachsideBundle\Entity\Booking;
6use Acme\BeachsideBundle\Form\BookingType;
7use Doctrine\ORM\EntityManagerInterface;
8use Contao\ContentModel;
9use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
10use Contao\CoreBundle\DependencyInjection\Attribute\AsContentElement;
11use Contao\CoreBundle\Twig\FragmentTemplate;
12use Symfony\Component\Form\FormFactoryInterface;
13use Symfony\Component\HttpFoundation\Request;
14use Symfony\Component\HttpFoundation\Response;
15
16#[AsContentElement(category: 'beachside', template: 'content_element/booking_form')]
17class BookingFormElementController extends AbstractContentElementController
18{
19 public function __construct(
20 private readonly FormFactoryInterface $formFactory,
21 private readonly EntityManagerInterface $entityManager
22 ) {
23 }
24
25 protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
26 {
27 // 1. Eine leere Booking-Entity erstellen (oder eine bestehende laden)
28 $booking = new Booking();
29
30 // Optional: Vorbelegung der Entity, z.B. den Strandkorb aus der URL oder Session holen
31 // $booking->setBeachChair($selectedChair);
32
33 // 2. Das Formular über die FormFactory generieren
34 $form = $this->formFactory->create(BookingType::class, $booking);
35
36 // 3. Den Request an das Formular übergeben (Magie passiert hier!)
37 $form->handleRequest($request);
38
39 // 4. Prüfen, ob das Formular abgeschickt UND valide ist
40 if ($form->isSubmitted() && $form->isValid()) {
41
42 // $form->getData() liefert uns das fertig befüllte Booking-Objekt zurück!
43 /** @var Booking $bookingData */
44 $bookingData = $form->getData();
45
46 // Zusätzliche Logik: Datum der Buchungserstellung setzen
47 $bookingData->setTstamp(time());
48
49 // Daten in der Datenbank speichern (Doctrine)
50 $this->entityManager->persist($bookingData);
51 $this->entityManager->flush();
52
53 // Erfolgsmeldung im Template setzen (Alternativ: Redirect auf eine Danke-Seite)
54 $template->set('success', true);
55 return $template->getResponse();
56 }
57
58 // 5. Das Formular-View-Objekt an das Twig-Template übergeben
59 $template->set('form', $form->createView());
60
61 return $template->getResponse();
62 }
63}
Die Magie hinter dem Code (Contao 5 Symfony Forms):
FormFactoryInterface: Wir instanziieren Formulare niemals mitnew BookingType(). DieFormFactoryübernimmt das und injiziert alle nötigen Symfony-Abhängigkeiten (wie CSRF-Token-Generatoren).handleRequest($request): Das ist der wichtigste Aufruf! Symfony nimmt den aktuellen POST-Request, sucht nach den Formularfeldern, validiert den CSRF-Token und – jetzt kommt das Beste – überträgt die Werte automatisch in unser$bookingObjekt. Es konvertiert sogar den Datums-String aus dem HTML-Feld in ein echtes\DateTimeImmutableObjekt!isValid(): Bevor diese Methodetruezurückgibt, jagt Symfony alle Eingaben durch die Validierungsregeln. Wenn der Kunde Buchstaben in das Datumsfeld tippt, istisValid()false und das Formular wird (inklusive Fehlermeldungen) wieder angezeigt.$form->createView(): Wir übergeben nicht das Formular-Objekt selbst an Twig, sondern einen "View". Dieser enthält alle HTML-Attribute, Label-Texte und Fehlermeldungen, die Twig zum Rendern braucht.
Wir haben jetzt die komplette Logik für die Datenverarbeitung geschrieben. Ohne unübersichtliche Hooks, ohne manuelle $_POST-Abfragen und mit 100% typsicherem Code.
3. Das Formular in Twig rendern: Von Daten zu HTML
In unserem Controller haben wir $template->set('form', $form->createView()) aufgerufen. In unserem Twig-Template steht uns nun die Variable form zur Verfügung.
Die einfachste Art, ein Contao 5 Symfony Form auszugeben, ist ein einziger Befehl: {{ form(form) }}. Das rendert das komplette Formular inklusive <form>-Tag, allen Inputs, Labels und dem Submit-Button. Für echte Projekte reicht das aber selten aus, da wir die Struktur an unser Grid-System anpassen wollen.
Schritt 1: Das Twig Template
Erstelle die Datei /src/BeachsideBundle/templates/content_element/booking_form.html.twig:
1{% extends "@Contao/content_element/_base.html.twig" %}
2
3{#
4 FORM THEME MAGIC:
5 Wir sagen Twig, dass es für dieses Formular das Bootstrap 5 Layout nutzen soll.
6 Dadurch erhalten alle Felder automatisch Klassen wie 'form-control' oder 'form-label'.
7#}
8{% form_theme form 'bootstrap_5_layout.html.twig' %}
9
10{% block content %}
11 <div>
12
13 {% if success %}
14 <div>
15 <h3>Vielen Dank für Ihre Buchung!</h3>
16 <p>Wir haben Ihre Anfrage erhalten und melden uns in Kürze.</p>
17 </div>
18 {% else %}
19
20 <h3>Strandkorb jetzt buchen</h3>
21
22 {# form_start generiert das <form> Tag und das CSRF-Token #}
23 {{ form_start(form, {'attr': {'class': 'needs-validation'}}) }}
24
25 {# Wir können Fehler für das gesamte Formular hier ausgeben #}
26 {{ form_errors(form) }}
27
28 <div>
29 <div>
30 {# form_row rendert Label, Input, Fehler und Help-Text auf einmal #}
31 {{ form_row(form.customerName) }}
32 </div>
33 </div>
34
35 <div>
36 <div>
37 {{ form_row(form.dateStart) }}
38 </div>
39 <div>
40 {{ form_row(form.dateEnd) }}
41 </div>
42 </div>
43
44 <div>
45 {# Den Submit Button manuell platzieren #}
46 {{ form_row(form.submit) }}
47 </div>
48
49 {# form_end schließt das Tag und rendert versteckte Felder (wie das CSRF-Token!) #}
50 {{ form_end(form) }}
51
52 {% endif %}
53
54 </div>
55{% endblock %}
Warum Form Themes ein Gamechanger sind:
Wenn du ein natives Contao 5 Symfony Form ohne Form Theme renderst, sieht der HTML-Code extrem nackt aus. Es fehlen CSS-Klassen für dein Framework. In der alten Contao-Welt musstest du oft die form_widget.html5 Templates updatesicher anpassen, was schnell in Chaos endete.
Mit Symfony Form Themes ist das ein Einzeiler: {% form_theme form 'bootstrap_5_layout.html.twig' %}. Symfony liefert native Themes für Bootstrap 3, 4, 5, Foundation und Tailwind CSS mit! Sobald du das Theme aktivierst, weiß Symfony exakt, wie ein Fehler-Status in Bootstrap aussieht (is-invalid Klasse am Input, <div> für die Nachricht) und baut das HTML vollautomatisch zusammen.
Globale Form Themes: Wenn deine ganze Website Bootstrap 5 nutzt, musst du den form_theme Tag nicht in jedes Template schreiben. Du kannst ihn in der config/packages/twig.yaml global für dein gesamtes Contao-Projekt definieren:
twig:
form_themes: ['bootstrap_5_layout.html.twig']Zusammenfassung: Wir haben das Formular jetzt visuell in unser Frontend integriert. Die Trennung ist perfekt: Die Logik liegt im BookingType, die Verarbeitung im Controller und das reine Markup flexibel im Twig-Template.
4. Komplexe Validierung: Eigene Constraints erstellen
Bisher haben wir in unserem Contao 5 Symfony Form Standard-Validierungen genutzt: Pflichtfelder (required => true) und korrekte Datumsformate. Aber was ist mit der echten Business-Logik? Was passiert, wenn der Kunde ein Abreisedatum wählt, das vor dem Anreisedatum liegt? Oder noch schlimmer: Was, wenn der Strandkorb in diesem Zeitraum bereits ausgebucht ist?
In der alten Contao-Welt hättest du das mühsam im processFormData-Hook geprüft und das Neuladen der Seite manipuliert. Bei Contao 5 Symfony Forms nutzen wir die native Symfony Validator Component. Wir erstellen einen eigenen Constraint (eine Validierungsregel), der unser Formular blockiert, bevor die Daten die Datenbank erreichen.
Schritt 1: Die Constraint-Klasse (Das Etikett)
Ein Constraint besteht immer aus zwei Klassen. Die erste ist extrem simpel und fungiert nur als Attribut (das "Etikett"), das wir später anheften.
Erstelle src/Validator/BeachChairAvailable.php:
1<?php
2
3namespace Acme\BeachsideBundle\Validator;
4
5use Symfony\Component\Validator\Constraint;
6
7#[\Attribute(\Attribute::TARGET_CLASS)]
8class BeachChairAvailable extends Constraint
9{
10 // Die Standard-Fehlermeldung
11 public string $message = 'Der Strandkorb ist in diesem Zeitraum leider schon belegt.';
12
13 // Wichtig: Wir wollen das gesamte Objekt (Start, End, Chair) prüfen, nicht nur ein einzelnes Feld.
14 public function getTargets(): string|array
15 {
16 return self::CLASS_CONSTRAINT;
17 }
18}Schritt 2: Der Constraint-Validator (Die Logik)
Jetzt kommt die Magie. Symfony sucht automatisch nach einer Klasse, die genauso heißt, aber auf Validator endet. Hier können wir dank Dependency Injection unseren AvailabilityChecker Service (aus Teil 5) nutzen!
Erstelle src/Validator/BeachChairAvailableValidator.php:
1<?php
2
3namespace Acme\BeachsideBundle\Validator;
4
5use Acme\BeachsideBundle\Entity\Booking;
6use Acme\BeachsideBundle\Service\AvailabilityChecker;
7use Symfony\Component\Validator\Constraint;
8use Symfony\Component\Validator\ConstraintValidator;
9use Symfony\Component\Validator\Exception\UnexpectedTypeException;
10
11class BeachChairAvailableValidator extends ConstraintValidator
12{
13 public function __construct(
14 private readonly AvailabilityChecker $checker
15 ) {
16 }
17
18 public function validate($value, Constraint $constraint): void
19 {
20 if (!$constraint instanceof BeachChairAvailable) {
21 throw new UnexpectedTypeException($constraint, BeachChairAvailable::class);
22 }
23
24 // Da wir es als Class-Constraint nutzen, ist $value unsere Booking Entity!
25 if (!$value instanceof Booking) {
26 return;
27 }
28
29 // Wenn Felder leer sind, übernimmt das die Standard-Required-Validierung
30 if (!$value->getBeachChair() || !$value->getDateStart() || !$value->getDateEnd()) {
31 return;
32 }
33
34 // Datumskontrolle: Abreise vor Anreise?
35 if ($value->getDateStart() >= $value->getDateEnd()) {
36 $this->context->buildViolation('Das Abreisedatum muss nach dem Anreisedatum liegen.')
37 ->atPath('dateEnd') // Fehler direkt an das Feld "dateEnd" heften
38 ->addViolation();
39 return;
40 }
41
42 // Business-Logik: Ist der Strandkorb frei?
43 $isFree = $this->checker->isAvailable(
44 $value->getBeachChair(),
45 $value->getDateStart(),
46 $value->getDateEnd()
47 );
48
49 if (!$isFree) {
50 // Fehler an das Formular hängen
51 $this->context->buildViolation($constraint->message)
52 ->addViolation();
53 }
54 }
55}
Schritt 3: Das Etikett anbringen
Da unser Contao 5 Symfony Form an die Booking-Entity gebunden ist (data_class => Booking::class), müssen wir den Constraint einfach nur als Attribut über unsere Entity-Klasse schreiben!
Öffne deine src/Entity/Booking.php (aus Teil 4) und ergänze das Attribut:
1<?php
2
3namespace Acme\BeachsideBundle\Entity;
4
5use Acme\BeachsideBundle\Validator\BeachChairAvailable; // Importieren!
6use Doctrine\ORM\Mapping as ORM;
7
8#[ORM\Entity]
9#[ORM\Table(name: "tl_booking")]
10#[BeachChairAvailable] // <-- HIER IST DIE MAGIE!
11class Booking
12{
13 // ... restlicher Code ...
14}Was passiert jetzt? Wenn der User im Frontend auf "Buchen" klickt, ruft der Controller $form->handleRequest($request) und $form->isValid() auf. Symfony erkennt das Attribut #[BeachChairAvailable] auf der Entity, instanziiert unseren Validator, injiziert den AvailabilityChecker aus dem Service-Container und führt die Prüfung aus.
Ist der Strandkorb belegt, schlägt isValid() fehl und das Formular wird in Twig sofort mit der roten Fehlermeldung "Der Strandkorb ist in diesem Zeitraum leider schon belegt." gerendert. Keine inkonsistenten Datenbankeinträge, keine gehackten Formulare. Absolute Sicherheit.
5. Nach dem Submit: E-Mails mit Symfony Mailer versenden
Wenn ein Contao 5 Symfony Form erfolgreich abgeschickt (isSubmitted) und validiert (isValid) wurde, haben wir in Iteration 3 die Daten per Doctrine in der Datenbank gespeichert. Der nächste logische Schritt in jedem Buchungssystem ist die Bestätigungs-E-Mail.
Contao 5 integriert den Symfony Mailer nativ. Er ersetzt nicht nur die alte \Contao\Email Klasse, sondern bringt Enterprise-Features mit: Unterstützung für asynchronen Versand (Queueing), fertige Twig-Integration für HTML-Mails und nahtlose Anbindung an Drittanbieter (SendGrid, Mailgun) via DSN.
Schritt 1: Den Controller anpassen (Dependency Injection)
Wir öffnen unseren BookingFormElementController aus Iteration 3 und injizieren den MailerInterface. Außerdem nutzen wir die Klasse TemplatedEmail, um unsere E-Mail direkt mit einem Twig-Template zu verknüpfen.
1<?php
2
3namespace Acme\BeachsideBundle\Controller\ContentElement;
4
5// ... bisherige Imports ...
6use Symfony\Bridge\Twig\Mime\TemplatedEmail;
7use Symfony\Component\Mailer\MailerInterface;
8use Symfony\Component\Mime\Address;
9
10#[AsContentElement(category: 'beachside', template: 'content_element/booking_form')]
11class BookingFormElementController extends AbstractContentElementController
12{
13 public function __construct(
14 private readonly FormFactoryInterface $formFactory,
15 private readonly EntityManagerInterface $entityManager,
16 private readonly MailerInterface $mailer // <-- NEU: Symfony Mailer injizieren
17 ) {
18 }
19
20 protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
21 {
22 $booking = new Booking();
23 $form = $this->formFactory->create(BookingType::class, $booking);
24 $form->handleRequest($request);
25
26 if ($form->isSubmitted() && $form->isValid()) {
27
28 /** @var Booking $bookingData */
29 $bookingData = $form->getData();
30 $bookingData->setTstamp(time());
31
32 $this->entityManager->persist($bookingData);
33 $this->entityManager->flush();
34
35 // --------------------------------------------------
36 // NEU: E-Mail Versand aufbauen
37 // --------------------------------------------------
38 $email = (new TemplatedEmail())
39 ->from(new Address('no-reply@beachside.de', 'Strandkorbvermietung Binz'))
40 // In einer echten App würden wir die Kunden-Mail aus dem Formular holen.
41 // Fürs Beispiel schicken wir es an den Admin.
42 ->to('admin@beachside.de')
43 ->subject('Neue Strandkorb-Buchung eingegangen!')
44
45 // Pfad zum Twig-Template für die E-Mail
46 ->htmlTemplate('@AcmeBeachside/emails/booking_notification.html.twig')
47
48 // Variablen an das E-Mail-Template übergeben
49 ->context([
50 'booking' => $bookingData,
51 ]);
52
53 // E-Mail absenden
54 $this->mailer->send($email);
55 // --------------------------------------------------
56
57 $template->set('success', true);
58 return $template->getResponse();
59 }
60
61 $template->set('form', $form->createView());
62 return $template->getResponse();
63 }
64}
Schritt 2: Das E-Mail Twig-Template erstellen
Wir haben im Controller @AcmeBeachside/emails/booking_notification.html.twig referenziert. Wir erstellen diese Datei nun im Ordner templates/emails/.
Hier können wir wieder die volle Power von Twig nutzen.
1{# templates/emails/booking_notification.html.twig #}
2<!DOCTYPE html>
3<html>
4<head>
5 <meta charset="UTF-8">
6 <title>Neue Buchung</title>
7</head>
8<body>
9 <h1>Neue Strandkorb-Buchung!</h1>
10
11 <p>Hallo Admin,</p>
12 <p>es ist eine neue Buchung über die Website eingegangen:</p>
13
14 <ul>
15 <li><strong>Kunde:</strong> {{ booking.customerName }}</li>
16 <li><strong>Strandkorb:</strong> Nr. {{ booking.beachChair.chairNumber }}</li>
17 <li><strong>Von:</strong> {{ booking.dateStart|format_date('medium') }}</li>
18 <li><strong>Bis:</strong> {{ booking.dateEnd|format_date('medium') }}</li>
19 </ul>
20
21 <p>Bitte logge dich ins Contao-Backend ein, um die Details zu prüfen.</p>
22</body>
23</html>Warum das in Contao 5 besser ist: Wenn du komplexe Contao 5 Symfony Forms baust, willst du oft strukturierte HTML-E-Mails versenden. Die TemplatedEmail Klasse von Symfony ermöglicht es dir sogar, CSS inline zu rendern (mit der CssInliner Extension) oder Frameworks wie Inky zu nutzen, um responsive E-Mails zu generieren, die auch in Outlook perfekt aussehen.
Zudem – und das ist für Performance-Enthusiasten wichtig – blockiert der Mail-Versand den User nicht. Wenn du den Symfony Messenger (Message Queue) in Contao konfigurierst, wird $this->mailer->send() die Mail nur in eine Datenbank-Warteschlange legen. Der User sieht sofort die "Vielen Dank"-Meldung, während ein Hintergrund-Prozess (Worker) die Mail asynchron verschickt.
6. Best Practices für komplexe Formulare

Du hast nun gesehen, wie mächtig Contao 5 Symfony Forms sind. Um deinen Code auch in großen Projekten wartbar zu halten, solltest du folgende Best Practices aus der Symfony-Welt verinnerlichen:
Übersetzungen (I18n) nutzen: Anstatt Labels hart in die
BookingType.phpzu schreiben ('label' => 'Ihr Name'), solltest du Translation-Keys verwenden ('label' => 'booking_form.customer_name'). Symfony übersetzt diese dann automatisch anhand deinermessages.de.yamlDateien. Das macht dein Formular sofort mehrsprachig.Data Transformers: Manchmal passt das Datenformat im HTML-Formular (z. B. ein String wie "01.08.2026") nicht zu dem, was deine Entity erwartet (z. B. ein Unix-Timestamp als Integer). Symfony bietet hierfür Data Transformers. Sie klinken sich zwischen das Formularfeld und die Entity ein und konvertieren die Daten in beide Richtungen transparent on-the-fly.
Form Types verschachteln (CollectionType): Will der Kunde mehrere Strandkörbe auf einmal buchen? Du kannst Formulare verschachteln! Ein
BookingTypekann eine Collection vonBeachChairSelectionTypeFormularen enthalten. Symfony kümmert sich um das dynamische Hinzufügen (via JavaScript) und die Validierung der Arrays.
Teil der Serie
Contao 5 Masterclass: The Beachside Project
Contao 5 Bundle Entwicklung: Die Masterclass für echte Entwickler Pillar
Contao 5 Bundle Setup: Das Fundament für professionelle Erweiterungen
Contao 5 Doctrine Entities: Moderne Datenmodellierung statt SQL-Chaos
Contao 5 DCA: Perfekte Backend-Masken für Entities erstellen
Contao 5 Doctrine Relations: Wir bauen die Buchungs-Logik
Contao 5 Service Layer: Business-Logik sauber kapseln
Contao 5 Unit Testing: Code-Absicherung mit PHPUnit & Test-Case
Contao 5 Twig Templates: Frontend-Ausgabe mit Fragment Controllern
Contao 5 Symfony Forms: Professionelle Formularverarbeitung
Contao 5 Notification Center: Zentrale Kommunikation & E-Mail-Workflows
Contao 5 Backend Dashboards & Custom Routing
Contao 5 Console Commands: Automatisierung & Hintergrund-Jobs
Contao 5 Headless CMS API: REST-Schnittstellen bauen
Häufig gestellte Fragen (FAQ)
Ausblick und Zusammenfassung
Das war ein massiver Schritt! Wir haben die "Bastel-Ecke" verlassen und Enterprise-Niveau erreicht. Durch den Einsatz von Contao 5 Symfony Forms profitierst du von:
Automatischer Datenbindung: Kein manuelles Auslesen von
$_POST-Arrays mehr.Höchster Sicherheit: CSRF-Schutz und strikte Objekt-Validierung sind fest integriert.
Perfekter Architektur: Logik (
FormType), Verarbeitung (Controller) und Design (Twig Form Themes) sind sauber getrennt.Asynchronem E-Mail-Versand: Dank der nahtlosen Integration des Symfony Mailers.
Dein Strandkorb-Buchungssystem im Frontend ist nun voll funktionsfähig. Der Kunde kann Termine prüfen, das Formular ausfüllen und erhält eine E-Mail.
Doch was passiert jetzt im Backend? Der Admin muss diese Buchungen verwalten, bestätigen oder stornieren können. Bisher haben wir dafür nur eine rudimentäre DCA-Tabelle. Im nächsten Teil bringen wir das Backend auf das nächste Level: Wir bauen eigene Backend-Routen, Dashboards und nutzen den Symfony Messenger für Hintergrund-Prozesse (wie den Rechnungsversand).
Bereit für Teil 9? Notification Center in Contao 5

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


