Je suis développeur web freelance et propose des formations à Symfony2 ! Contactez-moi pour en discuter.

Dans cet article, je vais parler de la génération de formulaires dynamiques avec Symfony2. Le but: pouvoir créer des formulaires dans lesquels on rajoute des champs sans recharger la page, et qui sont associés à des entités que l’on peut ensuite persister dans la base de données. C’est un sujet relativement mal documenté. J’ai utilisé en partie le travail de Khepin, qui s’était essayé au même exercice il y a quelque temps.

Pour réaliser cela, nous allons devoir mixer du PHP (via Symfony2) avec du JavaScript (avec jQuery). Nous allons faire au plus simple et utiliser seulement ces 2 librairies afin de faire marcher cette fonctionnalité. C’est loin des standards de développement modernes (et très loin des miens) où le but n’est pas simplement d’avoir un code qui fonctionne, mais qui soit maintenable. Si le sujet vous intéresse, sachez que côté JavaScript, des librairies comme Backbone.js ont de plus en plus la côte pour faire du code fonctionnel et maintenable côté client. Je reviendrais sur ce thème dans un autre article sous peu.

Pour vous éviter de souffrir, le code du pseudo bundle décrit dans cet article est disponible sur GitHub, avec les informations nécessaires pour le lancer : https://github.com/Keirua/KeiruaProdCustomerDemoBundle

Le but

Dans cet exemple, on va prendre une application ou on veut enregistrer des clients qui achètent plusieurs produits. J’étais en panne d’imagination le jour ou j’ai écrit le code, parce que j’aurais pu vous faire écrire un système de réservation de glaces pour licornes, mais passons. Le but est simple : avoir une page où l’on enregistre un utilisateur et sa commande, avec autant de produits qu’il le souhaite. Pour cela, il faut pouvoir ajouter dynamiquement autant de champs « produits » que nécessaire dans le formulaire, et pouvoir persister et utiliser par la suite ces informations.

Pour cette preuve de concept en quelques sortes, on va avoir besoin de 2 pages : une pour afficher tous les clients et leurs produits, et une pour afficher notre formulaire dynamique. Dans le code d’exemple, le bundle est ApplicationBundle dans l’espace de nom KeiruaProd (encore une fois, oui, j’aurais pu être plus inspiré).

Commençons par créer les routes :

KeiruaProdApplicationBundle_homepage:
pattern: /
defaults: { _controller: KeiruaProdApplicationBundle:Default:index }
KeiruaProdApplicationBundle_add:
pattern: /add
defaults: { _controller: KeiruaProdApplicationBundle:Default:add }

Ensuite, il nous faut 2 entités : une pour l’acheteur (classe Customer), et une pour les produits (classe Product). Entre elles, une relation OneToMany, un acheteur pouvant acheter plusieurs produits. Pour le reste, j’ai juste fait un attribut nom pour qu’on puisse afficher quelque chose, et ajouté un identifiant auto indexé. Rien de bien fou si vous avez déjà écrit ce genre de code.

Le code de la classe de l’entité Customer :


// KeiruaProd/ApplicationBundle/Entity/Customer.php
products = new \Doctrine\Common\Collections\ArrayCollection();
}

public function setProducts(\Doctrine\Common\Collections\ArrayCollection $products){
$this->products = $products;
foreach ($products as $prod){
$prod->setCustomer($this);
}
}
}

Et le code de l’entité Product :


// KeiruaProd/ApplicationBundle/Entity/Product.php

Si vous n'utilisez pas le code du bundle, n'oubliez pas de générer les getters/setters puis de créer le schéma de base de données associé :

php app/console doctrine:generate:entities KeiruaProd
php app/console doctrine:schema:create

Ensuite, on attaque la partie intéressante. On va créer une classe de formulaire pour ces 2 classes, car nous allons en avoir besoin. Le premier formulaire, pour les produits, est très simple:

  • Dans buildForm, on dit qu'on est intéressé uniquement par l'utilisation de l'attribut name dans le formulaire. On change son label (le texte affiché devant l'élément graphique du formulaire), et on dit qu'on veut un champ de texte
  • Dans getName, on donne un identifiant unique pour ce formulaire. Cet identifiant va nous servir par la suite.
  • Dans getDefaultOptions, on associe le formulaire au type d'entités qu'il manipule, des entités "Product"


