Aller au menu - Aller au contenu

Icône Créer des formulaires avec Symfony2

Avatar
Mise à jour : 02/05/2012
Difficulté : Intermédiaire Intermédiaire Durée d'étude : 1 heure Creative Commons BY-NC-SA
20 752 visites depuis 7 jours, dont 1 292 sur ce chapitre classé 17/786
Quoi de plus important sur un site Web que les formulaires ?

En effet, les formulaires sont l'interface entre vos visiteurs et votre contenu. Chaque commentaire, chaque article de blog, etc, tous passent par l'intermédiaire d'un visiteur et d'un formulaire pour exister dans votre base de données.

L'objectif de ce chapitre est donc, de vous donner enfin les outils pour créer efficacement ces formulaires grâce à la puissance du composant Form de Symfony2. Ce chapitre va de paire avec le prochain, dans lequel nous parlerons de la validation des données, celles que vos visiteurs vont rentrer dans vos nouveaux formulaires.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Gestion des formulaires

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. :p

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
1
2
3
4
5
6
7
8
{# 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 :
  1. Le nom du champ ;
  2. Le type du champ : liste complète dans la documentation ;
  3. 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 ! :magicien:

La liste complète des types de champ se trouve dans la doc. Je vous ordonne d'y aller, elle regorge de types très intéressants !


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.

N'hésitez pas à tester votre formulaire en ajoutant des articles ! Il est opérationnel, et les articles que vous ajoutez sont réellement enregistrés en base de données ;)


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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?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
1
2
3
4
5
6
7
8
<?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é.

Externaliser la gestion de ses formulaires

Pour bien externaliser la gestion de ses formulaires, il nous faut deux étapes. D'abord, nous allons sortir du contrôleur la définition du formulaire : ses champs, options, etc. Puis, nous allons sortir la gestion du formulaire : action à réaliser lorsqu'il est valide, etc.

Externaliser la définition du formulaire


Vous savez enfin créer un formulaire. Ce n'était pas très compliqué, nous l'avons rapidement fait et ce dernier se trouve être assez joli. Mais vous souvenez-vous de ce que j'avais promis au début : nous voulions un formulaire réutilisable ; or là, tout est dans le contrôleur, et je vois mal comment le réutiliser ! Pour cela, il faut détacher la définition du formulaire dans une classe à part, nommée ArticleType (par convention).

Définition du formulaire dans ArticleType


ArticleType n'est pas notre formulaire. Comme tout à l'heure, c'est notre constructeur de formulaire. Par convention, on va mettre tous nos xxxType.php dans le répertoire Form du bundle. En fait, allez voir, ce répertoire existe déjà, et le ArticleType également ! En effet, Symfony2 l'avait généré lorsque l'on avait créé le CRUD. Vous devriez donc vous retrouver avec ce code :

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
<?php
// src/Sdz/BlogBundle/Form/ArticleType.php

namespace Sdz\BlogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
	    $builder
            ->add('date')
            ->add('titre')
            ->add('contenu')
            ->add('auteur');
    }

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


Comme vous pouvez le voir, on n'a fait que déplacer la construction du formulaire, du contrôleur à une classe externe. Cela nous permettra de réutiliser exactement le même formulaire à d'autres endroits. Voilà la réutilisabilité !

Et prenez aussi dès maintenant un bon réflexe, que le générateur n'a malheureusement pas respecté. En effet, rappelez-vous, un formulaire se construit autour d'un objet, et avec la technique du ArticleType, nous pouvons économiser du temps en définissant directement dans ArticleType quel est l'objet sous-jacent. Ainsi, cela permettra d'indiquer à Symfony2 l'objet à utiliser dans bien des cas. La bonne pratique est donc d'ajouter très simplement la méthode getDefaultOptions() dans notre ArticleType :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
// src/Sdz/BlogBundle/Form/ArticleType.php

class ArticleType extends AbstractType
{

    // ...

    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'Sdz\BlogBundle\Entity\Article',
        );
    }
}

Le but est donc d'indiquer la classe de l'objet utilisé pour ce formulaire.

Cet ArticleType correspond donc en fait à la définition des champs de notre formulaire. Ainsi, si l'on utilise le même formulaire sur plusieurs pages différentes, on utilisera ce même ArticleType. Fini le copier-coller !

Le contrôleur épuré


Avec cet ArticleType, la construction du formulaire côté contrôleur s'effectue grâce à la méthode createForm() du contrôleur (et non plus createFormBuilder()). Cette méthode utilise le composant Form pour construire un formulaire à partir du ArticleType passé en argument. Depuis le contrôleur, on récupère donc directement un formulaire, on ne passe plus par le constructeur de formulaire comme plus haut. Voyez par vous-même :

Code : PHP
1
2
3
<?php
$article = new Article;
$form = $this->createForm(new ArticleType, $article);

