TD1 – Paquets PHP Composer, Routage via l'URL
Au travers des 5 TDs de ce cours, nous allons développer une API REST en PHP. Afin de pouvoir se concentrer sur l’apprentissage des nouvelles notions, nous allons partir du code existant d’un site Web de type réseau social appelé ‘The Feed’. Ce site contiendra un fil principal de publications et un système de connexion d’utilisateur.
L’intérêt de ce site est qu’il ne contient que 2 contrôleurs et un petit nombre d’actions :
- contrôleur
Publication:- lire les publications : action
afficherListe - écrire une publication : action
creerDepuisFormulaire
- lire les publications : action
- contrôleur
Utilisateur:- afficher la page personnelle avec seulement ses publications : action
afficherPublications - s’inscrire :
- formulaire (action
afficherFormulaireCreation), - traitement (action
creerDepuisFormulaire)
- formulaire (action
- se connecter :
- formulaire (action
afficherFormulaireConnexion), - traitement (action
connecter)
- formulaire (action
- se déconnecter : action
deconnecter
- afficher la page personnelle avec seulement ses publications : action
Mise en place du site de base
Ce TD nécessite de faire tourner un serveur Web PHP sous Docker. Si votre machine a gardé sa configuration du semestre 3, suivez les instructions de l’exercice 2 suivant (sautez l’exercice 1). Si vous devez installer Docker, suivez les instructions des exercices 1 et 2 suivants.
Installer Docker
Allez sur la page du dépôt Serveur Web Docker, et faites le tutoriel d’installation et de configuration qui se trouve dans la partie Tutoriel > Premier contact. Vous ne devez normalement pas y passer plus de 30 minutes.
Lancer votre serveur Web Docker
Démarrer Docker Desktop. Dans l’onglet Gauche Containers, relancez votre
conteneur à l’aide du bouton Play
.
Pour rappel, ce conteneur exécute un serveur Web PHP. Le port 80 du conteneur est relié par défaut au port 80 de la machine hôte. Le dossier /var/www/html du conteneur est relié par défaut au dossier public_html de la machine hôte.
En pratique, une fois le conteneur lancé, vous accédez aux pages Web situées
dans le dossier public_html de la machine hôte en naviguant à partir de l’URL
http://localhost via votre navigateur Web.
-
Rendez-vous dans cet espace et trouvez le fork du dépôt
TheFeeddéjà cloné dans un sous-groupe portant votre nom. -
Sur votre ordinateur, dans le dossier partagé
public_htmlutilisé par votre conteneur docker, créez un nouveau répertoireComplementWebet clonez le dépôt GitTheFeeddans ce nouveau répertoire. -
Sur l’interface de Docker Desktop, cliquez sur votre conteneur puis sur l’onglet Exec. Un terminal est alors affiché. Gardez ce terminal ouvert pour le reste du TD, vous allez être amené à y exécuter plusieurs commandes. Pour une utilisation plus confortable de ce terminal, vous pouvez exécuter la commande
bash. -
Dans le terminal de votre conteneur, déplacez-vous à la racine du projet, donc dans le répertoire
./ComplementWeb/TheFeed. - Il faut donner les droits en lecture / exécution à Apache (utilisateur
www-data). Dans le terminal de votre conteneur Docker, exécutez les instructions suivantes :chown -R 1000:www-data . chmod g+w ./ressources/img/utilisateurs/ - Importez les tables
utilisateursetpublicationsdans votre base de données SQL préférée :- Pour MySQL via phpMyAdmin, vous devez :
- exécuter le script d’import MySQL,
- mettre à jour le fichier de configuration
src/Configuration/ConfigurationBDDMySQL.phpavec votre login et mot de passe.
- Si vous souhaitez plutôt utiliser la base de données PostgreSQL fournie par le département, vous devez :
- exécuter le script d’import PostgreSQL,
- mettre à jour le fichier de configuration
src/Configuration/ConfigurationBDDPostgreSQL.phpavec votre login et mot de passe, - préciser la bonne classe de configuration
ConfigurationBDDPostgreSQLau niveau du constructeur desrc/Modele/Repository/ConnexionBaseDeDonnees.php - dans les classes
PublicationRepositoryetUtilisateurRepository, modifier les$data['nomDeColonne']pour mettre tous les noms de colonnes en minuscule. En effet, PostgreSQL passe en minuscule tous les identifiants (sauf s’ils sont entourés de guillemets doubles", auquel car il faudra toujours y faire référence avec des guillemets doubles).
- Pour MySQL via phpMyAdmin, vous devez :
-
Tentez d’accéder au site. L’URL devrait être http://localhost/ComplementWeb/TheFeed/web/controleurFrontal.php.
-
Créez un nouvel utilisateur via la page d’inscription, connectez-vous et créez une nouvelle publication.
Souvenez-vous bien de votre identifiant et mot de passe car nous nous en resservirons.* - Faites marcher le site. Explorez toutes les pages. Enfin, déconnectez-vous.
Dans l’optique de développer une API REST, nous aurons besoin que les URL des pages de notre site n’utilisent plus le query string.
Par exemple, la route
web/controleurFrontal.php?controleur=publication&action=afficherListe
va devenir web/publications. Et la route
web/controleurFrontal.php?controleur=utilisateur&action=afficherFormulaireConnexion
deviendra web/connexion.
Certaines routes paramétrées seront aussi adaptées. Par exemple, la route
web/controleurFrontal.php?controleur=utilisateur&action=afficherListePublications&idUtilisateur=1
va devenir web/utilisateurs/1/publications.
Pour ceci, nous allons utiliser une bibliothèque PHP existante, et donc un
gestionnaire de bibliothèques : Composer.
Le gestionnaire de paquets Composer
Composer est utilisé dans le cadre du développement d’applications PHP pour
installer des composants tiers. Composer gère un fichier appelé
composer.json qui référence toutes les dépendances de votre application.
Initialisation et Autoloading de Composer
Composer fournit un autoloader, c.-à-d. un chargeur automatique de classe,
qui satisfait la spécification PSR-4. En effet, cet autoloader est très
pratique pour utiliser les paquets que nous allons installer via Composer.
Commençons donc par remplacer notre autoloader Psr4AutoloaderClass.php par celui de Composer.
-
Créer un fichier
composer.jsonà la racine du votre dossierTheFeedavec le contenu suivant{ "autoload": { "psr-4": { "TheFeed\\": "src" } } } - Afin d’installer toutes les dépendances listées dans le fichier
composer.json, il faut exécuter la commande :composer installDans notre cas, cela va nous permettre d’installer l’autoloader.
Toujours dans votre terminal, à la racine de votre projet, sur docker, exécutez cette commande.
Dans le futur, si vous modifiez le fichier
composer.json(par exemple pour mettre à jour vos dépendances) vous devez exécuter la commande :composer update -
Quand on installe une application ou un nouveau composant,
composerplace les librairies téléchargées dans un dossiervendor. Il n’est pas nécessaire de versionner ce dossier souvent volumineux.
Rajoutez donc une ligne/vendor/à votre.gitignore. Dites aussi à Git d’ignorer son fichier de configuration interne/composer.lock. -
Modifiez le fichier
web/controleurFrontal.phpcomme suit :-use TheFeed\Lib\Psr4AutoloaderClass; - -require_once __DIR__ . '/../src/Lib/Psr4AutoloaderClass.php'; - -// initialisation en désactivant l'affichage de débogage -$chargeurDeClasse = new Psr4AutoloaderClass(false); -$chargeurDeClasse->register(); -// enregistrement d'une association "espace de nom" → "dossier" -$chargeurDeClasse->addNamespace('TheFeed', __DIR__ . '/../src'); +require_once __DIR__ . '/../vendor/autoload.php';Aide : Ce format montre une modification de fichier, similaire à la sortie de
git diff. Les lignes qui commencent par des+sont à ajouter, et les lignes avec des-à supprimer. - Testez votre site qui doit marcher normalement. Si tout est bon, vous pouvez alors supprimer notre
ancien autoloader :
Lib\Psr4AutoloaderClass.php.
Archivage du routeur par query string
Nous allons déplacer le code de routage actuel dans une classe séparée, dans le but de bientôt le remplacer.
-
Créez une nouvelle classe
src/Controleur/RouteurQueryString.phpcontenant une méthode statiquetraiterRequete, vide pour le moment:<?php namespace TheFeed\Controleur; class RouteurQueryString { public static function traiterRequete() : void { } } -
Dans le fichier
web/controleurFrontal.php, faites le changement suivant. Toutes les lignes supprimées de ce fichier doivent être déplacées dans la méthode statiquetraiterRequetedesrc/Controleur/RouteurQueryString.php.-// Syntaxe alternative -// The null coalescing operator returns its first operand if it exists and is not null -$action = $_REQUEST['action'] ?? 'afficherListe'; - - -$controleur = "publication"; -if (isset($_REQUEST['controleur'])) - $controleur = $_REQUEST['controleur']; - -$controleurClassName = 'TheFeed\Controleur\Controleur' . ucfirst($controleur); - -if (class_exists($controleurClassName)) { - if (in_array($action, get_class_methods($controleurClassName))) { - $controleurClassName::$action(); - } else { - $controleurClassName::afficherErreur("Erreur d'action"); - } -} else { - \TheFeed\Controleur\ControleurGenerique::afficherErreur("Erreur de contrôleur"); -} +\TheFeed\Controleur\RouteurQueryString::traiterRequete(); -
Testez votre site qui doit toujours marcher normalement.
Nouveau routeur par Url
-
Créez une nouvelle classe
src/Controleur/RouteurURL.phpvide avec le code suivant.<?php namespace TheFeed\Controleur; class RouteurURL { public static function traiterRequete() : void { } } -
Appelez ce nouveau routeur en modifiant
web/controleurFrontal.php:-TheFeed\Controleur\RouteurQueryString::traiterRequete(); +TheFeed\Controleur\RouteurURL::traiterRequete();
Nous allons maintenant coder ce nouveau routeur.
Le composant HttpFoundation
Comme le dit sa
documentation,
le composant HttpFoundation défini une couche orientée objet pour la
spécification HTTP. En PHP, une requête est représentée par des variables
globales ($_GET, $_POST, $_FILES, $_COOKIE, $_SESSION, …), et la
réponse est générée par des fonctions (echo, header(), setcookie(), …).
Le composant HttpFoundation de Symfony remplace ces variables globales et
fonctions par une couche orientée objet.
Pour information, Symfony est l’un des 2 principaux framework de
développement de site Web professionnels en PHP. Dans ce cours, nous nous
attacherons aux notions derrière Symfony plutôt qu’à Symfony lui-même.
Ainsi, vos connaissances vous permettront de vous adapter plus facilement à de
nouveaux outils, que ce soit Symfony ou autre chose… Pour ces raisons, nous
n’utiliserons que des composants de Symfony.
Dans notre cas, nous allons tout d’abord utiliser la classe Request de
HttpFoundation pour représenter une requête HTTP. Notez que HttpFoundation
possède des classes aussi pour les réponses HTTP, les en-têtes HTTP, les
cookies, les sessions (et les messages flash 😉). Les classes liées aux réponses
HTTP seront abordées dans le TD2.
-
Exécutez la commande suivante dans le terminal docker ouvert au niveau de la racine de votre projet :
composer require symfony/http-foundationÀ l’issue de l’exécution de cette commande, un nouveau composant est ajouté au répertoire
vendor.
Dans un premier temps, notre site va utiliser des URL comme
web/controleurFrontal.php/publications
web/controleurFrontal.php/connexion
web/controleurFrontal.php/utilisateurs/2/publications
La classe Request sera intéressante, notamment car elle permet de récupérer la
partie du chemin qui nous intéresse : /publications, /connexion ou /utilisateurs/2/publications.
Dans la plupart des exercices qui vont suivre, nous allons utiliser des classes qui proviennent des
composants installés grâce à composer. Il faudra donc souvent utiliser la directive use afin
d’importer ces classes et pouvoir l’utiliser dans la classe courante. Dans le code donné, nous
vous indiquons les imports à faire. Il faudra systématiquement placer ces imports tout en haut de la classe,
juste après la directive namespace.
- Dans
RouteurURL::traiterRequete(), initialisez l’instance suivante de la classeRequete//A placer en haut de la classe, après "namepsace" use Symfony\Component\HttpFoundation\Request; //A ajouter dans traiterRequete $requete = Request::createFromGlobals();Explication : La méthode
createFromGlobals()récupère les informations de la requête depuis les variables globales$_GET,$_POST, … Elle est à peu près équivalente à$requete = new Request($_GET,$_POST,[],$_COOKIE,$_FILES,$_SERVER); -
La méthode
$requete->getPathInfo()permet d’accéder au bout d’URL qui nous intéresse (/publications,/connexionou/inscription).Affichez temporairement le contenu de
$requete->getPathInfo()dansRouteurURL::traiterRequete()(par exemple avecvar_dump) et accédez aux URL précédentes pour voir le chemin s’afficher.Par exemple, tentez d’accéder à http://localhost/ComplementWeb/TheFeed/web/controleurFrontal.php/publications.
Une fois que vous avez terminé, vous pouvez cesser d’afficher le chemin.
Le composant Routing
Comme l’indique sa
documentation, le composant
Routing de Symfony va permettre de faire l’association entre une URL (par
ex. /publications ou /connexion) et une action, c’est-à-dire une fonction PHP comme
ControleurPublication::afficherListe.
- Exécutez la commande suivante dans le terminal docker ouvert au niveau de la racine
de votre projet :
composer require symfony/routing -
Créez votre première route avec le code suivant à insérer dans
RouteurURL::traiterRequete()://A placer en haut de la classe, après "namespace", avec les autres "use" use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; //A placer à la suite, dans traiterRequete $routes = new RouteCollection(); // Route afficherListe $route = new Route( path: "/publications", defaults: [ "_controller" => "\TheFeed\Controleur\ControleurPublication::afficherListe", ] ); $routes->add("afficherListe", $route);Explication : Une nouvelle
Route $routeassocie au chemin (paramètrepath)/publicationsla méthodeafficherListe()deControleurPublication(paramètredefaults). Puis cette route est ajoutée dans l’ensemble de toutes les routesRouteCollection $routessous le nom (unique)afficherListe. À noter que le nom donné à la route n’a pas besoin d’être le nom de la fonction visée, cela peut être ce que l’on veut.Le nom associé à la route est important et nous sera assez utile un peu plus loin dans le TD.
On peut éventuellement simplifier en créant directement une instance de
Routedans l’appel à$routes->add:$routes->add("afficherListe", new Route( path: "/publications", defaults: [ "_controller" => "\TheFeed\Controleur\ControleurPublication::afficherListe", ] )); - Les informations de la requête essentielles pour le routage (méthode
GETouPOST, query string, paramètres POST, …) sont extraites dans un objet séparé :use Symfony\Component\Routing\RequestContext; $contexteRequete = (new RequestContext())->fromRequest($requete);Ajoutez cette ligne (toujours dans
RouteurURL::traiterRequete()) et affichez temporairement son contenu (avecvar_dump). - Nous pouvons alors rechercher quelle route correspond au chemin de la requête
courante :
use Symfony\Component\Routing\Matcher\UrlMatcher; $associateurUrl = new UrlMatcher($routes, $contexteRequete); $donneesRoute = $associateurUrl->match($requete->getPathInfo());Ajoutez ce code et affichez temporairement le contenu de
$donneesRoute. Où se trouve l’information de la méthode PHP à appeler ? - Ajoutez le code suivant pour appeler enfin l’action PHP correspondante :
$donneesRoute["_controller"]();Explication : Ce code exécute la fonction dont le nom est stocké dans
$donneesRoute["_controller"]. - Votre site doit désormais répondre correctement à une requête à l’URL
web/controleurFrontal.php/publications(vérifiez), sauf les liens vers le CSS et les photos qui deviennent invalides.
Réécriture d’URL
Passons à notre deuxième route : /connexion.
-
Ajoutez la deuxième route :
use TheFeed\Controleur\ControleurUtilisateur; // Route afficherFormulaireConnexion $route = new Route( path: "/connexion", defaults: [ "_controller" => "\TheFeed\Controleur\ControleurUtilisateur::afficherFormulaireConnexion", // Syntaxes équivalentes // "_controller" => ControleurUtilisateur::class . "::afficherFormulaireConnexion", // "_controller" => [ControleurUtilisateur::class, "afficherFormulaireConnexion"], ] ); $routes->add("afficherFormulaireConnexion", $route);Notez les syntaxes équivalentes :
- l’attribut statique constant
NomDeClasse::classd’une classeNomDeClasseest remplacé par le nom de classe qualifié, c.-à-d. le nom de classe précédé du nom de package. Ici,ControleurUtilisateur::classa pour valeur la chaîne de caractères\TheFeed\Controleur\ControleurUtilisateur. - De manière générale, la valeur associée à
_controllerdevra être au formatcallable, car c’est ce qui est accepté lorsque l’on fait$donneesRoute["_controller"](). Parmi lescallable, on trouve le format"NomDeClasseQualifie::nomMethodeStatiqueou["NomDeClasseQualifie", "nomMethodeStatique"]pour les méthodes statiques, ou encore[$instanceDeLaClasse, "nomMethode"]ou$instanceDeLaClasse->nomMethode(...)pour les méthodes classiques.
- l’attribut statique constant
-
Testez la page
web/controleurFrontal.php/connexionqui doit marcher, sauf les liens vers le CSS et les photos qui deviennent invalides. Cherchez pourquoi ces liens se sont cassés.Aide : Dans le code source de la page Web (
Ctrl+U), cliquez sur ces liens cassés pour voir sur quel URL ils renvoient.
Nous allons régler ce problème en changeant l’URL de nos pages de
web/controleurFrontal.php/connexion vers une URL plus classique
web/connexion. Pour ceci, nous allons configurer Apache pour rediriger la
requête web/connexion (et les autres) vers l’URL web/controleurFrontal.php/connexion.
-
Téléchargez ce fichier de configuration d’Apache fourni par Symfony. Remplacez le contenu de
web/.htaccesspar celui du fichier téléchargé. -
Testez que la page
web/connexionmarche et que le CSS et les images sont revenus. En effet, l’URL de base des liens relatifs est de nouveauweb/.Remarque : Si la réécriture d’URL ne marche pas (message d’erreur
Internal Server Error), vous avez peut-être enregistré le fichier dans.htaccessà la racine du projet au lieu deweb/.htaccess. -
Changez les liens dans
vueGenerale.php:-<a href="controleurFrontal.php?controleur=publication&action=afficherListe"><span>The Feed</span></a> +<a href="./publications"><span>The Feed</span></a> -<a href="controleurFrontal.php?controleur=publication&action=afficherListe">Accueil</a> +<a href="./publications">Accueil</a> -<a href="controleurFrontal.php?action=afficherFormulaireConnexion&controleur=utilisateur">Connexion</a> +<a href="./connexion">Connexion</a>
Route selon la méthode HTTP
L’un des avantages de notre routage est qu’il peut rediriger différemment selon la méthode HTTP employée. Voici ce que nous allons faire :
- URL
/connexion, méthodeGET→ actionafficherFormulaireConnexiondu contrôleur utilisateur - URL
/connexion, méthodePOST→ actionconnecterdu contrôleur utilisateur
Pour limiter une route à certaines méthodes HTTP, on peut définir un paramètre methods lors de
la création de l’instance de Route :
$route = new Route(
path: "/chemin",
defaults: [
"_controller" => "...",
],
methods: [Request::METHOD_GET]
);
Comme vous pouvez le constater, comme methods est un tableau, il est éventuellement possible d’autoriser
plusieurs méthodes pour une même route.
Request::METHOD_GET est une constante (qui donne la chaîne de caractères GET). Il est bien sûr aussi
possible de spécifier directement le nom de la méthode sans passer par la constante, mais cela permet d’éviter
des éventuelles erreurs de saisie. Il existe des constantes pour chaque méthode HTTP.
-
Modifiez votre routeur pour avoir deux routes différentes avec le chemin
/connexionselon la méthode HTTP (une avecGETpour afficher le formulaire de connexion et une avecPOSTafin d’effectuer la connexion).Attention : Le nom de chaque route doit être unique (
$routes->add("nomRoute", $route);). Si vous définissez deux routes avec le même nom, la deuxième écrase la première. -
Corrigez l’URL vers laquelle pointe le formulaire dans
src/vue/utilisateur/formulaireConnexion.php. -
Essayez de vous connecter au site : vous devez avoir une erreur
Uncaught Symfony\...\NoConfigurationException. En effet, si la connexion réussit, alors elle redirige vers l’ancienne adresseweb/?action=afficherListe&controleur=publication. Comme cette adresse est désormais inconnue, Symfony nous renvoieNoConfigurationException. -
Pour régler temporairement le problème des redirections (qui sera traité proprement à la fin du TD), rajoutons une route pour l’URL
web/. Dupliquez la routeafficherListeen changeant le path (/publications→/) et en donnant une autre nom à la route pour ne pas écraser la précédente. -
Essayez de vous connecter au site. Cela doit marcher normalement.
Ajout des routes manquantes
- Ajoutez les routes manquantes (sauf celle vers
afficherPublications) :- Chemin
/deconnexion, méthodeGET→ actiondeconnecterdu contrôleur utilisateur - Chemin
/inscription, méthodeGET→ actionafficherFormulaireCreationdu contrôleur utilisateur - Chemin
/inscription, méthodePOST→ actioncreerDepuisFormulairedu contrôleur utilisateur - Chemin
/publications, méthodePOST→ actioncreerDepuisFormulairedu contrôleur publication.
Attention : Il y a déjà une autre route associée au chemin/publications(routeafficherListe). Il faudra donc spécifier que la routeafficherListeest limité à la méthodeGET. Pensez-bien à donner des noms uniques à vos routes !
- Chemin
- Modifiez les liens correspondants dans
src/vue/publication/liste.php,src/vue/utilisateur/formulaireCreation.phpsrc/vue/vueGenerale.php.
Rappel : un lien comme
controleurFrontal.php?controleur=utilisateur&action=afficherFormulaireCreationdevient./inscription.
Vous ne pouvez pas encore mettre à jour les liens vers
controleurFrontal.php?controleur=utilisateur&action=afficherPublications car
nous n’avons pas encore créé la route correspondante. C’est ce que nous allons
faire dans la prochaine section.
Routes variables
Avec l’ancien routeur RouteurQueryString, nous pouvions envoyer des
informations supplémentaires dans l’URL, par exemple l’identifiant d’un
utilisateur avec controleur=utilisateur&action=afficherPublications&idUtilisateur=19.
Dans notre nouveau système d’URL, certaines parties de l’URL serviront à
récupérer ces informations supplémentaires. Par exemple, nous allons configurer
notre site pour que l’URL web/utilisateurs/19/publications renvoie vers la liste des publications
de l’utilisateur d’identifiant 19. Le routeur fourni par Symfony permet des
routes variables /utilisateurs/{idUtilisateur}/publications qui permettront d’extraire $idUtilisateur de
l’URL et de le passer comme paramètre de la méthode liée à la route dans le contrôleur.
- Créez une nouvelle route :
- Nom
afficherPublications(mais cela pourrait être autre chose), URL/utilisateurs/{idUtilisateur}/publications, méthodeGET→ actionafficherPublicationsdu contrôleur utilisateur
- Nom
-
Modifiez
afficherPublications()(de la classeControleurUtilisateur) pour qu’il prenne$idUtilisateuren argument au lieu de le lire depuis le query string avec$_REQUEST['idUtilisateur'].-public static function afficherPublications(): void +public static function afficherPublications($idUtilisateur): void - if (isset($_REQUEST['idUtilisateur'])) { - $idUtilisateur = $_REQUEST['idUtilisateur']; - } else { - MessageFlash::ajouter("error", "Login manquant."); - ControleurUtilisateur::rediriger("publication", "afficherListe"); - }Attention à ne pas supprimer le reste ! La fonction doit toujours faire les autres instructions (récupérer l’utilisateur via le repository, charger la vue, etc.). On supprime seulement la partie qui récupère l’identifiant de l’utilisateur dans
$_REQUESTen introduisant un paramètre. -
Si vous testez la route, vous verrez qu’elle ne marche pas, car
$donneesRoute["_controller"]()appelleafficherPublicationssans lui donner d’arguments (il attend$idUtilisateur). - Affichez
$donneesRoute(avecvar_dump) pour voir commentUrlMatchera extraitidUtilisateurde l’URL.
Nous allons résoudre ce problème en introduisant un nouveau composant.
Le composant HttpKernel de Symfony
Selon sa
documentation, le
composant HttpKernel de Symfony fournit un processus structuré pour
convertir une Request en Response. Sa classe principale HttpKernel est
similaire à notre RouteurURL, mais en plus évolué. Nous ne nous servirons donc
pas de HttpKernel puisque nous recodons une version simplifiée plus
compréhensible.
Nous allons plutôt nous concentrer sur les classes ControllerResolver et
ArgumentResolver. La responsabilité du résolveur de contrôleur est de
déterminer le contrôleur et la méthode à appeler en fonction de la requête. La
classe ControllerResolver se limite plus ou moins à lire
$donneesRoute["_controller"]. Nous pourrions nous en passer, mais elle sera
utile plus tard quand vous aurez des actions qui sont des méthodes non statiques
(cf. séance sur les tests avec PhpUnit).
La classe ArgumentResolver va construire la liste des arguments de l’action du
contrôleur. Par exemple, c’est cette classe qui va créer l’argument $idUtilisateur
avec la valeur 19 pour la méthode ControleurUtilisateur::afficherPublications($idUtilisateur).
-
Exécutez la commande suivante dans le terminal docker ouvert au niveau de la racine de votre projet afin d’importer le composant
HttpKernelcomposer require symfony/http-kernel -
Faites évoluer le code de
RouteurURLen rajoutant à la fin (juste avant$donneesRoute["_controller"]())use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ControllerResolver; $requete->attributes->add($donneesRoute); $resolveurDeControleur = new ControllerResolver(); $controleur = $resolveurDeControleur->getController($requete); $resolveurDArguments = new ArgumentResolver(); $arguments = $resolveurDArguments->getArguments($requete, $controleur);et en modifiant
-$donneesRoute["_controller"](); +$controleur(...$arguments);Notez que les trois points dans
$controleur(...$arguments)font partie de la syntaxe. -
Testez la route
web/utilisateurs/19/publicationsen remplaçant19par un identifiant d’utilisateur ayant quelques publications. La page doit remarcher, mais pas le CSS ni les images.
Plus d’explications (optionnel) :
Revenons sur la classe ArgumentResolver pour expliquer son fonctionnement
(simplifié)
sur l’exemple afficherPublications() :
- En utilisant l’introspection de PHP, le code accède à la liste des arguments (type et nom)
- pour chaque argument, on essaye itérativement l’un des résolveurs
d’arguments
pour déterminer la valeur de l’argument.
Dans notre exemple, le premier résolveur (classeRequestAttributeValueResolver) va regarder si le nom de l’argumentidUtilisateurest présent dans$requete->attributes(équivalent à$donneesRoute). Comme c’est le cas alors on renvoie cette valeur.
L’avantage de ce mécanisme est qu’il permet de récupérer beaucoup de types d’arguments dans le contrôleur :
- un attribut extrait de la requête (attribut
GETouPOST). Pour ceci, le nom de l’attribut doit correspondre, - la requête
Request $requete(l’argument doit avoir le typeRequest), - la valeur par défaut d’une route variable,
- des services du conteneur de service (cf. futur TD sur les tests
avec
PHPUnit), - des éléments de la base de données si le type correspond à celui d’une entité
(
DataObjectdans ce cours)
Générateur d’URL et conteneur global
Les liens vers le style CSS et les images de profil de notre site sont souvent cassés car elles utilisent des URL relatives. En effet, la base de l’URL varie selon le chemin demandé :
- pour le chemin
web/connexion, les URL relatives utilisent la baseweb/. - pour le chemin
web/utilisateurs/19/publications, les URL relatives utilisent la baseweb/utilisateurs/19. Du coup, les liens relatifs sont cassés.
Nous allons utiliser des classes de Symfony pour générer automatiquement des
URL absolues. D’un côté, nous allons utiliser UrlHelper pour générer des URL
absolues à partir d’URL relatives :
use Symfony\Component\HttpFoundation\RequestStack;
$assistantUrl = new UrlHelper(new RequestStack(), $contexteRequete);
$assistantUrl->getAbsoluteUrl("../ressources/css/styles.css");
// Renvoie une URL absolue générée relativement à partir du point d'entrée du site, c'est-à-dire le fichier controleurFrontal.php qui se trouve dans le dosier web, et cela peu importe l'URL courante.
//On obtient donc une URL absolue .../ressources/css/styles.css calculée relativement au point d'entrée du site.
//Dans cet exemple et pour ntore site, on obtient donc l'URL http://localhost/ComplementWeb/TheFeed/ressources/css/styles.css
D’un autre côté, la classe UrlGenerator génère des URL absolues à partir du
nom d’une route. C’est pratique si on doit changer le chemin de la route a
posteriori.
use Symfony\Component\Routing\Generator\UrlGenerator;
$generateurUrl = new UrlGenerator($routes, $contexteRequete);
$generateurUrl->generate("afficherListe");
// Renvoie une URL vers la route ".../web/publications", dans notre cas http://localhost/ComplementWeb/TheFeed/web/publications
$generateurUrl->generate("afficherPublications", ["idUtilisateur" => 19]);
// Renvoie une URL vers la route ".../web/utilisateurs/19/publications"
Comme nous allons avoir besoin de ces services de génération d’URL dans différentes vues, il faut pouvoir les initialiser au début de l’application, et pouvoir y accéder globalement. Dans le cours de développement Web du semestre 3, nous avions fait le choix d’avoir des classes statiques utilisant le patron de conception Singleton. Ce choix a l’inconvénient de rendre difficile les tests.
En attendant le troisième TD sur les tests avec PhpUnit, nous allons utiliser
une classe Conteneur pour stocker globalement les services dont nous aurons
besoin.
-
Créez une classe
src/Lib/Conteneur.phpavec le code suivant :<?php namespace TheFeed\Lib; class Conteneur { private static array $listeServices; public static function ajouterService(string $nom, $service) : void { Conteneur::$listeServices[$nom] = $service; } public static function recupererService(string $nom) { return Conteneur::$listeServices[$nom]; } }Cette classe va nous permettre d’initialiser des services et de les charger n’importe où dans l’application. Dans un futur TD, nous le remplacerons par un conteneur de services importé, beaucoup plus complet et puissant.
-
Initialisez les deux services
$assistantUrlet$generateurUrldansRouteurUrl(juste après avoir défini la variable$contexteRequete). Puis stockez-les dans le conteneur.$generateurUrl = new UrlGenerator($routes, $contexteRequete); $assistantUrl = new UrlHelper(new RequestStack(), $contexteRequete); Conteneur::ajouterService("generateurUrl", $generateurUrl); Conteneur::ajouterService("assistantUrl", $assistantUrl); -
Récupérez les deux services en haut de la vue
vueGenerale.php.use Symfony\Component\HttpFoundation\UrlHelper; use Symfony\Component\Routing\Generator\UrlGenerator; use TheFeed\Lib\Conteneur; /** @var UrlGenerator $generateurUrl */ $generateurUrl = Conteneur::recupererService("generateurUrl"); /** @var UrlHelper $assistantUrl */ $assistantUrl = Conteneur::recupererService("assistantUrl");Puis utilisez-les dans toutes les vues pour passer tous les liens en URL absolues, soit à partir du nom d’une route (pour les liens qui pointent vers une action de l’application), soit à partir du chemin relatif d’un
asset(fichier css, image).Globalement, vous devez remplacer :
- Les différents liens, dans les menus, les formulaires (
action), les publications… - L’import du fichier de style
CSS. - L’image de profil de l’auteur d’une publication…
À la fin, vous devez avoir corrigé tous les liens :
<a href="">,<img src="">,<form action="">et<link href="">.Par exemple :
<img src="../ressources/img/monImage.png"> //Devient : <img src="<?=$assistantUrl->getAbsoluteUrl('../ressources/img/monImage.png')?>"> $nomFichier = "..."; <img src="../ressources/img/$nomFichier"> //Devient : $chemin = "../ressources/img/".rawurlencode($nomfichier); <img src="<?=$assistantUrl->getAbsoluteUrl($chemin)?>"> <a href="./publications"> //Devient : <a href="<?=$generateurUrl->generate('afficherListe')?>"> //Route /exemple/{idExemple} <a href="./exemple/{$objet->getId()}"> //Devient : <a href="<?=$generateurUrl->generate('nomRouteExemple', ['idExemple' => $objet->getId()])?>">Remarques :
$generateurUrl->generate()échappe les caractères spéciaux des URL. Vous devez donc lui donner les données brutes, et non celles échappées parrawurlencode().$assistantUrl->getAbsoluteUrl()n’échappe pas les caractères spéciaux des URL. À vous de le faire (avecrawurlencode).- Vous pouvez utiliser la syntaxe raccourcie
<?= $var ?>équivalente à<?php echo $var ?>pour améliorer la lisibilité de vos vues. - Les vues autres que
vueGenerale.phpvont détecter$generateurUrlet$assistantUrlcomme des variables erronées. C’est normal, car elles ne savent pas que ces variables sont déjà déclarées dansvueGenerale.php. Vous pouvez éventuellement déclarer le type de ces variables au début de chaque vue ou bien plus simplement ignorer cet avertissement. Nous allons changer notre façon de faire les vues au prochain TD, cela n’a donc pas beaucoup d’importance, cet avertissement n’aura plus lieu d’être prochainement.
- Les différents liens, dans les menus, les formulaires (
Il ne nous reste qu’à mettre à jour la méthode de redirection et notre site aura fini sa première migration pour des routes basées sur les URL !
-
Changer la méthode
ControleurGenerique::rediriger()pour qu’elle prenne en entrée le nom d’une route et un tableau associatif (optionnel) de paramètres pour les routes variables (mêmes arguments que$generateurUrl->generate()) :-protected static function rediriger(string $controleur = "", string $action = ""): void +protected static function rediriger(string $nomRoute, array $parametres = []): voidAdaptez maintenant le code de cette méthode afin de rediriger vers l’URL absolue correspondant au nom de la route donnée en entrée (et aux éventuels paramètres associés à cette route). Pour cela, vous aurez besoin de récupérer un certain service du
Conteneur. -
Mettez-à-jour tous les appels à
ControleurGenerique::rediriger(). Pour vous aider, vous pouvez vous aider dePHPSotrmpour trouver ces appels : Clic droit sur le nom de la méthode puisFind Usages. -
Testez votre site afin de vérifier que tout fonctionne.
Bonus : simplifier la création des routes
À ce stade, vous trouvez peut-être qu’il est plutôt pénible d’ajouter une nouvelle route :
cela alourdit la fonction traiterRequete de RouteurURL qui est déjà bien chargée et cela
peut vite devenir un enfer à debugguer (imaginez une application avec une centaine de routes !).
Heureusement, il est possible de simplifier tout cela en mettant en place un système de création de routes par attribut comme ce qui est fait dans le framework professionnel Symfony.
En PHP, le système d’attribut permet de configurer certaines méta-données au-dessus de méthodes, de classes, etc. On peut alors s’en servir afin de mettre un système en place pour définir nos routes ainsi plutôt que dans traiterRequete :
class ControleurPublication extends ControleurGenerique
{
#[Route(path: '/publications', name:'afficherListe', methods:["GET"])]
public static function afficherListe(): Response {
// ...
}
}
Ici, ont créé une route nommée afficherListe qui a pour chemin /publications et qui est
seulement accessible en GET. La méthode/action exécutée est celle placée sous l’attribut
(ici, la méthode afficherListe).
Si vous souhaitez simplifier le système de gestion des routes, suivez cette note complémentaire. Il est fortement recommandé de le faire : cela est plutôt rapide et de toute façon, dans un futur TD, vous devrez obligatoirement le faire si ce n’est pas déjà fait, alors autant le faire tout de suite !
Une fois que le système est mis en place, vous pourrez migrer toutes vos routes vers
des attributs et ainsi alléger traiterRequete.
Conclusion
Dans ce TD, nous avons découvert comment changer les URL associées à notre site pour qu’elles soient plus standard. Cela a été l’occasion de plonger dans le fonctionnement interne d’un routeur professionnel. Ceci vous sera utile si vous apprenez Symfony ou un autre framework backend plus tard. Concernant le cours Complément Web, le passage à ces URL est une étape nécessaire dans notre chemin pour développer une API REST.
Enfin, maintenant que vous connaissez les bases de Composer, vous pouvez facilement rajouter des bibliothèques à votre site web PHP.