05/02/2023

2. Controlleurs: autowiring & HTTP flow

Lu 347 fois Licence Creative Commons

Routing

Ouvrez le controlleur situé dans /src/Controller/HomeController.php.
La classe ne contient qu'une méthode index() qui correspond à la route de notre page d'accueil, ce qui se vérifie avec la présence de l'attribut Route déclaré juste au-dessus:

#[Route('/home', name: 'app_home')]
public function index(): Response

La valeur '/home correspond au chemin de la route, et app_home à son nom. Un controlleur peut contenir plusieurs routes.

« Pourquoi donner un nom à une route ? »

Nommer une route permet de facilement la référencer en cas de changement de chemin. Notamment pour créer des liens vers cette route !
Changez les informations de la route:

  • chemin: /
  • nom: homepage

Rouvrez ensuite le template _template.html.twig pour définir le lien du logo avec la fonction path():

<div class="navbar-brand">
    <a class="navbar-item" href="{{ path('homepage') }}">
        <p class="subtitle is-4 has-text-primary">
            <i class="fab fa-erlang"></i><b>ventPlace</b>
        </p>
    </a>
	
	...

Retournez sur http://localhost:8000 pour vérifier le résultat.

Request / Response

Reprenons les bases un instant:

  • le web est basé sur l'architecture client-serveur
  • le client envoie une requête et le serveur retourne une réponse
  • PHP est un langage exécuté côté serveur, il peut donc lire la requête et créer la réponse adaptée

C'est exactement pareil dans un controlleur: on a accès à la requête courante et il est impératif de créer une réponse, que la méthode doit retourner sous la forme d'un objet Symfony\Component\HttpFoundation\Response.
Pour notre page d'accueil on retourne le résultat de la méthode render() héritée de AbstractController, qui permet de créer un objet Response avec le contenu d'un template Twig, en lui passant une variable controller_name:

    public function index(): Response
    {
        return $this->render('home/index.html.twig', [
            'controller_name' => 'HomeController',
        ]);
    }

« Et comment récupére t-on la requête ? Peut-on utiliser les superglobales ? »

Il faut utiliser un objet Symfony\Component\HttpFoundation\Request qui contiendra les données des variables $_GET, $_POST, $_SERVER, etc.
Évitez à tout prix l'utilisation des superglobales dans Symfony car elles sont fortement liées au contexte d'exécution de PHP et rendent par conséquent le code plus difficile à tester.

Autowiring

Contrairement à l'objet Response, il ne suffit pas d'instancier un objet Request. Si on souhaite accéder aux données GET, POST, etc, il faut l'obtenir déjà configuré par le framework: grâce à l'autowiring.

Pour rappel, (presque) toutes les classes déclarées dans /src/ sont vues comme des services par le conteneur de services de Symfony. Et nos services peuvent utiliser l'autowiring pour récupérer les autres services dont ils ont besoin.
Il suffit de déclarer des arguments dans le constructeur d'une classe, en fonction de son type, le conteneur de services ira vérifier s'il est en mesure de vous fournir les services demandés:

public function __construct(private readonly MonSuperService $service)

« N'y a t-il pas des cas où je pourrai simplement faire new MonSuperService() dans le constructeur ? »
Non, surtout pas !

Il est important de respecter le principe d'injection de dépendances: lorsque la classe demande à injecter MonSuperService dans le constructeur, le conteneur de services va probablement injecter des services dans le constructeur de MonSuperService.
Si un jour le constructeur de MonSuperService est modifié, il n'y aura rien à faire car tout sera pris en charge par le conteneur de services, tandis qu'en effectuant manuellement l'instanciation il faudrait repasser sur chacune d'entre elles.

À retenir: La plupart de nos classes sont des services qui peuvent utiliser l'autowiring. Lorsqu'on a besoin d'utiliser une autre classe, on l'injecte en la déclarant comme argument dans le constructeur.

Essayons maintenant d'injecter l'objet Request dans le controlleur. Si nous ne savions pas quelle était la classe à utiliser pour l'injection de dépendances, nous pourrions utiliser la commande debug:autowiring:

php bin/console debug:autowiring request
[output]
[output]Autowirable Types
[output]=================
[output]
[output] The following classes & interfaces can be used as type-hints when autowiring:
[output] (only showing classes/interfaces matching request)
[output] 
[output] Request stack that controls the lifecycle of requests.
[output] Symfony\Component\HttpFoundation\RequestStack (request_stack)
[output] 
[output] Holds information about the current request.
[output] Symfony\Component\Routing\RequestContext (router.request_context)
[output] 
[output] Symfony\Component\Routing\RequestContextAwareInterface (router.default)

« Eh mais on ne voit pas Request dans la liste ! »
En effet, bien que l'on pourra l'utiliser dans les controlleurs grâce à un Argument Value Resolver, il faudra utiliser RequestStack dans les services.

Les controlleurs ont une particularité: ils bénéficient de l'autowiring dans les méthodes des routes, en plus du constructeur. Injectez la Request et passez le nom du controlleur actuel de manière dynamique:

use Symfony\Component\HttpFoundation\Request;

# ...

    public function index(Request $request): Response
    {
        return $this->render('home/index.html.twig', [
            'controller_name' => $request->attributes->get("_controller"),
        ]);
    }

La propriété Request::$attributes contient des informations personnalisées par le framework, mais on peut retrouver nos superglobales avec Request::$query (GET), Request::$request (POST), Request::$server ($_SERVER), ...