En effet, si l'on s'est donné la peine de créer un objet à l'extérieur du contrôleur, c'est pour que ce contrôleur soit plus simple. C'est réussi.
Au final, en utilisant cette externalisation et en supprimant les commentaires, voilà à quoi ressemble la gestion d'un formulaire dans Symfony2 :

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;
// N'oubliez pas de rajouter le ArticleType
use Sdz\BlogBundle\Form\ArticleType;

// ...

public function ajouterAction()
{
    $article = new Article;
    $form = $this->createForm(new ArticleType, $article);

    $request = $this->get('request');
    if( $request->getMethod() == 'POST' )
    {
        $form->bindRequest($request);
        if( $form->isValid() )
        {
            $em = $this->getDoctrine()->getEntityManager();
            $em->persist($article);
            $em->flush();

            return $this->redirect( $this->generateUrl('sdzblog_accueil') );
        }
    }

    return $this->render('SdzBlogBundle:Blog:ajouter.html.twig', array(
        'form' => $form->createView(),
    ));
}

Plutôt simple, non ? Au final, votre code métier, votre code qui fait réellement quelque chose se trouve là où l'on a utilisé l'EntityManager. Pour l'exemple, nous n'avons fait qu'enregistrer l'article en base de données, mais c'est ici que vous pourrez envoyer un email, ou toute autre action dont votre site Internet aura besoin.

Mais quelque chose devrait encore vous gêner... si on a bien externalisé la définition du formulaire, tout le traitement est resté dans le contrôleur. Et si on devait réutiliser le même formulaire à un autre endroit dans notre code, il faudrait copier-coller le traitement. Pour ne pas faire cette horrible chose qu'est le copier-coller, nous allons maintenant externaliser la gestion de notre formulaire.

Externaliser la gestion du formulaire


Vous l'aurez compris, l'objectif maintenant est de sortir le traitement du contrôleur, cela correspond aux lignes 15 à 23 du code ci-dessus : quand exécuter le traitement (requête en POST, formulaire valide) et que faire comme traitement (enregistrer en base de données).

Traitement du formulaire dans ArticleHandler


