Contrôleurs et routes

Contrôleurs et routes

1 mai 2021

Les contrôleurs et les routes sont des éléments essentiels dans un framework MVC. Les contrôleurs contiennent notre logique de code et les routes permettent de relier l'ensemble via des URL personnalisées.

Premier contrôleur, première page

La création d'un contrôleur se fera via un terminal de commande. Symfony se chargera de générer le fichier et la vue associée à celui-ci pour nous. ?

Commençons par créer notre premier contrôleur (notre première page) :

symfony console make:controller HomeController

# Vous pouvez aussi utiliser une autre commande
# php bin/console make:controller HomeController

Vous pouvez remplacer "Home" par le nom de votre choix. Par convention, on termine par "Controller" comme dans l'exemple ci-dessus. En cas d'oubli, il sera automatiquement ajouté à la fin.
Utilisez la méthode "UpperCamelCase" pour nommer votre contrôleur.

Deux fichiers seront créés. Le premier sera dans le dossier "src/Controller" sous le nom "HomeController.php" ; le second se trouvera quant à lui dans un dossier au nom du contrôleur "home", situé lui-même dans le dossier "templates" et s'appellera "index.html.twig".

Le fichier "HomeController.php" contient par défaut une méthode nommée "index()".

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class HomeController extends AbstractController
{
    /**
     * @Route("/home", name="home")
     */
    public function index()
    {
        return $this->render('home/index.html.twig', [
            'controller_name' => 'HomeController',
        ]);
    }
}

Évidemment, le contrôleur peut contenir autant de méthodes que l'on souhaite. Chacune des méthodes peut posséder une vue et une route associée.

Les routes

Détaillons un peu ce contrôleur et surtout la méthode présente. Au-dessus de celle-ci se situe un commentaire possédant une annotation "@Route()".

/**
 * @Route("/home", name="home")
 */

Il ne s'agit pas d'un simple commentaire, mais véritablement d'une méthode magique Symfony pour configurer une route. Cette annotation prend deux paramètres de base :

  1. la route pour accéder à notre vue
  2. le nom de la route

La route commencera toujours par un slash "/". Le nom de la route ne doit pas comporter de caractères spéciaux, hormis l'underscore "_". Pour accéder à la vue de cette méthode, on tapera dans notre navigateur après avoir démarré le serveur : http://127.0.0.1:8000/home.

Important ! La route et le nom doivent être uniques !

Paramètres dans les routes

Supposons que notre méthode ait besoin de récupérer un paramètre via sa route afin de travailler avec elle. Pour cela, il suffit de modifier notre annotation "@Route()" et d'ajouter entre accolades notre variable :

/**
 * @Route("/home/{var}", name="home")
 */

"{var}" précise à Symfony que notre route attend une valeur à ce niveau et qu'elle est obligatoire ! Bien entendu, pour l'exemple, j'ai utilisé le mot "var", mais vous pouvez y mettre ce que vous voulez, comme "topinambour" si ça vous plaît. ?‍♂️

/**
 * @Route("/home/{topinambour}", name="home")
 */

De notre côté, nous allons continuer avec la variable "var".

Maintenant, l'URL de départ qui était http://127.0.0.1:8000/home n'est plus valide. Il manque notre paramètre. Comme nous ne contrôlons pas la valeur passée, vous pouvez tester avec ce que vous désirez.

http://127.0.0.1:8000/home/12
http://127.0.0.1:8000/home/martin
http://127.0.0.1:8000/home/34martine

Si vous tentez d'accéder à cette page en omettant le paramètre, vous aurez le droit à cette belle erreur.
Vous pouvez passer plusieurs paramètres si nécessaire :

/**
 * @Route("/home/{var}/title/{slug}", name="home")
 */

http://127.0.0.1:8000/home/34/title/martine

Ou :

/**
 * @Route("/home/{var}/{slug}", name="home")
 */

http://127.0.0.1:8000/home/34/martine

