Les formulaires
Introduction
Les formulaires sont très développés sous Symfony. Une multitude d’options sont disponibles pour manipuler les champs de vos formulaires à votre guise. Nous allons vous montrer ici ce qui nous semble essentiel à connaître.
À titre d’exemple, nous allons développer la partie administrative de notre application qui nous permettra de gérer nos produits.
Ainsi, nous créons un nouveau contrôleur AdminContoller :
php bin/console make:controller
Supprimons l’action index() par défaut du contrôleur et créons trois actions : insert(), update() et delete().
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class AdminController extends AbstractController
{
#[Route('/insert', name: 'app_admin')]
public function insert(Request $request)
{
return $this->render('Admin/create.html.twig');
}
#[Route("/update/{id}"...
Form Builder
Important : avant de commencer à travailler avec les formulaires, il faut penser à désactiver l’outil Symfony Turbo que nous avions installé dans le chapitre Symfony UX Stimulus, section HotWire et Turbo.
En effet, Turbo, par défaut, prend en main les soumissions des formulaires et exécute une requête AJAX (au lieu d’appeler une page HTML) qui retournera les saisies du formulaire sous le format JSON. Ainsi, les saisies du formulaire pourront être traitées par une application JavaScript tout en restant sur la même page HTML (Single Page Application).
Ce n’est pas ce que nous souhaitons ici. Nous voulons que les résultats des saisies des formulaires soient transmis dans une nouvelle page HTML de manière standard.
Pour désactiver Turbo, rendez-vous sur le fichier assets/controllers.json et mettez la valeur de la propriété "enabled" à false, comme ceci :
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": false,
"fetch": "eager"
},
Puis, bien entendu, relancez la commande run watch pour que ces modifications soient prises en compte :
npm run watch
Vous devriez être tranquille pour travailler de manière standard sur ce chapitre.
Pour créer un formulaire dans l’action d’un contrôleur, il n’y a rien de plus simple.
Il suffit d’invoquer la méthode createFormBuilder() et d’ajouter les champs désirés avec la méthode add().
Syntaxe :
$form=$this->createFormbuilder()
->add('nomduchamp', Type de champ, [ options ])
Les types de champs disponibles sont définis dans la documentation de Symfony : https://symfony.com/doc/current/reference/forms/types.html
Vous avez beaucoup de types disponibles avec de nombreuses options également.
Pour utiliser un type, il faut récupérer la classe correspondante.
Exemple pour un TextType :
use Symfony\Component\Form\Extension\Core\Type\TextType;
$form=$this->createFormbuilder()
->add('nomduchamp', TextType::class, [ options ])
La notation ::class est utilisée pour la résolution des noms de classes. Dans PHP, vous pouvez récupérer une chaîne contenant le nom qualifié complet de la classe avec son espace de noms en utilisant ClassName::class.
Nous allons faire un exemple dans l’action insert(). Créons ce petit formulaire :
$form=$this->createFormbuilder()
->add('nom'...
Formulaires externalisés
1. Définition
Il est plus pratique d’avoir des formulaires indépendants des contrôleurs (sauf si le formulaire est vraiment spécifique à un contrôleur).
Les formulaires indépendants vont pouvoir être réutilisés dans plusieurs actions sans qu’il y ait besoin de les redéfinir. Nous pourrons également imbriquer ces formulaires, faire de l’héritage de formulaires... Les possibilités sont nombreuses.
Pour créer un formulaire externalisé, nous allons nous servir d’une commande sur le terminal :
php bin/console make:form
Prenons un exemple. Nous allons créer un formulaire pour notre entité Produit. Tous les noms de formulaires externalisés se terminent par Type. Nous allons appeler notre formulaire : ProduitType.
Précisez également dans le questionnaire le nom de l’entité jointe. Ici c’est Produit. Exécutez sur le terminal :
php bin/console make:form
The name of the form class (e.g. VictoriousJellybeanType):
> ProduitType
The name of Entity or fully qualified model class name that the new form
will be bound to (empty for none):
> Produit
created: src/Form/ProduitType.php
Success!
Un dossier src/Form a été créé avec, à l’intérieur, le formulaire externalisé ProduitType.
Tous les champs de l’entité ont été récupérés et mis dans le formulaire :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nom')
->add('prix')
->add('quantite')
->add('rupture')
->add('lienImage')
->add('reference')
->add('distributeurs') ;
}
Mais ceci n’est pas suffisant pour créer et utiliser le formulaire. Il faut compléter cette description avec les types de champs souhaités et les options éventuelles.
Voici comment compléter les champs dans notre exemple :
<?php ...
Personnalisation de l’affichage d’un formulaire
Pour l’instant, nous ne nous sommes pas préoccupés du design du formulaire. L’avantage avec Symfony, c’est nous pouvons développer un formulaire en PHP sans se soucier de la partie HTML. Nous nous sommes affranchis de l’écriture des balises <input>, qui sont lourdes et fastidieuses.
L’instruction :
{{ form(nom_du_formulaire) }}
s’occupe de tout.
Cette instruction utilise un thème par défaut, pour afficher le formulaire.
Il est possible de modifier ce thème par défaut et, par exemple, d’utiliser un thème intégrant Bootstrap.
Pour ce faire, il faut modifier le fichier config/packages/twig.yaml et y préciser le thème de Bootstrap 5 :
twig:
default_path: '%kernel.project_dir%/templates'
globals:
auteur: '%env(APP_AUTHOR)%'
form_themes: ['bootstrap_5_layout.html.twig']
Attention à l’indentation. Il y a quatre espaces de décalage pour les options de Twig. Le non-respect de l’indentation entraîne une erreur Symfony.
Il faut arrêter et relancer le serveur de Symfony pour que l’effet fonctionne :
symfony server:start
Pour l’instant, vous ne verrez pas grande différence sur le design du formulaire, mais par la suite, nous utiliserons dans le template, les classes de Bootstrap pour le mettre en forme.
En effet, il est possible d’aller plus loin dans le design en personnalisant chaque élément du thème.
Twig met à votre disposition des fonctions pour rendre les différentes parties du formulaire :
{{ form_start(my_form , { options }) }}
décrit la balise d’ouverture du formulaire. Le paramètre { options } est optionnel. Nous pouvons nous en servir pour ajouter des valeurs aux propriétés de form (exemple : { ’method’: ’POST’ }).
{{ form_end(my_form) }}
décrit la balise de fermeture du formulaire.
{{ form_label(my_form.name, 'nom du label', {'label_attr':
{'class':
'foo'...}}) }}
décrit la balise <label> d’un champ du formulaire . form.name est le nom du formulaire suivi du nom du champ (exemple my_form.nom).
’nom du label’ est le nom du label tel qu’il apparaîtra. Il est possible de conserver le nom du label défini dans le formulaire externe en mettant cette valeur à null.
Les options sont définies dans le dernier paramètre à l’aide de l’étiquette label_attr.
{{ form_widget(my_form.name, {'attr': {'class': 'foo'}})...
Traitement des données du formulaire
Le formulaire étant au point, il s’agit maintenant de nous préoccuper de la récupération des informations saisies par l’utilisateur et de l’insertion en base de données.
En Symfony, tout se passe dans la même action, la création et le traitement du formulaire. Nous allons commencer par traiter l’action insert() de AdminController.
Pour récupérer les données dans l’entité associée, il faut utiliser la méthode handleRequest() en lui passant l’objet $request :
$formProduit->handleRequest($request);
Puis il faut tester l’existence de la méthode Post dans l’objet $request. Il est conseillé de tester également la méthode isValid(). Cette méthode contrôle que toutes les données du formulaire vérifient les contraintes de validation. Nous reviendrons dans une prochaine rubrique sur la validation du formulaire.
if($request->isMethod('post') && $formFilm->isValid() ){...}
À l’intérieur de l’instruction de contrôle if, il faut pour pouvoir insérer les données dans l’entité.
En réalité, les données du formulaire sont déjà dans l’entité. Elles ont été transmises au formulaire par la méthode createForm() ci-dessus (pour notre exemple, c’est dans l’entité $produit). Il suffit de persister cet objet et d’exécuter la méthode flush() pour les injecter dans la base.
Mais, auparavant, il faut gérer la récupération de l’image.
L’image est stockée dans le champ lienImage du formulaire ($formProduit). Pour la récupérer, il faut utiliser la méthode getData() :
$file = $formProduit['lienImage']->getData();
$file nous permet ensuite de récupérer le nom original du fichier uploadé :
$filename=$file->getClientOriginalName();
Vous pourriez aussi créer une nouveau nom de fichier en récupérant uniquement l’extension du fichier transmis (jpg, pdf…) :
$extension = $file->guessExtension();
Il suffit ensuite de déplacer le fichier dans le dossier que l’on souhaite avec la méthode move() :
Syntaxe :
$file->move(nom_du_dossier, nom_du_fichier);
Pour notre exemple, nous allons insérer les affiches dans le dossier public/img. Pour configurer cet emplacement, nous allons créer un paramètre global contenant ce chemin (ce sera mieux pour structurer les images que vous pouvez avoir dans votre application).
Les paramètres globaux sont définis par défaut dans le fichier config/services.yaml. Nous allons y ajouter notre paramètre images_directory dans l’étiquette parameters :
parameters:
images_directory: '%kernel.project_dir%/public/img'
services:
...
%kernel.project_dir% est un autre paramètre, contenant le chemin de la racine de l’application.
Revenons dans notre action insert(). Nous allons déplacer...
Récupération des données de l’entité par défaut
Nous avons mis au point l’action insert(). Voyons comment mettre au point l’action update() avec un produit existant.
Le code est pratiquement le même. Vous pouvez faire un copier-coller du code de insert() dans update($id).
Voici ce qui va changer.
Nous récupérons l’entité correspondant à l’id transmis en paramètre dans l’action :
$produitRepository=$entityManager->getRepository(Produit::class);
$produit=$produitRepository->find($id);
Quelque chose de moins évident : nous allons sauvegarder dans une variable la valeur de la propriété lienImage de l’entité $produit.
Pourquoi cela ?
Lorsque nous allons récupérer les données issues du formulaire, elles vont hydrater automatiquement l’entité (méthode $formProduit->handleRequest($request);). Si le client souhaite conserver l’image de départ de l’entité, il ne va pas appuyer sur le bouton Browse. La propriété hydratée dans l’entité $produit sera null et nous aurons perdu la valeur de lienImage contenue dans l’entité au préalable. Il faut donc avant toute chose sauvegarder cette valeur :
$img=$produit->getLienImage();
La mise en forme du formulaire reste la même.
$formProduit= $this->createForm(ProduitType::class,$produit);
// ajoute un bouton submit
$formProduit->add('creer', SubmitType::class,array(
'label'=>'Mise à jour d\'un film'
));
$formProduit->handleRequest($request);
Le traitement des données est pratiquement le même à l’exception du contenu du else dans le cas où le client n’a pas chargé d’image. Cette fois, nous ne renvoyons plus une erreur mais nous hydratons l’entité produit avec la valeur de l’image qu’il contenait, c’est-à-dire celle que nous avons sauvegardée sauvegardée plus haut dans la variable $img :
if($request->isMethod('post') && $formProduit->isValid() ){
// insertion dans la base
$file = $formProduit['lienImage']->getData();
if(!is_string($file)){
$filename=$file->getClientOriginalName();
$file->move(
$this->getParameter('images_directory'),
$filename ...
Ajout des boutons de mise à jour dans la vue liste
Afin de tester l’action update, il est plus facile d’ajouter des boutons qui pointent vers cette action dans la vue liste_produits/index.html.twig. Nous en profitons pour ajouter également les boutons de suppression d’un produit et d’insertion d’un nouveau produit.
La vue liste_produits/index.html.twig devient :
{% extends 'base.html.twig' %}
{% block title %}Liste des produits
{% endblock %}
{% block body %}
<div class="alert alert-primary">Réduction de 20% sur le produit:
{{ lastproduit.nom }}</div>
<a class="btn btn-info mb-2" href="{{ path('insert') }}" >
Insertion d'un nouveau produit
</a>
<div class="d-flex flex-row justify-content-around flex-wrap">
{% for produit in listeproduits %}
<div class="card" style="width: 18rem;">
<img class="card-img-top" src="{{ asset
('img/'~produit.lienImage) }}" height="200px" alt="image">
<div class="card-body">
<h5 class="card-title">{{ produit.nom }}</h5>
<ul class="list-group list-group-flush">
<li...
Suppression d’une entité
Le traitement de l’action delete() n’est pas difficile. Il suffit de récupérer l’entité correspondant au paramètre $id transmis et de la supprimer avec la méthode remove() de l’Entity Manager.
Dans l’action delete() de l’AdminController :
#[Route("/delete/{id}", name:"delete")]
function delete(Request $request, $id,EntityManagerInterface $entityManager)
{
$produitRepository=$entityManager->getRepository(Produit::class);
$produit=$produitRepository->find($id);
$entityManager->remove($produit);
$entityManager->flush();
$session=$request->getSession();
$session->getFlashBag()->add('message','le produit a été supprimé');
$session->set('statut','success');
return $this->redirect($this->generateUrl('liste'));
}
Vous pouvez maintenant faire ce qu’on...
Traitement de la jointure OneToOne
Rappelez-vous, notre entité possède une propriété $reference qui fait appel à une jointure OneToOne.
Comment intégrer cette jointure dans notre formulaire ?
Nous allons procéder à de l’imbrication de formulaires.
Créons tout d’abord le formulaire externe pour l’entité Reference :
php bin/ console make:form
Il nous demande le nom de notre formulaire Type :
The name of the form class (e.g. DeliciousChefType):
> ReferenceType
Puis donnons le nom de l’entité sur laquelle doit s’appuyer le formulaire :
The name of Entity or fully qualified model class name that the new form
will be bound to (empty for none):
>Reference
Dans le formulaire ReferenceType.php, renseignons le type de champ :
<?php
namespace App\Form;
use App\Entity\Reference;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
class ReferenceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder,
array $options)
{
$builder
->add('numero',NumberType::class,array( ...
Traitement de la jointure ManyToMany
La jointure ManyToMany est un peu plus complexe à mettre en oeuvre puisque cette fois, il faut imbriquer plusieurs formulaires, autant de fois qu’il y a besoin de jointures avec l’entité inverse.
Il faudra toujours utiliser la propriété qui sert de jointure, mais cette fois, le type de champ utilisé sera CollectionType.
Exemple : créez un formulaire pour l’entité Distributeur.
php bin/console make:form
Il vous demande le nom du formulaire :
The name of the form class (e.g. OrangeElephantType):
> DistributeurType
Puis donnez le nom de l’entité sur laquelle doit s’appuyer le formulaire :
The name of Entity or fully qualified model class name that the new form
will be bound to (empty for none):
>Distributeur
Dans le formulaire DistributeurType.php, renseignez le type du champ : nom.
Supprimez le type de la propriété produit inutile ici et complétez le champ nom avec le type TextType :
<?php
namespace App\Form;
use App\Entity\Distributeur;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class DistributeurType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder,
array $options)
{
$builder->add('nom',TextType::class,array(
'label'=>'Nom du distributeur'
)) ;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Distributeur::class,
]);
}
}
Puis, dans le formulaire ProduitType.php, il faut ajouter le CollectionType vers ce formulaire (ne pas oublier le use vers CollectionType) :
<?php
namespace App\Form;
use App\Entity\Produit;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
class ProduitType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder,
array $options)
{
$builder
...
Type EntityType
Ce type permet de gérer les distributeurs et les jointures déjà existantes. Il possède plusieurs options.
Voici un exemple pour les distributeurs. Mettez en commentaire le add(’distributeurs’) avec CollectionType effectué précédemment et ajoutez dans ProduitType.php le champ suivant :
->add('distributeurs',EntityType::Class,array(
'class' => Distributeur::class,
'choice_label'=>'nom',
'label' =>'Selection des distributeurs',
'multiple' => true,
'required' => false
))
Ne pas oublier le use pour EntityType :
use \Symfony\Bridge\Doctrine\Form\Type\EntityType;
ainsi...
Création de types de champs personnalisés
Il peut vous arriver d’avoir besoin d’adapter un type de champ existant pour y ajouter des options supplémentaires qui n’existent pas. Symfony vous offre cette possibilité, grâce à la classe AbstractType. Voici un exemple.
Pour agrémenter l’affichage de la propriété rupture afin de mettre en évidence ce champ (champ de type Checkbox pour l’instant), nous allons créer un nouveau type de champ. Appelons-le MyCheckboxType.
Créez le fichier correspondant dans un sous-dossier de form : src/Form/Type/ MyCheckboxType.php
Tous les types de champ héritent de la classe AbstractType. Vous pouvez copier-coller un exemple de classe depuis la page : https://symfony.com/doc/current/form/create_custom_field_type.html
Personnalisez le contenu de votre classe MyCheckboxType de cette manière :
<?php
namespace App\Form\Type;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType ;
class MyCheckboxType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'attr' => array('class'=>'cPerso'),
'label_attr'=>array('class'=>'cEtiquette') ...
Validation des formulaires
Notre formulaire d’administration fonctionne bien, mais il n’est pas à l’abri de certaines failles de sécurité.
En effet, nous ne contrôlons pas le contenu des champs remplis par l’utilisateur. Celui-ci peut très bien « s’amuser » à mettre du code JavaScript dans le champ Nom produit par exemple. Ce code sera exécuté automatiquement à chaque fois que nous récupérerons le nom du produit pour l’afficher (dans la liste des produits par exemple). Vous imaginez les dégâts que cela peut produire.
Pour éviter ce genre de désagrément, nous allons ajouter des règles de validation dans notre formulaire.
1. Règles de validation
Il y a deux façons de définir des règles de validation. La première et la plus pratique est de définir ces règles dans l’entité grâce aux annotations. Il est également possible de définir vos règles dans un fichier à part en utilisant le format YAML ou PHP, mais c’est beaucoup moins pratique.
Nous allons voir comment ajouter des règles de validation grâce aux annotations.
Ces règles sont appelées des contraintes (Asserts). Pour les utiliser, il faut ajouter, dans l’entité, le use correspondant. Par exemple ajoutons-le dans notre entité Produit :
use Symfony\Component\Validator\Constraints as Assert;
Ce qui donne dans Produit.php :
<?php
namespace App\Entity;
use App\Repository\ProduitRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: ProduitRepository::class)]
class Produit
{
...
Nous pouvons désormais définir des assertions pour toutes les propriétés de l’entité Produit.
La liste de toutes les assertions disponibles est visible sur la page de Symfony : https://symfony.com/doc/current/reference/constraints.html
Prenons un exemple.
Dans l’entité Produit, nous souhaitons que la taille du nom du produit soit limitée entre deux et cinquante caractères. Nous allons utiliser l’assertion Length.
Vous trouverez sa description sur la page : https://symfony.com/doc/current/reference/constraints/Length.html
Copiez-collez le petit exemple dans l’entité Produit pour la propriété nom et modifiez-le ainsi :
#[ORM\Column(length: 200)]
#[Assert\Length(
min: 2,
max: 50,
minMessage: 'Votre nom doit faire au moins {{ limit }} caractères',
maxMessage: 'Votre nom ne doit pas dépasser {{ limit }} caractères',
)]
private ?string $nom = null;
...
Testez cette contrainte avec la requête localhost:8000/insert et insérez...