Pour réaliser cette externalisation, nous allons créé une deuxième classe dans le répertoire Form : ArticleHandler. Cette classe va contenir trois méthodes pour l'instant, mais on pourra bien sûr en ajouter d'autres si notre traitement le demande :
  • Un constructeur : Il va nous permettre de donner au Handler les outils dont il a besoin. Pour savoir lesquels, c'est très simple : on regarde tout ce que le contrôleur utilise entre les lignes 15 à 23, que je vous remets ici :
    Code : PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    <?php
        $request = $this->get('request');
        if( $request->getMethod() == 'POST' )
        {
            $form->bindRequest($request);
            if( $form->isValid() )
            {
                $em = $this->getDoctrine()->getEntityManager();
                $em->persist($article);
                $em->flush();
    

    Le contrôleur se sert de 1/ la requête, 2/ le formulaire et 3/ l'EntityManager : on va donc tout simplement passer ces trois outils au constructeur. Bien entendu, si votre traitement fait appel à d'autres outils (SwiftMailer pour l'envoi d'email par exemple), il faudra les rajouter en argument à votre handler. Voici donc le code du constructeur :
    Code : PHP
    1
    2
    3
    4
    5
    6
    7
    <?php
        public function __construct(Form $form, Request $request, EntityManager $em)
        {
            $this->form    = $form;
            $this->request = $request;
            $this->em      = $em;
        }
    

  • Une méthode pour savoir quand déclencher le traitement. Cela correspond aux différents tests que l'on a fait dans le contrôleur : vérifier que la requête est en POST et que le formulaire est bien valide. Par convention, on nomme cette méthode "process", la voici donc :
    Code : PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?php
        public function process()
        {
            if( $this->request->getMethod() == 'POST' )
            {
                $this->form->bindRequest($this->request);
    
                if( $this->form->isValid() )
                {
                    // On appelle la méthode onSucess qui va effectuer le traitement, on la décrit juste en dessous.
                    // $this->form->getData() représente l'objet traité par le formulaire, une instance d'Article dans notre cas.
                    $this->onSuccess($this->form->getData());
    
                    return true;
                }
            }
    
            return false;
        }
    

    Vous pourriez même mettre des arguments à cette méthode process(), afin d'effectuer un traitement différents selon les cas.
  • Une méthode qui va effectivement exécuter le traitement. Cela correspond à l'enregistrement de l'article en base de données que nous avions dans le contrôleur. Par convention, on nomme cette méthode "onSucess", pour dire qu'on l'exécute uniquement lorsque le formulaire vérifie les conditions définies dans la méthode process. Voici son code :
    Code : PHP
    1
    2
    3
    4
    5
    6
    <?php
        public function onSuccess(Article $article)
        {
            $this->em->persist($article);
            $this->em->flush();
        }
    

    Bien entendu et comme je vous l'ai déjà dit, vous pouvez faire tous les traitements que vous voulez ici. On s'est limité pour l'exemple à enregistrer l'article en base de données.

Pour résumer, créez notre ArticleHandler avec ce code complet :
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
<?php
// src/Sdz/BlogBundle/Form/ArticleHandler.php

namespace Sdz\BlogBundle\Form;

use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManager;
use Sdz\BlogBundle\Entity\Article;

class ArticleHandler
{
    protected $form;
    protected $request;
    protected $em;

    public function __construct(Form $form, Request $request, EntityManager $em)
    {
        $this->form    = $form;
        $this->request = $request;
        $this->em      = $em;
    }

    public function process()
    {
        if( $this->request->getMethod() == 'POST' )
        {
            $this->form->bindRequest($this->request);

            if( $this->form->isValid() )
            {
                $this->onSuccess($this->form->getData());

                return true;
            }
        }

        return false;
    }

    public function onSuccess(Article $article)
    {
        $this->em->persist($article);
        $this->em->flush();
    }
}


Le contrôleur très épuré


Voici maintenant notre contrôleur final. Nous avons externalisé la définition, puis la gestion du formulaire. Vous devez vous y attendre : il ne reste plus grand chose dans le contrôleur ! Tout à fait, mais c'était bien l'objectif initial. Voyez par vous-mêmes :

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
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

namespace Sdz\BlogBundle\Controller;

// ... vos uses

use Sdz\BlogBundle\Entity\Article;
use Sdz\BlogBundle\Form\ArticleType;
// N'oubliez pas de rajouter le ArticleHandler
use Sdz\BlogBundle\Form\ArticleHandler;

class BlogController extends Controller
{
    // ... d'autres méthodes

    public function ajouterAction()
    {
        $article = new Article;

        // On crée le formulaire
        $form = $this->createForm(new ArticleType, $article);

        // On crée le gestionnaire pour ce formulaire, avec les outils dont il a besoin
        $formHandler = new ArticleHandler($form, $this->get('request'), $this->getDoctrine()->getEntityManager());

        // On exécute le traitement du formulaire. S'il retourne true, alors le formulaire a bien été traité
        if( $formHandler->process() )
        {
            return $this->redirect( $this->generateUrl('sdzblog_voir', array('id' => $article->getId())) );
        }

        // Et s'il retourne false alors la requête n'était pas en POST ou le formulaire non valide.
        // On réaffiche donc le formulaire.
        return $this->render('SdzBlogBundle:Blog:ajouter.html.twig', array(
            'form' => $form->createView(),
        ));
    }
}

Les formulaires imbriqués

Intérêts de l'imbrication


En fait, pourquoi imbriquer des formulaires ?
C'est souvent le cas lorsque vous avez des relations entre vos objets : vous souhaitez ajouter un objet, mais en même temps un autre qui sera lié au premier. Exemple concret : vous voulez ajouter un client à votre application, votre Client est lié à une Adresse, mais vous avez envie d'ajouter l'adresse sur la même page que votre client, depuis le même formulaire. S'il fallait deux pages pour ajouter client puis adresse, votre site ne serait pas très ergonomique. Voici donc toute l'utilité de l'imbrication des formulaires !

Un formulaire est un champ


Eh oui, voici tout ce que vous devez savoir pour imbriquer des formulaires entre eux. Considérez un de vos formulaires comme un champ, et appelez ce simple champ depuis un autre formulaire !

O.K., facile à dire, mais il faut savoir le faire derrière.

D'abord, créez le TagType pour notre entité Tag. Essayez de le faire vous-même, c'est vraiment simple, et une fois fini, vérifiez votre code avec le mien. Inspirez-vous de ArticleType, nul besoin de faire du par cœur.
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
<?php
// src/Sdz/BlogBundle/Form/TagType.php

namespace Sdz\BlogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class TagType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('nom');  // Notez ici que l'on n'a pas précisé le type de champ : c'est parce que
                               // le composant Form sait le reconnaître… depuis nos annotations Doctrine !
    }

    public function getName()
    {
        return 'sdz_blogbundle_tagtype';  // N'oubliez pas de changer le nom du formulaire.
    }

    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'Sdz\BlogBundle\Entity\Tag', // Ni de modifier la classe ici.
        );
    }
}


Ensuite, il existe deux façons d'imbriquer des formulaires :
  • avec une relation simple où un seul formulaire est imbriqué dans un autre. C'est le cas le plus courant, celui de notre Client avec une seule Adresse ;
  • avec une relation multiple, où vous voulez imbriquer plusieurs fois le même formulaire dans un formulaire parent. C'est le cas de notre Article, par exemple, qui peut contenir plusieurs objets Tag.


Relation simple : imbriquer un seul formulaire


