Custom Types und Daten-Transformatoren
Das Symfony Form-Component ist eines der mächtigsten und komplexesten im Ökosystem. Wer nur Built-in-Types nutzt, verschenkt die Hälfte des Potenzials. Custom Form Types kapseln Wiederverwendbarkeit, Data Transformers überbrücken die Kluft zwischen Formulareingabe und Domänenobjekten — ohne Boilerplate in jedem einzelnen Formular.
Inhaltsverzeichnis
- 1. Das Symfony Form-System verstehen
- 2. Eigenen Form Type erstellen: Schritt für Schritt
- 3. Form-Options und configureOptions richtig nutzen
- 4. Data Transformers: zwischen Eingabe und Domäne übersetzen
- 5. Reverse Transformer: Fehlerbehandlung bei ungültiger Eingabe
- 6. Compound Form Types: mehrere Felder in einem Type
- 7. Eigenes Twig-Widget für Custom Form Types
- 8. Validierung in Custom Form Types integrieren
- 9. Custom Type vs. Form Events vs. Data Mapper
- 10. Zusammenfassung
- 11. FAQ
1. Das Symfony Form-System verstehen
Das Symfony Form-Component verarbeitet Formulardaten in drei Schichten: der View-Schicht (HTML-Eingabe des Nutzers), der Norm-Schicht (die interne Darstellung im Form-Objekt) und der Model-Schicht (das PHP-Objekt, das das Formular befüllt). Ein Custom Form Type definiert, wie Daten zwischen diesen drei Schichten transformiert werden. Ein DateType beispielsweise empfängt aus der View drei separate Eingaben (Tag, Monat, Jahr), kombiniert sie in der Norm-Schicht zu einem DateTimeInterface-Objekt und übergibt es als DateTime an das Model. Wer diesen dreischichtigen Datenfluss versteht, kann jeden Custom Form Type präzise implementieren.
Der Datenfluss beim Submit: Rohdaten aus dem Request landen in der View-Schicht. View-Transformer wandeln sie in die Norm-Darstellung um (z.B. String zu DateTime). Model-Transformer wandeln die Norm-Darstellung in das Modell-Objekt um (z.B. DateTime zu einer eigenen DateValue-Klasse). Beim Anzeigen des Formulars läuft derselbe Prozess rückwärts: Model-Objekt zu Norm, Norm zu View. Das Verständnis dieser Richtung ist entscheidend für die Implementierung von Data Transformers, weil die transform()-Methode (Modell zu View) und die reverseTransform()-Methode (View zu Modell) unterschiedliche Fehlerszenarien haben und unterschiedlich mit ungültigen Eingaben umgehen müssen.
2. Eigenen Form Type erstellen: Schritt für Schritt
Ein Custom Symfony Form Type ist eine PHP-Klasse, die AbstractType erweitert. Die Methode buildForm() definiert die Felder des Typs. configureOptions() definiert die verfügbaren Optionen mit Standardwerten und Validierungsregeln. getParent() gibt den Elterntyp an, von dem das neue Feld erbt — für einfache Wrapper-Typen ist das meist ein Built-in-Type wie TextType oder IntegerType. Für vollständig neue Typen ohne Vererbung wird FormType als Parent verwendet, was zu einem eigenständigen Form-Baum-Knoten ohne vorgeerbtes Verhalten führt.
Ein häufiges Anwendungsbeispiel ist ein MoneyType, der einen Betrag in Cent als Integer im Modell erwartet, aber dem Nutzer einen Euro-Betrag mit zwei Dezimalstellen anzeigt. Das Built-in MoneyType von Symfony hat begrenzte Konfigurierbarkeit für spezifische Geschäftsanforderungen — zum Beispiel für mehrere Währungen, die von der Datenbank geladen werden, oder für spezifische Formatierungen je nach Locale. Ein eigener MoneyInputType kapselt diese Logik einmal und ist über alle Formulare des Projekts hinweg wiederverwendbar, ohne dass jedes Formular die Transformationslogik dupliziert.
<?php
declare(strict_types=1);
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Form\Transformer\MoneyTransformer;
/**
* Custom Form Type: renders a money amount as decimal string,
* stores and retrieves value as integer cents in the model.
*/
final class MoneyInputType extends AbstractType
{
public function __construct(
private readonly MoneyTransformer $moneyTransformer,
) {}
/**
* Add the money-to-cents transformer to the form field.
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// Add model transformer: converts cents (int) ↔ decimal string (view)
$builder->addModelTransformer($this->moneyTransformer);
}
/**
* Define type options with defaults and allowed values.
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'currency' => 'EUR',
'decimal_places' => 2,
'attr' => ['placeholder' => '0.00', 'inputmode' => 'decimal'],
]);
$resolver->setAllowedTypes('currency', 'string');
$resolver->setAllowedValues('decimal_places', [0, 1, 2, 3]);
}
/**
* Inherit from TextType — renders as <input type="text"> with transformer applied.
*/
public function getParent(): string
{
return TextType::class;
}
/**
* Block prefix for Twig widget customization: form_widget(field, {custom_options}).
*/
public function getBlockPrefix(): string
{
return 'money_input';
}
}
3. Form-Options und configureOptions richtig nutzen
Der OptionsResolver in configureOptions() ist das Konfigurationssystem jedes Custom Form Types. Er validiert Optionen beim Erstellen des Formulars und gibt klare Fehlermeldungen, wenn unbekannte oder ungültige Optionen übergeben werden. Das verhindert stille Fehlkonfigurationen, die nur zur Laufzeit auffallen. Mit setRequired() werden Pflichtoptionen definiert, mit setDefault() Standardwerte, mit setAllowedTypes() wird der Typ einer Option eingeschränkt und mit setAllowedValues() ein Enum-artiger Wertebereich.
Lazy Defaults sind ein häufig unterschätztes Feature: Eine Option kann einen Standardwert bekommen, der von einer anderen Option abhängt. $resolver->setDefault('label', fn(Options $options) => ucfirst($options['currency']) . ' Amount') berechnet den Default-Label-Wert aus der currency-Option, wenn der Aufrufer keine eigene label-Option übergibt. Das verhindert duplizierte Logik: Der Custom Form Type verwaltet die Abhängigkeit zwischen Optionen intern, der Aufrufer muss nur die notwendigsten Optionen angeben. Normalisierung mit setNormalizer() transformiert übergebene Optionswerte vor der Validierung — zum Beispiel um Strings zu uppercase zu konvertieren.
4. Data Transformers: zwischen Eingabe und Domäne übersetzen
Data Transformers implementieren das DataTransformerInterface mit zwei Methoden: transform($value) wandelt den Modellwert in die View-Darstellung um (für die Anzeige des Formulars), und reverseTransform($value) wandelt die View-Darstellung in den Modellwert um (nach dem Submit). Ein Data Transformer für einen Money-Type wandelt eine Integer-Zahl in Cent (Modell) in einen formatierten Decimal-String (View) und zurück. Bei transform(null) muss immer eine sichere View-Darstellung zurückgegeben werden (leerer String), weil das Formular beim ersten Rendern möglicherweise noch keinen Wert hat.
Die Unterscheidung zwischen Model-Transformern und View-Transformern ist wichtig für komplexe Szenarien. Model-Transformer (addModelTransformer()) transformieren zwischen Modell und Norm-Darstellung. View-Transformer (addViewTransformer()) transformieren zwischen Norm und HTML-Eingabe. Für die meisten Custom Form Types reicht ein Model-Transformer. View-Transformer sind nur nötig, wenn die Norm-Darstellung selbst komplexer als ein einfacher Scalar ist — etwa beim DateType, der in der Norm-Schicht ein DateTime-Objekt hat und in der View-Schicht mehrere separate Strings für Tag, Monat und Jahr.
<?php
declare(strict_types=1);
namespace App\Form\Transformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Data Transformer: converts between integer cents (model) and decimal string (view).
* 1234 cents → "12.34" (transform) | "12.34" → 1234 cents (reverseTransform)
*/
final class MoneyTransformer implements DataTransformerInterface
{
public function __construct(
private readonly int $decimalPlaces = 2,
) {}
/**
* Transform model value (integer cents) to view value (decimal string).
* Called when rendering the form field.
*/
public function transform(mixed $value): string
{
if ($value === null) {
return ''; // Safe empty state for new form
}
if (!is_int($value)) {
throw new TransformationFailedException(
sprintf('Expected int cents, got %s.', get_debug_type($value)),
);
}
// Convert cents to decimal: 1234 → 12.34
$divisor = 10 ** $this->decimalPlaces;
return number_format($value / $divisor, $this->decimalPlaces, '.', '');
}
/**
* Reverse transform view value (decimal string) to model value (integer cents).
* Called after form submit — throws TransformationFailedException on invalid input.
*/
public function reverseTransform(mixed $value): ?int
{
if ($value === null || $value === '') {
return null; // Allow empty — NotBlank constraint handles required validation
}
// Normalize locale-specific decimal separators (comma → dot)
$normalized = str_replace(',', '.', (string) $value);
if (!is_numeric($normalized)) {
throw new TransformationFailedException(
sprintf('"%s" is not a valid monetary amount.', $value),
);
}
$floatValue = (float) $normalized;
if ($floatValue < 0) {
throw new TransformationFailedException('Monetary amount cannot be negative.');
}
// Convert decimal to cents: 12.34 → 1234
$multiplier = 10 ** $this->decimalPlaces;
return (int) round($floatValue * $multiplier);
}
}
5. Reverse Transformer: Fehlerbehandlung bei ungültiger Eingabe
Die reverseTransform()-Methode eines Data Transformers ist für die Fehlerbehandlung bei ungültiger Benutzereingabe verantwortlich. Wenn die Eingabe nicht transformiert werden kann — eine nicht-numerische Eingabe in einem Money-Feld, eine unbekannte Entity-ID in einem EntityType — muss der Transformer eine TransformationFailedException werfen. Symfony fängt diese Exception und fügt einen Validierungsfehler zum Formularfeld hinzu, ohne dass der Entwickler im Formular selbst Exception-Handling implementieren muss. Der Fehlertext aus der Exception erscheint im Formular als Validierungsmeldung.
Ein subtiles Problem: Die TransformationFailedException löst eine INVALID-Meldung aus, die standardmäßig den internen PHP-Fehlertext anzeigt — nicht benutzerfreundlich. Um eine eigene, lokalisierte Fehlermeldung anzuzeigen, wird in configureOptions() die Option invalid_message gesetzt: 'invalid_message' => 'Bitte gib einen gültigen Betrag ein.'. Symfony ersetzt den internen Transformer-Fehler durch diese benutzerfreundliche Meldung. Platzhalter wie { { value } } geben den ungültigen Eingabewert in der Fehlermeldung aus und helfen dem Nutzer, seinen Fehler nachzuvollziehen.
6. Compound Form Types: mehrere Felder in einem Type
Compound Custom Form Types sind Types, die aus mehreren Unterfeldern bestehen. Ein AddressType beispielsweise enthält Felder für Straße, Hausnummer, PLZ und Stadt — aber aus Sicht des aufrufenden Formulars ist es ein einziges Feld. Der Compound Type baut die Unterfelder in buildForm() mit $builder->add() auf, und Symfony verwaltet den Datenfluss zu den Unterfeldern automatisch. Das aufrufende Formular sieht nur ->add('address', AddressType::class) — die Unterfelder sind vollständig gekapselt.
Ein Data Mapper ist die Verbindung zwischen dem Compound Type und dem Domänenobjekt. Standardmäßig nutzt Symfony Property Access: Felder mit dem Namen street werden auf $address->street gemappt. Wenn das Domänenobjekt eine andere Struktur hat — zum Beispiel ein Value Object mit Factory-Methode statt öffentlicher Properties — implementiert man einen eigenen Data Mapper, der DataMapperInterface implementiert. Die mapDataToForms()-Methode füllt die Unterfelder aus dem Domänenobjekt, mapFormsToData() erstellt aus den Unterfeld-Werten nach dem Submit ein neues Domänenobjekt.
<?php
declare(strict_types=1);
namespace App\Form\Type;
use App\Form\DataMapper\AddressDataMapper;
use App\ValueObject\Address;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Compound Form Type for postal address — wraps 4 fields into a single reusable type.
* Uses a custom Data Mapper to create an immutable Address Value Object on submit.
*/
final class AddressType extends AbstractType
{
public function __construct(
private readonly AddressDataMapper $mapper,
) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// All four address sub-fields defined once — reused across all address forms
$builder
->add('street', TextType::class, [
'label' => 'Straße',
'constraints' => [new Assert\NotBlank(), new Assert\Length(max: 100)],
])
->add('houseNumber', TextType::class, [
'label' => 'Hausnummer',
'constraints' => [new Assert\NotBlank(), new Assert\Length(max: 10)],
])
->add('postalCode', TextType::class, [
'label' => 'PLZ',
'constraints' => [new Assert\NotBlank(), new Assert\Regex('/^\d{5}$/')],
])
->add('city', TextType::class, [
'label' => 'Stadt',
'constraints' => [new Assert\NotBlank(), new Assert\Length(max: 100)],
]);
// Custom Data Mapper: maps Address Value Object ↔ form fields
$builder->setDataMapper($this->mapper);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Address::class,
'empty_data' => null, // Mapper handles empty state
'label' => false, // Compound type usually has no outer label
]);
}
}
// Usage in a parent form — AddressType appears as a single compound field:
// $builder->add('deliveryAddress', AddressType::class, ['label' => 'Lieferadresse']);
// $builder->add('billingAddress', AddressType::class, ['label' => 'Rechnungsadresse']);
7. Eigenes Twig-Widget für Custom Form Types
Jeder Custom Form Type kann mit einem eigenen Twig-Widget gerendert werden. Das Widget definiert das HTML, das Symfony beim Rendern des Feldes via { { form_widget(field) } } ausgibt. Ohne eigenes Widget erbt der Custom Type das Rendering seines Parent-Types. Mit einem eigenen Widget kann das HTML beliebig gestaltet werden — für spezielle Input-Komponenten, für Currency-Symbole die visuell vor oder nach dem Inputfeld angezeigt werden, oder für komplexe Compound-Types, die eine spezifische Layout-Struktur brauchen.
Das Widget wird in einer Twig-Datei als Block mit dem Naming-Pattern blockprefix_widget definiert. Der Block-Präfix wird in getBlockPrefix() des Custom Form Types festgelegt. Die Twig-Datei wird in twig.yaml als Form-Theme registriert: entweder global für alle Formulare oder lokal pro Template mit {% form_theme form 'form/money_input.html.twig' %}. Im Widget-Block stehen alle Symfony-Form-Variablen zur Verfügung: id, name, value, required, attr und alle Custom-Optionen, die via vars an die View übergeben wurden.
8. Validierung in Custom Form Types integrieren
Validierung in Custom Form Types erfolgt auf zwei Ebenen: Constraint-basiert auf dem Datenobjekt (via Symfony Validator) und Transformer-basiert im Data Transformer (via TransformationFailedException). Transformer-Fehler prüfen die syntaktische Korrektheit der Eingabe (ist es ein gültiger Geldwert überhaupt?), Constraints prüfen die semantische Korrektheit (ist der Betrag größer als 0? Ist er kleiner als das verfügbare Budget?). Die Trennung ist wichtig: Transformer-Fehler verhindern, dass ungültige Daten in das Domänenobjekt gelangen. Constraints prüfen Domänenregeln, die voraussetzen, dass die Daten bereits korrekt deserialisiert wurden.
Default-Constraints für einen Custom Form Type werden in configureOptions() mit 'constraints' => [new Assert\NotBlank()] als Default-Wert gesetzt. Diese Constraints gelten für alle Instanzen des Types, sofern sie nicht vom Aufrufer überschrieben werden. Eigene Constraints, die für den Type spezifische Regeln abprüfen — zum Beispiel, dass ein Geldwert nicht größer als ein Maximum ist, das als Option übergeben wurde — implementiert man als Constraint-Klasse plus zugehörigen Validator. Der Validator empfängt den bereits transformierten Modellwert und prüft ihn gegen die Options-Konfiguration des Types.
9. Custom Type vs. Form Events vs. Data Mapper
Symfony Forms bieten mehrere Mechanismen für komplexe Szenarien: Custom Form Types, Form Events und Data Mapper. Die Wahl des richtigen Ansatzes hängt davon ab, was angepasst werden soll. Custom Form Types sind für Wiederverwendung: Wenn dasselbe Feld-Verhalten in mehreren Formularen vorkommt, kapselt es ein Custom Type. Form Events (PRE_SET_DATA, POST_SUBMIT) sind für dynamisches Formularverhalten: Felder hinzufügen oder entfernen basierend auf Datenwerten. Data Mapper sind für komplexe Zuordnung zwischen Formularfeldern und Domänenobjekten, die nicht dem einfachen Property-Access-Muster folgen.
| Ansatz | Bestes Einsatzgebiet | Wiederverwendung | Komplexität |
|---|---|---|---|
| Custom Form Type | Wiederkehrende Feldtypen, Datentransformation | Hoch — einmal bauen, überall nutzen | Mittel |
| Data Transformer | Eingabe ↔ Domänenobjekt übersetzen | Hoch — als Service injizierbar | Mittel |
| Form Events | Dynamische Felder, konditionale Logik | Gering — spezifisch für ein Formular | Hoch |
| Data Mapper | Value Objects, Factory-Methoden | Mittel — spezifisch für ein Domänenobjekt | Hoch |
| Compound Type | Mehrere Felder als ein wiederverwendbarer Block | Sehr hoch — AddressType überall | Mittel |
In der Praxis werden diese Ansätze kombiniert: Ein Compound Custom Form Type mit eigenem Data Mapper für Value Objects, ein Data Transformer für spezifische Eingabeformate, und Form Events für das dynamische Hinzufügen von Feldern basierend auf der bereits eingegebenen Auswahl. Die Kombination aller drei macht komplexe Formulare in Symfony vollständig kontrollierbar, ohne Logik in Controller oder Twig zu verlagern.
Mironsoft
Symfony-Formular-Architektur, Custom Types und Domain-Integration
Komplexe Symfony-Formulare professionell implementieren?
Wir entwickeln skalierbare Symfony-Form-Architekturen mit Custom Types, Data Transformers und eigenen Twig-Widgets — von der ersten Form-Analyse bis zur produktionsreifen Implementierung.
Custom Form Types
Eigene Feldtypen für Domänenobjekte — Money, Date, Address und projektspezifische Types
Data Transformer
Datentransformation zwischen Formular und Domäne mit sauberer Fehlerbehandlung
Form-Architektur
Compound Types, Data Mapper und Form-Events für komplexe domänengetriebene Formulare
10. Zusammenfassung
Custom Form Types und Data Transformers sind die Bausteine, die Symfony-Formulare von einfachen CRUD-Masken zu ausdrucksstarken, domänengerechten Interfaces machen. Ein Custom Form Type kapselt wiederverwendbares Feldverhalten: Konfiguration, Validierung, Transformation und Rendering an einem Ort. Data Transformers überbrücken die Kluft zwischen Benutzereingabe und Domänenobjekten ohne Boilerplate in jedem Formular. Compound Types fassen mehrere Felder zu einem wiederverwendbaren Block zusammen. Data Mapper verbinden Compound Types mit Value Objects, die keine einfache Property-Access-Struktur haben.
Das Zusammenspiel aller Komponenten ergibt Formulare, die genau die Domänensprache sprechen: Ein AddressType liefert ein unveränderliches Address-Value-Object, ein MoneyInputType liefert Integer-Cents — ohne Transformationslogik im Controller und ohne Mapping-Code in jedem einzelnen Formular. Das Ergebnis ist ein wiederverwendbares, testbares und klar verständliches Formular-System, das mit wachsender Projektgröße an Wert gewinnt, statt zur Boilerplate-Sammlung zu werden.
Symfony Custom Form Types und Data Transformers — Das Wichtigste auf einen Blick
Custom Form Type
Erweitert AbstractType. buildForm() definiert Felder, configureOptions() definiert Optionen, getParent() gibt den Elterntyp an. Einmal gebaut, überall wiederverwendbar.
Data Transformer
transform(): Modell → View. reverseTransform(): View → Modell. TransformationFailedException bei ungültiger Eingabe — Symfony zeigt den Fehler im Formular an.
Compound Type
Mehrere Unterfelder in einem wiederverwendbaren Type. Data Mapper für Value Objects. Aufrufendes Formular sieht nur einen ->add('address', AddressType::class).
Twig Widget
Block blockprefix_widget in einer Form-Theme-Datei. Als Form-Theme registrieren. Vollständige HTML-Kontrolle über das Rendering jedes Custom Types.