Embedded Field in Entity und der FormType
Custom FormType mit Doctrine-Embedded Value Object
Anleitung am Beispiel von StringField / StringFieldType.
Der Custom FormType kapselt ein Doctrine-Embedded Value Object mit eigenem Layout (Form-Theme), sodass im Zieltemplate nur noch {{ form_row(form.fieldName) }} steht.
Überblick
Drei Bausteine gehören zusammen:
- Value Object als
#[ORM\Embeddable]mit Properties und passenden Accessoren - FormType, der die Subfelder aufbaut und als
data_classdas Value Object setzt - Form-Theme mit
*_row/*_widgetBlocks, global registriert
Im aufnehmenden Entity wird das Value Object via #[ORM\Embedded(class: …)] eingebettet und im Konstruktor initialisiert.
1. Value Object (Embeddable)
// src/Entity/ValueObject/StringField.php
namespace App\Entity\ValueObject;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Embeddable]
class StringField
{
#[ORM\Column(type: 'boolean')]
private bool $visibility = true;
#[ORM\Column(type: 'boolean')]
private bool $canEdit = true;
#[ORM\Column(type: 'string', length: 255)]
private string $value = '';
public function __construct(bool $visibility = true, bool $canEdit = true, string $value = '')
{
$this->visibility = $visibility;
$this->canEdit = $canEdit;
$this->value = $value;
}
// Wichtig: Symfony PropertyAccess benötigt passende Accessoren
// zum Property-Namen. Property `visibility` → getVisibility()/setVisibility().
public function getVisibility(): bool { return $this->visibility; }
public function setVisibility(bool $v): void { $this->visibility = $v; }
public function getCanEdit(): bool { return $this->canEdit; }
public function setCanEdit(bool $v): void { $this->canEdit = $v; }
public function getValue(): string { return $this->value; }
public function setValue(string $v): void { $this->value = $v; }
}
Fallstricke:
- Properties nicht nullable, wenn die Setter
string/boolerwarten — sonst Fehler beim Binden leerer Werte (→ sieheempty_dataim FormType). - Accessor-Namen müssen zum Property-Namen passen.
isVisible()reicht nicht für Propertyvisibility— Symfony suchtgetVisibility/isVisibility/hasVisibility. #[ORM\Embeddable](keine Entity-Annotation, keine ID).
2. Embedded-Field im Entity
// src/Entity/AnimalCard.php
use App\Entity\ValueObject\StringField;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class AnimalCard
{
#[ORM\Embedded(class: StringField::class)]
private StringField $animalType;
public function __construct()
{
$this->animalType = new StringField(); // zwingend, sonst null bei neuen Entities
}
public function getAnimalType(): StringField { return $this->animalType; }
public function setAnimalType(StringField $s): void { $this->animalType = $s; }
}
Fallstricke:
- Das Embedded-Property muss im Konstruktor initialisiert werden, sonst schlägt das Formular beim Rendern eines neuen Entities fehl.
- Doctrine legt pro Property eine Spalte mit Prefix an:
animal_type_visibility,animal_type_can_edit,animal_type_value. Migration erzeugen viaddev exec php bin/console make:migration.
3. FormType
// src/Form/FieldType/StringFieldType.php
namespace App\Form\FieldType;
use App\Entity\ValueObject\StringField;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class StringFieldType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('value', TextType::class, [
'label' => $options['value_label'],
'required' => false,
'help' => $options['value_help'],
'attr' => $options['value_attr'],
'empty_data' => '', // leere Eingabe → '' statt null
])
->add('visibility', CheckboxType::class, [
'label' => false,
'required' => false,
'attr' => ['title' => 'Öffentlich sichtbar …'],
])
->add('canEdit', CheckboxType::class, [
'label' => false,
'required' => false,
'attr' => ['title' => 'Bearbeitbar …'],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => StringField::class,
'value_label' => 'Wert',
'value_help' => null,
'value_attr' => [],
]);
$resolver->setAllowedTypes('value_label', ['string', 'bool']);
$resolver->setAllowedTypes('value_help', ['string', 'null']);
$resolver->setAllowedTypes('value_attr', 'array');
}
}
Fallstricke:
data_classauf das Value Object setzen, damit Symfony ein Objekt hydratisiert und nicht ein Array.empty_data => ''auf Textfeldern, deren Setterstring(nicht?string) erwartet.- Eigene Optionen (
value_label,value_help, …) immer viaconfigureOptions()registrieren und Typen einschränken.
Block-Prefix
Symfony leitet den Block-Prefix automatisch aus dem Klassennamen ab:
StringFieldType → string_field. Das ist der Name für die Theme-Blocks. Bei Bedarf überschreibbar via getBlockPrefix().
4. Form-Theme
{# templates/form/string_field_theme.html.twig #}
{% block string_field_row %}
<div class="mb-3">
{% if label is not same as(false) and label %}
<label for="{{ form.value.vars.id }}" class="form-label">{{ label }}</label>
{% endif %}
{{ block('string_field_widget') }}
{% if form.value.vars.help %}
<div class="form-text">{{ form.value.vars.help }}</div>
{% endif %}
{{ form_errors(form) }}
</div>
{% endblock %}
{% block string_field_widget %}
<div class="input-group">
{{ form_widget(form.value) }}
<span class="input-group-text" title="{{ form.visibility.vars.attr.title }}">
{{ form_widget(form.visibility) }}
</span>
<span class="input-group-text" title="{{ form.canEdit.vars.attr.title }}">
{{ form_widget(form.canEdit) }}
</span>
</div>
{% endblock %}
Globale Registrierung in config/packages/twig.yaml:
twig:
form_themes:
- 'form/bootstrap_5_layout.html.twig'
- 'form/string_field_theme.html.twig'
Block-Konventionen:
{block_prefix}_row— komplette Zeile (Label + Widget + Help + Errors){block_prefix}_widget— nur das Widget (ohne Rahmen){block_prefix}_label— nur das Label{block_prefix}_errors— nur Fehleranzeige
5. Nutzung im übergeordneten FormType
// src/Form/AnimalCardType.php
$builder->add('animalType', StringFieldType::class, [
'label' => 'Tierart',
'value_label' => 'Tierart',
'value_help' => 'z.B. Hund, Katze, Kaninchen',
'value_attr' => ['placeholder' => 'z.B. Hund'],
]);
6. Rendering im Template
{{ form_row(form.animalType) }}
Das reicht — das Form-Theme übernimmt Label, Layout, Help-Text und Fehler.
Checkliste neuer Field-Type
- Value Object unter
src/Entity/ValueObject/mit#[ORM\Embeddable]und passenden Accessoren anlegen - FormType unter
src/Form/FieldType/mitdata_classauf das Value Object empty_datafür non-nullable String-/Bool-Felder setzen- Custom Options via
configureOptions()+setAllowedTypes() - Form-Theme unter
templates/form/anlegen ({prefix}_row+{prefix}_widget) - Theme in
config/packages/twig.yamleintragen - Im Ziel-Entity das Property via
#[ORM\Embedded]einbinden und im Konstruktor initialisieren - Migration:
ddev exec php bin/console make:migration→ prüfen, danndoctrine:migrations:migrate - Im übergeordneten FormType via
->add('propertyName', DeinFieldType::class, […])nutzen - Im Template
{{ form_row(form.propertyName) }}genügt