C'est le cas le plus courant, mais qui ne correspond malheureusement pas à notre exemple de l'article et ses tags. :p Pour imbriquer un seul formulaire en étant cohérent avec une entité, il faut qu'elle dispose d'une relation OneToOne ou ManyToOne. Du coup, suivez bien, nous allons modifier temporairement notre entité Article pour transformer la relation ManyToMany en ManyToOne, et remplacer tous les tags en tag : comme si l'on ne pouvait en fait attacher qu'un seul tag à un article. Voici ce que donne la modification :

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
<?php
// src/Sdz/BlogBundle/Entity/Article.php

namespace Sdz\BlogBundle\Entity;

use Sdz\BlogBundle\Entity\Tag;

class Article
{
    private $id;
    private $date;
    private $titre;
    private $contenu;
    private $auteur;

    /**
     * Évidemment, nous devons modifier la définition de la relation : on passe à un ManyToOne !
     * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Tag")
     */
    private $tag;

    // ...

    public function setTag(Tag $tag) // Attention, ici, c'est setTag et non plus addTags.
    {
        $this->tag = $tag;
    }
    public function getTag()
    {
        return $this->tag;
    }
}


N'oubliez pas de mettre à jour la base de données avec la commande php app/console doctrine:schema:update --force !


Voilà, maintenant, nous pouvons imbriquer nos formulaires. C'est vraiment simple : allez dans ArticleType et rajoutez un champ « tag » (du nom de la propriété de notre entité), de type… TagType, bien sûr !

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
// src/Sdz/BlogBundle/Form/ArticleType.php

    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('date',    'date')
            ->add('titre',   'text')
            ->add('contenu', 'textarea')
            ->add('auteur',  'text')
            ->add('tag',     new TagType)
        ;
    }

Et voilà ! Allez sur la page d'ajout : http://localhost/Symfony/web/app_dev.p [...] outer/article. Le formulaire est déjà à jour, avec une partie « Tag » où l'on peut remplir le seul champ de ce formulaire, le champ nom. C'était d'une facilité déconcertante, n'est-ce pas ?

Essayez d'ajouter un Article avec son Tag. Alors ? Ça ne fonctionne pas, évidemment. Essayons de réfléchir un peu… Dans le ArticleHandler, nous avons dit à l'EntityManager de persister l'article, mais on ne lui a rien précisé sur le tag contenu dans l'article (accessible via <?php $article->getTag(), tout simplement). C'est pour cela que Symfony2 nous dit :

Citation : Symfony2
A new entity was found through the relationship 'Sdz\BlogBundle\Entity\Article#tag'
...
Explicitly persist the new entity

On va l'écouter et ajouter un persist() sur notre tag dans la méthode onSuccess du ArticleHandler :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
// src/Sdz/BlogBundle/Form/ArticleHandler.php

// ...

    public function onSuccess(Article $article)
    {
        $this->em->persist($article);
        $this->em->persist($article->getTag());  // Ici, on rajoute ce persist().
        $this->em->flush();
    }

Et voilà, cette fois-ci, le formulaire est pleinement opérationnel ; lancez-vous dans les tests !
Pour vérifier que cela fonctionne, rendez-vous sur la page de modification d'un article : http://localhost/Symfony/web/app_dev.p [...] _de_l_article.

C'est fini pour l'imbrication simple d'un formulaire dans un autre. Passons maintenant à l'imbrication multiple. :)

Relation multiple : imbriquer un même formulaire plusieurs fois


On va donc revenir à notre vrai cas où l'on peut ajouter plusieurs tags à un seul article. Tout d'abord, modifiez de nouveau l'entité Article pour remettre au pluriel les tags :
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
<?php
// src/Sdz/BlogBundle/Entity/Article.php

namespace Sdz\BlogBundle\Entity;

use Sdz\BlogBundle\Entity\Tag;

class Article
{
    private $id;
    private $date;
    private $titre;
    private $contenu;
    private $auteur;

    /**
     * @ORM\ManyToMany(targetEntity="Sdz\BlogBundle\Entity\Tag")
     */
    private $tags;

    public function __construct()
    {
        $this->tags = new ArrayCollection(); // N'oubliez pas l'ArrayCollection.
        $this->date = new \Datetime();
    }

    // ...

    public function addTag(Tag $tag)
    {
        $this->tags[] = $tag;
    }
    public function getTags()
    {
        return $this->tags;
    }
}

Et bien sûr, mettez à jour la base de données.


Notre TagType ne va pas changer d'un poil. Un tag est un tag, qu'il soit ajouté une fois ou mille dans un article ne change rien pour lui. Voici un bel exemple du code découplé que nous permet de faire Symfony2 !

