Image de couverture de l'article Symfony2 et l'affichage des formulaires imbriqués
Retour aux articles

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.

Oui, mais !

Alors vous me direz "Je peux le faire simplement sans devoir enregistrer la variable twig". Oui mais imaginez de l'imbrication d'imbrication... d'imbrication (de "l'imbrica-ception de formulaire"), on commence vite à s'y perdre. Avec cette méthode, on peut découper son formulaire de manière linéaire.

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.

Photo de Jocelyn Faihy auteur de l'article

Jocelyn Faihy

Commentaires

Bonjour, bon ben je suis du niveau sous le débutant !!! J’ai beau chercher mais j’ai toujours une erreur de Key « prototype » for array with keys « value,…,action, submitted » does not exist in AcmeDemoBundle:Welcome:add.html.twig. Je ne suis pas sur d’avoir bien créé mon controller. Est ce qu’il y aurait une source complète avec tout les fichiers que je puisse comprendre. Merci
Benoit
Merci
Je vais y arriver… un jour !
Photo de Jocelyn Faihy auteur du commentaire

Jocelyn Faihy

Il y a 8 ans

Bonjour Benoit,

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
Super :)

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
Photo de Pixy auteur du commentaire

Pixy

Il y a 9 ans

Merci pour l’astuce !
Photo de Jocelyn Faihy auteur du commentaire

Jocelyn Faihy

Il y a 9 ans

Merci :)

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
Photo de Mimisab auteur du commentaire

Mimisab

Il y a 9 ans

Merci énormément trop, j’en avais tellement besoin et j’ai beau cherché sur la toile j’ai rien trouvé.. Merci encore une fois :)
Photo de Jocelyn Faihy auteur du commentaire

Jocelyn Faihy

Il y a 9 ans

Heureux d’avoir été utile

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

Photo de sidi auteur du commentaire

sidi

Il y a 7 ans

Bonjour si vous avez eu la solution pourriez vous la partager svp !

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) 

Photo de Oussema Mahdi auteur du commentaire

Oussema Mahdi

Il y a 7 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

Merci pour le tuto claire,simple et bien fait.
Photo de Jocelyn Faihy auteur du commentaire

Jocelyn Faihy

Il y a 8 ans

Merci :)