// KeiruaProd/ApplicationBundle/Form/ProductType.php
add('name', 'text', array ('label' => 'Nom du produit', 'max_length' => 255));
}

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

public function getDefaultOptions(array $options){
return array('data_class' => 'KeiruaProd\ApplicationBundle\Entity\Product');
}
}

La classe CustomerType est similaire à celui de ProductType, mais la méthode buildForm est plus complexe. On ajoute le nom du client, et pour l'attribut products, on pointe sur une collection de formulaires ProductType, et on autorise l'ajout d'éléments dans la collection. Le reste n'est pas différent de ce qu'on a déjà vu.


// KeiruaProd/ApplicationBundle/Form/CustomerType.php
add('name')
->add('products', 'collection', array(
'label' => 'Nom du client',
'type' => new ProductType(),
'allow_add' => true,
'by_reference' => false,
));
}

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

public function getDefaultOptions(array $options)
{
return array('data_class' => 'KeiruaProd\ApplicationBundle\Entity\Customer');
}
}

Maintenant, on va passer à l'affichage. Avant toutes choses, modifiez le code entre et du fichier app/Resources/views/base.html.twig pour ajouter le chargement de jQuery dans l'application.



Ici, je lie directement le fichier depuis le site de jQuery. Comme c'est un fichier qui est utilisé sur plein de sites, il est probablement déjà en cache chez l'utilisateur, ce qui peut réduire le temps de téléchargement de la page. Toutefois, dans la pratique, pour éviter les problèmes de rétrocompatibilité, il peut être bon de rapatrier le fichier sur votre serveur et utiliser Assetic pour compresser le fichier. Là, c'est à vous de voir.

On crée maintenant la vue pour l'affichage des clients et de leurs produits. Pour cela, on étend le layout de base de l'application, et itère sur les objets.


{# src\KeiruaProd\ApplicationBundle\Resources\views\Default\home.html.twig #}
{% extends '::base.html.twig' %}

{% block body %}
Affichage

{% for customer in customers %}
{{ customer.name}}
{% for prod in customer.products %}
- {{ prod.name }}
{% endfor %}
{% endfor %}

Ajouter un client

{% endblock %}

On écrit ensuite le contrôleur associé, très simple : on récupère les entités Customer, et les fournit à une vue, un fichier template Twig.


// src\KeiruaProd\ApplicationBundle\Controller\DefaultController.php
getDoctrine()->getEntityManager();

$customers = $em->getRepository('KeiruaProdApplicationBundle:Customer')->findAll();

return $this->render('KeiruaProdApplicationBundle:Default:home.html.twig', array('customers' => $customers));
}
}

On peut ensuite écrire le controleur de la page d'ajout. Il crée un Client et lui ajoute un produit (pas d'intérêt de prendre la commande si le client n'as rien commandé), et les associe à un formulaire CustomerType.
Si le formulaire est soumis, on valide les données et les persiste dans la base de données. Sinon, on affiche le formulaire, qui contient donc 2 champs au départ : 1 pour le client, et un pour son produit. Ajoutez le code qui suit dans le contrôleur que nous venons d'écrire :


// src\KeiruaProd\ApplicationBundle\Controller\DefaultController.php
public function addAction()
{
$entity = new Customer();
$entity->getProducts()->add(new Product());
$request = $this->getRequest();
$form = $this->createForm(new CustomerType(), $entity);
$form->bindRequest($request);

if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$em->persist($entity);
$em->flush();

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

return $this->render('KeiruaProdApplicationBundle:Default:add.html.twig', array(
'entity' => $entity,
'form' => $form->createView()
));
}

Et enfin, le code d'affichage de la page d'ajout. 2 choses se passent ici:
On affiche le formulaire
On gère l'ajout dynamique de champs, via un bout de javascript à la fin du code sur lequel je vais revenir.


// src\KeiruaProd\ApplicationBundle\Resources\views\Default\add.html.twig
{% extends '::base.html.twig' %}

{% block body %}

Creation d'une commande

Par defaut, un client commande au moins un produit. Par la suite, il est possible de rajouter des produits en cliquant sur le lien adéquat. On valide la commande via le bouton valider. Les données sont persistées dans la base de données, et sont visibles sur la page d'accueil.

{{ form_widget(form) }}
{{ form_errors(form) }}
{{ form_rest(form) }}


{% endblock %}