Par contre, notre ArticleType, lui, va changer. On va changer le champ « tag » de type TagType, en champ « tags » (toujours du nom de la propriété), de type « collection ». En fait, oui, ça pourrait être une liste (collection) de n'importe quoi, même de champs de type "string" ! On va se servir des options du champ pour signaler que ce doit être une liste de plusieurs TagType. Voici ce que ça donne :

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
<?php
// src/Sdz/BlogBundle/Form/ArticleType.php

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('date')
            ->add('titre')
            ->add('contenu')
            /*
             * Rappel :
             ** - 1er argument : nom du champ ;
             ** - 2e argument : type du champ ;
             ** - 3e argument : tableau d'options du champ.
             */
            ->add('tags', 'collection', array('type'      => new TagType,
                                              'prototype' => true,
                                              'allow_add' => true))
        ;
    }
// ...
}

Encore une fois, c'est vraiment tout ce qu'il y a à modifier côté formulaire. Vous pouvez d'ores et déjà observer le résultat. Pour cela, actualisez la page d'ajout d'un article. Ah mince, « Tags » est bien inscrit, mais il n'y a rien en dessous. :D Ce n'est pas un bug, c'est bien voulu par Symfony2. En effet, à partir du moment où vous pouvez ajouter plusieurs champs ou en supprimer, vous avez besoin de JavaScript pour le faire. Donc Symfony2 part du principe que de toute façon, vous gérerez ça avec du code JavaScript. O.K., ça ne nous fait pas peur !

D'abord, affichez la source de la page et regardez l'étrange balise <div> que Symfony2 a rajoutée :
Code : HTML
1
2
3
4
5
6
7
8
9
<div id="sdz_blogbundle_articletype_tags" data-prototype="&lt;div&gt;
&lt;label class=&quot; required&quot;&gt;$$name$$&lt;/label&gt;
&lt;div id=&quot;sdz_blogbundle_articletype_tags_$$name$$&quot;&gt;
&lt;div&gt;&lt;label for=&quot;sdz_blogbundle_articletype_tags_$$name$$_nom&quot;
class=&quot; required&quot;&gt;Nom&lt;/label&gt;
&lt;input type=&quot;text&quot; id=&quot;sdz_blogbundle_articletype_tags_$$name$$_nom&quot;
name=&quot;sdz_blogbundle_articletype[tags][$$name$$][nom]&quot; required=&quot;required&quot;
maxlength=&quot;255&quot; value=&quot;&quot; /&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;">
</div>


Vous connaissez l'attribut « data-prototype » ? C'est en fait un attribut (au nom arbitraire) rajouté par Symfony2 et qui contient ce à quoi doit ressembler le code HTML pour ajouter un tag. Essayez de le lire, vous voyez qu'il contient les balises <label> et <input> (tout ce qu'il faut pour notre champ « nom », en fait).
Du coup, on le remercie, car grâce à ce template, ajouter des champs en JavaScript est un jeu d'enfant. Je vous propose un script JavaScript rapidement fait qui emploie la librairie jQuery et qui est à mettre dans notre fichier Blog/formulaire.html.twig :

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
29
30
31
32
33
34
<!-- Ajout d'un lien pour ajouter un champ tag supplémentaire. -->
<a href="#" id="add_tag">Ajouter un tag</a>

<!-- On charge la librairie jQuery. Ici, je la prends depuis le site jquery.com, mais si vous l'avez en local, changez simplement l'adresse. -->
<script src="http://code.jquery.com/jquery-1.6.2.min.js"></script>

<script type="text/javascript">
$(document).ready(function() {
    // On récupère la balise <div> en question qui contient l'attribut « data-prototype » qui nous intéresse.
    var $container = $('#sdz_blogbundle_articletype_tags');
	
    // On définit une fonction qui va ajouter un champ.
    function add_tag() {
        // On définit le numéro du champ (en comptant le nombre de champs déjà ajoutés).
        index = $container.children().length;

        // On ajoute à la fin de la balise <div> le contenu de l'attribut « data-prototype »,
        // après avoir remplacé la variable $$name$$ qu'il contient par le numéro du champ.
        $container.append(
            $($container.attr('data-prototype').replace(/\$\$name\$\$/g, index))
        );
    }
	
    // On ajoute un premier champ directement s'il n'en existe pas déjà un.
    if($container.children().length == 0) {
        add_tag();
    }
	
    // On ajoute un nouveau champ à chaque clic sur le lien d'ajout.
    $('#add_tag').click(function() {
        add_tag();
    });
});
</script>


Appuyez sur F5 sur la page d'ajout. Voilà qui est mieux !

Cependant, comme tout à l'heure, pour que le formulaire soit pleinement opérationnel, il nous faut adapter un peu la méthode onSuccess de notre ArticleHandler pour qu'il persiste tous nos tags :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
// src/Sdz/BlogBundle/Form/ArticleHandler.php

