Les formulaires dans Symfony2

Dans ce chapitre, nous allons aborder la création de formulaires dans Symfony 2. Nous verrons comment créer une classe de formulaire, comment bien la structurer et comment l'utiliser dans twig.




Création de notre premier formulaire Symfony2


La création de formulaires dans Symfony2 est, pour moi, vraiment plus simplifiée et efficace que dans Symfony 1.x. (Ma bête noire dans l'ancienne version du framework, pas vous?).

Les formulaires reposent sur le composant Form, qui peut d'ailleurs être utilisé en standalone dans d'autres projets non sf2.

Les formulaires sont rattachés à des classes: Soit une entité sf2 soit une classe dédiée au formulaire (Comme une classe ForgetPassword).

Il est possible de générer un formulaire directement dans une action.
Prenons un cas pratique, nous voulons générer le formulaire de création d'un bureau avec titre, résumé et description.
Voici à quoi ressemblerait notre action:

Code php:
    public function newAction(Request $request)
    {
        // On initialise notre objet Desk
        $desk = new Desk();
    
        // On créé l'objet form à partir du formBuilder (En passant en param l'objet Desk)
        $form = $this->createFormBuilder($desk)
            ->add('title', 'text') // On ajoute le champ titre dans un input text
            ->add('summary', 'textarea') // Idem pour le résumé mais dans un champ textarea
            ->add('description', 'textarea') // Idem pour description
            ->getForm(); // On récupère l'objet form 
    
        if ($request->getMethod() == 'POST') { // Si on a soumis le formulaire
            $form->bindRequest($request); // On bind les valeurs du POST à notre formulaire
    
            if ($form->isValid()) { // On teste si les données entrées dans notre formulaire sont valides
                
                // On sauvegarde, redirige etc.
    
            }
        }

    }


On a créé notre formulaire, il ne reste plus qu'à l'envoyer dans le template Twig.

Pour rester dans de bonnes pratiques, nous n'allons pas créer le formulaire dans nos contrôleurs (il faut toujours éviter de mettre trop de code dans les contrôleurs).
Nous allons créer des classes de formulaires.

Créez à présent les répertoires suivants:
  • src/Wmd/WatchMyDeskBundle/Form
  • src/Wmd/WatchMyDeskBundle/Form/Type
  • src/Wmd/WatchMyDeskBundle/Form/Data

Vous l'aurez compris, tous ce qui concerne les formulaires sera stocké dans le répertoire Form.
Le répertoire Type contiendra toutes les classes de formulaire.
Le répertoire Data quant à lui contiendra les classes "de définition" qui ne sont pas des entités (Exemple une classe Contact qui définira les champs du formulaire de contact).

Passons à la pratique maintenant. Nous allons créer le formulaire de la classe Desk.
Créez le fichier Form/Type/DeskType.php

Par convention, le nom de fichier des formulaires est à suffixer de "Type".



Code php:
<?php

namespace Wmd\WatchMyDeskBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Wmd\WatchMyDeskBundle\Form\Type\DeskPictureType;

class DeskType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('summary')
            ->add('description');
    }

    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'Wmd\WatchMyDeskBundle\Entity\Desk',
        );
    }

    public function getName()
    {
        return 'Desk';
    }
}


