14 - Symfony : Gardez la “form” !
Objectifs
- ✅ Comprendre les formulaires Symfony.
- ✅ Construire et afficher un formulaire
- ✅ Récupérer les informations fournies par un utilisateur
Pré-requis
Avoir terminé la quête
Introduction
User stories 📋
en tant que visiteur, je souhaite récupérer les données d'une série
en tant que visiteur, je souhaite récupérer la liste des séries
en tant que contributeur, je souhaite ajouter une série
In progress 🤸
en tant qu'administrateur, je souhaite créer une catégorie
Doing 🏗️
création du formulaire CategoryType
Done ✅
en tant que visiteur, je souhaite afficher les séries par catégories
en tant que visiteur, je souhaite visualiser la fiche d'une série
Cette quête va t’apprendre à mettre en place un formulaire en utilisant le composant Form de Symfony afin de récupérer des informations utilisateurs.
Sommaire
Installation
Avant toute chose, comme pour les précédentes quêtes, si tu es parti d’un projet Symfony complet (l’option ---webapp), tu n’as rien à faire, sinon :
composer require symfony/form
Présentation
Instant nostalgie : les formulaires php-style
Jusqu’à présent, tu avais l’habitude d’écrire tes formulaires directement dans tes vues en HTML. Mais si tu y réfléchis, à chaque formulaire tu fais des choses assez similaires et répétitives, par exemple :
- Ajouter une balise form.
- Ajouter des éléments dans le form (input, select, textarea).
- Préciser les attributs de chaque champ (type, name, value, placeholder, required, …)
Puis, dans le script PHP qui traite le formulaire :
- Vérifier la présence de l'ensemble des champs dans
$_POST - Valider les valeurs de chacun de ces champs (longueur maxi, type de données,…)
C'est un peu long et surtout très répétitif et tu l'as déjà compris, en informatique, on n'aime pas se répéter.
Les formulaires à la sauce Symfony
Symfony c'est de la Programmation Orientée Objet donc on écrit des classes qui nous permettent d'instancier des objets qui font le travail pour nous.
Et pour les objets de nature similaire, on fait de l'héritage pour ne pas avoir à recopier du code.
Dans Symfony, le principe c'est que chaque formulaire sera un objet d'une classe de formulaire héritant de la classe AbstractType.
On peut imaginer, par exemple :
- CategoryType : un formulaire pour créer/mettre à jour une catégorie
- ActorSearchType : un formulaire de recherche d'acteur
- etc.
Plutôt que de construire un tableau avec les données de $_POST (comme tu l'as fait jusqu'à présent avec les formulaires), Symfony va plutôt hydrater un objet d'une classe reliée à ce formulaire, généralement une entité (par exemple Category pour CategoryType).
Hydrater un objet, signifie tout simplement donner des valeurs aux propriétés de l'objet. Ici, ces valeurs proviendront des données saisies par l'utilisateur dans le formulaire associé à l'objet en question.
Création d'un formulaire
La gestion d'un formulaire Symfony classique s'effectue en 5 étapes.
1. Déclaration de la classe de données
La User Story en cours : Ajouter une catégorie à la base.
L'objet que tu veux hydrater, c'est une catégorie, donc ta classe existe déjà, c'est l'entité Category.
Étape 1 terminée et pas une goutte de sueur ;-)
Dans la pratique, les classes de données derrière les formulaires sont souvent des entités Doctrine. Ainsi les formulaires permettent de remplir la base. Mais ce n'est pas obligatoire et il arrive aussi qu'il s'agisse d'une classe PHP quelconque.
2. Déclaration de la classe de formulaire
Pour s'intégrer avec le composant Form, une classe de formulaire doit suivre certaines règles :
- Hériter de la classe abstraite
Symfony\Component\Form\AbstractType. - Définir la méthode
buildFormpour préciser les champs souhaités dans le formulaire - Définir la méthode
configureOptionspour préciser la classe de données associée
Au niveau des bonnes pratiques de nommage et d'organisation des fichiers, on prendra soin de :
- Avoir un nom de classe de formulaire se terminant par
Type - Mettre la classe dans le dossier
src/Formet donc être dans le namespace App\Form
Pour simplifier tout ça, le MakerBundle propose une commande pour créer cette classe :
symfony console make:form
💻 Place-toi dans le dossier du projet et lance la commande.
Il te demande :
-
Le nom de la classe de formulaire que tu veux (
CategoryType) et si tu oublies leTypefinal, il le rajoute pour toi -
Le nom de la classe de données associée (
Category)
Et le tour est joué !
🔎 Tu n'as plus qu'à vérifier et compléter le code généré.
<?php
namespace App\Form;
use App\Entity\Category;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Category::class,
]);
}
}
Tu constates dans la méthode configureOptions un tableau avec 'data_class' => Category::class.
C'est l'association entre CategoryType et Category.
Tu vois dans la méthode buildForm un ou des appels à la méthode add().
Ce sont des ajouts de champs au formulaire.
Le maker crée autant de champs que de propriétés dans l'entité associée.
D'ailleurs le premier argument de chaque méthode add() est une chaîne de caractères qui contient le nom d'une propriété de cette classe.
C'est grâce à cette astuce que Symfony sait comment faire la correspondance entre le formulaire et l'objet.
Le champ de formulaire créé par add('name') sert à remplir la propriété name d'un objet de la classe Category
Il n'est pas obligatoire d'avoir un champ de formulaire pour chaque propriété. On peut décider de laisser certaines propriétés de l'objet à leur valeur par défaut et donc ne pas mettre de champ de formulaire pour elles.
En revanche, il manque l'indication du type de champ de formulaire souhaité.
C'est le second paramètre de la méthode add().
Il est facultatif, car Symfony peut dans certains cas deviner le type de champ. Mais comme il se trompe souvent, il est recommandé de donner un second argument à add() systématiquement.
Ce paramètre est une chaîne de caractère contenant le FQCN d'une classe servant à représenter un champ de formulaire particulier.
Tu peux utiliser Symfony\Component\Form\Extension\Core\Type\TextType qui représente un champ texte court <input type='text'>
💻 Ajoute
use Symfony\Component\Form\Extension\Core\Type\TextType;
et complète add() avec :
->add('name', TextType::class);
Note que le nom TextType correspond à une classe de formulaire. En effet, pour Symfony les classes Type sont des ensembles de (éventuellement un seul) champ de formulaire. On peut donc les imbriquer. Mais tu verras ça plus tard ;)
3. Le contrôleur
Tout est prêt, tu peux maintenant créer un contrôleur pour construire, afficher puis traiter ton formulaire.
Pour construire le formulaire, 2 étapes :
-
Créer l'objet à hydrater (dans le cas d'un formulaire de création)
Tu peux le créer comme n'importe quel objet PHP standard avec
new.Dans le cas d'un formulaire de modification, tu récupères plutôt un objet existant dans la base.
-
Créer l'objet formulaire
Ici, on utilise la méthode
createFormhéritée deAbstractController. Cette méthode a deux paramètres :- Le nom de la classe de formulaire à créer (string)
- L'objet à hydrater
Tu peux directement utiliser $this->render(); en lui passant $form en paramètre pour ta vue
💻 Tu peux donc écrire un contrôleur ressemblant à celui-ci :
// src/Controller/CategoryController.php
use App\Form\CategoryType;
//...
#[Route('/new', name: 'new')]
public function new(): Response
{
$category = new Category();
// Create the form, linked with $category
$form = $this->createForm(CategoryType::class, $category);
// Render the form
return $this->render('category/new.html.twig', [
'form' => $form,
]);
}
Attention : Comme la route est de la forme /category/new, veille à bien la placer au dessus de la méthode show(). En effet, si tu la places après, le routeur de Symfony s'arrêtant à la première route qui "match", il va penser que tu veux afficher la catégorie qui a pour name "new" et tu auras une erreur 404.
4. La vue
Twig dispose de plusieurs fonctions pour t'aider à afficher le formulaire.
La plus simple : form(formView) qui affiche l'intégralité d'un formulaire (dont la représentation visuelle est accessible via la variable formView)
💻 Teste là en utilisant une vue de ce type dans ton contrôleur :
{# templates/category/new.html.twig #}
{% extends "base.html.twig" %}
{% block body %}
{{ form(form) }}
{% endblock %}
Aïe, ton formulaire s'affiche mais n'a pas de bouton pour le soumettre !
C'est normal, puisqu'on n'a pas ajouté de champ SubmitType dans notre classe de formulaire.
C'est d'ailleurs une bonne pratique.
Cette classe sert à chaque fois que l'on veut renseigner un objet associé. Ce qui arrive quand on veut le créer et quand on veut le modifier. Ce qui change, en général, entre le formulaire de création et celui de modification, pour l'affichage, c'est le texte du bouton.
L'usage est donc d'ajouter le bouton à la main dans la vue.
Pour ça, Twig nous permet de séparer la création du formulaire en plusieurs étapes :
form_start(formView): génère la balise HTML formform_widget(formView): ajoute l'ensemble des champs de formulaireform_end(formView): ferme la balise HTML form (et ajoute tous les champs manquants)
💻 Tu peux donc ajouter un bouton entre 2 étapes :
{% extends "base.html.twig" %}
{% block body %}
{{ form_start(form) }}
{{ form_widget(form) }}
<button type="submit">Add category</button>
{{ form_end(form) }}
{% endblock %}
5. Le traitement du formulaire
Excellent ! Ton formulaire s'affiche. Mais il est inactif...
Reviens sur ton contrôleur pour traiter le formulaire lorsqu'il est soumis.
Pour récupérer les informations du formulaire soumis, Symfony a besoin d'aller piocher dans la requête HTTP. Cette requête prend, là aussi, la forme d'un objet.
C'est une instance de la classe Symfony\Component\HttpFoundation\Request et pour y accéder, tu dois la passer en paramètre de ton contrôleur.
Maintenant que tu as cette Request sous la main, tu peux lancer le traitement du formulaire avec la méthode handleRequest de ton formulaire.
Ton code sera confronté à (au moins) 2 cas possibles :
- Soit il n'y a pas de données correspondant à notre formulaire dans la
RequestEt dans ce cas, il semble judicieux d'afficher le formulaire pour récupérer ces données. - Soit il y a les données voulues Dans ce cas, l'objet est maintenant hydraté avec les données du formulaire et il ne reste plus qu'à les traiter. Par exemple en stockant l'objet en base.
Et pour différencier ces 2 cas, utilise la méthode isSubmitted() sur ton formulaire.
💻 Reprends ton contrôleur pour ajouter ces 2 cas.
/**
* The controller for the category add form
* Display the form or deal with it
*/
#[Route('/new', name: 'new')]
public function new(Request $request) : Response
{
// Create a new Category Object
$category = new Category();
// Create the associated Form
$form = $this->createForm(CategoryType::class, $category);
// Get data from HTTP request
$form->handleRequest($request);
// Was the form submitted ?
if ($form->isSubmitted()) {
// Deal with the submitted data
// For example : persiste & flush the entity
// And redirect to a route that display the result
}
// Render the form
return $this->render('category/new.html.twig', [
'form' => $form,
]);
}
Et bien sûr : n'oublie pas le
usepour l'autoloader.
// Don't forget the Request use !!
use Symfony\Component\HttpFoundation\Request;
La persistance des données en base
Une fois ton formulaire soumis, il faut insérer la nouvelle catégorie en base de données. Pour ce faire tu vas utiliser l'EntityManagerInterface qui est similaire à l'ObjectManager que nous avons vu dans la quête sur les fixtures. Tu te souviens persist() et flush() 🤓 ?
Ajoute dans les paramètres de la méthode une variable $entityManager du type EntityManagerInterface.
💻 Modifie la méthode du contrôleur afin d'enregistrer la nouvelle catégorie en base de données. D'abord grâce à la méthode persist() à laquelle tu passeras $category pour dire à Doctrine que tu souhaites sauvegarder cette nouvelle entité, puis flush() qui exécutera l'insertion en BDD. Redirige ensuite l'utilisateur vers la liste des catégories.
#[Route('/new', name: 'new')]
public function new(Request $request, EntityManagerInterface $entityManager) : Response
{
$category = new Category();
$form = $this->createForm(CategoryType::class, $category);
$form->handleRequest($request);
if ($form->isSubmitted()) {
$entityManager->persist($category);
$entityManager->flush();
// Redirect to categories list
return $this->redirectToRoute('category_index');
}
// Render the form
return $this->render('category/new.html.twig', [
'form' => $form,
]);
}
Pour rappel, lorsque le formulaire est soumis, comme l'objet
$categoryest lié au formulaire, ses propriétés ont été automatiquement mises à jour avec les données renseignées dans les champs du formulaire.
IMPORTANT : Tu noteras qu'aucune vérification concernant la validité des informations du formulaire n'est effectuée pour l'instant. Bien entendu, ceci n'est pas sécurisé ! Pas de panique, tu verras cela dans une prochaine quête.
💪 Challenge
Créer des catégories & des séries
Si tu as bien suivi la quête, l'étape 1 est normalement déjà réalisée.
- Tu dois créer une page contenant un formulaire qui permet d’ajouter une catégorie à la base de données en précisant uniquement son nom.
À partir des ressources citées plus haut, à toi de définir dans leCategoryControllerexistant une méthodenew().
Celle-ci utilise une vue contenant un formulaire permettant de saisir les informations à fournir pour créer une nouvelle catégorie.
À la soumission du formulaire, les données saisies sont enregistrées en base de données.
- Fais de même dans ton
ProgramControllerexistant afin de pouvoir ajouter une série.
Tu remarques en te rendant sur l'url /program/new que Symfony, par défaut, affiche une liste déroulante d'id pour la propriété $category de notre Program.
Il va falloir modifier le FormType du champ category dans notre ProgramType en lui spécifiant la propriété de Category qui va servir de label dans notre liste déroulante (ici name) :
->add('category', EntityType::class, ['class' => Category::class, 'choice_label' => 'name'])
Symfony va choisir automatiquement le bon type pour le champ en fonction des attributs définis dans ton entité Program (dans notre cas, il s'agit d'un ManyToOne vers Category, il va donc choisir une liste déroulante qui affichera la propriété name en tant que label).
Tu auras des explications approfondies et des exemples plus complexes lors d'une prochaine quête.
Enregistre une démo de ta fonctionnalité et met le lien dans README.md
Partage le tout sur une nouvelle branche dans ton dépôt github.
Critères de validation
- La classe
App\Form\CategoryTypeest présente dans le dossier src/Form. - La route
/category/newaffiche bien le formulaire de création de catégorie. - Le formulaire fonctionne (il crée une catégorie).
- La classe
App\Form\ProgramTypeest présente dans le dossier src/Form. - La route
/program/newaffiche bien le formulaire de création de série. - Le formulaire fonctionne (il crée une série dans la catégorie sélectionnée).
- Le code est disponible sur un repository GitHub, avec une branche correspondant à cette quête.
Ressources partagées sur cette quête
- Pour ajouter un résumé et le posterhttps://www.allocine.fr/series/ficheserie_gen_cserie=19136.html
- Site d'upload de vidéos au format webm. Pour les utilisateurs d'ubunutu, pour enregistrer une vidéo, il suffit de faire : ctrl + alt + maj + R :)https://webmshare.com/
- Tuto pour customiser ses formulaire avec bootstrap.https://www.youtube.com/watch?v=mr1jQPdzJS0