// ...

    public function onSuccess(Article $article)
    {
        $this->em->persist($article);

        // On persiste tous les tags de l'article.
        foreach($article->getTags() as $tag)  
        {
            $this->em->persist($tag);
        }

        $this->em->flush();
    }


Et voilà, votre formulaire est maintenant opérationnel ! Vous pouvez vous amuser à créer des articles contenant plein de tags en même temps. Vérifiez également depuis la page de modification, vous voyez que vous pouvez modifier les tags en direct. Vraiment pratique !

Pour information, ce champ de type collection que l'on vient de voir s'utilise avec n'importe quel autre type de champ, pas forcément un formulaire. Dans l'ArticleType, à la place du <?php new TagType, vous auriez pu mettre « text » ou « file » ou n'importe quel autre type de champ classique.

Application : les formulaires de notre blog

Théorie


Nous n'avons qu'un seul formulaire pour l'instant, celui pour créer et modifier l'objet Article. C'est un même formulaire qui est utilisé sur deux pages. Comme nous avons déjà notre ArticleType et notre ArticleHandler, vous n'avez plus qu'à vous occuper de la gestion du formulaire au niveau de notre BlogController dans les méthodes ajouterAction() et modifierAction().

Au boulot ! Essayez d'implémenter vous-même la gestion du formulaire dans les actions correspondantes. Ensuite seulement, lisez la suite de ce paragraphe pour avoir la solution.


Pratique


Entité Article


J'ai modifié rapidement notre entité Article, afin de pré-remplir la date directement dans le constructeur lorsque l'on instancie l'objet :
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
<?php
// src/Sdz/BlogBundle/Entity/Article.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;


/**
 * Sdz\BlogBundle\Entity\Article
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\ArticleRepository")
 */
class Article
{
    // ...

    public function __construct()
    {
        $this->tags = new ArrayCollection();
        $this->date = new \Datetime();
    }

    // ...
}

Ainsi, en faisant <?php $article = new Article; la valeur $article->getDate() vaut déjà la date d'aujourd'hui.

Le ArticleType


On l'a déjà fait au cours de ce chapitre, mais le voici en rappel :

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
<?php
// src/Sdz/BlogBundle/Form/ArticleType.php

namespace Sdz\BlogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
	    $builder
            ->add('date',    'date')
            ->add('titre',   'text')
            ->add('contenu', 'textarea')
            ->add('auteur',  'text')
            ->add('tags',    'collection', array('type'      => new TagType,
                                                 'prototype' => true,
                                                 'allow_add' => true))
            ;
    }

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

    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'Sdz\BlogBundle\Entity\Article',
        );
    }
}


Le TagType lui n'a pas changé du tout.

Le ArticleHandler


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
<?php
// src/Sdz/BlogBundle/Form/ArticleHandler.php

namespace Sdz\BlogBundle\Form;

use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManager;
use Sdz\BlogBundle\Entity\Article;

class ArticleHandler
{
    protected $form;
    protected $request;
    protected $em;

    public function __construct(Form $form, Request $request, EntityManager $em)
    {
        $this->form    = $form;
        $this->request = $request;
        $this->em      = $em;
    }

    public function process()
    {
        if( $this->request->getMethod() == 'POST' )
        {
            $this->form->bindRequest($this->request);

            if( $this->form->isValid() )
            {
                $this->onSuccess($this->form->getData());

                return true;
            }
        }

        return false;
    }

    public function onSuccess(Article $article)
    {
        $this->em->persist($article);

        // On persiste tous les tags de l'article.
        foreach($article->getTags() as $tag)
        {
            $this->em->persist($tag);
        }

        $this->em->flush();
    }
}


L'action « ajouter » du contrôleur


Ici pas de changement, on l'a également fait au cours du chapitre :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

    public function ajouterAction()
    {
        $article = new Article;

        $form        = $this->createForm(new ArticleType, $article);
        $formHandler = new ArticleHandler($form, $this->get('request'), $this->getDoctrine()->getEntityManager());

        if($formHandler->process())
        {
            return $this->redirect( $this->generateUrl('sdzblog_voir', array('id' => $article->getId())) );
        }

        return $this->render('SdzBlogBundle:Blog:ajouter.html.twig', array(
            'form' => $form->createView(),
        ));
    }


L'action « modifier » du contrôleur


Voilà l'action que vous deviez faire tout seul. Pas de piège particulier :

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
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

    public function modifierAction($id)
    {
        $em = $this->getDoctrine()->getEntityManager();

        // On vérifie que l'article d'id $id existe bien, sinon, erreur 404.
        if( ! $article = $em->getRepository('Sdz\BlogBundle\Entity\Article')->find($id) )
        {
            throw $this->createNotFoundException('Article[id='.$id.'] inexistant');
        }

        // On passe l'$article récupéré au formulaire
        $form        = $this->createForm(new ArticleType, $article);
        $formHandler = new ArticleHandler($form, $this->get('request'), $em);

        if($formHandler->process())
        {
            return $this->redirect( $this->generateUrl('sdzblog_voir', array('id' => $article->getId())) );
        }

        return $this->render('SdzBlogBundle:Blog:modifier.html.twig', array(
            'form' => $form->createView(),
        ));
    }
