Hey salut, bienvenue dans cette deuxième partie de la série Créer une application web avec Symfony. Dans ce tutoriel, nous allons parler des contrôleurs et créer les contrôleurs de notre application.
Mais avant, il faut récupérer les sources de la dernière partie et te placer sur le tag introduction pour pouvoir pratiquer:
$ git clone https://github.com/kaherecode/symfony-ticketing-app.git
$ cd symfony-ticketing-app
$ git checkout introduction
$ composer install
Il faudra faire cela avant de commencer chaque tutoriel dans cette série, seul la troisième ligne va changer, cela va te permettre d'être au même niveau que moi sur les sources pour que tu puisses pratiquer.
Aller on commence tout de suite.
Symfony et MVC
Symfony repose sur un modèle MVC (Model - View - Controller ou Modèle - Vue - Contrôleur), et le contrôleur joue un rôle central dans cette architecture. Il est chargé de recevoir la requête du client, charger les modèles nécessaires depuis une source de données puis construire la vue et la retourner à l'utilisateur.
On dit qu'une image vaut mieux que milles mots, alors voici:
Architecture MVC
Voici un peu une image de l'architecture MVC, le contrôleur reçoit une requête HTTP, récupère les données nécessaires et retourne une réponse soit en HTML, JSON ou autres.
C'est donc sur cet architecture que repose Symfony, avec une couche supérieur. Avec Symfony, toutes les requêtes passent par un premier contrôleur, le Front Controller (Contrôleur Frontal). Le Front Controller de Symfony agit un peu comme un aiguilleur, toutes les requêtes qui arrivent sur notre application passent par ce contrôleur, le contrôleur frontal va ensuite appeler le contrôleur qui va traiter la requête reçu.
Une image vaut toujours mieux que milles mots, alors regarde un peu l'image ci-dessous:
Le contrôleur frontal de Symfony
Concrètement, qu'est-ce qu'il se passe?
Prenons l'exemple de cette page sur laquelle tu te trouves, ici même, en train de lire ce tutoriel, lorsque tu as rentrer l'URL de cet article dans ton navigateur (Chrome, Firefox, ...), qui représente le client, une requête HTTP a été envoyer par ton navigateur, puis, par la magie d'internet et du DNS, cette requête a atteint le serveur de Kaherecode, de là, le contrôleur frontal vérifie la route qui compose l'URL, /tutorial
dans ce cas, il va donc vérifier qu'il y a un contrôleur associer à cette route, si c'est le cas, il appelle donc ce contrôleur qui va charger le modèle nécessaire (le tutoriel que tu es en train de lire), puis construire la vue avec le contenu du tutoriel. Une fois la vue construite, le contrôleur va retourner la vue (une page HTML dans notre cas) au contrôleur frontal, et enfin le contrôleur frontal va retourner la page HTML à ton navigateur qui va te l'afficher pour que tu puisses lire ce tutoriel. Mais qu'est-ce qu'il se passe si aucun contrôleur n'est associé à la route? Tu as alors une erreur 404
, que tu dois connaître qui indique que l'URL que tu cherches n'existe pas sur ce serveur.
Voici un peu comment fonctionne le contrôleur frontal de Symfony. Le contrôleur frontal c'est le fichier public/index.php
, c'est donc ce fichier le point d'entrée de notre application Symfony.
Les contrôleurs
En Symfony, le contrôleur est une fonction PHP qui est relié à une route. Une route sert à faire le lien entre l'URL de la requête et le contrôleur. Les contrôleurs sont définis dans le dossier src/Controller
, tu peux donc toi même créer une classe PHP dans ce dossier et y définir tes contrôleurs, ou tu peux me laisser te montrer une méthode paresseuse simple pour le faire. Pour cela, il faut simplement exécuter la commande:
$ symfony console make:controller CoreController
Je choisis d'appeler cette première classe CoreController
, le choix du nom de la classe est personnel, mais il faut que le nom est le suffixe Controller
. Et il faut essayer d'organiser les contrôleurs dans différents classes, ce que j'aime faire par exemple, c'est de définir une classe CoreController
qui va contenir les contrôleurs de base de mon application comme la page d'accueil, une page contact, ... ensuite avoir une classe EventController
par exemple, qui va contenir les contrôleurs sur la gestion des évènements et ainsi de suite. Ceci est une organisation personnel, j'ai commencé par organiser mes contrôleurs en bundles avec Symfony 3, puis à mettre tout mes contrôleurs dans une seule classe avant d'en venir à cette organisation, qui n'est pas forcément la meilleure, mais ça je le saurais avec la pratique et le temps. Tu peux donc utiliser ta propre logique pour organiser tes contrôleurs. Il faut juste avoir une organisation qui va te faciliter la maintenance de ton application plus tard.
Pour créer la classe CoreController
, nous allons exécuter la commande:
$ symfony console make:controller CoreController
Et comme par magie (alors que c'est juste Symfony), la console nous indique que la commande a créer les fichiers src/Controller/CoreController.php
et templates/core/index.html.twig
.
Si tu ouvres le fichier src/Controller/CoreController.php
:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class CoreController extends AbstractController
{
/**
* @Route("/core", name="core")
*/
public function index(): Response
{
return $this->render('core/index.html.twig', [
'controller_name' => 'CoreController',
]);
}
}
La classe CoreController
hérite de la classe AbstractController
qui est défini dans le namespace Symfony\Bundle\FrameworkBundle\Controller
, ensuite nous avons une méthode index()
qui retourne un objet Response
qui est aussi défini par Symfony. La fonction index()
est décoré d'une annotation @Route("/core", name="core")
, cette annotation permet de définir une route /core
qui est rattaché au contrôleur index
, si tu ouvres ton navigateur et que tu te rends sur l'URL https://127.0.0.1:8000/core, tu vas avoir une page comme ci-dessous:
Ça commence à bien le faire la, on continue encore un peu et tu seras un développeur Symfony confirmé.
Ce qu'il s'est passé, c'est que le contrôleur frontal (tu t'en rappelles non?) à reçu la requête, il a sorti la route /core
et à regarder le contrôleur rattaché à cette route, la méthode index()
dans la classe CoreController
, cette fonction a retourner un fichier index.html.twig
dont nous allons parler dans la prochaine partie et le contrôleur frontal à renvoyer ce fichier au navigateur, simple et efficace.
Dans notre cas, nous voulons que quand l'utilisateur arrive à la racine de notre site, le contrôleur index
soit appelé et que celui-ci soit la page d'accueil, il faut donc modifier la définition de la route comme ceci:
<?php
// src/Controller/CoreController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class CoreController extends AbstractController
{
/**
* @Route("/", name="homepage")
*/
public function homepage(): Response
{
return $this->render('core/index.html.twig', [
'controller_name' => 'CoreController',
]);
}
}
Si tu retournes sur le lien https://127.0.0.1:8000/core, tu vas avoir une erreur 404
qui dit que la route GET /core
n'existe pas, et oui tu l'as supprimer, il faut maintenant aller sur le lien https://127.0.0.1:8000 pour voir la page.
Actuellement le contrôleur affiche une vue et c'est plutôt cool, mais nous parlerons des vues dans la prochaine partie, nous allons donc pour l'instant retourner un objet Response
:
<?php
// src/Controller/CoreController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class CoreController extends AbstractController
{
/**
* @Route("/", name="homepage")
*/
public function homepage(): Response
{
return new Response("Hello toi dev Symfony, Bienvenue sur Kaherecode!");
}
}
Maintenant le contrôleur homepage
retourne un objet Response
qui contient le texte "Hello toi dev Symfony, bienvenue sur Kaherecode!", on regarde ce qu'on a dans le navigateur:
Et c'est bien ça, on a notre premier contrôleur, yesssss!
Bon j'avoue qu'on a encore du chemin à faire, mais on peut aussi retourner du HTML pour que le texte soit un peu plus lisible:
<?php
// src/Controller/CoreController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class CoreController extends AbstractController
{
/**
* @Route("/", name="homepage")
*/
public function homepage(): Response
{
return new Response("
<h1>Hello toi dev Symfony,</h1>
<h2>Bienvenue sur Kaherecode!</h2>
");
}
}
Et on a maintenant des balises HTML. Sois fier de toi, on a une page web qui s'affiche, nous allons l'améliorer plus tard.
Les routes
Nous avons un peu parler des routes dans les paragraphes précédents, dans cette section, nous allons essayer d'aller un peu plus en profondeur.
Annotations ou fichiers YAML
Pour notre contrôleur homepage
, nous avons défini la route en utilisant une annotation sur la méthode homepage()
, c'est plus pratique de faire ainsi, comme cela nous avons la définition de nos routes et contrôleurs dans le même fichier, pas besoin d'aller chercher dans un autre fichier pour savoir la route associer à un contrôleur. Mais il est aussi possible de définir les routes de notre application dans un fichier YAML, XML ou aussi PHP, je vais te présenter le fichier YAML ici, tu peux regarder la documentation pour en savoir plus sur les autres formats.
Pour définir nos routes dans un fichier YAML, nous allons utiliser un fichier routes.yaml
qui se trouve dans le dossier config/
. Nous allons par exemple définir une route /about
qui va afficher une page à propos. Cett route va être rattaché à un contrôleur about
dans la classe CoreController
:
# config/routes.yaml
about:
path: /about
controller: App\Controller\CoreController::about
Voilà, on commence par donner un nom about
à la route, puis dans path
, on spécifie l'URL à partir de laquelle cette route va être appeler, enfin on renseigne le contrôleur associé.
Il faut ensuite créer ce contrôleur dans la classe CoreController
:
<?php
// src/Controller/CoreController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class CoreController extends AbstractController
{
/**
* @Route("/", name="homepage")
*/
public function homepage(): Response
{
return new Response("
<h1>Hello toi dev Symfony,</h1>
<h2>Bienvenue sur Kaherecode!</h2>
");
}
public function about()
{
return new Response("<h1>About Symfony Ticketing App</h1>");
}
}
Et voilà, quand tu accèdes à la route https://127.0.0.1:8000/about, tu as la page avec le message "About Symfony Ticketing App" qui s'affiche.
Pour cette série de tutoriel nous allons utiliser les annotations pour définir nos routes, c'est un choix personnel encore une fois et je le trouve plus pratique.
Paramètres de routes
Jusque là, nous avons des routes statiques, c'est à dire une route dont l'URL ne change pas, comme la route /about
par exemple. Si nous voulons avoir des routes dynamiques, une route pour afficher un tutoriel sur Kaherecode par exemple, il va falloir ajouter des paramètres à la définition de notre route.
Nous allons créer un contrôleur pour afficher un événement spécifique sur notre application. Je vais d'abord créer une classe EventController
:
$ symfony console make:controller EventController
Je vais ensuite créer un contrôleur show()
dans cette classe:
<?php
// src/Controller/EventController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class EventController extends AbstractController
{
/**
* @Route("/events/{id}", name="show_event")
*/
public function show($id): Response
{
return new Response("<h1>Affichage de l'événement {$id}.</h1>");
}
}
Je crée la route /event/{id}
, et cette fois-ci la fonction show()
prend un paramètre $id
comme dans la route.
Pour ajouter une variable à la route, il faut donc mettre le nom de la variable dans les accolades {}
et ensuite définir un paramètre dans la définition du contrôleur avec le même nom que la variable sur la route. Si tu accèdes maintenant à l'URL https://127.0.0.1:8000/events/4, tu vas avoir cette page:
Tu peux changer la valeur de id
dans la route comme avec https://127.0.0.1:8000/events/100, https://127.0.0.1:8000/events/250 et tu verras le changement sur la page.
Mais qu'est-ce qui se passe si tu ne mets pas id
dans la route? On essaie avec https://127.0.0.1:8000/events
On a une erreur qui dit que la route GET /events
n'existe pas. On a pas défini de route /events
, mais plutôt /events/{id}
. Si tu veux avoir un paramètre facultatif, comme une catégorie par exemple, nous allons avoir une route comme ceci:
<?php
// src/Controller/EventController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class EventController extends AbstractController
{
// ...
/**
* @Route("/events/{category}", name="list_events")
*/
public function list($category = null): Response
{
$htmlMessage = "<h1>Liste des événements";
if ($category) {
$htmlMessage .= " ave la catégorie: ${category}";
}
$htmlMessage .= "</h1>";
return new Response($htmlMessage);
}
}
Nous définissons un contrôleur list()
qui va afficher la liste de tous les événements ou seulement les événements d'une catégorie si la catégorie est renseigné. La route ne change pas de ce qu'on avait tout à l'heure, mais le paramètre de la fonction list()
est par défaut mis à null
, et c'est tout ce qu'on a besoin.
Si tu accèdes à l'URL https://127.0.0.1:8000/events/php tu vas voir une page:
Oopssss!!!! Je crois que nous venons de créer notre premier bug, BRAVO!!!
Qu'est-ce qui s'est passé au juste? Tu vois le problème? Pour rappel, voici les contrôleurs que nous avons dans la classe EventController
:
<?php
// src/Controller/EventController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class EventController extends AbstractController
{
/**
* @Route("/events/{id}", name="show_event")
*/
public function show($id): Response
{
return new Response("<h1>Affichage de l'événement {$id}.</h1>");
}
/**
* @Route("/events/{category}", name="list_events")
*/
public function list($category = null): Response
{
$htmlMessage = "<h1>Liste des événements";
if ($category) {
$htmlMessage .= " ave la catégorie: ${category}";
}
$htmlMessage .= "</h1>";
return new Response($htmlMessage);
}
}
Alors tu vois mieux? Nous avons deux routes: /events/{id}
et /events/{category}
, pour nous ces routes sont différentes, mais pas pour Symfony. Quand Symfony reçoit une URL /events/1
il le traite de la même manière qu'une URL /events/php
. Il regarde la première route qui correspond à ce schéma /events/{param}
et appel son contrôleur. Dans notre cas, le premier contrôleur est show($id)
, si tu mets la définition du contrôleur list($category = null)
avant, il va être appeler en premier. Est-ce la solution est d'organiser nos contrôleurs du plus prioritaire au moins? Pas forcément.
Le contrôleur show($id)
prend un id
en paramètre, les ids sont des entiers, et le contrôleurs list($category = null)
prend category
en paramètre, qui est une chaine. Nous allons donc définir le contrôleur show
pour ne passer que si le paramètre est un entier. Si ce n'est pas un entier, le contrôleur list
va être appeler.
<?php
// src/Controller/EventController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class EventController extends AbstractController
{
/**
* @Route("/events/{id}", name="show_event", requirements={"id"="\d+"})
*/
public function show($id): Response
{
return new Response("<h1>Affichage de l'événement {$id}.</h1>");
}
/**
* @Route("/events/{category}", name="list_events")
*/
public function list($category = null): Response
{
$htmlMessage = "<h1>Liste des événements";
if ($category) {
$htmlMessage .= " ave la catégorie: ${category}";
}
$htmlMessage .= "</h1>";
return new Response($htmlMessage);
}
}
Nous ajoutons un attribut requirements
à la définition de la route show_event
pour lui dire que la variable id
est un entier \d+
. Si tu appelles maintenant la route https://127.0.0.1:8000/events/100 tu as bien le contrôleur pour afficher un événement, et la route https://127.0.0.1:8000/events/php affiche la liste des événements en php
Et si tu renseignes pas la catégorie comme ici https://127.0.0.1:8000/events, tu as la liste des événements:
Maintenant, dans ce cas ci, si tu définis le contrôleur list
avant show
, ton contrôleur show
ne sera jamais appeler, parce que la route pour le contrôleur list
va prendre le dessus, vu que cette route n'a aucun requirements
, et donc que le paramètre soit une chaîne ou un entier, elle va passer.
Et voilà, tu peux avoir autant de variable pour une route que tu veux, il faut juste mettre chacune variable dans les accolades /events/{category}/{city}
, définir un paramètre sur le contrôleur avec le même nom que dans la route list($category, $city)
et ne pas avoir deux variables avec le même nom.
Il y a plein d'autres fonctionnalités avec les routes que je vais te laisser découvrir sur la documentation.
Tu peux voir la liste des routes de ton application avec la commande:
$ symfony console debug:router
Les routes que nous avons défini sont tout en bas, les autres sont définis par Symfony et ne sont accessible qu'en développement.
Maintenant que nous avons nos routes et nos contrôleurs, dans la prochaine partie, nous allons créer nos différentes vues et retourner des pages HTML comme une application web qui se respecte au lieu de retourner de simple balise h1
.
D'ici là, essaie de pratiquer en créant un nouveau projet, des contrôleurs et de comprendre comment tout cela fonctionne. Si tu as des questions, n'hésite pas à les poser dans les commentaires ci-dessous ou nous retrouver dans la chaîne #symfony
sur le chat discord pour discuter. A très bientôt.
Participe à la discussion