TD1 – Découverte du framework Symfony 1/2 Routing, Doctrine, Twig, Formulaires
Petite note à l’attention des étudiants du parcours IAMSI : le TD fait parfois référence au cours de complément web de l’année dernière, car nous revenons sur certaines notions, technologies et concepts déjà abordés dans ce cours. Malheureusement, de votre côté, vous n’avez pas eu ce cours spécifique au parcours RACDV. Mais pas de panique ! Le TD a été aussi pensé pour vous et ces notions sont réexpliquées complètement. Simplement, là où certaines sections feront office de rappel pour le parcours RACDV, dans votre cas, cela sera une découverte. Donc, ne vous en faites pas s’il est fait mention de ce cours de l’année dernière. Ce TD a été conçu pour être accessible par tous !
Mise en place de l’environnement de développement avec Docker
Afin de simplifier la mise en place de l’environnement de développement en évitant les divers problèmes de compatibilité selon votre machine ou votre système d’exploitation, le travail sur les différents TPs s’effectuera au travers d’un conteneur Docker configuré adéquatement et possédant tous les outils dont vous aurez besoin.
Allez sur la page du dépôt du conteneur Docker, et faites le tutoriel d’installation et de configuration (instructions au niveau du fichier README.md). Vous ne devez normalement pas y passer plus de 30 minutes.
Toutes les commandes citées dans ce TP (et les prochains) devront être exécutées dans le conteneur du serveur web.
Introduction
L’année dernière, dans le cadre du cours de complément web (pour le parcours RACDV), vous avez étudié et mis en pratique les notions essentielles afin de construire un framework web. Nous nous sommes notamment intéressés aux notions de conteneur à injection de dépendances (conteneur IoC), aux design patterns et globalement aux outils utiles permettant de rendre notre framework simple d’utilisation et hautement paramétrable.
Mais concrètement, qu’est-ce qu’un framework ? Un framework est un “cadre de développement” fournissant une architecture et des outils permettant de créer une application (dans notre cas une application web, mais il en existe aussi pour d’autres types de logiciels). Un framework est composé de différentes briques logicielles et est généralement construit de manière à favoriser les bonnes pratiques de conception (utilisation de patterns, architecture organisée en couche, faible couplage, principes SOLID…). La sécurité (basique) de l’application est généralement assurée sans action du développeur (par exemple, pour le web, protection par défaut contre l’injection SQL, le CSRF, la faille XSS…).
La plupart des frameworks utilisent un ORM permettant de gérer simplement la couche “stockage” d’une application. Nous avions notamment utilisé l’ORM Hibernate basé sur Java dans le cours de programmation des bases de données du semestre 3.
On parle de “cadre” car afin de coder son application, le développeur va devoir respecter le fonctionnement imposé par le framework et utiliser ses outils, ce qui l’empêche de dériver en proposant une conception et une manière de développer hors du “cadre” imposé. Et c’est une bonne chose car, sans s’en rendre compte, le développeur utilise de bonnes pratiques, ce qui augmente la maintenabilité et la lisibilité de son programme. De plus, le code d’un programme développé sous un framework précis sera rapidement compréhensible par un autre développeur connaissant aussi cette même technologie.
Avant tout, les frameworks sont surtout faits pour faciliter la vie du développeur et accélérer le développement de l’application.
L’année dernière, le framework que vous avez développé était fonctionnel, mais encore incomplet. Il est donc temps de passer à l’apprentissage d’un outil professionnel concret !
Dans le cadre de ces quatre premiers TDs, nous allons donc étudier le framework PHP Symfony.
Symfony est un framework web PHP français créé en 2005. Assez tôt, il a connu une forte popularité dans le pays. Aujourd’hui, nous en sommes à la version 8. Dans le cadre de ce cours, nous n’utiliserons pas la version 8 mais plutôt la version 7.4. En effet, cette version est dite LTS, ce qui signifie “Long-Term Support”. Cela veut dire que cette version recevra des mises à jour de sécurité et des patchs de bugs pendant une longue période. Par exemple, la version 8.1 de Symfony sera maintenue jusqu’en janvier 2027, alors que la version 7.4 sera maintenue jusqu’en novembre 2029. La version LTS 8.x n’est pas encore publiée.
Le principal concurrent de Symfony est le framework Laravel. Nous avons donc fait le choix de plutôt vous apprendre à utiliser Symfony par rapport à Laravel car il y a plus de chances que les offres d’emplois relatives au développement web avec PHP (pour vos stages ou votre futur travail après la fin de vos études) concernent du développement sous Symfony, si vous restez en France.
Cependant, pas de panique, il y a beaucoup de similitudes entre Symfony et Laravel (même certaines choses identiques !). Une fois un framework maîtrisé, il vous sera facile de passer à l’autre. Par ailleurs, Laravel utilise certains composants développés pour Symfony.
Symfony permet de créer des sites web classiques en mode “server-side rendering” (génération de la page web du côté du serveur, comme ce que vous faisiez jusqu’ici), des API REST et même des briques logicielles (appelées bundles) à intégrer à Symfony, pour lui ajouter de nouvelles fonctionnalités.
Ce framework utilise (par défaut) l’ORM Doctrine (syntaxe assez similaire à Hibernate, en Java). Concernant les templates (génération de pages HTML), il utilise Twig que vous avez déjà utilisé l’année dernière dans le cadre du cours de compléments Web.
Concernant le thème de l’application que nous allons construire au fil des cours, nous allons reprendre le concept du réseau social The Feed en l’améliorant. Concernant la partie sur twig, vous allez donc un peu refaire certaines choses que vous aviez faites en compléments Web, mais pas avec le même framework et nous allons aussi aller plus loin.
Nous allons suivre la progression suivante :
-
Création du site “The Feed” en utilisant Symfony. L’objectif est d’arriver au même résultat que vous aviez obtenu (niveau fonctionnalités) à la fin des cours de compléments web.
-
Amélioration du site. Nous allons ajouter de nouvelles fonctionnalités, des rôles pour les utilisateurs, la suppression de compte, de publications, une boutique pour acheter le mode “premium” du site…
-
Création d’une API REST pour The Feed. Nous allons créer une API complète pour le site, ce qui permettra de complètement découpler l’application de la partie “front-end” et ainsi la relier et l’utiliser avec n’importe quelle technologie côté client (ce qui sera le thème de la suite des cours de cette ressource !).
Pour cette première séance, recréons donc The Feed en utilisant ce nouvel outil ! Vous allez constater que certaines parties vont aller beaucoup plus rapidement (et facilement) que l’année dernière !
Installation et mise en route
Tout d’abord, il va falloir créer un projet avec Symfony. Nous pouvons faire cela simplement en utilisant l’outil composer (installateur de bibliothèques PHP que vous avez déjà utilisé l’an dernier).
-
Depuis le terminal (dans le conteneur docker), assurez-vous d’être bien placé dans
/var/www/html. -
Exécutez les commandes suivantes :
composer create-project symfony/skeleton:"7.4.*" the_feed cd the_feed composer require webappIl se peut que composer vous demande de faire un choix concernant Docker, tapez simplement “n”.
Cet ensemble de commandes crée les fichiers de base de votre projet et télécharge les briques logicielles essentielles pour le développement d’un site web.
-
Afin de gérer correctement la redirection d’URL avec Apache, il nous faut un fichier
.htaccesspour l’application (comme vous l’avez vu en cours de compléments Web l’an dernier si vous étiez dans le parcours RACDV). Au lieu de créer manuellement ce fichier, il est possible de le générer facilement en installant le paquetagesymfony/apache-packgrâce àcomposer. Pour cela, exécutez donc la commande suivante (toujours dans le conteneur docker) :composer require symfony/apache-packImportant : une question vous sera posée dans le terminal, répondez
y(oui).Pour vérifier que cela a bien fonctionné, affichez les fichiers cachés dans le sous-dossier
publicde l’application afin de vérifier que le fichier.htaccessa bien été généré (ls -ladans un terminal, ou bienCTRL+Hdans l’explorateur de fichiers). -
Donnez au serveur web les permissions pour créer et éditer des fichiers dans votre projet (à exécuter depuis la racine du projet) :
chown -R 1000:www-data .On rappelle encore une fois qu’il faut exécuter ces commandes dans le terminal qui s’exécute dans votre conteneur docker (cette commande donne les permissions à l’utilisateur
1000dans le conteneur qui fait le lien avec l’utilisateur qui exécute docker sur la machine hôte, puis au groupewww-dataqui correspond à apache).Il peut y avoir des erreurs et certaines permissions non accordées, ce n’est pas grave.
-
Sur votre machine, ouvrez le répertoire du projet (
the_feed) avec votre IDE favori (de préférence,PHPStorm). Celui-ci se trouve dans le dossier partagéshared/public_html(là où vous avez installé le conteneur docker). -
Testez que votre projet a bien été initialisé en vous rendant à l’adresse https://localhost/the_feed/public
Vous devriez maintenant voir la page par défaut de Symfony, signe que tout est bien installé. Vous êtes prêts à débuter le développement du site !
Comme vous pouvez le constater, il y a beaucoup de fichiers qui ont été générés. Au lieu de tous vous les présenter dans une section dédiée, nous parlerons plutôt de chaque fichier/dossier utile individuellement quand nous aurons besoin de l’utiliser.
Quand vous uploaderez votre projet sur git, certains dossiers comme vendor (contenant les bibliothèques importées) ou bien le cache du site seront ignorés. Pour installer un projet déjà existant (chez vous, sur une autre machine, dans un autre conteneur docker…) il suffit d’exécuter la commande suivante à la racine du projet :
composer install
Composer va notamment utiliser les dépendances listées dans le fichier composer.json pour télécharger tout ce qu’il faut.
Premiers pas
Dans cette première section, nous allons voir comment créer des contrôleurs, des routes et générer puis renvoyer la page HTML désirée. Nous parlerons aussi de la gestion des messages flash avec Symfony.
Contrôleur et Routing
Symfony propose diverses commandes qui permettent d’initialiser (voir de créer en quasi-totalité) des classes de certaines catégories (entités, contrôleurs, formulaires…) et de la placer au bon endroit dans l’architecture de l’application. Ces commandes doivent s’exécuter à la racine du projet;
Toutes les commandes de Symfony s’exécutent via un script PHP nommé console se trouvant dans le sous-dossier bin. On utilise donc le programme php (généralement à partir de la racine du projet) pour exécuter ce script. Il suffit ensuite de préciser le nom de la commande (de Symfony) souhaitée et d’éventuels paramètres et/ou options.
php bin/console macommande param1 param2 --option1 --option2
Afin de créer un nouveau contrôleur, nous pouvons notamment utiliser la commande suivante (à partir de la racine du projet) :
php bin/console make:controller ExempleController
Ce qui aura pour effet de placer dans le dossier src/Controller le fichier suivant :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ExempleController extends AbstractController
{
#[Route('/exemple', name: 'app_exemple')]
public function index(): Response
{
return $this->render('exemple/index.html.twig', [
'controller_name' => 'ExempleController',
]);
}
}
Une route et une fonction d’exemple sont créées par défaut, mais nous les supprimerons pour placer nos propres fonctions.
Bien entendu, vous pouvez aussi créer un contrôleur à la main, mais la commande a aussi l’avantage d’inclure les premiers imports nécessaires.
Pour rappel, le routing est le fait d’associer un chemin du site (par exemple /coucou) et une méthode HTTP (GET, POST, PUT, PATCH ou DELETE) à une fonction (du contrôleur) qui va traiter la requête puis renvoyer la réponse (page HTML ou des données sous un format comme du JSON pour une API…)
Attributs en PHP
Pour créer une route avec Symfony, nous utilisons une fonctionnalité introduite depuis PHP 8 : les attributs (on retrouvera parfois le terme d’annotations pour désigner les attributs, qui était l’ancienne façon de faire…)
Les attributs sont des informations que nous allons ajouter à un élément de l’application : une fonction, une classe entière, une propriété de la classe… Ce sont des méta-données et des règles qui vont pouvoir être lues (ici par le framework) interprétées et utilisées pour configurer notre application. Ces attributs sont aussi paramétrables.
Un attribut se présente ainsi :
#[ClasseAttribut(param1: ..., param2: ...)]
En réalité, un attribut est une classe. Les paramètres que l’on précise quand on utilise un attribut sont des données passées au constructeur de l’attribut (en fait, on crée une nouvelle instance chaque fois qu’on précise un attribut). Il est ensuite possible d’analyser une classe, une fonction, etc… afin de récupérer ses attributs (il y a des méthodes PHP dédiées, par exemple : ReflectionProperty::getAttributes, ReflectionClass::getAttributes).
Lors de l’utilisation de l’application, les différentes briques qui composent Symfony se chargent de lire ces attributs et d’exécuter des actions dans différents contextes, par exemple, pour enregistrer les informations sur les routes de l’application, ou bien sur les permissions.
Arguments nommés en PHP
Vous aurez remarqué que le constructeur de ClasseAttribut utilise une syntaxe un peu particulière pour son initialisation. En fait, depuis PHP 8, nous ne sommes pas obligés de préciser les paramètres d’une fonction dans l’ordre (cela inclut les constructeurs). On peut directement indiquer le nom du paramètre souhaité (même si celui-ci se trouve, par exemple en 3ᵉ position) et lui associer sa valeur. Ce mécanisme est appelé arguments nommés et est similaire à ce qu’il est possible de faire en python, notamment. Plus d’informations sur la documentation officielle.
Attribut pour le routage
Afin de relier une route à une méthode d’un contrôleur avec Symfony, il suffit donc d’ajouter l’attribut Route suivant, au-dessus de la méthode désirée :
#[Route('/exemple', name: 'route_exemple', methods: ["GET", "POST", ...])]
public function methodeExemple(): Response
{
...
}
-
Le premier paramètre correspond au sous-chemin à partir de la racine de votre site web. Dans l’exemple ci-dessus, si par exemple votre projet est hébergé sur
https://monsite.com, alors cela correspond à l’urlhttps://monsite.com/public/exemple(dans un cas réel, on pointerait directement le nom de domaine sur le sous-dossier “public” du projet, et pas à sa racine, ce qui donnerait alors directementhttps://monsite.com/exemple). -
Le second paramètre
namecorrespond au nom de la route. Celui-ci doit être unique dans toute l’application (pas deux routes avec le même nom). Ce nom de route est très important, car on pourra l’utiliser au lieu du chemin pour rediriger l’utilisateur, ou bien générer des URLs dans nos pages HTML. -
Le dernier paramètre correspond aux méthodes HTTP autorisées sur cette route. Par exemple, on peut autoriser seulement la méthode
GETà être appelée sur cette route, ou bien seulementGETouPOST.
Vous noterez que si le nom d’une route doit être unique, il est toutefois possible d’avoir un chemin identique pour deux routes différentes si les actions autorisées ne sont pas les mêmes.
Par exemple, on pourrait regrouper le traitement des actions GET et POST sur une seule méthode pour le chemin /exemple :
#[Route('/exemple', name: 'route_exemple', methods: ["GET", "POST"])]
public function methodeExemple(): Response
{
...
}
Ou bien diviser son traitement avec deux routes distinctes :
#[Route('/exemple', name: 'route_exemple_get', methods: ["GET"])]
public function methodeExempleGet(): Response
{
...
}
#[Route('/exemple', name: 'route_exemple_post', methods: ["POST"])]
public function methodeExemplePost(): Response
{
...
}
Il existe d’autres paramètres utiles dont nous pouvons nous servir, par exemple locale qui permet de restreindre une route selon le pays de l’utilisateur.
Paramètre des actions
Concernant les paramètres des méthodes, nous pouvons notamment ajouter des services qui seront injectés automatiquement (nous en reparlerons plus tard) ainsi qu’un objet de type Request qui permet de lire des données envoyées, par exemple dans le query string, ou bien pour traiter un formulaire.
#[Route('/exemple', name: 'route_exemple_get', methods: ["GET"])]
public function methodeExempleGet(Request $request): Response
{
$paramQueryString = $request->get("param");
...
}
Il est aussi possible d’ajouter des paramètres dans le chemin des routes de la manière suivante :
#[Route('/exemple/{id}', name: 'route_exemple_get', methods: ["GET"])]
public function methodeExempleGet($id): Response
{
//J'ai accès au $id passé dans l'URL
}
Dans l’exemple ci-dessus, si l’utilisateur accède à la route /exemple/5, alors la variable $id vaudra “5”. Il faut que le paramètre de la méthode ait le même nom que celui de la route (ici id => $id)
Il est possible de placer des paramètres n’importe où dans le chemin :
#[Route('/exemple/{id}/coucou/{nom}', name: 'route_exemple_get', methods: ["GET"])]
public function methodeExempleGet($id, $nom): Response
{
...
}
Je peux par exemple déclencher cette route/méthode avec le chemin /exemple/2/coucou/test, ce qui affectera $id à “2” et $nom à “test”.
Dans un premier temps, vous allez faire un contrôleur simple ne renvoyant pas encore de pages HTML, seulement du texte brut. Pour cela, il vous suffira de renvoyer un objet Response :
#[Route('/exemple', name: 'route_exemple_get', methods: ["GET"])]
public function methodeExempleGet(): Response
{
...
$contenu = ...
return new Response($contenu);
}
-
En utilisant la commande
make:controller(à la racine du projet), créez un contrôleur nomméDemoController(répondez non (n) quand on vous demande si vous voulez créer des tests unitaires). On rappelle (une dernière fois) que les commandes doivent être exécutées dans le terminal s’exécutant dans votre conteneur docker ! -
Supprimez la méthode d’exemple générée par défaut dans votre nouveau contrôleur.
-
Créez une méthode ayant une route visant le chemin
/hello, nomméehello_getet autorisant seulement la méthodeGET. Cette méthode doit renvoyer une réponse contenant “Hello world” à l’utilisateur. Testez votre route sur votre site. -
Créez une deuxième méthode/route nommée
hello_get2similaire à la première, mais permettant d’ajouter un paramètre “nom” dans le chemin et qui doit renvoyer une réponse contenant “Hello (nom)” où le nom est celui passé dans l’URL. Testez votre nouvelle route sur votre site https://localhost/the_feed/public/hello/Paul.
Twig
Comme vous l’avez constaté dans la section précédente, les différentes méthodes des contrôleurs doivent retourner un objet de type Response. Ces réponses peuvent être variées selon le type d’application, mais dans notre cas, nous souhaitons renvoyer une page HTML à l’utilisateur.
Pour faciliter le développement de la partie front-end, Symfony utilise le moteur de template twig que vous avez déjà utilisé l’année dernière.
Les fichiers twig sont appelés templates et on les nomme généralement ainsi : nom.format_genere.twig (en effet, twig ne sert pas seulement à générer du HTML!). Dans notre cas, comme nous allons générer du HTML, nous les nommerons donc nom.html.twig. Ces templates sont rangés dans le sous-dossier templates à partir de la racine du projet.
Afin de demander à Symfony de générer une page HTML à partir d’un template twig, on utilise la méthode render disponible dans tous les contrôleurs :
#[Route('/exemple', name: 'route_exemple_get', methods: ["GET"])]
public function methodeExempleGet(): Response
{
...
return $this->render("chemin_template/template.html.twig", ["param1" => ..., "param2" => ...]);
}
-
Le premier paramètre de la méthode
renderest le chemin vers le template désiré à partir du dossier templates. Si par exemple, mon template se trouve simplement directement dans le dossier template, j’indiquemon_template.html.twig. Par contre, s’il se trouve dans le sous-dossier “demo”, j’indiquedemo/mon_template.html.twig. -
Le second paramètre est un tableau associatif qui permet de passer des données nommées directement au template, qui pourra alors les utiliser directement pour générer la page HTML. Ce paramètre est optionnel. On ne le spécifie donc pas s’il n’y a pas de paramètres à passer au template.
Chaque paramètre passé au template est accessible sous le même nom dans le template (le “nom” correspond à la valeur de la clé associée à la donnée dans le tableau associatif). Dans l’exemple précédent, on a alors accès dans le template à deux variables param1 et param2.
Avant tout, quelques rappels sur le langage utilisé par ce moteur de templates (vous pouvez aller rapidement sur cette partie si vous vous souvenez bien des cours de l’année dernière)
-
L’instruction
{{ donnee }}permet d’afficher une donnée. Elle sera automatiquement échappée pour le HTML.On peut accéder à une méthode d’un objet avec
{{ donnee.methode() }}, et à un attribut avec{{ donnee.attribut }}. Twig essayera d’abord de trouver un attribut public$donnes->attribut, puis appellera sinon$donnes->getAttribut(),$donnes->isAttribut()et$donnes->hasAttribut()(cf. documentation de Twig).<p>{{ donnee }}</p><p>{{ monObjet.attribut }}</p> <p>{{ monObjet.methode() }}</p> -
On peut définir des variables locales :
{% set exemple = "coucou" %} <p>{{ exemple }}</p> -
La structure conditionnelle
ifpermet de ne générer une partie du document que si une condition est remplie :{% if test %} Code HTML.... {% endif %} -
Il est bien sûr possible de construire des conditions complexes avec les opérateurs :
not,and,or,==,<,>,<=,>=, etc… par exemple :{% if test and (not (user.name == 'Smith') or user.age <= 20) %} Code HTML.... {% endif %} -
La structure répétitive
forpermet de parcourir une structure itérative (par exemple, un tableau) :{% for data in tab %} <p>{{ data }}</p> {% endfor %} -
Si c’est un tableau associatif et qu’on veut accéder aux clés et aux valeurs en même temps :
<ul> {% for key, value in tab %} <li>{{ key }} = {{ value }}</li> {% endfor %} </ul> -
On peut aussi faire une boucle variant entre deux bornes :
{% for i in 0..10 %} <p>{{ i }}ème valeur</p> {% endfor %} -
Une syntaxe
{% else %}permet de traiter le cas particulier d’un tableau vide :{% for data in tab %} <p>{{ data }}</p> {% else %} Pas de données dans le tableau {% endfor %}
Dans l’exemple donné plus tôt avec la méthode render de symfony, j’ai accès à deux paramètres param1 et param2, que je peux directement utiliser dans le template.
Pages simples
Pour vérifier que vous avez bien compris le fonctionnement basique de twig et comment l’utiliser avec Symfony, nous allons faire quelques petits exercices simples.
Parfois, après plusieurs changements dans un template, il se peut qu’au rechargement de la page, les modifications ne soient pas visibles immédiatement (cela est dû à la gestion du cache, notamment avec le conteneur docker que nous utilisons). Si cela vous arrive, vous pouvez exécuter la commande suivante à la racine du projet pour régler le problème :
php bin/console cache:clear
Ou, en abrégé :
php bin/console c:c
-
Supprimez les templates
base.html.twigetdemo/index.html.twigcréés par défaut par Symfony. -
Créez trois templates
demo1.html.twig,demo2.html.twigetdemo3.html.twigdans le sous-dossierdemo.Ces templates auront le squelette suivant :
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Demo</title> </head> <body> <!-- Zone à compléter par la suite --> </body> </html> -
Le template
demo1doit simplement afficher (dans son body) “Hello world”. Modifiez votre routehello_getpour qu’elle affiche une page HTML générée avec ce template. Testez votre route dans votre navigateur. Vous pouvez notamment inspecter le code de la page ou bien l’afficher en entier avecCTRL+U. -
Le template
demo2doit afficher (dans son body) une variablenompassé en paramètre du template. Modifiez votre routehello_get2pour qu’elle affiche une page HTML générée avec ce template, en passant le nom récupéré dans l’URL au template via la méthoderender. -
Le template
demo3doit afficher (dans son body) une liste non ordonnée (<ul></ul>) contenant des éléments (<li></li>) correspondant aux éléments d’un tableaulisteCoursespassé en paramètre du template. Ajouter une nouvelle routecoursesdans votreDemoController(en GET, avec le chemin de votre choix) qui initialise simplement (en dur) un tableau de chaînes de caractères contenant différents produits (par exemple, lait, pain, œufs…). Cette route doit renvoyer une page générée avec le templatedemo3. Vérifiez que votre liste de courses s’affiche correctement en résultat, sur votre site. -
Modifiez le template
demo3pour faire en sorte que si une chaîne de caractères du tableaulisteCoursesest égale à"Livre sur Symfony", celle-ci s’affiche en gras. Vous pouvez faire cela en ajoutant une balise<strong></strong>autour du texte affiché.
Messages flash
Vous souvenez-vous du mécanisme des messages flash ? Pour rappel, il s’agit de messages informatifs stockés dans la session de l’utilisateur et affichés après chargement de la page. On peut s’en servir, par exemple, pour afficher un message d’erreur lié à un formulaire. Ou pour notifier l’utilisateur que son inscription est complète. Ces messages sont effacés une fois qu’ils ont été lus (ils ne seront donc pas ré-affichés) : c’est le sens de flash.
De ce côté, Symfony a tout prévu ! Il vous suffit d’appeler la méthode addFlash dans votre contrôleur :
#[Route('/exemple', name: 'route_exemple_get', methods: ["GET"])]
public function methodeExempleGet(): Response
{
...
$this->addFlash(type, message);
...
}
-
Le premier paramètre
typeest une chaîne de caractères qui correspond à la catégorie du message flash (par exemple, succès, erreur…). -
Le second paramètre est le message à afficher.
Côté twig, il n’y a pas besoin de passer explicitement les messages en paramètres template lors du render(). Ils sont directement accessibles via app.flashes() de la manière suivante :
{% for flashMsg in app.flashes(type) %}
...
{{ flashMsg }}
...
{% endfor %}
L’instruction app.flashes(type) permet d’obtenir un tableau de tous les messages flash d’un type donné. On peut donc parcourir ce tableau avec twig et afficher les messages de la manière que l’on souhaite. Si on a plusieurs catégories de messages à afficher, il faut répéter l’opération en changeant de type.
-
Modifiez simplement une de vos routes pour ajouter des messages flash de la catégorie de votre choix.
-
Modifiez le template lié à la route modifié à l’étape précédente pour afficher ces messages flash.
-
Vérifiez que tout fonctionne.
Les publications
Maintenant que vous maîtrisez les bases, il est temps de commencer à développer concrètement notre application. Tout d’abord, nous allons créer une entité Publication. Nous ferons le nécessaire pour la relier à la base de données puis nous ajouterons les fonctionnalités de lecture du feed et de création d’une nouvelle publication.
Création d’une entité
Comme pour les contrôleurs, Symfony propose une commande permettant de créer une entité de manière interactive. Elle va générer les propriétés de la classe, ainsi que les getters/setters. De plus, la commande va aussi configurer les attributs PHP de chaque propriété afin de préparer la synchronisation avec la base de données, via Doctrine :
php bin/console make:entity Nom
À chaque étape, la commande demande le nom du nouvel attribut à ajouter, puis son type (une liste des types possibles est affichable en tapant ?), éventuellement sa taille maximum dans la base (si c’est une chaîne de caractères) et enfin, si l’attribut peut être null dans la base de données (on répond y ou n pour yes/no).
Il n’y a pas besoin de spécifier d’attribut id (c’est-à-dire, l’attribut correspondant à la clé primaire, en base). Cet attribut (entier) est généré et ajouté automatiquement sans avoir besoin de le préciser. Quand on ne souhaite plus ajouter de champs, on envoie simplement un message vide dans la console.
En plus de générer la classe de l’entité, un fichier de repository est également généré, permettant d’effectuer des opérations sur cette entité en base.
-
À l’aide de la commande
make:entity, créez une entitéPublicationrespectant les contraintes suivantes :-
Un attribut
messagede typetext(non null). -
Un attribut
datePublicationde typedatetime(non null).
-
-
Une fois terminé, quittez la commande (en envoyant un message vide) puis allez observer le code des deux classes générées :
src/Entity/Publicationetsrc/Repository/PublicationRepository.
Dans votre nouvelle classe Publication, vous remarquerez les fameux attributs PHP au-dessus de la classe et de chaque propriété. Ces annotations de type ORM fixent les règles relatives à la base de données. Ce sont ces informations que Doctrine va lire pour créer et maintenir les différentes tables de votre base de données. Il est aussi indiqué quel repository est lié à cette entité.
De manière globale :
-
L’attribut
#[ORM\Column]indique que le champ doit être lié à une colonne dans la table correspondante. Différents paramètres sont configurables. Ici, “nullable” (définissant si la colonne peut être nulle ou non) n’est pas précisé, car il vautfalsepar défaut, et nous avons justement demandé que ces colonnes ne puissent pas être nulles. -
L’attribut
#[ORM\Id]permet d’indiquer une propriété faisant partie de la clé primaire. -
L’attribut
#[ORM\GeneratedValue]permet de demander à la base de données de générer automatiquement la valeur de cette colonne, par exemple, pour un entier en modeAUTO_INCREMENT.
Concernant la classe PublicationRepository, vous remarquerez que celle-ci est plutôt vide pour le moment, hormis quelques exemples commentés. En fait, toutes les opérations génériques du CRUD sont déjà prises en charge par la classe mère ServiceEntityRepository et un autre service appelé EntityManagerInterface. On peut néanmoins ajouter des méthodes plus spécifiques si besoin. Dans ce cas, nous ne codons pas les requêtes avec du SQL, mais avec un langage dérivé appelé le DQL (doctrine query langage). Cependant, les outils de base doctrine permettent déjà de faire des requêtes assez précises avec très peu de lignes de code.
Doctrine impose son propre langage pour assurer la compatibilité entre tous les SGBD et les autres sources de données possibles, ainsi, nous ne dépendons jamais d’un SGBD ou d’une manière de stockage précise et il devient alors très facile d’en changer.
En l’état, tout est bon, il n’y a rien de plus à ajouter dans ces classes, l’entité est prête à être synchronisée !
Si dans le futur vous avez besoin d’ajouter de nouveaux champs, il suffit de ré-exécuter la commande make:entity. Symfony détectera que l’entité existe déjà et vous proposera d’ajouter de nouveaux champs.
Mise en place de la base de données
Nous allons maintenant nous intéresser au fichier .env situé à la racine de votre projet. Ce fichier est un fichier de configuration contenant des variables d’environnement, notamment pour utiliser des services externes, comme une base de données.
Chaque variable est définie ainsi : valeur="donnee"
Il est aussi possible de créer un fichier .env.local où vous pouvez définir les mêmes variables ou bien écraser les variables déjà définies dans .env. Ce fichier n’est pas versionné (sur git), il peut donc servir si votre configuration locale change, d’un environnement de travail à l’autre, ou d’un développeur à l’autre. Par exemple, si chaque développeur travaille avec une base de données en local, il vaudrait mieux placer la configuration de la base de données dans le fichier .env.local. Dans notre cas, nous allons seulement travailler sur .env.
Nous nous intéressons au paramètre DATABASE_URL. Globalement, il se configure comme suit :
DATABASE_URL=sgbd://username:password@ip:port/nom_base?serverVersion=version
La partie sgbd correspond au SGBD utilisé : mysql, postgres, sqlite, oracle, etc…
Vous aurez remarqué que votre conteneur Docker est divisé en deux sous-conteneurs :
-
Le conteneur du serveur web dans lequel vous exécutez les commandes.
-
Un conteneur mysql qui permet de gérer diverses bases de données.
Si vous jetez un œil au fichier compose.yaml vous observerez que le service mysql est nommé db. Une autre ligne dans le service server indique une dépendance avec le service db (section depends_on). Concrètement, cela veut dire que, dans le conteneur du serveur web, le service de base de données est accessible via le nom d’hôte db (qui se traduit par un certaine IP). Au lieu d’utiliser une IP, on utilisera donc le nom d’hôte db dans la configuration de DATABASE_URL.
Par rapport au nom d’utilisateur et au mot de passe, le service db est configuré pour avoir un utilisateur root dont le mot de passe est root. Le port utilisé est 3306. La version du serveur MySQL utilisé est 26.7.0.
En résumé, pour utiliser le service de base de données inclus dans le multi-conteneur docker au travers du conteneur du serveur Web, on peut utiliser la configuration suivante :
DATABASE_URL="mysql://root:root@db:3306/nom_base?serverVersion=26.7.0"
Bien sûr, dans d’autres contextes, cette configuration devra être adaptée.
Sur la machine hôte (donc votre machine, hors du conteneur) le service est exposé via le port 3306. On n’y accède pas via db mais directement via localhost (ou 127.0.0.1). Cela sera utile pour visualiser et interagir avec la base au travers d’une interface (avec PhpStorm, DataGrip ou DBeaver) comme nous le ferons juste après.
Une fois l’adresse de la base de données configurée, il faut la créer. Pour cela, il suffit d’exécuter une commande :
php bin/console doctrine:database:create
Si tout va bien, un message indique que la base a été créée.
Ensuite, il faut générer et exécuter une migration. Une migration est un fichier généré par doctrine contenant les requêtes nécessaires pour mettre à jour la structure de la base de données et aussi annuler ces modifications, si besoin ! Chaque migration est stockée dans un dossier dédié, ce qui permet de conserver un historique. Globalement, on peut dire que les migrations fournissent un gestionnaire de version de la structure de la base de données (comme un git pour la BD).
Pour générer puis exécuter une migration, on utilise les deux commandes suivantes :
php bin/console make:migration
php bin/console doctrine:migrations:migrate
On doit effectuer une migration dès que l’on crée ou que l’on modifie une entité existante (nom des attributs, assertions de type ORM modifiés…) afin de garder la structure de la base de données à jour.
Afin de visualiser votre base de données et interagir avec, deux solutions s’offrent à vous :
-
Si vous utilisez
PHPStorm, vous pouvez cliquer sur le boutonDatabasedans la barre latérale droite, puis ajouter une nouvelle connexionMySQL:
Ensuite, il faut configurer la connexion comme suit :
-
Sinon, vous pouvez utiliser un autre logiciel de gestion de bases de données :
DataGrip,DBeaver, etc.La configuration est :
-
IP :
localhost -
Utilisateur :
root -
Mot de passe :
root -
Port :
3306 -
Nom de la base :
the_feed(ou autre si vous aviez mis un autre nom)
-
-
Configurez le paramètre
DATABASE_URLdans le fichier.env(remplacez celui déjà présent) afin de connecter votre application à votre service de base de données. Donnez le nom que vous souhaitez à votre base (par exemple,the_feed). -
Exécutez la commande
doctrine:database:createafin de créer la base de données. -
Utilisez les commandes nécessaires afin de créer et d’exécuter votre première migration.
-
Connectez-vous à l’interface de gestion de votre SGBD (avec
PHPStormou un autre logiciel) et observez. Vous devriez trouver votre nouvelle tablepublication!
Contrôleur et template de base
Nous allons maintenant créer la route (et la page) qui se chargera d’afficher la liste des publications. Il s’agira en fait de la page d’accueil du site.
Dans le prochain exercice, vous allez utiliser de fausses publications générées à la main dans le contrôleur, puis vous adapterez un template twig afin d’afficher ces publications. Il n’y a pas encore de CSS, mais cela viendra juste après.
Vous allez aussi être amené à utiliser des filtres de twig. Les filtres permettent de convertir une donnée en autre chose. On les utilise ainsi : donnee|filtre(param1, param2, ...).
-
Par exemple, le filtre
lowerpermet de convertir une chaîne de caractères en minuscules. -
Le filtre
capitalizepermet de convertir la première lettre d’une chaîne en majuscule. -
Le filtre
date(format)permet d’afficher une date selon un format désiré. Un exemple de format est"Y-m-d"pour avoir un affichage du style “2023-09-01”.
Par exemple :
<p> {{ texte|capitalize }}</p>
<p> {{ user.dateDeNaissance|date(...) }}</p>
Vous pouvez retrouver l’ensemble des filtres disponibles sur cette page.
-
Créez un nouveau contrôleur
PublicationController(vous pouvez encore une fois utiliser la commandemake:controller, mais attention, un fichier de templatedemo/index.html.twigest généré par défaut, il faudra le supprimer). Ajoutez une nouvelle route nomméefeed, pointant sur le chemin/(racine du site) et accessible enGETseulement (pour le moment). Pour le moment, n’ajoutez rien dans le corps de la méthode, nous le ferons juste après. -
Dans le dossier
templates/publication, créez un templatefeed.html.twigcontenant le squelette suivant :<!DOCTYPE html> <html lang="fr"> <head> <title>The Feed</title> <meta charset="utf-8"> </head> <body> <header> <div id="titre" class="center"> <a href=""><span>The Feed</span></a> <nav> <a href="">Accueil</a> </nav> </div> </header> <main id="the-feed-main"> <div id="feed"> <!-- Pour chaque publication, afficher (avec une boucle) le code HTML ci-dessous !--> <div class="feedy"> <div class="feedy-header"> <a href=""> <img class="avatar" src="" alt="avatar de l'utilisateur"> </a> <div class="feedy-info"> <span>Anonyme</span> <span> - </span> <span><!-- Date de la publication sous le format jour/mois/annee --></span> <p><!-- Message de la publication --></p> </div> </div> </div> <!-- Fin de la boucle --> </div> </main> </body> </html>Nous le compléterons dans une prochaine étape.
-
Dans la méthode liée à votre route
feed, construisez trois “fausses” publications et placez-les dans un tableau. Générez puis renvoyez une page HTML construite avec le template créé à l’étape précédente, en passant votre tableau de publications en paramètre.Pour construire vos publications, vous pouvez notamment utiliser les setters :
// A importer au début de la classe use App\Entity\Publication; // Dans une méthode, création d'une publication... $publication1 = new Publication(); $publication1->setMessage("Coucou"); $publication1->setDatePublication(new \DateTime());Il faudra importer la classe
Publicationdans le contrôleur (comme montré ci-dessus). Normalement, PHPStorm vous propose de vous le faire, en surlignant le nom de la classe en jaune.Il ne faut surtout pas modifier les paramètres du constructeur de la classe
Publication. Votre ORM a besoin que ce constructeur ne prenne aucun paramètre. -
Dans ce template, remplacez toutes les sections commentées afin d’afficher correctement la liste des publications, passée depuis le contrôleur.
Concernant la date de publication, il faut la convertir une chaîne de caractères en appliquant un filtre. Le format souhaité est
d/m/Y(jour, mois, année). -
Testez votre route et vérifiez que vos fausses publications s’affichent bien (il n’y a pas de style ni d’image de profil pour le moment, c’est normal).
Tout cela manque un peu de style ! Et d’image de profil pour les publications ! Comme nous n’avons pas encore d’utilisateurs, nous allons utiliser une image “anonyme”.
En Symfony, la gestion des assets (images, fichiers css, js, etc…) peut être effectuée de différentes manières :
-
En plaçant simplement les assets dans le dossier
public(déconseillé en phase de développement, il s’agit généralement du dossier où se trouveront les assets quand le site sera en production). -
En utilisant une bibliothèque symfony (
bundle) dédiée (par exemple, Webpack Encore). -
En utilisant le récent système
AssetMapperinstallé par défaut dans une application web symfony (cela se passe au niveau du dossierassets). C’est l’option recommandée.
Le système AssetMapper propose divers avantages lors du développement :
-
Le versionning des fichiers : les noms de fichiers sont associés à un suffixe lors du chargement de la page, liés à leurs versions. Si un changement intervient dans un des fichiers (par exemple, modification du js ou du css), le suffixe changera et le navigateur détectera donc qu’il doit recharger ce fichier (utile si le navigateur met en cache certains fichiers).
-
L’utilisation de bibliothèques “clientes” externes disponibles sur
npm(node package manager) sans avoir besoin d’installernpmsur votre machine. L’import est aussi très facile. Il en va de même pour les différents frameworks css. -
On peut créer différentes configurations et charger celle qu’on souhaite sur une page donnée, ou même plusieurs.
Lors de la mise en production, l’objectif final est d’avoir nos assets dans le dossier public. Il suffira alors d’exécuter une commande pour tout “compiler” et copier dans le dossier en question (nous verrons cette commande un peu plus tard).
Concernant l’utilisation des assets dans une page, on distinguera :
- Les ressources “globales” à importer (css et javascript utilisé sur toutes les pages, par exemple, ou sur un ensemble de pages)
- Les ressources relatives à une page (une image, un fichier javascript ou css seulement utilisé dans une page…).
En fait, lors de la compilation des assets (dont nous reparlerons ultérieurement), tout est déplacé dans le dossier public.
Concernant les ressources globales, tout se passe dans les fichiers xxx.js à la racine du dossier assets. Le nom du fichier correspond au nom de la configuration (on nomme généralement ce fichier “entry point”). Par exemple, le fichier app.js déjà créé permet d’importer les ressources globales de l’entry point app. Mais on peut mettre le nom de fichier que l’on veut.
Regardons de plus près un exemple de fichier app.js :
//Obligatoire
import './stimulus_bootstrap.js';
//Import des ressources globales souhaitées
import './css/styles.css';
import './js/main.js';
//Etc...
//Du code js a exécuté lors du chargement de la page (optionnel)
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
Lorsque l’on souhaite charger les ressources d’une configuration dans une page donnée, il suffit de faire un appel de fonction twig dans la section head du template de la page. Par exemple, avec app.js :
<!DOCTYPE html>
<html lang="fr">
<head>
<title>...</title>
<meta charset="utf-8">
{{ importmap('app') }}
</head>
<body>
...
</body>
Les fichiers dynamiques (par exemple, une image uploadée par un utilisateur) ne sera pas gérée par l’asset mapper, mais sera directement placée dans public lors de l’upload.
Concernant les ressources non globales, dans un template twig, on construit le chemin vers chaque asset en utilisant la fonction {{ asset(chemin) }} (dans un bloc twig permettant d’afficher des données). Pour le chemin à spécifier, la fonction va d’abord chercher dans le dossier assets, et s’il ne trouve rien, il va chercher dans le dossier public. La racine se trouve donc directement dans le dossier assets ou public selon là où on le programme cherche la ressource, on indique donc un sous-chemin à partir d’un de ces dossiers.
Par exemple, si je possède le fichier suivant : assets/exemple/coucou.jpg, je peux construire le chemin vers cette image en utilisant l’instruction : {{ asset("exemple/coucou.jpg") }} dans mon template (typiquement, dans la partie src).
Si j’ai une image uploadée par un utilisateur dans public/user/img.jpg, je pourrais aussi y accéder via {{ asset("user/img.jpg") }} : la fonction cherche d’abord dans assets/user/img.jpg et comme il ne trouve rien, il cherche dans public/user/img.jpg et trouve l’image. Comme nous le verrons prochainement, il existe différents environnements (développement, production). en mode développement asset va chercher dans les deux répertoires (assets puis public). En mode production, il va chercher uniquement dans public (les assets seront compilés et transférées dans ce dossier au préalable).
Dans un template, vous ne pouvez faire appel qu’une seule fois à importmap, mais il est possible de charger plusieurs points d’entrées (fichier xxx.js dans assets). Pour l’instant, nous nous contenterons de la configuration globale app.js.
-
Faites un peu de ménage dans
assetsen supprimant les dossiers suivants dont nous ne nous servirons pas :controllers/hello_controller.jset le dossierstyles. -
Créez un dossier
cssdansassetset importez la feuille de style styles.css (clic-droit puis “Enregistrer la cible du lien…”) à l’intérieur de ce nouveau répertoire. -
Modifiez
app.jsafin de ne garder que les imports essentiels et rien d’autre :import './stimulus_bootstrap.js'; import './css/styles.css'; -
Importez l’image anonyme.jpg dans le dossier
assets/img/utilisateursqu’il faudra créer préalablement. -
Dans votre template
feed.html.twig:-
Dans la section
head, importez la configuration globaleappà l’aide de la fonctionimportmap. -
Faites en sorte d’afficher l’image anonyme sur chaque publication (il faut ajouter un attribut
srcsur la baliseimgde chaque publication) en utilisant la fonctionasset.
-
-
Testez. C’est un peu mieux, non ?
Enfin, il reste un problème auquel nous allons faire face : construire les liens vers nos autres pages (notamment pour le menu de navigation).
Pour gérer cela, Symfony propose d’utiliser la fonction path('nomRoute') dans twig. Cette fonction permet de générer le chemin de la route passée en paramètre.
Par exemple, si j’ai une route nommée exemple ayant pour chemin /exemple/test/bonjour, alors le template twig suivant :
<a href="{{ path('exemple') }}">Mon lien</a>
générera la balise <a> suivante
<a href="/exemple/test/bonjour">Mon lien</a>
Cela permet notamment de changer très facilement de chemin pour une route, si besoin, sans avoir besoin de modifier nos templates twig. Par contre, si le nom de la route change, il faudra mettre à jour les appels à path utilisant cette route dans nos templates.
De manière générale, chaque fois que vous aurez besoin de créer un lien interne à l’application dans un template, vous utiliserez toujours path.
-
Dans le template
feed.html.twig, utilisezpathpour les deux liens “The Feed” et “Accueil” en pointant sur la routefeed. -
Rechargez la page et vérifiez que le lien est bien généré (un simple
/car c’est le chemin associé à la routefeed).
Affichage de la liste des publications contenues dans la base
Au lieu de nous contenter de nos “fausses” publications, nous allons directement charger les publications depuis notre base de données ! Pour cela, nous devons utiliser la classe PublicationRepository.
Il est temps de faire un point sur les méthodes essentielles disponibles (par défaut) avec un repository lié à une entité :
-
findAll(): renvoie toutes les entrées de l’entité sous la forme d’un tableau d’objets (du type de l’entité). -
find($id): renvoie l’entrée de l’entité dont l’id passé en paramètre correspond à la valeur de sa clé primaire. L’objet renvoyé correspond au type de l’entité. -
findBy($criteria, $orderBy): renvoie toutes les entrées de l’entité sous la forme d’un tableau d’objets (du type de l’entité) respectant tous les critères passés en paramètres et ordonnés selon les attributs précisés.-
$criteriacorrespond à un tableau associatif qui associe des attributs de l’entité à une valeur souhaitée. En fait, cela correspond à unWHERE column1 = ... AND column2 = .... S’il n’y a aucun critère de sélection, on donne un tableau vide[]. Dans ce cas, toutes les entités seront alors sélectionnées. -
$orderBy(optionnel) correspond aussi à un tableau associatif qui liste les attributs selon lesquels on veut que les résultats soient triés, associés au sens (DESCouASC). Si on ne veut pas faire de tri, on ne précise pas ce paramètre.
Par exemple, si j’ai une entité “Livre” possédant notamment une année de publication, un auteur et un genre, et que je souhaite trouver tous les livres de fantasy écrits par J.R.R Tolkien, ordonnés par année de publication de manière croissante, je peux utiliser :
$livres = $livreRepository->findBy(["genre" => "Fantasy", "auteur" => "J.R.R Tolkien"], ["anneePublication" => "ASC"]);Note importante : on utilise le nom des attributs de la classe de l’entité, pas ceux de la base de données (qui peuvent être nommés sous un autre format).
-
-
findOneBy($criteria): même chose quefindBysauf qu’elle renvoie le premier objet correspond aux critères (et non pas un tableau de plusieurs entités). Utile si on est sûr d’obtenir une entité précise selon les critères recherchés.
Ces repositories fournissent seulement des opérations de lecture. Les opérations de création, de modification et de suppression sont confiées à un service appelé EntityManagerInterface (dont nous reparlerons plus tard).
Mais, comment utiliser ce repository dans votre contrôleur ? Avec de l’injection de dépendances bien sûr ! Et avec Symfony, cela fonctionne très simplement grâce à un système appelé autowiring.
Globalement, dans votre contrôleur, dès que vous avez besoin d’un service (repositories ou autre) dans une de vos méthodes, vous avez juste à l’ajouter comme paramètre (en précisant son type) de la méthode et… c’est tout !
Par exemple, si je veux accéder à une instance de PublicationRepository et de EntityManagerInterface, j’ai juste à faire :
use App\Repository\PublicationRepository;
use Doctrine\ORM\EntityManagerInterface;
#[Route('/exemple', name: 'route_exemple_get', methods: ["GET"])]
public function methodeExempleGet(PublicationRepository $repository, EntityManagerInterface $entityManager): Response
{
//Je peux utiliser $repository et $entityManager, Symfony se charge de les injecter pour moi...
}
Il faut donc bien préciser le type de la classe/service souhaité dans la fonction et penser à l’importer en début de fichier.
Tout cela fonctionne sur la base d’un conteneur IoC que vous aviez déjà utilisé l’année dernière, configuré et géré par Symfony. L’autowiring est un système permettant de détecter et d’injecter automatiquement les dépendances. Et cela ne se limite pas qu’aux contrôleurs ! Il est possible d’injecter des services dans d’autres classes (généralement via le constructeur) et il est aussi très facile de construire ses propres services et de les utiliser de la même façon, comme vous le ferez dans le prochain TP.
À noter que, si vous avez besoin d’un service qui est utilisé dans chaque route de votre contrôleur (ou quasiment partout), vous pouvez l’importer une seule fois de manière globale, en utilisant le constructeur du contrôleur :
use App\Repository\PublicationRepository;
public function __construct(private PublicationRepository $publicationRepository)
{}
#[Route('/exemple', name: 'route_un_exemple_get', methods: ["GET"])]
public function methodeUnExempleGet(): Response
{
$result = $this->publicationRepository->methodeA(...);
}
#[Route('/exemple', name: 'route_deux_exemple_get', methods: ["GET"])]
public function methodeUnExempleGet(): Response
{
$this->publicationRepository->methodeB(...);
}
Avec cette configuration, dès que le contrôleur est chargé, le repository sera chargé aussi (il ne faut donc plus l’injecter dans chaque méthode). Il faut donc être sûr qu’il va bien être utile à quasiment toutes nos méthodes. Si ce n’est pas le cas, il est alors plus optimisé de l’injecter seulement là où on en a besoin.
-
Éditez votre base de données (via
PHPStormou autre) en ajoutant quelques publications avec des dates différentes. -
Modifiez le code associé à votre route
feed(dansPublicationController) : supprimez vos “fausses” publications de tests et à la place, récupérez le tableau de publications directement depuis la base de données, en utilisantPublicationRepository. -
Testez votre route et vérifiez que les publications s’affichent bien.
Tout fonctionne ? Très bien ! Mais il y a un petit souci : dans un réseau social, les publications sont généralement affichées de la plus récente à la plus ancienne. Hors, ici, c’est l’inverse.
Dans votre contrôleur, vous avez sans doute utilisé la méthode findAll, hors, celle-ci ne permet pas trier les résultats. Il serait alors peut-être plus judicieux d’utiliser findBy. Il est possible d’utiliser cette méthode en précisant un tableau vide pour les critères. Cela aura pour effet de renvoyer tous les résultats, mais comme nous l’avons vu plus tôt, le deuxième paramètre vous permet de préciser l’attribut de tri.
-
Modifiez votre route
feedafin que la base de données renvoie les publications triées par ordre décroissant de la date de publication. -
Testez et vérifiez que l’ordre est respecté.
-
Comme ce bout de code pourrait resservir, créez plutôt une méthode
findAllOrderedByDatedans la classesrc/Repository/PublicationRepositoryqui renvoie les publications triées, comme vous l’avez fait dans l’étape 1. Utilisez cette méthode dans votre routefeedet vérifiez que tout fonctionne toujours.Ci-dessous, le squelette de cette méthode :
public function findAllOrderedByDate() : array { //A compléter }Pour rappel :
PublicationRepository, grâce à l’héritage, possède les méthodesfind,findAll, etc.
Maintenant que nous pouvons afficher les publications, il est temps de pouvoir en créer directement depuis le site !
Formulaire de validation
Comme vous le savez, qui dit création d’une entité, dit formulaire et validation des données transmises au serveur. Ici aussi, Symfony nous propose des outils permettant de simplifier plusieurs étapes de ce processus avec un type d’objet permettant de générer, gérer et valider les formulaires de notre site.
De manière générale, on va créer des formulaires liés à une entité (dans notre cas, Publication), mais si besoin, il est aussi possible de créer des formulaires indépendants.
Formulaire de validation – Côté PHP
Pour créer un formulaire lié à une entité, on utilise la commande make:form :
php bin/console make:form FormulaireType Entity
-
Le premier paramètre correspond au nom de la classe de formulaire créée (généralement, le nom de l’entité puis
Type). -
Le second paramètre correspond au nom de l’entité à laquelle le formulaire est relié.
La commande génère la classe du formulaire dans le dossier src/Form.
De base, la classe générée se présente sous cette forme :
class ExempleType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('champ1')
->add('champ2')
->add('champ3')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Exemple::class,
]);
}
}
-
La première méthode liste les champs disponibles dans le formulaire. Par défaut, tous les champs de la classe cible sont inclus (ce qui n’est pas forcément souhaitable !). Il est alors possible de configurer des options pour chaque champ.
-
La seconde méthode configure certains aspects du formulaire, comme la classe/entité cible, s’il y en a une.
La fonction add du builder permet de configurer :
-
Le nom du champ. Attention, si ce champ est lié à une entité, il faut qu’il porte exactement le même nom que la propriété correspondante dans la classe cible.
-
Le type du champ (au sens d’un formulaire HTML), par exemple,
TextType,PasswordType, etc… cela permettra de générer le code HTML de certaines parties du formulaire très facilement ! -
Des options éventuelles, qui permettent principalement de paramétrer certains attributs HTML du champ. Ce paramètre est un tableau associatif.
Par exemple, si je souhaite créer un champ de type “mot de passe” avec une regex pour valider la force du mot de passe, et un placeholder, je vais utiliser PasswordType ainsi :
$builder
->add('motDePasse', PasswordType::class, ["attr" => ["pattern" => "regex de validation", "placeholder" => "Trouvez un mot de passe sécurisé!"]]);
Il faut aussi penser à ajouter un bouton d’envoi du formulaire de type SubmitType, qui a pour texte “Valider!” :
$builder
->add('valider', SubmitType::class, ["label" => "Valider!"]);
Le tableau associatif des options contient des options générales (comme “label” pour configurer le texte du bouton d’envoi) puis un second tableau associatif imbriqué de clé attr qui contient des attributs HTML. Dans attr il est possible de configurer des attributs comme class. Cependant, comme cet attribut est généralement utilisé du côté du css pour customiser l’affichage du formulaire selon la page où il est chargé, il est préférable de déléguer cela au template twig et ne pas en faire une règle générale, comme nous le verrons après. Même logique pour l’id, mais qui se place en dehors de attr.
-
Utilisez la commande
make:formpour créer une classe de formulaire appeléePublierTypereliée à l’entitéPublication. -
Observez le code généré.
-
Supprimez le champ lié à la date de publication : celle-ci sera générée automatiquement côté serveur, et pas fournie par l’utilisateur.
-
Configurez le type du champ
messagepour lui assignerTextareaType::class(zone textuelle, avec plusieurs lignes, correspondant donc à<textarea></textarea>en HTML), puis ajoutez les options['attr' => ['placeholder' => "Qu'avez-vous en tête?"]])en troisième paramètre (afin d’afficher un placeholder au niveau du champ).Il faudra importer la classe suivante :
use Symfony\Component\Form\Extension\Core\Type\TextareaType; -
Ajoutez aussi un champ
publierde typeSubmitType::class(bouton d’envoi du formulaire) puis ajoutez les options['label' => 'Feeder!']en troisième paramètre (permet d’afficher le texte “Feeder!” sur le bouton).Il faudra importer la classe suivante :
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
Formulaire de validation – Côté Twig
Votre formulaire est prêt à être utilisé ! Nous allons d’abord commencer par l’afficher sur notre page web.
Comme d’habitude, tout se passe au niveau du contrôleur. Pour créer le formulaire et le passer à notre template, on procède comme suit, dans la méthode liée à la route dont la page affichera le formulaire :
use App\Form\ExempleType;
#[Route('/exemple', name: 'route_exemple', methods: ["GET"])]
public function methodeExemple(): Response
{
//On initialise l'objet qui sera lié au formulaire.
//En cas de mise à jour d'une entité, cela permet de précharger les données dans le formulaire.
$exemple = new Exemple();
$form = $this->createForm(ExempleType::class, $exemple, [
//On précise la méthode utilisée par le formulaire (GET, POST, ...)
'method' => '...',
//On précise l'URL vers lequel le formulaire sera envoyé.
//La méthode generateURL permet de générer une URL à partir du nom d'une route (pas son chemin!)
'action' => $this->generateURL('nomRoute')
]);
...
//On passe le formulaire comme paramètre du template.
return $this->render(..., ["formulaireExemple" => $form, ...]);
}
Du côté de twig, on peut alors générer le formulaire en utilisant plusieurs fonctions :
-
form_start(formulaireExemple, {'attr' : {'id' : '...', 'class' : '...'}}): permet de générer la balise ouvrante du formulaire. La partieattrpermet de configurer des attributs HTML supplémentaires : (identifiants HTML, classes…) -
form_end(formulaireExemple): génère la balise fermante du formulaire. -
form_widget(formulaireExemple.nomChamp, {'id' : '...', 'attr' : {'class' : '...'}}): permet de générer la balise HTML correspondant au champ (en utilisant le nom du champ de la classe du formulaire). On peut aussi configurer ses attributs HTML, son id, ses classes, etc… -
form_rest(formulaireExemple): permet de générer ce qu’il “reste” : généralement, un champ caché appelé token CSRF. Ce token est généré aléatoirement à chaque affichage de formulaire permet de se protéger d’une attaque appelée cross site request forgery ou plus simplementCSRF. Cette attaque consiste à vous faire exécuter une requête sur un site cible depuis un site extérieur.
La variable formulaireExemple correspondant au nom de la variable associée au formulaire dans le tableau associatif passé au template par le contrôleur.
Vous noterez que dans le cas de form_widget, l’identifiant ne se place pas dans attr, contrairement à form_start. De manière assez générale, tous les attributs “normaux” d’un input de formulaire (par exemple placeholder) se placeront à l’intérieur de attr.
Nous avons vu précédemment que les attributs autre que id et class seront générés directement dans le tableau d’options des champs du formulaire, dans la classe dérivée de AbstractType. Donc, dans le template twig, nous configurerons uniquement id et class afin d’appliquer des règles d’affichage via le css.
Attention, dans le cas du bouton d’envoi du formulaire, on l’affiche aussi avec form_widget :
{{ form_widget(formulaireExemple.nomChampSubmit) }}
Voici une petite démonstration, avec l’exemple de formulaire précédent (contenant un champ motDePasse et un champ valider, correspondant au bouton d’envoi) :
{{ form_start(formulaireExemple, {'attr': {'id': 'monForm'}}) }}
<div class="form-elt">
{{ form_widget(formulaireExemple.motDePasse, {'id': "mdp", 'attr': {'class': 'field'}}) }}
</div>
{{ form_widget(formulaireExemple.valider, {'id': "exemple-submit"}) }}
{{ form_rest(formulaireExemple) }}
{{ form_end(formulaireExemple) }}
Bien sûr, on ne précise l’id ou la class que s’ils sont utiles au css ou à un script javascript.
Concernant l’attribut method et action du formulaire, ils sont définis dans le contrôleur, lors de l’appel de la méthode createForm, comme montré dans un précédent exemple.
-
Modifiez le code de votre route
feedafin d’initialiser le formulaire de création d’une publication et de le passer au template. Il utilisera la méthodePOSTet l’adresse de son action visera la routefeed. -
Dans votre template
feed.html.twig, au tout début dudivd’identifiantfeedinsérez (à l’intérieur du div) et complétez le template suivant :<!-- Génération de la balise <form> --> {{ form_start(..., {'attr': {'id': "feedy-new"}}) }} <fieldset> <legend>Nouveau feedy</legend> <div> <!-- Le textarea généré --> {{ form_widget(...) }} </div> <div> <!-- Le bouton de validation --> {{ form_widget(..., {'id': "feedy-new-submit"}) }} </div> </fieldset> <!-- Génération des balises restantes (token CSRF) --> {{ form_rest(...) }} <!-- Génération de la balise fermante </form> --> {{ form_end(...) }}Pour rappel, le nom de votre formulaire correspond à celui que vous avez passé en paramètre du template dans votre contrôleur.
-
Rechargez votre page, le formulaire devrait maintenant s’afficher !
Si vous inspectez le champ correspondant au CSRF, vous verrez que la valeur csrf_token est stockée dans le champ. Cela peut sembler étrange, car ce token est censé être une chaîne générée aléatoirement. En fait, depuis les dernières versions de Symfony, une nouvelle couche de sécurité a été ajoutée à ce système qui permet de gérer des tokens via les headers HTTP et des cookies. Le jeton CSRF est injecté dans la requête quand le formulaire est envoyé et n’est jamais directement stocké dans le code HTML de la page.
Il y a d’autres méthodes utiles que vous pourriez utiliser. En fait, l’intégralité du formulaire peut être générée sans écrire de HTML (on peut même utiliser une boucle), même pour les attributs liés aux balises html (id, class…). Néanmoins, Symfony nous permet de garder la main sur certains aspects, et ainsi, choisir ce qui est généré automatiquement ou non, si on souhaite customiser certaines parties.
Afin de garder une séparation entre les données du formulaire, sa validation et son affichage, nous allons choisir de ne pas coder les aspects liés au “style” (classes, id) dans les classes définissant les formulaires, et de plutôt les définir (si besoin) nous-même dans le template twig, à l’aide du paramètre attr.
Création d’une publication
Maintenant que vous savez générer et afficher un formulaire, vous allez pouvoir le traiter et le valider côté back-end.
Pour cela, on regroupe généralement la route/action qui affiche (GET) et traite (POST) le formulaire sur la même méthode, dans le contrôleur :
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
#[Route('/exemple', name: 'route_exemple', methods: ["GET", "POST"])]
public function methodeExemple(Request $request, EntityManagerInterface $entityManager): Response
{
$exemple = new Exemple();
$form = $this->createForm(ExempleType::class, $exemple, [
'method' => '...',
'action' => $this->generateURL('nomRoute')
]);
//Traitement du formulaire
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
// À ce stade, le formulaire et ses données sont valides
// L'objet "Exemple" a été mis à jour avec les données, il ne reste plus qu'à le sauvegarder
$entityManager->persist($exemple);
$entityManager->flush();
//On redirige vers la page suivante
return $this->redirectToRoute('maRoute');
}
return $this->render(..., ["formulaireExemple" => $form, ...]);
}
-
On a ajouté la méthode
POSTdans le paramètremethodsde l’attributRoute. -
On a ajouté la requête
Request $requestdans les paramètres de la méthode. -
La méthode
handleRequestpermet de remplir le formulaire avec les données envoyées par le client et également affecter les champs correspondants à l’objet traité par le formulaire s’il y en a un. -
La méthode
isSubmittedvérifie que le formulaire a bien été soumis et que sa méthode correspond bien à la méthode de l’objet formulaire qui estPOSTpar défaut. En d’autres termes, par défaut, si on soumet un formulaire sans méthodePOST, et si l’objet correspondant au formulaire n’a pas d’options particulières,isSubmittedrenverrafalse. -
La méthode
isValidpermet de vérifier que les données soumises sont valides, au niveau des contraintes placées par le développeur (nous y reviendrons juste après). -
L’objet
EntityManagerInterface $entityManagerpermet de sauvegarder l’objet dans la base de données. La méthodepersistprépare la sauvegarde de l’objet etflushl’exécute. Vous noterez qu’on utilise ici aussi l’injection de dépendances par autowiring! -
Il est possible de séparer l’affichage et le traitement du formulaire en deux méthodes distinctes, mais tout regrouper dans une seule méthode permet de conserver les données du formulaire et ainsi préremplir les champs si le formulaire doit être ré-affiché, en cas d’erreur.
-
Après enregistrement, on redirige vers une autre route en utilisant la méthode
redirectToRoute. Cela peut éventuellement être la même route.
Cependant, dans notre cas, il reste un problème : quand et comment la date de publication va être générée si l’utilisateur ne la transmet pas dans le formulaire ?
Dans certains cas, des attributs d’une classe donnée doivent être générés automatiquement avant d’être enregistrés dans la base, comme pour notre date de publication. Pour cela, il suffit d’ajouter un attribut #[ORM\HasLifecycleCallbacks] à votre classe-entité puis une méthode (du nom que vous souhaitez) en lui affectant l’attribut #[ORM\PrePersist] :
#[ORM\Entity(repositoryClass: ExempleRepository::class)]
#[ORM\HasLifecycleCallbacks]
class Exemple {
...
#[ORM\PrePersist]
public function prePersistExempleChamp() : void {
$this->exempleChamp = ...;
}
...
}
-
Dans votre classe
Publication, ajoutez une méthodeprePersistDatePublicationpermettant d’initialiser la date de publication avant son enregistrement en base. Pour rappel, la date de publication peut simplement s’initialiser avec unnew \DateTime(). -
Dans votre route
feed, ajoutez le code pour traiter le formulaire et créer la publication (on regroupe à la fois l’affichage du feed et du formulaire, pour leGETet son traitement, pour lePOST). Après sauvegarde, on redirige vers la même route :feed(on doit faire cela, sinon les données du formulaire ne seront pas effacées lors de l’affichage de la page…). N’oubliez pas qu’il faut autoriser une autre méthode dans la configuration de votre route ! -
Rechargez la page principale et tentez d’ajouter des nouvelles publications. Vos publications devraient s’afficher sur votre feed.
Comme expliqué plus haut, nous aurions aussi pu diviser ce traitement en deux méthodes, mais sans pouvoir préremplir le formulaire en cas de données invalides :
#[Route('/exemple', name: 'route_exemple_get', methods: ["GET"])]
public function methodeExempleGet(): Response
{
$exemple = new Exemple();
$form = $this->createForm(ExempleType::class, $exemple, [
'method' => '...',
'action' => $this->generateURL('nomRoute')
]);
return $this->render(..., ["formulaireExemple" => $form, ...]);
}
#[Route('/exemple', name: 'route_exemple_post', methods: ["POST"])]
public function methodeExemple(Request $request, EntityManagerInterface $entityManager): Response
{
$exemple = new Exemple();
$form = $this->createForm(ExempleType::class, $exemple);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$entityManager->persist($exemple);
$entityManager->flush();
}
return $this->redirectToRoute('route_exemple_get');
}
Nous aurions pu afficher la même vue que dans route_exemple_get (au lieu de rediriger) pour résoudre ce problème, en passant le formulaire à la vue. Cependant, notre route route_exemple_get exécute potentiellement d’autres actions qui sont nécessaires à l’affichage de notre vue (dans notre cas, récupérer toutes les publications) et il faudrait alors recopier toutes ces actions dans route_exemple_post. On obtiendrait alors plus ou moins un copié/collé de route_exemple_get et on sortirait du cadre logique de l’action POST. C’est pour cela que nous préférons plutôt rediriger vers la route (en GET) affichant la page. Mais dans ce cas, les données formulaire sont “perdues” et les champs ne seront pas préremplis en cas d’erreur.
Assertions (contraintes)
Vous savez maintenant comment créer des publications via un formulaire, mais pour l’instant, vous ne vérifiez pas vraiment les données qui sont soumises. Comment faire, par exemple, pour limiter la taille du message ? Est-ce que cela se fait avec le contrôleur ? Pas du tout, pour cela, Symfony a prévu un système appelé assertions.
Une assertion est une contrainte liée à un champ du formulaire ou une propriété de l’entité. Quand un formulaire est soumis, Symfony va vérifier que toutes les contraintes sont respectées. Si une ou plusieurs annotations ne sont pas validées, des erreurs sont générées (dans le contrôleur, on vérifie cela avec $form->isValid()).
Du côté de l’entité, on peut directement placer des assertions au niveau de champ propriété de la classe, en utilisant des attributs PHP.
Cet attribut se présente ainsi : #[Assert\...(param1 : ..., param2: ...)].
Quelques exemples :
-
#[Assert\Email]: vérifie que la propriété est une adresse email bien formatée. -
#[Assert\Count(min : x, max: y)]: vérifie que la collection possède bien entrexetyéléments. -
#[Assert\Length(min: x, max: y)]: vérifie que la propriété a une taille entrexetycaractères. -
#[Assert\Regex(pattern: ...)]: vérifie que la propriété vérifie l’expression régulière spécifiée. -
#[Assert\NotBlank]: vérifie que la propriété possède une valeur nonnullet non vide (pas de chaîne vide). On peut configurer l’assertion pour autoriser la valeurnull, si besoin. -
#[Assert\NotNull]: vérifie que la propriété n’est pas nulle (du côté de l’application). Cela signifie que la propriété transmise n’a pas la valeurnull(mais elle peut être vide). Cela peut paraître redondant avec le fait que la propriété ne peut pas être nulle dans la base, mais avec cette assertion la vérification est faite au niveau de l’application et non pas du côté de la base. Si on veut s’assurer que la propriété n’est pas nulle et bien transmise, on utilisera plutôt#[Assert\NotBlank]qui est plus strict.
L’assertion When est également intéressante.
Bref, il en existe des tas. Sur la plupart des assertions, on peut aussi ajouter un paramètre message pour préciser un message d’erreur personnalisé en cas d’échec de validation. Il est bien sûr possible d’apposer plusieurs attributs d’assertions au-dessus d’une propriété. Vous pouvez retrouver la liste des types d’assertions disponibles ici.
Au niveau PHP, il faut importer les assertions ainsi :
use Symfony\Component\Validator\Constraints as Assert;
Un exemple avec la classe d’une entité :
use Symfony\Component\Validator\Constraints as Assert;
class Exemple {
#[Assert\NotBlank]
#[Assert\Length(min: 2, minMessage: 'Il faut au moins {{ limit }} caractères!')] //{{ limit }} est automatiquement traduit
private ?string $champ1 = null;
}
Au niveau des classes de type formulaire, on peut aussi ajouter des propriétés au niveau des champs (qui ne sont pas liés à l’entité) en utilisant la classe de l’assertion via le tableau d’options (le troisième paramètre) en listant les contraintes dans un tableau associé à la clé constraints :
class ExempleType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// Champ lié à ma classe Exemple -> l'assertion est dans la classe Exemple
->add('champ1', TextType::class)
// Champ qui n'est pas lié à mon entité Exemple
// On rajoute l'option "mapped => false" pour dire que ce champ n'appartient pas à la classe "Exemple"
->add('champ2', TextType::class, [
"mapped" => false,
"constraints" => [
new NotBlank(),
new Regex(pattern: '...', message: 'Format non respecté')
]
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Exemple::class,
]);
}
}
-
Dans votre classe
Publicationrajoutez :-
Une assertion pour vérifier que le message n’est pas blanc (donc, qu’il est non
nullet non vide). -
Une assertion vérifiant que la taille du message est comprise entre 4 et 200 caractères. Spécifiez également les paramètres
minMessageetmaxMessagepour configurer les messages d’erreurs si le contenu est trop court ou trop long.
-
-
Essayer de créer (sur le site) une publication avec moins de 4 caractères ou plus de 200. Vérifiez qu’elle ne s’affiche pas après soumission du formulaire.
Les messages d’erreurs ne s’affichent pas encore, c’est normal, nous allons régler cela à la prochaine étape.
Gestion des erreurs
Les erreurs du formulaire sont générées et stockées dans l’objet lié au formulaire lors de l’appel à la méthode handleRequest. Il est donc possible de s’en servir pour les afficher dans le template via ce même objet.
Cependant, dans un premier temps, nous allons plutôt afficher ces messages comme des messages flashs (ce qui nous permettra de mettre en place l’affichage des messages flashs).
De manière générale, nous allons définir deux types de messages flash :
-
success: quand l’utilisateur a terminé un processus avec succès (par exemple, inscription/connexion…) -
error: pour tout type de messages d’erreurs (notamment, ceux liés aux formulaires…)
Dans notre template twig, on utilisera le design suivant pour nos messages flash :
<div id="flashes-container">
<!-- Boucle, pour chaque message du type "success" -->
<span class="flashes flashes-success">Message</span>
<!-- Fin boucle -->
<!-- Boucle, pour chaque message du type "error" -->
<span class="flashes flashes-error">Message</span>
<!-- Fin boucle -->
</div>
Pour obtenir chaque message d’erreur d’un formulaire côté PHP/contrôleur, on peut utiliser une boucle, comme dans l’exemple qui suit :
$errors = $form->getErrors(true);
foreach ($errors as $error) {
$errorMsg = $error->getMessage();
}
On utilisera ce bout de code après avoir vérifié qu’un formulaire n’est pas valide, pour ajouter les erreurs sous la forme de messages flash.
-
Dans votre template
feed.html.twig, prenez en charge l’affichage des messages flash en ajoutant (et en complétant/adaptant) la divflashes-containerprésentée précédemment. Placez cette div dans lebody, juste après leheader. Il faut que les messages des typeserroretsuccesspuissent être affichés. -
Dans votre route
feed, faites en sorte d’enregistrer les messages d’erreurs du formulaire comme messages flash du typeerrorsi le formulaire n’est pas valide. -
Rechargez la page puis essayer à nouveau de créer une publication avec moins de 4 caractères ou plus de 200. Vérifiez que le message d’erreur que vous aviez configuré plus tôt avec les assertions apparaît bien.
Gestion des erreurs - Affichage sur le formulaire
Plutôt que d’afficher les erreurs comme des messages flashs, il semble plus intéressant de directement afficher les messages d’erreurs au-dessus des champs concernés (ici, nous n’en avons qu’un seul). Cela améliore l’expérience utilisateur, car il sait directement quel champ il doit corriger.
Pour inclure ces erreurs, il suffit d’ajouter le code suivant dans le template, au niveau du champ concerné (avant le widget) :
{{ form_errors(formulaire.champ) }}
-
Dans votre template
feed.html.twig, ajoutez le code nécessaire juste avant le widget qui gère la zone de texte pour la saisie de la publication afin de lister les erreurs relatives à ce champ. -
Dans
PublicationControllerau niveau de votre routefeed, revenez en arrière et annulez l’ajout des erreurs comme des messages flashs (mais ne supprimez pas la gestion des messages flashs niveau du template twig, nous nous en reservirons dans le prochain TP!). -
Tentez de publier un message invalide (par exemple, avec moins de 4 caractères) et constatez l’horrible affichage que cela produit.
Comme vous avez pu le constater, l’affichage des erreurs est assez laid. Pour rendre cela plus propre, deux solutions s’offrent à nous :
- Entourer le code d’erreur d’une balise
divet customiser le css. - Ou bien Utiliser un thème de formulaire. C’est cette option que nous allons choisir.
Sur Symfony, il est possible de créer et utiliser des thèmes de formulaires qui permettent de customiser la manière dont sont structurées et affichées les différentes sections du formulaire via des templates twig dédiés. Un thème peut être alors utilisé sur un ou plusieurs formulaires.
Symfony dispose d’un thème par défaut, mais il est notamment possible d’en créer soi-même ou bien d’en importer si on utilise un framework css compatible : par exemple, Bootstrap ou TailwindCSS proposent leur propre thème de formulaire pour Symfony, ce qui permet d’uniformiser les formulaires de tout le site avec la bibliothèque utilisée et un minimum de code HTML.
Pour notre problème, nous allons créer notre propre thème dérivé de celui par défaut en réécrivant seulement la partie concernant l’affichage des erreurs, pour avoir quelque-chose de plus sympathique.
-
Dans
templatescréez un sous-dossierform, puis, dans ce nouveau dossier, créez un fichiertheme.html.twig, avec le contenu suivant :{% block form_errors %} <div class="form-errors"> {%for error in errors %} <span>{{ error.message }}</span> {% endfor %} </div> {% endblock %}Explications : Dans ce template un peu spécial, on spécifie les blocs que l’on veut changer. Ici, un seul,
form_errors. On donne ensuite le code d’affichage (la variableerrorssera passée par Symfony). La classe cssform-errorsest déjà définie dans notre fichierstyles.css. -
Dans le fichier
config/packages/twig.yamlajoutez la ligne suivante pour prendre en compte notre thème :twig: ... form_themes: - 'form/theme.html.twig' -
Tentez de publier un message invalide et constatez l’affichage qui devrait être un peu meilleur.
Gestion des erreurs - Validation côté client
Il serait bien de détecter les erreurs de saisies avec des contraintes côté “client” (au niveau du HTML) pour ne pas envoyer la requête inutilement. Par exemple, avec les attributs minlength et maxlength, pour la longueur du champ.
Il est possible de paramétrer ces contraintes en utilisant le tableau d’option attr du champ dans la méthode buildForm.
Par exemple, si je veux que le navigateur vérifie (avant envoi) la longueur de mon champ, je pourrais faire ceci :
class ExempleType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('champ1', TextType::class, [
'attr' => [
'minlength' => 4,
'maxlength' => 10
]
])
;
}
}
Ainsi, quand j’utiliserai form_widget, ces contraintes seront automatiquement générées dans les attributs du champ HTML :
{{ form_widget(form.champ1) }}
-
Modifiez la classe
PublierTypeafin d’ajouter les contraintesminlengthetmaxlengthsur votre champmessage(min 4, max 200). -
Rechargez la page principale et vérifiez que les contraintes sont bien appliquées (vous pouvez notamment vérifier cela en inspectant le code HTML, pour voir si les attributs sont bien présents, ou bien même en vérifiant que le formulaire n’est pas envoyé si ces contraintes ne sont pas respectées).
Architecture des templates
Bientôt, nous allons ajouter de nouvelles pages à notre site. Mais il serait assez peu concevable que nous devions répéter le code HTML de certaines parties de nos pages, comme le bandeau, le head, le menu de navigation, le footer… Heureusement, pour pallier à ce problème, twig possède une fonctionnalité appelée blocks.
Un block est une zone d’un template qui pourra être redéfini dans un sous-template. On délimite la zone du block simplement en utilisant {% block nom_block %} et {% endblock %} :
{% block nom_block %}
Contenu du bloc...
{% endblock %}
Dans un template, on peut étendre un autre template. Il suffit d’ajouter dans notre template l’instruction suivante :
{% extends "nomFichier.html.twig" %}
Le chemin à spécifier pour le template étendu est le même que quand on génère la page HTML à renvoyer depuis le contrôleur : on se base par rapport à la racine du dossier templates.
Par exemple, imaginons le template suivant, test.html.twig :
<html>
<head>
<title>{% block titre %}Test {% endblock %}</title>
</head>
<body>
<header>...</header>
<main>{% block main %} ... {% endblock %}</main>
<footer>...</footer>
</body>
</html>
Vous pouvez alors créer le sous-template suivant qui copiera exactement le contenu de test.html.twig et modifiera seulement le titre et le contenu du main :
{% extends "test.html.twig" %}
{% block titre %}Mon titre custom{% endblock %}
{% block main %} <p>Coucou!</p> {% endblock %}
Il n’est pas obligatoire de redéfinir tous les blocks quand on étend un template. Dans l’exemple ci-dessus, on aurait pu seulement redéfinir le bloc main sans changer le titre de la page, par exemple.
Il est tout à fait possible d’utiliser un block de structure à l’intérieur d’un autre block de structure. Il est aussi tout à fait possible de créer un block redéfinissable à l’intérieur d’un autre block… Il est aussi possible de faire des sous-templates de sous-templates…
Voyez ce système comme une hiérarchie entre classes ! On étend une classe parente et les blocks sont comme des méthodes qu’il est possible de redéfinir !
Pour notre site, nous allons donc adopter la stratégie suivante :
-
Un template de base, qui sera étendu par chaque page. Il contiendra notamment le bandeau, le menu de navigation et définira un block dans son
bodyqui pourra être récrit par les sous-templates. -
Chaque page étend le template de base, réécrit le block définissant le contenu du body (et d’autres blocks si nécessaire…)
-
Créez un template
base.html.twigà la racine du dossiertemplates. À l’intérieur, déplacez globalement tout ce qui se trouve dans le templatefeed.html.twigsauf lemain(en gros, tout ce qui sera a priori commun à toutes les pages de notre site…). -
Dans le
headde ce template, ajoutez la ligne suivante :<meta name="turbo-cache-control" content="no-preview">Nous reviendrons plus en détail sur le terme “turbo” dans le troisième TD, mais pour l’instant, tout ce que vous devez savoir est que cette ligne désactive un système de preview de pages qui pourrait être utile dans plusieurs contextes, mais qu’il est préférable de désactiver dans notre cas afin d’éviter des petites “anomalies” visuelles lors d’un changement de page.
-
Dans le
bodyde ce template, juste après la zone affichant les messages flash, créez un blockpage_content. -
Dans
feed.html.twig, faites en sorte d’étendrebase.html.twigpuis de réécrire le blockpage_contentde manière adéquate, en ne gardant que le contenu propre à cette page (lemain). Si ce n’est pas déjà fait, supprimez tout le reste (qui est redondant avec ce qui est déjà contenubase.html.twig). -
Rechargez votre page principale et vérifiez qu’elle s’affiche toujours correctement. Vérifiez le code HTML généré pour être sûr qu’il n’y a pas d’erreur.
-
On souhaite que le titre (contenu de
<title></title>) de chaque page puisse être redéfini par chaque sous-template étendantbase.html.twig. Faites cela en créant un block (vide) nommépage_titledansbase.html.twig, puis, dans le templatefeed.html.twig, faites en sorte de nommer la page “The Feed” en utilisant ce nouveau block. Vérifiez que cela fonctionne bien.
Conclusion
Vous maîtrisez maintenant les fondamentaux du framework Symfony : son système de routing, ses commandes, le moteur de template twig, les services et l’utilisation de l’ORM Doctrine. Vous pouvez déjà construire un petit site assez facilement. Il reste cependant un aspect majeur à aborder : la gestion des utilisateurs. L’objectif du second TD sera donc de mettre en place tout ce qu’il faut pour inscrire, connecter, déconnecter et gérer les permissions (basiques) d’un utilisateur.
Lien avec le cours de compléments web (RACDV)
Une petite dernière section supplémentaire à l’attention du parcours RACDV. Dans ce TD, vous avez réutilisé des concepts que vous aviez abordés en cours de compléments web. Le but de cette section est de faire lien avec tout cela pour comparer la façon de faire de Symfony :
-
En cours de compléments web, nous avions configuré un autoloader afin de charger nos classes. Symfony gère aussi ce chargement automatique à partir du nom de classe complet. Le namespace de base est
Appet pointe sur le dossiersrcde l’application. Vous pouvez notamment retrouver cette configuration dans le fichiercomposer.jsonà la source du projet! -
Symfony réécrit l’URL pour appeler toujours le script de base public/index.php en transmettant l’information de l’URL relative. En utilisant le serveur de Symfony, cela se fait tout seul, sinon, le fichier
.htaccessdu dossierpublicest utilisé. Vous aviez vous-même inclut un fichier similaire pour obtenir le même comportement, dans votre framework “maison”. -
Symfony contient un routeur. Dans notre projet, la déclaration des routes s’est faite en lisant les attributs
#[Route(...)]comme nous le faisions (à terme) l’année dernière. Mais il est tout à fait possible de les déclarer avec du code PHP (ou bien un fichier de configuration). Si on regarde la documentation officielle, on constate qu’il est possible de switcher entre quatre manières de faire. L’ongletPHPdevrait vous remémorer certaines choses ! D’ailleurs, dans le frameworkLaravelil est obligatoire d’utiliser du code PHP pour déclarer les routes (sauf si vous installez quelques bibliothèques). Cela a pour avantage de centraliser le code des routes au lieu de les disperser dans divers contrôleurs.Fait amusant : si vous êtes amené à utiliser Laravel, vous pourrez constater que le framework importe le routeur de Symfony et l’étend ! Et c’est une bonne chose : on évite de réinventer la roue.
-
Symfony utilise un
ControllerResolveret unArgumentResolverpour appeler la bonne action (bon controller, bonne méthode) avec les bons arguments. Vous aviez déjà utilisé exactement ces classes (issues de Symfony) dans votre framework maison ! En effet, nous les avions installées avec composer. -
La majeure partie de la technologie
twig(syntaxe, blocs…) a été vue l’année dernière. -
Nous avions ajouté deux fonctions à
twig:assetpour récupérer les assets (images, fichiers…) de notre application etroutepour générer le lien d’une route à partir de son nom (et éventuellement ses paramètres). Ici, Symfony inclut directement ces fonctions avecassetetpath(à la place deroute). -
La notion d’injection de dépendances a été abordée lors du TD4 de compléments web. Nous avions utilisé un conteneur IoC afin d’enregistrer et configurer toutes nos dépendances et les injecter de manière adéquate dans chaque classe (nous avions d’ailleurs utilisé celui de Symfony, donc c’est le même qui gère tout cela en arrière-plan ici aussi).