L'ajout d'éléments dans le formulaire se fait via le bout de javascript suivant, qui permet d'ajouter des champs lorsque l'on clique sur le bouton "ajouter" :

$('a.product_add').live('click', function(event){
event.preventDefault();

var collectionHolder = $('#keiruaprod_applicationbundle_customertype_products');
var prototype = collectionHolder.attr('data-prototype');
form = prototype.replace(/\$\$name\$\$/g, collectionHolder.children().length);
collectionHolder.append(form);
});

Comment est ce que ça marche ? jQuery nous permet de sélectionner le bouton "Ajouter" et d'associer une fonction à exécuter lorsque l'on clique dessus.
Dans cette fonction, on récupère le modèle de code du formulaire à ajouter pour afficher un nouvel élément de formulaire, grâce à l'attribut data-prototype qu'a généré pour nous Symfony2.
On récupère le code grâce à son identifiant keiruaprod_applicationbundle_customertype_products, vous savez, celui qu'on avait mis dans la méthode getName de ProductType. Lorsqu'on génère un formulaire, on génère un div avec cet identifiant.
Le code généré pour l'attribut data-prototype ressemble à ça :


data-prototype="<div><label class=" required">$$name$$</label><div id="keiruaprod_applicationbundle_customertype_products_$$name$$"><div><label for="keiruaprod_applicationbundle_customertype_products_$$name$$_name" class=" required">Nom du produit</label><input type="text" id="keiruaprod_applicationbundle_customertype_products_$$name$$_name" name="keiruaprod_applicationbundle_customertype[products][$$name$$][name]" required="required" maxlength="255" /></div></div></div>"

Effectivement, c'est assez indigeste. Mais on s'en fout, parce que ça n'a pas été fait pour être lu par l'homme et que tout ce qu'on doit en faire, c'est générer un identifiant pour notre nouvel élément, qui remplace alors "$$name$$" dans le data-prototype. Il suffit ensuite d'ajouter ce code à ce qu'il y avait déjà pour l'affichage du formulaire. De son côté, Symfony2 se charge de trier les données lors de la soumission.

Victoire, on a terminé ! Si on lance la page, on peut maintenant créer des utilisateurs, ajouter des produits, et les sauvegarder. Sur la page d'accueil, on peut visualiser les entités, les clients et leurs commandes. La classe ! Par contre niveau maintenabilité, avec nos pauvres lignes de JavaScript à la fin du .twig, on est pas terribles. Je reviendrais sur le sujet d'ici peu de temps.

4 Réponses à “Formulaires dynamiques avec Symfony2”

  1. Diabgate Dit:

    s’il vous plait je souhaiterait afficher la liste de tous les produit d’un client dans un formulaire contenant une liste deroulant de client.

  2. aymen Dit:

    imaginons que l’on veux connaitre la quantité acheté d’un produit x on fait comment ?

  3. Fabien Dit:

    Bonjour, article intéressant.
    Cependant c’est du déjà vu sur le net, un classique en somme…

    Comment faire si les produits (de l’exemple ci-dessus) ne possédent pas tous les mêmes attributs ?

    Imaginons « produit » comme une classe mère, ayant par exemple deux classes filles (disons « ordinateur » et « téléviseur »).

    Je ne trouve aucune documentation expliquant comment faire pour gérer l’apparition des formulaires représentant les classes filles à la demande (par exemple un menu déroulant « ajouter un ordinateur ou ajouter un téléviseur).

    Si vous avez une solution, je suis preneur…

    Merci et bonne journée.

  4. Khalid Dit:

    Bonjour, j’ai essayé la procédure mais j’ai une erreur avec le bindRequest

    « UndefinedMethodException: Attempted to call method « bindRequest » on class « Symfony\Component\Form\Form » in D:\wamp\www\ykjextranet\src\Extranet\DispositionBundle\Controller\PdfMailController.php line 189. »

    Vous serez-t-il possible de publier sur git ou autre le code complet afin qu’on puisse vérifier dans les use s’il ne manqe pas quelque chose.

    Sinon le tuto est très pédagogique contrairement à certains ou on a plus tendences à copier coller sans chercher à comprendre. Mais bon ce serait bien si j’arrivais a faire marcher mon appli 🙂

Répondre

Unable to load the Are You a Human PlayThru™. Please contact the site owner to report the problem.