Accéder au contenu principal

4min.

Comment utiliser les attributs PHP sur un contrôleur Symfony ?

PHP 8.0 a introduit les attributs, et c’est une excellente fonctionnalité ! Le code devient plus lisible, plus simple à écrire, et bénéficie pleinement de la coloration syntaxique et du linting de notre IDE.

Symfony a très vite adopté cette nouveauté. On les utilise aujourd’hui partout : pour configurer les routes, ajouter des contraintes de validation ou déclarer des listeners.

Dans cet article, nous allons voir comment créer et ajouter un attribut PHP personnalisé sur un contrôleur pour lui injecter un comportement automatique, comme du logging.

Info

Nous avions déjà parlé des attributs en 2021 pour limiter le débit de vos API (Rate Limit). Cependant, Symfony a bien évolué depuis, et leur utilisation est devenue encore plus simple.

Section intitulée 1-creer-un-attribut1. Créer un attribut

La première étape consiste à créer une classe PHP classique. Pour indiquer qu’elle servira d’attribut, on lui ajoute elle-même l’attribut natif #[\Attribute] :

namespace App\AuditLog;

use Psr\Log\LogLevel;

#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
class Loggable
{
    public function __construct(
        public string $level = LogLevel::INFO,
    ) {
    }
}

Les paramètres du constructeur sont optionnels. Ils permettent de passer des options au moment où on utilise l’attribut. Ici, notre propriété $level sert à définir le niveau de log souhaité.

Section intitulée 2-utiliser-l-attribut-sur-un-controleur2. Utiliser l’attribut sur un contrôleur

Une fois notre classe créée, nous pouvons l’appliquer directement sur un contrôleur, soit sur la classe entière, soit sur une méthode (action) spécifique :

namespace App\Controller;

use App\AuditLog\Loggable;
use Psr\Log\LogLevel;
use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;

#[Loggable()]
final class HomepageController extends AbstractController
{
    #[Loggable(LogLevel::CRITICAL)]
    #[Route('/', name: 'app_homepage')]
    #[Template('homepage/index.html.twig')]
    public function index(): void
    {
    }
}

Ici, la méthode index hérite de la configuration par défaut de la classe, et ajoute un nouveau niveau de log plus critique (CRITICAL).

Section intitulée 3-creer-le-listener-pour-activer-le-comportement3. Créer le listener pour activer le comportement

Pour que notre attribut serve à quelque chose, il faut intercepter l’appel du contrôleur. Symfony déclenche l’événement ControllerEvent juste avant d’exécuter l’action d’un contrôleur. C’est le moment idéal pour vérifier la présence de notre attribut.

Selon votre version de Symfony, l’implémentation est devenue de plus en plus simple.

Section intitulée avec-symfony-6–2Avec Symfony 6.2+

On utilise la méthode getAttributes() de l’événement pour récupérer notre attribut et appliquer notre logique :

namespace App\AuditLog;

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ControllerEvent;

class LoggerListener
{
    public function __construct(
        private readonly LoggerInterface $logger = new NullLogger(),
    ) {
    }

    #[AsEventListener(priority: -100)]
    public function logSymfony62(ControllerEvent $event): void
    {
        foreach ($event->getAttributes()[Loggable::class] ?? [] as $attribute) {
            $this->logger->log($attribute->level, 'Controller is loggable', [
                'controller' => $event->getController(),
            ]);
        }
    }
}

Section intitulée avec-symfony-8–1Avec Symfony 8.1+

Symfony 8.1 a introduit des événements spécifiques aux attributs. Plus besoin de boucler manuellement, l’événement contient directement l’attribut ciblé :

use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class LoggerListener
{
    #[AsEventListener(KernelEvents::CONTROLLER . '.' . Loggable::class, priority: -100)]
    public function logSymfony81(ControllerAttributeEvent $event): void
    {
        $this->logger->log($event->attribute->level, 'Controller is loggable', [
            'controller' => $event->kernelEvent->getController(),
        ]);
    }
}

Section intitulée avec-symfony-8–2Avec Symfony 8.2+

La version 8.2 simplifira encore la syntaxe grâce à un attribut dédié (AsControllerAttributeListener) qui cible directement notre classe :

use Symfony\Component\HttpKernel\Attribute\AsControllerAttributeListener;
use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent;
use Symfony\Component\HttpKernel\Event\ControllerEvent;

class LoggerListener
{
    #[AsControllerAttributeListener(ControllerEvent::class, Loggable::class)]
    public function logSymfony82(ControllerAttributeEvent $event): void
    {
        $this->logger->log($event->attribute->level, 'Controller is loggable', [
            'controller' => $event->kernelEvent->getController(),
        ]);
    }
}

Section intitulée conclusionConclusion

Les attributs apportent une grande souplesse à l’écosystème PHP, et Symfony propose des outils parfaits pour les exploiter.

Grâce à eux, vous pouvez ajouter des comportements à vos contrôleurs de manière propre et déclarative, sans polluer vos méthodes avec du code répétitif.

Bien sûr, Symfony intègre déjà nativement des attributs puissants pour gérer la sécurité (#[IsGranted]), le cache (#[Cache]), ou encore le rate limiting (#[RateLimit]). Mais créer vos propres attributs ouvre la porte à des cas d’usage métiers très intéressants :

  • Le Feature Flipping (#[FeatureFlag('new-dashboard')]) : pour activer ou désactiver l’accès à une route selon le déploiement progressif d’une fonctionnalité ;
  • La télémétrie et l’observabilité (#[TrackActivity('checkout')]) : pour envoyer des statistiques précises à des outils tiers comme OpenTelemetry ou Plausible dès qu’un utilisateur visite une page clé ;
  • La transformation de réponse (#[Serialize]) : pour intercepter le retour de vos contrôleurs et le formater automatiquement selon un standard précis (comme le format JSON d’une API spécifique).

Les possibilités n’ont de limite que votre imagination !

Commentaires et discussions

Nos formations sur ce sujet

Notre expertise est aussi disponible sous forme de formations professionnelles !

Voir toutes nos formations

Ces clients ont profité de notre expertise