Occasionnellement une requête a besoin d’une information supplémentaire. C’est souvent le cas pour les API, par exemple si elle propose les mêmes méthodes mais pour plusieurs sections, la sélection de la section peut être unifiée en envoyant un identifiant de section dans les en-têtes plutôt qu’en argument de chaque méthode.

Certains contrôleurs auront besoin du modèle complet plutôt qu’un identifiant. Une manière de faire serait de créer un contrôleur abstrait qui contiendra une méthode appelant un service retournant l’objet voulu. Toutefois ça oblige à ajouter l’appel dans tous les contrôleurs où ce sera nécessaire.

Une autre manière de faire est de tirer profit du côté évènementiel de Symfony. La requête est traitée par le kernel HTTP qui déclenche les évènements au fur et à mesure de la résolution.

Résolution de la requête
Résolution de la requête

Sur ce schéma, on constate que plusieurs étapes arrivent avant le contrôleur, c’est ce qui va nous intéresser. Par rapport à ce schéma, on constate également que le code peut être amené à faire des sous-requêtes. À cause de ça, il faut faire attention à bien identifier si on est dans la requête maître ou dans une sous-requête.

L’évènement sur lequel s’appuyer correspond à la case request (la deuxième, en bleu) est :

The kernel.request Event

Typical Purposes: To add more information to the Request, initialize parts of the system, or return a Response if possible (e.g. a security layer that denies access).1

Par rapport au schéma, on constate qu’on pourrait directement donner une réponse à la requête, dans le cas où l’en-tête serait obligatoire, ce serait pratique.

# services.yml
app.listener.section:
    class: AppBundle\EventListener\SectionListener
    tags:
      - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 255 }

On déclare un nouveau listener qui contiendra le code métier. Le tag sert à indiquer sur quel évènement on souhaite se brancher, la méthode a appelé dans la classe et la priorité. Cette dernière permet d’intervenir sur l’ordre dans lequel les évènements seront traités. Un nombre élevé placera l’écouteur dans les premiers (si on souhaite bloquer le traitement, autant arriver vite) et un nombre faible (voir négatif) placera l’écouteur dans les derniers.

<?php

namespace AppBundle\EventListener;

// ...

class SectionListener
{
    const SECTION_HEADER = 'X-SECTION';

    /**
     * @param KernelEvent $event
     */
    public function onKernelRequest(KernelEvent $event)
    {
        if (!$event->isMasterRequest()) {
            return;
        }

        $request = $event->getRequest();

        if (!$request->headers->has(self::SECTION_HEADER)) {
            return;
        }

        $section = // Do something with the header's value which give you a Section object

        $request->attributes->set('section', $section);
    }
}

Dans l’écouteur, on vérifie qu’on est bien dans le cas d’une requête maître et que l’en-tête est bien présent avant de faire le traitement qui exploite la valeur de l’en-tête. Il est ensuite possible d’attacher le résultat du traitement à la requête en le plaçant dans les attributs de celle-ci. Il sera ensuite disponible partout où la requête est exploitée, notamment dans les contrôleurs, notre cible. Si l’en-tête est obligatoire, vous pouvez choisir de créer une réponse 400 et l’associer à la requête.

<?php

namespace AppBundle\Controller;

// ...

class MyController extends Controller
{
    public function somethingAction(Section $section)
    {
        // ...
    }
}

Automatiquement, le framework va détecter que votre action attend un argument “section” et va aller le récupérer dans les attributs de la requête. Le typehint n’est pas obligatoire pour cette fonctionnalité, mais il est toujours préférable de vérifier le format des données attendues (et votre IDE vous proposera de l’autocomplétion).

Attention toutefois, étant donné que le traitement sera effectué sur toutes les requêtes avec l’en-tête, il faut que le traitement soit rapide car il va impacter les performances générales de votre application.

Références

1. Documentation “The HttpKernel Component” sur Symfony.com