Notre DeskType comme toute classe Type va étendre la classe AbstractType qui propose un certain nombre de méthodes.
  • Dans la méthode buildForm, on va ajouter les champs souhaités du formulaire.
  • getDefaultOptions permet de spécifier, à travers la clé data_class quelle est l'entité ou la classe liée au formulaire.
  • Et enfin getName permet de donner un nom unique au formulaire. (C'est ce nom qui sera utilisé comme préfixe dans le nom des inputs en HTML.)



Vous suivez toujours ?


Les types de champs dans les formulaires Symfony2


Nous avons utilisé des types de champs dans notre formulaire: text et textarea.

Il n'est pas obligatoire de spécifier le type de champ dans la méthode add. En effet, Symfony2 détecte automatiquement quel est le type de données et adapte le champ en fonction.

Les champs disponibles sont les suivants:

text



Champ input text classique
Code php:
$builder->add('test', 'text', array('max_length' => 20, 'required' => false, 'label' => 'Login', 'trim' => true, 'read_only' => false, 'error_bubbling' => false));

max_length: Attribut max_length du champ input text.
required: Champs requis ? (html5 validation)
label: Label du champ
trim: Appliquer un trim sur la valeur ?
read_only: Champ en lecture seule ?
error_bubbling: Faut-il sortir l'erreur du champ dans les erreurs générales du form ?

Exemple de rendu:

Code html:
<input type="text" value="Ceci est un test de phrase assez longue pour ne pas tenir dans toute la boite" maxlength="20" name="nomform[test]" id="nomform_test">


textarea



Champ de type textarea
Code html:
<textarea required="required" name="nomform[test]" id="nomform_test">


email



Champ input email (html5)
Code html:
<input type="email" value="" required="required" name="nomform[test]" id="nomform_test">


search



Champ input type search (html5)

url



Champ input type url(html5)
Code php:
$builder->add('test', 'url', array('default_protocol' => 'http', 'max_length' => 120, 'required' => false, 'label' => 'Site web', 'trim' => true, 'read_only' => false, 'error_bubbling' => false));

Une option spécifique:
default_protocol: Permet de rajouter le suffixe spécifié (http:// par défaut) à la valeur saisie si elle ne commence pas par ce préfixe.
Code html:
<input type="url" id="nomform_test" name="nomform[test]" maxlength="120" value="">


password



Champ input type password

integer



Champ input number (html5). Permet d'appliquer différentes options pour convertir une valeur décimale en entier par exemple (Arrondir inférieur ou supérieur...).
Code php:
$builder ->add('test', 'integer', array('rounding_mode' => IntegerToLocalizedStringTransformer::ROUND_CEILING, 'grouping' => NumberFormatter::GROUPING_USED));

rounding_mode: permet de choisir le mode d'arrondissement.
grouping: permet de mettre en forme le nombre suivant la locale.

Exemple de rendu pour la valeur 123456789.123:

Code html:
<input type="number" value="1" required="required" name="nomform[test]" id="nomform_test">


number



Champ input number (html5) similaire à integer mais permet l'affichage de décimales.

Code php:
$builder ->add('test', 'number', array('precision' => 2, 'rounding_mode' => IntegerToLocalizedStringTransformer::ROUND_CEILING, 'grouping' => \NumberFormatter::GROUPING_USED));

rounding_mode: permet de choisir le mode d'arrondissement.
grouping: permet de mettre en forme le nombre suivant la locale.
precision: Précision après la virgule

Exemple de rendu pour la valeur 123456789.23456:

Code html:
<input type="text" value="123 456 789,23" required="required" name="nomform[test]" id="nomform_test">


money



Champ input text permettant de gérer les prix / monnaies.

Code php:
$builder->add('test', 'money', array('currency' => 'EUR', 'precision' => 2, 'grouping' => NumberFormatter::GROUPING_USED));

currency: Permet d'afficher après l'input le signe de la devise.
precision: Précision après la virgule
divisor: Coefficient diviseur (Afficher en k€ ...)
grouping: permet de mettre en forme le nombre suivant la locale.

Exemple de rendu pour la valeur 123456789.23456:

Code html:
<input type="text" value="123 456 789,23" required="required" name="nomform[test]" id="nomform_test">€


percent



Champ input text permettant de gérer les prix / monnaies.

Code php:
$builder->add('test', 'percent', array('precision' => 2));

type: fractional ou integer suivant si le chiffre est stocké en décimale ou en entier.
precision: Précision après la virgule

Exemple de rendu pour la valeur 123456789.23456:

Code html:
<input type="text" value="12 345 678 923,46" required="required" name="nomform[test]" id="nomform_test">%


date



Le widget date permet d'afficher un sélecteur de date pouvant prendre 3 formes:
  • Un widget composé de 3 select (Année, mois, jour): Valeur "choice" dans l'option widget.
  • Un widget composé de 3 inputs text (Année, mois, jour): Valeur "text" dans l'option widget.
  • Un widget composé d'un seul input text affichant la date: Valeur "single_text" dans l'option widget.


Code php:
$builder->add('test', 'date', array('widget' => 'choice', 
                                            'input' => 'timestamp', 
                                            'format' => 'd/M/y', 
                                            'empty_value' => array('year' => 'Année', 'month' => 'Mois', 'day' => 'Jour'),
                                            'pattern' => "{{ day }}/{{ month }}/{{ year }}",
                                            'data_timezone' => "Europe/Paris",
                                            'user_timezone' => "Europe/Paris"
                                            )
                    );

widget: Type de widget à rendre: choice, text ou single_text
input: Format de la donnée en entrée: string, datetime, array ou timestamp
format: Format de la date sur le widget
pattern: Pattern de la date en fonction du format (Un peu étrange)
empty_value: Libellé de la valeur vide
data_timezone: Timezone de la date en entrée
user_timezone: Timezone de l'utilisateur actuel
days, months et years: Tableau des valeurs à afficher dans le widget choice.

time



Le widget time permet d'afficher un sélecteur d'heure / minute / secondes.
Similaire au widget date.


Code php:
        $builder->add('test', 'time', array('widget' => 'choice', 
                                            'input' => 'timestamp', 
                                            'with_seconds' => false, 
                                            'data_timezone' => "Europe/Paris",
                                            'user_timezone' => "Europe/Paris"
                                            )
                    );

widget: Type de widget à rendre: choice, text ou single_text
input: Format de la donnée en entrée: string, datetime, array ou timestamp
format: Format de la date sur le widget
with_seconds: Avec ou sans les secondes
data_timezone: Timezone de la date en entrée
user_timezone: Timezone de l'utilisateur actuel
hours, minutes et seconds: Tableau des valeurs à afficher dans le widget choice.

datetime



Le widget datetime reprend les widgets date + time.

birthday



Le widget birthday n'est ni plus ni moins un widget date avec une sélection spéciale au niveau des années. Il prend les 120 dernières années dans le select.

choice



Le widget choice permet de proposer une liste de choix à l'utilisateur sous forme de select, radio ou checkbox en fonction des différentes options spécifiées.
Code php:
$builder->add('test', 'choice', array('choices' => array(1 => "choix 1", 2 => "choix 2", 3 => "choix 3"), 
                                            'multiple' => false, 
                                            'expanded' => true, 
                                            'preferred_choices' => array(2),
                                            'empty_value' => '- Choisissez une option -',
                                            'empty_data'  => -1
                                            )
                    );

choices: Array contenant la liste des choix en mode associatif
choice_list: ChoiceListInterface listant les choix
multiple: Si passé à true, il sera possible de sélectionner plusieurs valeurs dans la select box. Si expanded est à true, alors le widget affichera des checkbox au lieu du select.
expanded: Si expanded est à true, des radios seront affichés à la place de la select
preferred_choices: Choix de réponse mis en avant dans la select
empty_value: Libellé de l'option du select quand aucune valeur n'est sélectionnée
empty_data: Valeur de l'option du select ci-dessus

entity



Le widget entity permet de générer un widget de choix automatique à l'aide d'une relation entre deux entités. Exemple: Rendre la liste des catégories liées à une entité Page.
Il dispose des mêmes options que le widget choice avec quelques spécificités en plus, comme la possibilité de passer une requête QueryBuilder Doctrine pour classer les résultats par ordre alphabétique par exemple...
Code php:
$builder->add('comments', 'entity', array(
    'class' => 'WmdWatchMyDeskBundle:DeskComment',
    'query_builder' => function(EntityRepository $er) {
        return $er->createQueryBuilder('dc')
            ->orderBy('dc.createdAt', 'DESC');
    },
));

class: Nom de la classe liée
property: Nom du champ de l'entité servant de libellé dans le select
query_builder: Fonction retournant une DQL Doctrine pour affiner les résultats à afficher dans la select
em: Pour passer un EntityManager différent que celui par défaut.

country



Permet d'afficher la liste des pays dans la langue actuelle de l'utilisateur.
La valeur des options est au format ISO-3166 (Code pays sur 2 caractères)

language



Comme pour les pays, le widget language va lister toutes les langues du monde, dans la langue de l'utilisateur.

locale



Liste des locales du monde, langues associées aux pays. Ex: Français (Suisse) ou Français (Belgique)

timezone



Liste des timezone du monde, classées par continents.

checkbox



Widget input checkbox.
Code php:
$builder->add('test', 'checkbox', array("label" => "Publier l'actu ?", "required" => false, "value" => "ValeurCheckbox"));

value: Valeur de la propriété value de l'input

radio



Widget input radio.
Inutile à l'utilisation en l'état... Privilégier les choice ou entity avec l'option expanded

hidden



Widget input hidden.

file



Champ de type input file pour uploader des fichiers.

collection



Ce champ est plus complexe. Il permet de combiner un ensemble d'autres champs.
Par exemple, nous l'utiliserons pour créer une liste de champs d'upload pour les images de notre bureau.
C'est ainsi que l'on peut "embed" d'autres formulaires dans un form.
Code php:
$builder->add('pictures', 'collection', array(
                                'type' => new DeskPictureType(),
                                'allow_add' => true,
                                'allow_delete' => true,
                                'prototype' => true,
                                'label' => 'Pictures'));

type: Le type d'objet à combiner. Peut être "email", "url"... ou un objet FormType.
options: Les options à passer à nos widgets embed
allow_add: Si activé, il sera possible d'ajouter de nouveaux item via du JS
allow_delete: Si activé, il sera possible de supprimer des items existants
prototype: Faut il générer le prototype HTML du widget embed utilisé ensuite par un script JS pour insérer de nouveaux items (si allow_add = true)

Ne pas oublier d'importer la classe utilisée dans les namespaces du formulaire.

Nous verrons comment utiliser ce widget plus en détail dans la suite du tuto.

repeated



Un widget très pratique, notamment pour le fameux "Répéter le mot de passe".
Code php:
$builder->add('password', 'repeated', array(
    'type' => 'password',
    'invalid_message' => 'Les mots de passe entrés ne correspondent pas.',
    'options' => array('label' => 'Password'),
    'first_name' => "password",
    'second_name' => "repassword",
));

type: Type du champ
invalid_message: L'erreur à afficher si les champs ne correspondent pas
options: Les options à passer aux widgets répétés
first_name: Le nom du champ 1
second_name: Le nom du champ 2


Toute la liste des widgets de formulaires Symfony2 et leurs options détaillées est disponible sur le site officiel du framework.



Gérer notre formulaire Symfony2 dans les contrôleurs


Nous n'avons pas besoin de créer une classe dans le répertoire Data/ dans ce cas car le formulaire est directement lié à notre Entité Desk.

Passons à présent à l'intégration dans notre contrôleur et dans un template Twig.

Ouvrez le fichier DeskController.php, puis ajoutez le namespace pour inclure DeskType et Desk:
Code php:
use Wmd\WatchMyDeskBundle\Form\Type\DeskType;
use Wmd\WatchMyDeskBundle\Entity\Desk;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;


Nous allons créer l'action d'ajout d'un nouveau bureau:

Code php:
    /**
     * @Route("/add", name="desk_add")
     * @Template()
     */
    public function addAction()
    {
        $request = $this->get('request'); // On récupère l'objet request via le service container
        $desk = new Desk(); // On créé notre objet Desk vierge
        
        $form = $this->get('form.factory')->create(new DeskType(), $desk); // On bind l'objet Desk à notre formulaire DeskType
        
        if ('POST' == $request->getMethod()) { // Si on a posté le formulaire
            $form->bindRequest($request); // On bind les données du form
            if ($form->isValid()) { // Si le formulaire est valide
                
                $this->get('wmd.desk_manager')->saveDesk($desk); // On utilise notre Manager pour gérer la sauvegarde de l'objet
                
                // On envoi une 'flash' pour indiquer à l'utilisateur que le bureau est ajouté
                $this->get('session')->setFlash('notice', 
                    $this->get('translator')->trans('Desk added')
                );
                
                // On redirige vers la page de modification du bureau
                return new RedirectResponse($this->generateUrl('desk_edit', array(
                    'deskId' => $desk->getId()
                )));
            }
        }

        return array('form' => $form->createView(), 'desk' => $desk); // On passe à Twig l'objet form et notre objet desk
    }


Pour créer notre formulaire, nous utilisons directement le service form.factory, mais nous aurions pu utiliser le raccourci: $this->createForm(new DeskType(), $desk); étant donné que notre contrôleur étant la classe Controller



Nous aurons aussi besoin de l'action desk_edit qui sera appelée une fois le bureau inséré.
Ajoutez:

Code php:
/**
     * @Route("/edit/{deskId}", name="desk_edit")
     */
    public function editAction($deskId)
    {
        $request = $this->get('request');
        
        // On vérifie que l'ID du bureau existe
        if (!$desk = $this->get('wmd.desk_manager')->loadDesk($deskId)) {
            throw new NotFoundHttpException(
                $this->get('translator')->trans('This desk does not exist.')
            );
        }
        
        // On bind le bureau récupéré depuis la BDD au formulaire pour modification
        $form = $this->get('form.factory')->create(new DeskType(), $desk);
        
        // Si l'utilisateur soumet le formulaire
        if ('POST' == $request->getMethod()) {
            $form->bindRequest($request);
            if ($form->isValid()) {
                
                $this->get('wmd.desk_manager')->saveDesk($desk);
                
                $this->get('session')->setFlash('notice',
                    $this->get('translator')->trans('Desk updated.')
                );
                
                return new RedirectResponse($this->generateUrl('desk_edit', array(
                    'deskId' => $desk->getId()
                )));
            }
        }

        return $this->render('WmdWatchMyDeskBundle:Desk:add.html.twig',array('form' => $form->createView(), 'desk' => $desk)); // On change le template par défaut et on réutilise celui de add qui est le même
    }


Vous noterez que les routes sont simplifiées, peut être même un peu trop. Il serait préférable d'avoir un prefixe /desk/ dans les routes concernant le contrôleur Desk.
Nous allons ajouter l'annotation suivante au niveau du contrôleur:
Code php:
/**
 * @Route("/desk")
 */
class DeskController extends Controller
{

Cette annotation bien pratique va permettre de préfixer toutes les routes des actions du contrôleur par /desk. Pratique si un jour vous souhaitez changer ce préfixe, vous n'aurez qu'à le changer à un endroit.

C'est tout pour la partie contrôleur. Dernière étape pour pouvoir tester notre formulaire, la création de nos templates Twig.


Création des vues pour notre formulaire Symfony2


Nous allons créer la vue qui servira à la fois pour ajouter et modifier notre bureau: add.html.twig.

Créez le fichier Resources/views/Desk/add.html.twig et ajoutez:
Code html:
{% extends '::base.html.twig' %}
{% block title %}{% if desk.id %}{{ 'Edit desk'|trans }}{% else %}{{ 'Add desk'|trans }}{% endif %}{% endblock %}

{% block body %}
<form action="{% if desk.id %}{{ path('desk_edit', {'deskId':desk.id}) }}{% else %}{{ path('desk_add') }}{% endif %}" method="post" {{ form_enctype(form) }}>
    <h1>{% if desk.id %}{{ 'Edit desk'|trans }}{% else %}{{ 'Add desk'|trans }}{% endif %}</h1>
    <div>
    {{ form_errors(form.title) }}
    {{ form_label(form.title, 'Title'|trans) }}
    {{ form_widget(form.title) }}
    </div>
    <div>
    {{ form_errors(form.summary) }}
    {{ form_label(form.summary, 'Summary'|trans) }}
    {{ form_widget(form.summary) }}
    </div>
    <div>
    {{ form_errors(form.description) }}
    {{ form_label(form.description, 'Description'|trans) }}
    {{ form_widget(form.description) }}
    </div>
    
    {{ form_rest(form) }}
    <input type="submit" id="submit" value="{% if desk.id %}{{ 'Update'|trans }}{% else %}{{ 'Add'|trans }}{% endif %}" name="submit" />
</form>

{% endblock %}


Quelques explications sur ce template twig



{% extends '::base.html.twig' %}: On étend le layout de base.
{% block title 'Add desk'|trans %}: On change la balise title de la page
{{ form_errors(form.title) }}: Permet d'afficher les erreurs éventuelles concernant le champ title.
{{ form_label(form.title, 'Title'|trans) }}: Permet de générer le label du champ title avec le libellé renseigné (Ou celui par défaut si aucun n'est renseigné).
{{ form_widget(form.description) }}: Va générer le widget en HTML pour notre champ title.
{{ form_rest(form) }}: Génère tous les champs restants du formulaire (qui n'ont pas été déclarés manuellement dans le template), ainsi que les champs cachés.

Vous noterez que l'on utilise le modifier twig trans qui nous permettra plus tard de traduire les champs de la vue en fonction de la langue des utilisateurs.

Il ne nous reste plus qu'à tester notre formulaire.
Rendez-vous sur la page /desk/add de votre projet, vous devriez obtenir le formulaire suivant (sans mise en page):
Formulaire Symfony2 Twig


Si l'on regarde le code généré par nos markup twig voilà ce que ça donne:
Code html:
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Add desk</title>
                <link rel="stylesheet" href="/bundles/wmdwatchmydesk/css/style.css" type="text/css" media="all" />
                <link rel="shortcut icon" href="/favicon.ico" />
    </head>
    <body>

        <form action="/app_dev.php/desk/add" method="post" >
    <h1>Add desk</h1>
    <div>
    
    <label for="Desk_title" class=" required">Title</label>
    <input type="text" id="Desk_title" name="Desk[title]" required="required" maxlength="255" value="" />
    </div>
    <div>
    
    <label for="Desk_summary" class=" required">Summary</label>

    <textarea id="Desk_summary" name="Desk[summary]" required="required"></textarea>
    </div>
    <div>
    
    <label for="Desk_description" class=" required">Description</label>
    <textarea id="Desk_description" name="Desk[description]" required="required"></textarea>
    </div>
    
    <input type="hidden" id="Desk__token" name="Desk[_token]" value="bc5b2345db01020bb2f15cd4615c3c890a13de1" />

    <input type="submit" id="submit" value="Add" name="submit" />
</form>

</body>
</html>


Vous remarquerez dans le code l'utilisation d'attributs HTML5 comme les required.
En fin de formulaire, le markup {{ form_rest(form) }} à généré l'input hidden du token anti attaques CSRF, essentiel si vous souhaitez que votre formulaire soit validé.

Si vous validez le formulaire sans avoir rempli un champ "required", votre navigateur (moderne uniquement) vous le signalera directement:
sf2 forms validation HTML5

Ça peut être embêtant pour certains cas, mais il est possible de le désactiver en rajoutant l'attribut novalidate="novalidate" dans notre balise form:
Code html:
<form action="{% if desk.id %}{{ path('desk_edit', {'deskId':desk.id}) }}{% else %}{{ path('desk_add') }}{% endif %}" method="post" {{ form_enctype(form) }} novalidate="novalidate">


Notre formulaire fonctionne, on peut poster des bureaux, mais aucune donnée n'est vérifiée! Si vous désactivez la validation HTML5 et que vous ne mettez rien dans le champ title, vous allez avoir une belle erreur:
Erreur de post du formulaire sans validation


Ne vous inquiétez pas, c'est l'objet du prochain chapitre !





Rechercher sur la Ferme du web