Adaptez l'URL que vous taperez dans votre navigateur pour tester.
Il reste possible de rendre le paramètre de la route non obligatoire. Pour cela, un simple point d'interrogation avant l'accolade fermante est suffisant :

/**
 * @Route("/home/{var?}", name="home")
 */

Vous pouvez maintenant accéder de deux manières différentes votre page :

http://127.0.0.1:8000/home/34 ou http://127.0.0.1:8000/home

Tester les paramètres

Jusque-là tout va bien, on peut accéder à notre vue et passer des paramètres, mais gardez bien à l'esprit que tout ce qui vient de l'utilisateur doit être contrôlé ! N'ayez jamais confiance dans les données issues de vos visiteurs (ouais, je sais, la confiance ne règne pas là...).

Dans l'annotation, on peut mettre une validation de paramètre pour limiter le type de données à recevoir. Pour être plus précis, si vous attendez un entier, vous ne devriez pas recevoir une chaine de caractères. Pour cela, un paramètre nous permet de limiter le type de donnée à réceptionner :

/**
 * @Route("/home/{var}", name="home", requirements={"var"="\d+"})
 */

Dans mon exemple, je n'accepte comme paramètre que des entiers. Si vous faites l'essai, vous ne pourrez accéder à votre page que si le paramètre est bien un nombre. Si mon paramètre est une chaine de caractères, Symfony vous retournera une erreur 404 (Page non trouvée).

Depuis Symfony 4.3, on peut mettre cette vérification directement dans le paramètre attendu comme ceci :

/**
 * @Route("/home/{var<\d+>}", name="home")
 */

Plutôt pas mal non ? ?

C'est bien tout ça, mais comment je fais pour récupérer cette variable dans ma méthode ?
Excellente question Jamie !

Pour cela, il suffit de mettre un argument dans votre méthode ayant le même nom que la variable située dans votre route :

/**
 * @Route("/home/{var<\d+>}", name="home")
 */
public function index(int $var)
{
    // ...
}

Voilà ! Rien de plus. Si plusieurs variables sont présentes dans votre route, vous pouvez faire comme ceci :

/**
 * @Route("/home/{var<\d+>}/{slug}", name="home")
 */
public function index(int $var, string $slug)
{
    // ...
}

Maintenant, il ne vous reste plus qu'à travailler avec votre variable "$var".

Récupérer automatiquement une information

Jusque-là, nous avons vu comment récupérer un paramètre passé par la route et l'utiliser dans le contrôleur. Une astuce permet, grâce à ce paramètre, de récupérer une information directement en base de données.

Pour cela, nous allons utiliser l'injection de dépendances. Dans votre contrôleur, injectez l'entité dans laquelle vous souhaitez récupérer une information. Symfony se chargera du reste, c'est-à-dire qu'il utilisera le paramètre et effectuera la requête nécessaire.

/**
 * @Route("/agence/{id<\d+>}", name="view_agence")
 */
public function index(Agences $agence)
{
    dd($agence);
}

Dans mon exemple, j'ai injecté mon entité "Agences". Symfony va récupérer l'ID passé en paramètre de ma route et aller trouver la donnée correspondante en base de données. Dorénavant, "$agence" contient les différentes valeurs de mon agence. Si jamais il venait à ne trouver aucune information liée à cet ID, une erreur 404 nous serait retournée.

Méthodes HTTP

Pour le moment, toutes nos routes sont accessibles par la méthode GET par défaut.
Pour changer cela et préciser que l'on désire qu'une route ne soit accessible qu'en méthode POST, nous allons rajouter un nouvel élément à notre route :

/**
 * @Route("/home", name="home", methods={"POST"})
 */

Le paramètre "methods" est un tableau dans lequel nous pouvons passer plusieurs paramètres. Nous pourrions dire qu'une route est accessible aussi bien en GET qu'en POST par exemple :

/**
 * @Route("/home", name="home", methods={"GET", "POST"})
 */

