L'enjeu des formulaires
Vous avez déjà créé des formulaires en HTML et PHP, vous savez donc que c'est une vraie galère ! À moins d'avoir créé vous-même un système dédié, gérer correctement des formulaires s'avère être un peu mission impossible.
Par correctement, j'entends de façon maintenable, mais surtout,
réutilisable. En effet, que ceux qui pensent pouvoir réutiliser facilement leur formulaire de création d'un article de blog dans une autre page lèvent la main… Personne ? C'est bien ce que je pensais.
Heureusement, le composant
Form de Symfony2 arrive à la rescousse !
N'oubliez pas que les composants peuvent être utilisés hors d'un projet Symfony2. Vous pouvez donc reprendre le composant Form dans votre site même si vous n'utilisez pas Symfony2.
Un formulaire Symfony2, qu'est-ce que c'est ?
La vision Symfony2 sur les formulaires est la suivante :
un formulaire se construit sur un objet existant, et son objectif est d'hydrater cet objet.
Reprenons cette définition.
Un objet existant
Il nous faut donc des objets de base avant de créer des formulaires. Mais en fait, ça tombe bien : on les a déjà, ces objets ! En effet, un formulaire pour ajouter un article de blog va se baser sur l'objet
Article, objet que nous avons construit lors du chapitre précédent. Tout est cohérent.
Je dis bien « objet » et non « entité Doctrine2 ». En effet, les formulaires n'ont pas du tout besoin d'une entité pour se construire, mais uniquement d'un simple objet. Heureusement, nos entités sont de simples objets avant d'être des entités, donc elles conviennent parfaitement.
Prenons un exemple pour illustrer cela : un formulaire de recherche.
A priori, nous n'avons pas d'entité
Recherche car cela n'a pas de sens (sauf si vous souhaitez enregistrer en base de données toutes les recherches effectuées, pourquoi pas), et pourtant, on a bien besoin d'un formulaire de recherche ! Il suffit donc de créer un simple objet
Recherche, composé d'un seul attribut « requête », par exemple. Cet objet est suffisant pour construire notre formulaire, et ce n'est pas une entité Doctrine2.
Pour la suite de ce chapitre, nous allons utiliser notre objet
Article. C'est un exemple simple qui va nous permettre de construire notre premier formulaire. Je rappelle son code, sans les annotations pour plus de clarté (et parce qu'elles ne nous regardent pas ici) :
Secret (cliquez pour afficher)Code : PHP 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70 | <?php
namespace Sdz\BlogBundle\Entity;
use Sdz\BlogBundle\Entity\Tag;
class Article
{
private $id;
private $date;
private $titre;
private $contenu;
private $auteur;
private $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
public function getId()
{
return $this->id;
}
public function setDate($date)
{
$this->date = $date;
}
public function getDate()
{
return $this->date;
}
public function setTitre($titre)
{
$this->titre = $titre;
}
public function getTitre()
{
return $this->titre;
}
public function setContenu($contenu)
{
$this->contenu = $contenu;
}
public function getContenu()
{
return $this->contenu;
}
public function getAuteur()
{
return $this->auteur;
}
public function setAuteur($auteur)
{
$this->auteur = $auteur;
}
public function addTag(Tag $tag)
{
$this->tags[] = $tag;
}
public function getTags()
{
return $this->tags;
}
}
|
Rappel : la convention pour le nom des getters/setters est importante : lorsque l'on parlera du champ « auteur », le composant Form utilisera l'objet via les méthodes setAuteur() et getAuteur() (comme le faisait Doctrine2 de son côté). Donc si vous aviez eu "set_auteur()" ou "recuperer_auteur()", ça n'aurait pas fonctionné.
Objectif : hydrater cet objet
Hydrater ? Un terme précis pour dire que le formulaire va remplir les attributs de l'objet avec les valeurs entrées par le visiteur. Faire
<?php setAuteur('winzou') ?> et
<?php setDate(new \Datetime()), etc., c'est
hydrater l'objet
Article.
Le formulaire en lui-même n'a donc comme seul objectif que d'hydrater un objet. Ce n'est qu'une fois l'objet hydraté que vous pourrez en faire ce que vous voudrez : faire une recherche dans le cas d'un objet
Recherche, enregistrer en base de données dans le cas de notre objet
Article, envoyer un mail dans le cas d'un objet
Contact, etc. Le système de formulaire ne s'occupe pas de ce que vous faites de votre objet, il ne fait que l'hydrater.
Une fois que vous avez compris ça, vous avez compris l'essentiel. Le reste n'est que de la syntaxe à connaître.
Gestion basique d'un formulaire
Concrètement, pour créer un formulaire, il nous faut deux choses :
- un objet (on a toujours notre objet Article) ;
- un moyen pour construire un formulaire à partir de cet objet, un FormBuilder, constructeur de formulaire en français.
Pour faire des tests, placez-vous dans l'action
ajouterAction() de notre contrôleur
Blog. Modifiez-la comme suit :
Code : PHP 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | <?php
// src/Sdz/BlogBundle/Controller/BlogController.php
use Sdz\BlogBundle\Entity\Article;
// ...
public function ajouterAction()
{
// On crée un objet Article.
$article = new Article();
// On crée le FormBuilder grâce à la méthode du contrôleur.
$formBuilder = $this->createFormBuilder($article);
// On ajoute les champs de l'entité que l'on veut à notre formulaire.
$formBuilder
->add('date', 'date')
->add('titre', 'text')
->add('contenu', 'textarea')
->add('auteur', 'text');
// Pour l'instant, pas de tags, on les gérera plus tard.
// À partir du formBuilder, on génère le formulaire.
$form = $formBuilder->getForm();
// On passe la méthode createView() du formulaire à la vue
// afin qu'elle puisse afficher le formulaire toute seule.
return $this->render('SdzBlogBundle:Blog:ajouter.html.twig', array(
'form' => $form->createView(),
));
}
|
Pour le moment, ce formulaire n'est pas opérationnel. On va pouvoir l'afficher, mais il ne se passera rien lorsqu'on va le valider.
Avant cela, essayons de comprendre le code présenté. Dans un premier temps, on récupère le
FormBuilder. Cet objet n'est pas le formulaire en lui-même, c'est un
constructeur de formulaire. On lui dit : «
Crée un formulaire autour de l'objet $article. », puis : «
Ajoute les champs « date », « titre », « contenu » et « auteur ». » Et enfin : «
Maintenant, donne-moi le formulaire construit avec tout ce que je t'ai dit avant. ».
Prenons le temps de bien faire la différence entre les attributs de l'objet hydraté et les champs du formulaire. Un formulaire n'est pas du tout obligé d'hydrater tous les attributs d'un objet. On pourrait très bien ne pas utiliser le pseudo pour l'instant par exemple, et ne pas mettre de champ "auteur" dans notre formulaire. L'objet lui contient toujours l'attribut "auteur", mais il ne sera juste pas hydraté par le formulaire. Bon en l'occurrence ce n'est pas le comportement que l'on veut (on va considérer le pseudo comme obligatoire pour un article), mais sachez que c'est possible