Voilà ! Ce chapitre important n'était pas si compliqué dans le fond !

Bien entendu, vous ne pouvez pas vous arrêter en si bon chemin. Maintenant que vos formulaires sont opérationnels, il faut bien vérifier un peu ce que vos visiteurs vont y mettre comme données ! C'est l'objectif du prochain chapitre, qui traite de la validation des données justement. Il vient compléter le chapitre actuel, continuez donc la lecture !
Chapitre précédent Sommaire Chapitre suivant

Partager

78 commentaires pour "Créer des formulaires avec Symfony2"
Note moyenne : 3.75 / 4 (245 votes)
Pseudo Commentaire
Hors ligne ldewez # Posté le 12/04/2012 à 17:46:43

Avis : Bon

Bonjour,
Bon tuto ceci dit comment on peut faire pour créer une Entité d'un objet en plusieurs étapes ?
Par exemple pour créer un objet annonce, avoir :
- 1 formType pour la référence de l'annonce,
- 1 formType pour la description,
- 1 formType pour l'ajout des images,

Et surtout comment faire pour sauvegarder notre objet annonce qui contient des relations ManyToOne sans utiliser cascade={"PERSIST"} qui duplique des clés déjà existante.

Merci d'avance pour votre aide ^^
Hors ligne Julien xD # Posté le 21/04/2012 à 05:02:07
Victor Numquam Cedit
Avatar

Avis : Très bon

Ville : Plouzane
Pays : France métropolitaine
Études : ENIB (Ecole Nationale d'Ingénieurs de Brest) - Bretagne

Une petite coquille :

Rappel : la convention pour le nom des getters/setters est importante : lorsque l'on parlera du champ « nom », 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é.

Plutôt : lorsque l'on parlera du champ « auteur ».

Merci pour ce tutoriel.

Le Monde appartient à ceux qui se couchent tard. :soleil:
 
Hors ligne aziza_1989 # Posté le 23/04/2012 à 19:03:59

Bonsoir,
Merci pour le tuto ,c'est très intérréssant juste l'action supprimer ne supprime rien pour moi.Pourrriez me direr pourquoi?
Merci.
Hors ligne akrramo # Posté le 10/05/2012 à 22:47:23 Message supprimé pour le motif suivant : Merci de poster vos problèmes sur le forum, pas ici..

Je remercie @winzou pour ce super tutorial qui est très bien expliqué.

Je voulais faire un test d'imbriquer un formulaire multiple dans un formulaire imbriqué (multiple aussi), par exemple, nous avons Un Article ou on va imbriqué des formulaires de Tag, et pour chaque Tag je voudrais imbriqué un formulaire "Signification" pour celà, j'ai créer une entité Signification

Entity\Signification.php
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
<?php

namespace Sdz\BlogBBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Sdz\BlogBBundle\Entity\Signification
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBBundle\Entity\SignificationRepository")
 */
class Signification
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string $valeur
     *
     * @ORM\Column(name="valeur", type="string", length=255)
     */
    private $valeur;

   
   

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set valeur
     *
     * @param string $valeur
     */
    public function setValeur($valeur)
    {
        $this->valeur = $valeur;
    }

    /**
     * Get valeur
     *
     * @return string 
     */
    public function getValeur()
    {
        return $this->valeur;
    }

    

}



et dans l'entité Tag j'ai ajouté significations après avoir défini la relation comme un ManyToMany

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<?php

namespace Sdz\BlogBBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Sdz\BlogBBundle\Entity\Tag
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBBundle\Entity\TagRepository")
 */
class Tag
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string $nom
     *
     * @ORM\Column(name="nom", type="string", length=255)
     */
    private $nom;
	
	/**
     * @ORM\ManyToMany(targetEntity="Sdz\BlogBBundle\Entity\Signification")
     */
	private $significations;

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set nom
     *
     * @param string $nom
     */
    public function setNom($nom)
    {
        $this->nom = $nom;
    }

    /**
     * Get nom
     *
     * @return string 
     */
    public function getNom()
    {
        return $this->nom;
    }
    public function __construct()
    {
        $this->significations = new \Doctrine\Common\Collections\ArrayCollection();
    }
    
    /**
     * Add significations
     *
     * @param Sdz\BlogBBundle\Entity\Signification $significations
     */
    public function addSignification(\Sdz\BlogBBundle\Entity\Signification $significations)
    {
        $this->significations[] = $significations;
    }

    /**
     * Get significations
     *
     * @return Doctrine\Common\Collections\Collection 
     */
    public function getSignifications()
    {
        return $this->significations;
    }
}



