/form-render — Rendu Twig d'un formulaire Symfony
Tu écris ou révises le template qui affiche un form. Tu privilégies form_row et un thème global, tu ne casses la granularité qu'en cas de besoin réel (layout complexe, champs côte à côte, widgets intercalés).
Détection préalable
Lire composer.json ; vérifier symfony/twig-bundle et symfony/form. Mentionner Sylius en une ligne si présent (Sylius a ses propres thèmes de form côté admin/shop — les templates de resource sont surchargés via @SyliusAdmin ou @SyliusShop).
Règles fondamentales
- Thème défini globalement dans
config/packages/twig.yamlsousform_themes. Ne pas mettre{% form_theme form 'bootstrap_5_layout.html.twig' %}dans chaque template — c'est fait une fois, partout. form_rowpar défaut. Rendreform_label+form_widget+form_errorsmanuellement est réservé aux layouts qui l'exigent (form horizontal custom, widgets côte à côte, texte intercalé).{{ form(form) }}est acceptable quand le form est simple et n'a besoin d'aucun HTML autour des champs. Dès qu'un<fieldset>, un séparateur, ou deux champs sur une même ligne entre en jeu → passer à la forme explicite avecform_start/form_end.form_endappelleform_restqui rend les champs oubliés + le CSRF. Ne jamais désactiverrender_rest: falsesauf nécessité absolue (API-like avec CSRF désactivé).novalidateactivé sur la form pendant le développement pour tester la validation serveur ; retirer en prod si la validation HTML5 est acceptable.- Attributs via
attr(côté FormType ou dansform_widget(…, {'attr': {…}})), pas de concaténation HTML dans Twig. - Customisation par bloc Twig, pas par surcharge HTML à la main. Le bloc
{% block _<form_name>_<field>_widget %}surcharge un champ précis ;{% block form_row %}dans un thème local surcharge tous les rows.
Configuration globale
# config/packages/twig.yaml
twig:
form_themes:
- 'bootstrap_5_layout.html.twig'
# - 'bootstrap_5_horizontal_layout.html.twig' # label à gauche du champ
# - 'tailwind_2_layout.html.twig'
# - 'foundation_6_layout.html.twig'
Ordre important : les thèmes de bas en haut s'appliquent en cascade — le plus haut prime. Pour une surcharge projet, ajouter un fichier maison en bas (ex: form/_project.html.twig) qui étend bootstrap_5_layout.html.twig.
Patterns de rendu
Rendu basique (form simple)
{{ form_start(form) }}
{{ form_widget(form) }}
<button type="submit" class="btn btn-primary">Enregistrer</button>
{{ form_end(form) }}
Rendu granulaire (layout custom)
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
<div class="row">
<div class="col-md-6">
{{ form_row(form.firstName) }}
</div>
<div class="col-md-6">
{{ form_row(form.lastName) }}
</div>
</div>
<fieldset>
<legend>Adresse</legend>
{{ form_row(form.address.street) }}
{{ form_row(form.address.city) }}
</fieldset>
{{ form_rest(form) }}
<button type="submit" class="btn btn-primary">Enregistrer</button>
{{ form_end(form) }}
form_rest rend tout ce qui n'a pas encore été rendu explicitement, y compris le token CSRF. Le garder est une assurance contre l'oubli d'un champ.
Passer des attributs HTML ponctuels
{{ form_row(form.email, {
'attr': {'autocomplete': 'email', 'placeholder': 'vous@exemple.fr'},
'label_attr': {'class': 'form-label-sm'},
'help': 'Votre email professionnel',
}) }}
Pour des attributs systématiques, les définir dans le FormType :
->add('email', EmailType::class, [
'attr' => ['autocomplete' => 'email'],
])
Changer action / méthode
{{ form_start(form, {
'action': path('product_new'),
'method': 'POST',
'attr': {'class': 'my-form'},
}) }}
method: 'PUT'|'PATCH'|'DELETE' rend un <form method="POST"> + champ caché _method — Symfony le traduit côté serveur si framework.http_method_override: true.
Désactiver la validation HTML5
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
Afficher les erreurs
{{ form_errors(form) }} {# erreurs globales (contraintes de classe) #}
{{ form_errors(form.email) }} {# erreurs du champ email (déjà inclus par form_row) #}
Ne jamais doubler : form_row inclut déjà form_errors du champ.
Customisation par bloc
Dans un thème local templates/form/_project.html.twig :
{% use 'bootstrap_5_layout.html.twig' %}
{# Surcharge du widget de TOUS les champs money #}
{% block money_widget %}
<div class="input-group">
{{ parent() }}
<span class="input-group-text">€</span>
</div>
{% endblock %}
Pour un champ unique d'un form précis, cibler par le block prefix (nom du FormType + nom du champ) :
{% block _product_sku_widget %}
<div class="font-monospace">
{{ block('form_widget_simple') }}
</div>
{% endblock %}
Le nom du block vient de getBlockPrefix() du FormType — par défaut dérivé du nom de la classe (ProductType → product). Surchargeable si besoin.
Delta Sylius
- Les templates de resource (admin/shop) sont dans
@SyliusAdmin/@SyliusShop. Surcharger viatemplates/bundles/SyliusAdminBundle/…. - Sylius utilise ses propres thèmes (
@SyliusAdmin/Form/theme.html.twig). Ajouter un thème projet après danstwig.yamlpour surcharger sans casser les widgets vendor. - Champs traduisibles (
TranslationsType) → le rendu standard fonctionne, mais le layout multi-onglets par locale vient du thème Sylius — ne pas le remplacer à la main.
Déroulement
- Identifier le template concerné (
templates/<resource>/<action>.html.twig). - Vérifier
config/packages/twig.yaml— le thème global est-il bien défini ? - Choisir le niveau de granularité :
{{ form(form) }},form_widget, ouform_rowpar champ. - Déplacer tout attribut systématique vers le
FormType; garder dans le template seulement ce qui est spécifique au template. - Si customisation de widget récurrente → créer/éditer le thème local (
templates/form/_project.html.twig) et l'ajouter àtwig.yaml. - Tester via le contrôleur (→
/symfony:form-handle).
Argument optionnel
/symfony:form-render templates/product/new.html.twig — audit du template (granularité, attributs qui devraient migrer dans le FormType, thème).
/symfony:form-render ProductType — crée le template correspondant à un FormType existant.
/symfony:form-render sans argument — demande le template ou le FormType.