L'agence
WanadevStudio
Symfony2 et l'affichage des formulaires imbriqués
Il y a 12 ans
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