J'ai modifié le fichier TagType comme ceci:

TagType.php

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
<?php

namespace Sdz\BlogBBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class TagType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('nom')
			->add('significations', 'collection', array('type'      => new SignificationType,
                                              'prototype' => true,
                                              'allow_add' => true))
        ;
    }

    public function getName()
    {
        return 'sdz_blogbbundle_tagtype';
    }
    public function getDefaultOptions(array $options)
    {
	return array(
		'data_class' => 'Sdz\BlogBBundle\Entity\Tag',
	);
    }
}



et j'ai crée le fichier SignificationType.php
SignificationType.php
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
<?php
// src/Sdz/BlogBundle/Form/ArticleType.php

namespace Sdz\BlogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class SignificationType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
	    $builder
            ->add('valeur');
    }

    public function getName()
    {
        return 'sdz_blogbundle_significationtype';
    }
	 public function getDefaultOptions(array $options)
    {
	return array(
		'data_class' => 'Sdz\BlogBBundle\Entity\Signification',
	);
    }
}


et j'ai laissé les fichiers Article et ArticleType comme il sont dans le tutorial.



Article.php
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
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<?php

namespace Sdz\BlogBBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

use Sdz\BlogBBundle\Entity\Tag ;
/**
 * Sdz\BlogBBundle\Entity\Article
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBBundle\Entity\ArticleRepository")
 */
class Article
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var datetime $date
     *
     * @ORM\Column(name="date", type="datetime")
     */
    private $date;

    /**
     * @var string $titre
     *
     * @ORM\Column(name="titre", type="string", length=255)
     */
    private $titre;

 /**
     * @var string $auteur
     *
     * @ORM\Column(name="auteur", type="string", length=255)
     *@Assert\MinLength(limit=5, message = "L'auteur doit avoir au moins 3 caractère")
     */
    private $auteur;

    /**
     * @var text $contenu
     *
     * @ORM\Column(name="contenu", type="text")
     */
    private $contenu;
    /**
     * @ORM\ManyToMany(targetEntity="Sdz\BlogBBundle\Entity\Tag")
     */
    private $tags;

    public function __construct()
    {
	$this->tags = new ArrayCollection();
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set date
     *
     * @param datetime $date
     */
    public function setDate($date)
    {
        $this->date = $date;
    }

    /**
     * Get date
     *
     * @return datetime 
     */
    public function getDate()
    {
        return $this->date;
    }

    /**
     * Set titre
     *
     * @param string $titre
     */
    public function setTitre($titre)
    {
        $this->titre = $titre;
    }

    /**
     * Get titre
     *
     * @return string 
     */
    public function getTitre()
    {
        return $this->titre;
    }

    /**
     * Set contenu
     *
     * @param text $contenu
     */
    public function setContenu($contenu)
    {
        $this->contenu = $contenu;
    }

    /**
     * Get contenu
     *
     * @return text 
     */
    public function getContenu()
    {
        return $this->contenu;
    }
   
    public function addTag(Tag $tag)
    {
	$this->tags[] = $tag;
    }
	

    /**
     * Set auteur
     *
     * @param string $auteur
     */
    public function setAuteur($auteur)
    {
        $this->auteur = $auteur;
    }

    /**
     * Get auteur
     *
     * @return string 
     */
    public function getAuteur()
    {
        return $this->auteur;
    }

    /**
     * Get tags
     *
     * @return Doctrine\Common\Collections\Collection 
     */
    public function getTags()
    {
        return $this->tags;
    }
}



ArticleType.php
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
<?php

namespace Sdz\BlogBBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('titre')
            ->add('contenu')
	    ->add('auteur')
	                ->add('tags', 'collection', array('type'      => new TagType,
                                              'prototype' => true,
                                              'allow_add' => true))
        ;

    }

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



mais quand je veux ajouter un Article je reçois ce message d'erreur:

Code : HTML
1
The autoloader expected class "Sdz\BlogBBundle\Form\SignificationType" to be defined in file "/Library/WebServer/Documents/Symfony/app/../src/Sdz/BlogBBundle/Form/SignificationType.php". The file was found but the class was not in it, the class name or namespace probably has a typo.

the future belongs to those who prepare for it today
:lol:
 
Hors ligne keo # Posté le 21/05/2012 à 18:07:46
symfony dur dur
Avatar

Merci pour ce tuto :).
Est t'-il possible de faire apparaître 2 champs à remplir pour le formulaire d'ajout ?

- le champs"Tags" à remplir ça c'est déjà fait
- un autre champs niveau (j'invente :))

Le Tag aurait ainsi une entité à 2 champs.

CNAM TECHNICIEN DÉVELOPPEMENT APPLICATIONS INFORMATIQUES JAVA
 

Voir tous les commentaires