L'agence
WanadevStudio
Symfony2 et l'affichage des formulaires imbriqués
L'imbrication des formulaires Symfony2 peut parfois être une prise de tête que l'on aimerait bien pouvoir éviter. Dans cet article nous allons voir une méthode pour ne pas se mélanger les pinceaux.
Cet article est simple, pour ne pas dire destiné aux personnes qui débutent avec Symfony2. En revanche, presque tous les projets Symfony2 sont susceptibles d'afficher des formulaires imbriqués. Pour les plus avancés sur le sujet, les rappels et les petits tips ne font jamais de mal !
En guise de préliminaires, commençons par créer nos entités et les formulaires qui vont avec.
Les entités et formulaires
Pour cet exercice, nous prenons l'exemple d'une bibliothèque de mangas. Nous voulons créer une base de données pour lister notre collection de mangas. Pour le schéma de la base de données, on ne va pas chercher midi à 14h : un manga sera défini par son titre, son auteur ainsi que par un ou plusieurs tomes qui ont chacun leur numéro et leur nombre de pages.
Je vous laisse créer les entités (Mais pour les préssés, je donne la solution juste en dessous).
<?php namespace Project\DemoBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table(name="manga") * @ORM\Entity */ class Manga { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string", length=255) */ private $title; /** * @ORM\Column(type="string", length=255) */ private $author; /** * @ORM\OneToMany(targetEntity="Project\DemoBundle\Entity\Tome", mappedBy="manga", cascade={"persist","remove"}) * @ORM\JoinColumn(nullable=true) */ private $tomes; /* vous avez les attributs, mais n'oubliez pas les habituels construct, getters et setters */ }
<?php namespace Project\DemoBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table(name="tome") * @ORM\Entity */ class Tome { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="integer") */ private $number; /** * @ORM\Column(type="integer") */ private $nbPages; /** * @ORM\ManyToOne(targetEntity="Project\DemoBundle\Entity\Manga", inversedBy="tomes", cascade={"persist"}) */ private $manga; /* à nouveau, vous avez les attributs, mais n'oubliez pas là non plus les habituels construct, getters et setters */ }
}
C'est tout bon pour vous ? Alors continuons en créant les formulaires d'ajout associés à ces entités.
// Project\DemoBundle\Form\MangaType.php <?php namespace Project\DemoBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class MangaType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options){ $builder->add('title', 'text') ->add('author', 'text') // c'est ici que nous imbriquons notre formulaire de Tome : TomeType, défini un peu plus bas ->add('tomes', 'collection', [ 'type' => new TomeType, 'allow_add' => true, 'allow_delete' => true ]) ; } public function setDefaultOptions(OptionsResolverInterface $resolver){ $resolver->setDefaults([ 'data_class' => 'Project\DemoBundle\Entity\Manga' ]); } public function getName(){ return 'project_demobundle_manga'; } }
// Project/DemoBundle/Form/TomeType.php <?php namespace Project\DemoBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class TomeType extends AbstractType{ public function buildForm(FormBuilderInterface $builder, array $options){ $builder->add('number', 'integer') ->add('nbPages', 'integer') ; } public function setDefaultOptions(OptionsResolverInterface $resolver){ $resolver->setDefaults([ 'data_class' => 'Project\DemoBundle\Entity\Tome' ]); } public function getName(){ return 'project_demobundle_tome'; } }
Voilà, nous avons toutes les clés en main pour pouvoir rentrer dans le dur du sujet : l'affichage personnalisé des formulaires Symfony2.
Le rendu des formulaires
Maintenant que nous avons tous nos entités ainsi que nos deux formulaires (Manga et Tome) Symfony, il faut pouvoir les utiliser. Créez une action d'ajout dans un controller. Seul l'ajout nous intéresse pour notre exemple, il n'est pas nécessaire de générer un CRUD complet.
Une fois l’action d’ajout créée, il faut désormais ajouter le fichier twig qui affichera le formulaire.
{# Projet/DemoBundle/Resources/views/Manga/add.html.twig #} {% form_theme form 'bootstrap_3_layout.html.twig' %} <form action="{{ path('project_manga_add') }}" method="POST" {{ form_enctype(form) }} class="form"> <section class="form-group"> {{ form_label(form.title) }} {{ form_widget(form.title) }} </section> <section class="form-group"> {{ form_label(form.author) }} {{ form_widget(form.author) }} </section> <section id="container-tome"> <div> <a href="#" id="add-tome" class="btn btn-success">Ajouter un tome</a> </div> </section> {{ form_widget(form._token) }} </form>
Avec les lignes précédentes, il n'y a que le formulaire d'ajout de Manga qui est affiché pour le moment. Le formulaire d'ajout de tomes n'est pas du tout câblé. Un bouton "Ajouter un tome" est néanmoins affiché.
Câblons dès maintenant la partie d'ajout de Tome directement dans le formulaire d'ajout de Manga. Pour cela, récupérons notre prototype de tomes et enregistrons-la dans une variable twig. Un prototype contient les champs d'un champs du formulaire. Je m'explique : les champs du formulaire imbriqués sont accessible via le prototype du champs du formulaire parent. Dans cet exemple, les champs du formulaire d'un tome se trouve dans le prototype du champs "tomes" du formulaire du manga. Voici quelques explications supplémentaires. Récupérons-la via le code ci-dessous :
{% set prototype_tome = form.tomes.vars.prototype %}
Son rendu se fait aussi simplement qu'un formulaire standard :
<div id="prototype-tome" class="prototype-tome hide"> <section class="form-group"> {{ form_widget(prototype_tome.cover_page) }} </section> <section class="form-group"> {{ form_widget(prototype_tome.nb_pages) }} </section> </div>
Faisons un point, qu'avons-nous à présent ? Deux entités avec leurs formulaires Symfony2 et un twig affichant le formulaire d'ajout d'un manga ainsi que les champs liés à l'entité un tome, via son prototype.
Il ne manque qu'à dynamiser tout ça pour permettre de lier un tome créé à son manga respectif.
Je vais vous donner le petit bout de javascript qui vous permet de faire ça. (Ici, j'utilise JQuery)
$(document).ready(function(){ //Attention à bien vérifier que vos selecteurs correspondent à votre code $('#add-tome').on('click', function(event){ event.preventDefault(); event.stopPropagation(); var $prototypeTome = $('#prototype-tome').clone(); $prototypeTome = $($prototypeTome.html().replace(/__name__/g, $('.prototype-tome').length); var $linkDelete = $('<div><a href="#" class="btn btn-danger delete-tome">Supprimer</a></div>'); $prototypeTome.append($linkDelete); $linkDelete.on('click', function(e){ e.preventDefault(); e.stopPropagation(); $prototypeTome.remove(); }); $('#container-tome').prepend($prototypeTome); }); });
Regardons un peu mieux ce précédent script.
Au clic sur le lien d'ajout, il récupère une copie du prototype du tome, y ajoute un lien pour le supprimer et l'ajoute dans le formulaire. Il ajoute aussi un listener pour qu'au clic sur le lien de suppression le prototype du tome disparaisse.
Conclusion
J'espère que vous pourrez y voir plus clair dans l'affichage de vos formulaires imbriqués créés avec Symfony2 grâce à ce petit trick. Si vous avez des retours ou des questions, n'hésitez pas, la barre de commentaires est faite pour ça ;) !
P.S. : Ce tuto n'est plus compatible avec Symfony3. La façon de déclarer ses formulaires ayant changer depuis, un nouvel article sera disponible avec la mise à jour. N'hésitez pas à nous suivre sur Twitter afin d'être mis au courant des mise à jour.
Jocelyn Faihy
Commentaires
Benny
Il y a 9 ans
Benoit
Benny
Il y a 9 ans
Je vais y arriver… un jour !
Jocelyn Faihy
Il y a 9 ans
En effet, je n’ai pas précisé que seuls les champs du type ‘collection’ ont accès à la variable ‘prototype’. Une petite erreur de ma part que je vais corriger.
Pour les champs imbriqués simples, descendre dans les champs est aussi simple qu’en JSON. Par exemple, imaginons que le manga ait un auteur avec un nom et un prénom. Un formulaire AuthorType est créé avec ceux deux champs et est ensuite imbriqué dans MangaType.
Et pour afficher le champ : {{ form_widget(form.author.name) }}
Je vais mettre à jour cet article avec un exemple du même genre.
Un travail est en cours pour mettre en place des projets sur github :)
Jocelyn
Florent Viel
Il y a 10 ans
Pourquoi ne pas utiliser les form_theme au lieu de devoir répéter la class form-control? Il y en a même un par défaut pour bootstrap 3 dans le code de symfony
Pixy
Il y a 10 ans
Jocelyn Faihy
Il y a 10 ans
En effet, ce serait une solution plus propre pour gérer le style du formulaire. Je ne connais pas encore très bien son fonctionnement, c’est pour ça que je ne l’ai pas utilisé. Je ne savais qu’il en existait un pour Bootstrap3, très intéressant. Se sera sûrement le sujet d’un nouveau tips
Mimisab
Il y a 10 ans
Jocelyn Faihy
Il y a 10 ans
Baimi Badjoua
Il y a 8 ans
Bonsoir, juste une petite indulgence, quelqu'un peut-il m'aider a faire un formulaire en plusieurs etapes. Genre, remplir deux champs, cliquez sur continuer, remplir encore deux champs et cliquer sur enregistrer. Merci d'avance
sidi
Il y a 8 ans
Bonjour si vous avez eu la solution pourriez vous la partager svp !
David
Il y a 8 ans
Merci beaucoup pour ce tuto c'est le top ;)
Une librairie intéressante si besoin : https://github.com/ninsuo/symfony-collection (plugin jquery pour les collections de fomulaire)
Oussema Mahdi
Il y a 8 ans
Merci pour l'astuce, mais j'ai rencontré un pb lors de l'envoi du formulaire.
"An invalid form control with name='sce_userbundle_jen[missions][__name__][titre]' is not focusable"
malgré que les noms sont bien remplacés mais apparemment le compilateur essaye d'envoyer les données du formulaire standard
karlithoss
Il y a 10 ans
Jocelyn Faihy
Il y a 9 ans