Astuce Les formulaires dans Symfony

Cet article est une synthèse de la documentation officielle : Forms.

Généralités

Dans Symfony, un formulaire et un champ sont représentés tous deux par la FormInterface.

Un formulaire contient des champs ou des sous-formulaires enfants, pouvant eux-mêmes en avoir.
(Il s'agit là du Design Pattern Composite.)

Type de champ/formulaire

La FormTypeInterface permet de représenter un type de champ/formulaire (ex: ContactFormType, BirthdayType, ...). En fonction de ce type, le champ/formulaire aura telles ou telles options, et tel ou tel rendu.

La classe BaseType l'implémente. En héritent deux sous-classes : FormType pour les champs et ButtonType pour les boutons.

Définition de formulaire

Pour définir un nouveau type de champ/formulaire, il faut donc implémenter FormTypeInterface.
Pour simplifier cette tâche, on étend AbstractType, qui implémente déjà les méthodes avec des comportements neutres.

De plus, on surcharge éventuellement sa méthode getParent(), pour indiquer le type précis dont on veut hériter les propriétés (ex: EmailType, EntityType, ...).

Cette mécanique consistant à ne pas utiliser l'héritage direct PHP des FormTypes qui nous intéresse, évite de devoir appeler nous-même les méthodes parentes avant d'y ajouter nos traitements spécifiques.
L'appel au parent est fait automatiquement et nous n'avons qu'à nous occuper des traitements spécifiques.

Exemples natifs :

use \Symfony\Component\Form\AbstractType;

// Héritage PHP du type neutre
class DateType extends AbstractType {
    public function getParent() {
        // Héritage des propriétés/options/... du type de base
        return FormType::class;
    }
}

// Héritage PHP du type neutre
class BirthDayType extends AbstractType {
    public function getParent() {
        // Héritage des propriétés/options/... de DateType
        return DateType::class;
    }
}

// Héritage PHP du type neutre
class FormType extends AbstractType {
    public function getParent() {
        // Pas d'héritage de propriétés/options/...
        return null;
    }
}

Extension de type

Pour des modifications légères de types déjà existants (par exemple ajouter des options), on peut implementer l'interface FormTypeExtensionInterface.

Documentation

Création d'un formulaire

Pour créer un formulaire, on utilise un FormBuilder ou on laisse Symfony deviner les champs en fonction d'une classe.

Pour cela :

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

// Depuis un contrôleur étendant AbstractController
$form = $this->createFormBuilder($task)
        ->add('task', TextType::class)
        ->add('dueDate', DateType::class)
        ->add('save', SubmitType::class, ['label' => 'Create Task'])
        ->getForm();

// ou en laissant Symfony deviner
$form = $this->createForm(Task::class, $task, $options);

// Depuis n'importe où
/** @var \Symfony\Component\Form\FormFactoryInterface $form */
$form = $this->formFactory
        -> createBuilder(Task::class, $task, $options)
        ->add('task', TextType::class)
        ->add('dueDate', DateType::class)
        ->add('save', SubmitType::class, ['label' => 'Create Task'])
        ->getForm();

// ou en laissant Symfony deviner
$form = $this->formFactory->create(Task::class, $task, $options);

Dans le second cas, Symfony se base sur des Guesser qui devinent les champs à partir :

  • du mapping Doctrine (pour les entités)
  • du typage PHP des propriétés (>= 7.4)
  • des contraintes de validation

Vérification de la requête

FormInterface définit la méthode handleRequest(), chargée de vérifier que tout est valide puis éventuellement d'appeler la méthode submit() du formulaire :

  • Vérification du verbe HTTP (POST, PUT, ...)
  • Récupération des données depuis la query ou dans le corps, selon la configuration
  • Vérification des paramètres serveur (ex: post_max_size)
  • Soumission éventuelle du formulaire

Soumission du formulaire

À la soumission du formulaire (ak. méthode submit()) :

  • les données sont enregistrées dans le formulaire
  • chaque champ est validé, selon ses contraintes
    • en cas d'erreur, la méthode addError() est appelée sur le champ
  • les évènements sont dispatchés

Listes des options d'un FormType

La commande suivante permet de lister toutes les options d'un type de champ/formulaire et d'indiquer si elles sont héritées ou surchargées :

# Remplacer EntityType par n'importe quel FormType
bin/console debug:form EntityType

Rendu avec Twig

Il est considéré comme une bonne pratique, de rendre directement un formulaire ou un de ses champs via les fonctions de rendu dédiées. Toute modification du balisage qu'elles génèrent devrait être réalisée via la surcharge de thème.

Par défaut, plusieurs thèmes sont disponibles, notamment Bootstrap 3 et 4. Pour en ajouter :

# config/packages/twig.yaml

twig:
  form_themes:
    - 'form/my_custom_form_theme.html.twig'

Un formulaire ou un champ dans un template est un objet de type FormView.

Choix du thème

Il est possible de choisir un thème spécifique pour rendre un formulaire ou un champ, via :

{% form_theme my_form 'form/app_custom_form_theme.html.twig' %}

{% form_theme my_form.my_field 'form/app_custom_form_theme.html.twig' %}

Surcharge du thème

Chaque thème définit un certain nombre de blocs, nommés d'une manière à définir quel type de champ il est susceptible de rendre (ex: integer_widget).

Pour le surcharger, il suffit de redéfinir le bloc dans le template courant, comme n'importe quel bloc.

Les suffixes disponibles pour les noms de bloc sont les suivants : _widget, _row, _label, _help.
Les préfixes disponibles sont listés dans la variable block_prefixes présente dans le contexte du bloc.

Exemple : ['form','text','textarea', '_post_content'], pour un champ nommé content, de type TextAreaType (héritant de TextType héritant lui-même de FormType), au sein du type de formulaire spécifique PostType.

Data transformers

Un data transformer est chargé de transformer une donnée (sous forme de tableau, objet, entier, ...) vers une chaîne (ou un tableau de chaînes) exploitable dans un formulaire, et vice versa.

Il est représenté par l'interface DataTransformerInterface et requiert deux méthodes :

  • transform() : Donnée => (tableau de) chaîne(s) de caractères
  • reverseTransform() : (Tableau de) chaîne(s) de caractères => donnée

Pour en ajouter un à formulaire, on utilise le FormBuilder :

$formBuilder->addViewTransformer(new MyCustomTransformer());

// Note: il existe également cette méthode si la transformation doit être effectuée en amont de la transformation
// de la précédente
$formBuilder->addModelTransformer(new MyCustomModelTransformer());

Évènements

Tous les évènements liés aux formulaires sont des FormEvent, permettant d'accéder aux données du formulaire et éventuellement de les modifier.

Ils ont lieu dans cet ordre chronologique :

  • FormEvents::PRE_SET_DATA : on peut y modifier la donnée en fonction du formulaire

  • Hydratation du formulaire et ses enfants, avec la donnée

  • FormEvents::POST_SET_DATA : on peut y modifier le formulaire en fonction de la donnée (ex: champ affiché en fonction d'un autre champ)

  • FormEvents::PRE_SUBMIT : on peut y changer la donnée soumise dans la requête (ex: la normaliser, la trimmer, ...)

  • Transformation vers un "objet" de données, via les méthodes reverseTransform() des DataTransformer.

  • FormEvents::SUBMIT : les données sont prêtes, mais peut-être pas celles des parents

  • FormEvents::POST_SUBMIT : les données sont prêtes, le composant Validation écoute cet évènement pour les valider

90% du temps, on n'a besoin d'agir que sur les évènements FormEvents::POST_SET_DATA ou FormEvents::PRE_SUBMIT uniquement.