. D'ailleurs si vous avez l'oeil, vous avez remarqué qu'on n'ajoute pas de champ "id" : comme il sera rempli automatiquement par Doctrine (grâce à l'auto-incrémentation), le formulaire n'a pas besoin de remplir cet attribut.
Enfin, c'est avec cet objet
$form généré que l'on pourra gérer notre formulaire : vérifier qu'il est valide, l'afficher, etc. Par exemple, ici, on utilise sa méthode
<?php $form->createView() qui permet à la vue d'afficher ce formulaire. Concernant l'affichage du formulaire, j'ai une bonne nouvelle pour vous : Symfony2 nous permet d'afficher un formulaire simple en... une seule ligne HTML ! Si si, rendez-vous dans la vue
Blog/formulaire.html.twig et ajoutez ces quelques lignes là où nous avions laissé un trou :
Code : HTML & Django | {# src/Sdz/BlogBundle/Resources/views/Blog/formulaire.html.twig #}
<form method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<input type="submit" />
</form>
|
Ensuite, admirez le résultat à l'adresse suivante :
http://localhost/Symfony/web/app_dev.php/blog/ajouter, impressionnant non ? Grâce à la fonction Twig
form_widget() on peut afficher un formulaire entier en une seule ligne. Alors bien sûr il n'est pas forcément à votre gout pour le moment, mais voyez le bon côté des choses : pour l'instant on est en plein développement, on veut tester notre formulaire. On s'occupera de l'esthétique plus tard

. N'oubliez pas également de rajouter les balises
<form> HTML et le bouton de soumission, car cette fonction n'affiche que l'intérieur du formulaire.
Bon évidemment, comme je vous l'ai dit, ce code ne fait qu'afficher le formulaire. Il n'est pas encore question de gérer la soumission du formulaire. mais patience, on y arrive
Ajouter des champs
Vous pouvez le voir, ajouter des champs à un formulaire se fait assez facilement avec la méthode
<?php $formBuilder->add() du
FormBuilder. Les arguments sont les suivants :
- Le nom du champ ;
- Le type du champ : liste complète dans la documentation ;
- Les options du champ, sous forme de tableau.
Par « type de champ », il ne faut pas comprendre « type HTML » comme « text », « password » ou « select ». Il faut comprendre « type sémantique ». Par exemple, le type « date » que l'on a utilisé affiche trois champs « select » à la suite pour choisir le jour, le mois et l'année. Il existe aussi un type « timezone » pour choisir le fuseau horaire. Bref, il en existe pas mal et ils n'ont rien à voir avec les types HTML, ils vont bien plus loin que ces derniers ! N'oubliez pas, Symfony2 est magique !
Gestion de la soumission d'un formulaire
Afficher un formulaire c'est bien, mais faire quelque chose lorsqu'un visiteur le soumet, c'est quand même mieux !
- Pour gérer l'envoi du formulaire, il faut tout d'abord vérifier que la requête est de type « POST » : cela signifie que le visiteur est arrivé sur la page en cliquant sur le bouton submit du formulaire. Lorsque c'est le cas, on peut traiter notre formulaire.
- Ensuite, il faut faire le lien entre les variables de type « POST » et notre formulaire, pour que les variables de type « POST » viennent remplir les champs correspondants du formulaire. Cela se fait via la méthode bindRequest() du formulaire. Cette méthode dit au formulaire : « Voici la requête d'entrée (nos variables de type « POST » entre autres). Lis cette requête, récupère les valeurs qui t'intéressent et hydrate l'objet. » Comme vous pouvez le voir, elle fait beaucoup de choses !
- Enfin, une fois que notre formulaire a lu ses valeurs et hydraté l'objet, il faut tester ces valeurs pour vérifier qu'ils sont valident avec ce que l'objet attend. Il faut valider notre objet. Cela se fait via la méthode isValid() du formulaire.
Ce n'est qu'après ces trois étapes que l'on peut traiter notre objet hydraté : sauvegarder en base de données, envoyer un email, etc.
Vous êtes un peu perdu ? C'est parce que vous manquez de code. Voici comment faire tout ce que l'on vient de dire, dans le contrôleur :
Code : PHP 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 | <?php
// src/Sdz/BlogBundle/Controller/BlogController.php
use Sdz\BlogBundle\Entity\Article;
// ...
public function ajouterAction()
{
$article = new Article;
// J'ai raccourci cette partie, car plus rapide à écrire !
$form = $this->createFormBuilder($article)
->add('date', 'date')
->add('titre', 'text')
->add('contenu', 'textarea')
->add('auteur', 'text')
->getForm();
// On récupère la requête.
$request = $this->get('request');
// On vérifie qu'elle est de type « POST ».
if( $request->getMethod() == 'POST' )
{
// On fait le lien Requête <-> Formulaire.
$form->bindRequest($request);
// On vérifie que les valeurs rentrées sont correctes.
// (Nous verrons la validation des objets en détail plus bas dans ce chapitre.)
if( $form->isValid() )
{
// On l'enregistre notre objet $article dans la base de données.
$em = $this->getDoctrine()->getEntityManager();
$em->persist($article);
$em->flush();
// On redirige vers la page d'accueil, par exemple.
return $this->redirect($this->generateUrl('sdzblog_accueil'));
}
}
// À ce stade :
// - soit la requête est de type « GET », donc le visiteur vient d'arriver sur la page et veut voir le formulaire ;
// - soit la requête est de type « POST », mais le formulaire n'est pas valide, donc on l'affiche de nouveau.
return $this->render('SdzBlogBundle:Blog:ajouter.html.twig', array(
'form' => $form->createView(),
));
}
|
Si le code paraît long, c'est parce que j'ai mis plein de commentaires ! Prenez le temps de bien le lire et de bien le comprendre : vous verrez, c'est vraiment simple.
N'hésitez pas à le tester. Essayez de ne pas remplir un champ pour observer la réaction de Symfony2, par exemple. Vous voyez que ce formulaire gère déjà très bien les erreurs, il n'enregistre l'article que lorsque tout va bien.
Gérer les valeurs par défaut du formulaire
L'un des besoins courant dans les formulaires, c'est de mettre des valeurs prédéfinies dans les champs. Ça peut servir pour des valeurs par défaut (pré-remplir la date, par exemple) ou alors lors de l'édition d'un objet déjà existant (pour l'édition d'un article, on souhaite remplir le formulaire avec les valeurs de la base de données).
Heureusement, cela se fait très facilement. Il suffit de modifier l'instance de l'objet, ici $article, avant de le passer en argument à la méthode
createFormBuilder, comme ceci :
Code : PHP | <?php
// On crée un nouvel article
$article = new Article;
// Ici, on pré-rempli avec la date d'aujourd'hui, par exemple.
// Cette date sera donc pré-affichée dans le formulaire, cela facilite le travail de l'utilisateur.
$article->setDate(new \Datetime());
// Et on construit le formulaire avec cette instance d'article.
$form = $this->createFormBuilder($article);
|
Et si vous voulez modifier un article déjà enregistré en base de données, alors il suffit de le récupérer avant la création du formulaire, comme ceci :
Code : PHP | <?php
// Récupération d'un article déjà existant, d'id $id.
$article = $this->getDoctrine()
->getRepository('Sdz\BlogBundle\Entity\Article')
->find($id);
// Et on construit le formulaire avec cette instance d'article, comme précédemment.
$form = $this->createFormBuilder($article);
|
Personnaliser l'affichage d'un formulaire
Jusqu'ici, nous n'avons pas du tout personnalisé l'affichage de notre formulaire. Voyez quand même le bon côté des choses : on travaillait côté PHP, on a pu avancer très rapidement sans se soucier d'écrire les balises
<input> à la main, ce qui est long et sans intérêt.
Mais bon, à un moment donné, il faut bien mettre la main à la pâte et faire des formulaires dans le même style que son site. Pour cela, je ne vais pas m'étendre, mais voici un exemple qui vous permettra de faire à peu près tout ce que vous voudrez :
Code : HTML 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | <form action="{{ path('votre_route') }}" method="post" {{ form_enctype(form) }}>
<!-- Les erreurs générales du formulaire. -->
{{ form_errors(form) }}
<div>
<!-- Génération du label. -->
{{ form_label(form.titre, "Titre de l'article") }}
<!-- Affichage des erreurs pour ce champ précis. -->
{{ form_errors(form.titre) }}
<!-- Génération de l'input. -->
{{ form_widget(form.titre) }}
</div>
<!-- Idem pour un autre champ. -->
<div>
{{ form_label(form.contenu, "Contenu de l'article") }}
{{ form_errors(form.contenu) }}
{{ form_widget(form.contenu) }}
</div>
<!-- Génération des champs pas encore écrits.
Dans cet exemple, ça serait « date », « auteur » et « tags »,
mais aussi le champ CSRF (géré automatiquement par Symfony !)
et tous les champs cachés (type « hidden »). -->
{{ form_rest(form) }}
|
Plus d'informations sur le Cross Site Request Forgeries (CSRF).
Créer des types de champs personnalisés
Il se peut que vous ayez envie d'utiliser un type de champ précis, mais que ce type de champ n'existe pas par défaut. Heureusement, vous n'êtes pas coincé, vous pouvez vous en sortir en créant votre propre type de champ. Vous pourrez ensuite utiliser ce champ comme n'importe quel autre dans vos formulaires.
Imaginons par exemple que vous n'aimiez pas le rendu du champ « date » avec ces trois balises
<select> pour sélectionner le jour, le mois et l'année. Vous préféreriez un joli
datepicker en JavaScript. La solution ? Créer un nouveau type de champ !
Je ne vais pas décrire la démarche ici, mais sachez que ça existe et que
c'est bien documenté.