Ce paramètre est très utile si vous développez une API.

Préfixons nos routes

Pourquoi préfixer les routes ?
Tout simplement pour éviter d'avoir à se répéter.

Par exemple, vous avez un contrôleur avec différentes routes comme : "/article, /article/read, ...". On retrouve en répétition le mot "article" dans la route. Pour éviter de l'écrire à chaque nouvelle vue, on a la possibilité de préfixer toutes les routes contenues dans un contrôleur. Il en va de même pour le nom de nos différentes routes.

Avant modification, notre contrôleur ressemble à cela :

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class ArticleController extends AbstractController
{
    /**
     * @Route("/article/", name="article_index")
     */
    public function index()
    {
        return $this->render('article/index.html.twig');
    }

    /**
     * @Route("/article/read", name="article_read")
     */
    public function read()
    {
        return $this->render('article/read.html.twig');
    }
}

Après modification :

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/article", name="article_")
 */
class ArticleController extends AbstractController
{
    /**
     * @Route("/", name="index")
     */
    public function index()
    {
        return $this->render('article/index.html.twig');
    }

    /**
     * @Route("/read", name="read")
     */
    public function read()
    {
        return $this->render('article/read.html.twig');
    }
}

J'ai tout simplement ajouté une annotation "@Route()" avant ma classe "ArticleController.php". Avec cette annotation, je rappelle les éléments qui sont répétés dans mes autres annotations "@Route()". ?

Créer une autre page à partir du même contrôleur ?
Pas de commande dans le terminal cette fois-ci. Il va falloir taper sur votre clavier... ou copier/coller...

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class HomeController extends AbstractController
{
    /**
     * @Route("/home/{var<\d+>}", name="home")
     */
    public function index(int $var)
    {
        return $this->render('home/index.html.twig', [
            'controller_name' => 'HomeController',
        ]);
    }

    /**
     * @Route("/contact", name="contact")
     */
    public function contact()
    {
        return $this->render('home/contact.html.twig');
    }
}

Créez une nouvelle méthode à la suite de celle déjà existante en pensant à bien changer le nom de la méthode, de la route et de la vue (surtout si vous utilisez le copier-coller). Vous devez aussi créer le fichier de vue associé dans le dossier "home", dans "templates".

Vous pouvez accéder à votre nouvelle page maintenant : http://127.0.0.1:8000/contact

Les vues

Comme je vous l'ai précisé en haut de cet article, un autre article complet sera consacré aux vues. Comme je n'ai pas fini de détailler ma méthode, j'aimerais vous parler de la fonction "$this->render()".

Cette fonction prend en charge un premier paramètre : le chemin vers le fichier de vue à afficher dans le navigateur. Elle ira automatiquement chercher le bon fichier dans le dossier "templates" de votre projet, c'est à vous de lui indiquer le chemin restant à parcourir.

return $this->render('home/index.html.twig')

Le deuxième paramètre de cette fonction est un tableau associatif afin de transférer des données à votre vue. La clé de votre tableau est le nom de la variable à utiliser dans le fichier Twig et la valeur... ben, la valeur de la variable utilisée dans Twig... ?

return $this->render('home/index.html.twig', [
    'ma_variable' => 'Sa valeur',
]);

Vous pouvez en passer autant que vous le souhaitez.

Une précision et pas des moindres, cette fonction est la dernière chose qui doit être appelée dans votre méthode. Traitez d'abord vos données (requêtes SQL, algorithme...) et enfin, appelez votre vue en lui transférant des variables si nécessaire.

J'ai oublié les routes que j'ai créées ?
Symfony a tout prévu pour ça. Depuis votre terminal, tapez la commande suivante :

symfony console debug:router --env=prod

# Vous pouvez aussi utiliser une autre commande
# php bin/console debug:router -env=prod

En retour, votre terminal affichera toutes les routes que vous avez créées ainsi que les noms associés.