Les Lazy Services de Symfony : Boostez votre DX en utilisant les Service Subscribers

· Steven Renaux · Temps de lecture: 6 minutes
Two images: on the left many cars stuck in a traffic jam with the sign "All directions" above, on the right a blue car moving forward alone on the highway with the sign "Service Subscriber" and a Symfony logo above

Optimisez la performance de votre application Symfony et l'expérience développeur ! Apprenez à utiliser les Service Subscribers et les attributs de chargement différé des services afin de réduire l'instanciation rapide, de simplifier les dépendances et de créer un code modulaire et maintenable.

Dans Symfony, il est tout aussi important de structurer proprement vos services que d'optimiser leur performance. En effet, un code maintenable est plus évolutif à long terme que des micro-optimisations. Un service étroitement couplé avec des dépendances surchargées est plus difficile à tester, à étendre, voire à comprendre.

Dans cet article, nous allons voir l'utilité des Service Subscribers à travers un exemple que j'ai pu observer.

🚨 Le problème : Des constructeurs trop lourds à l'instanciation

Imaginez une classe de service comme celle-ci :

class ReportGenerator
{
   public function __construct(
       private LoggerInterface $logger,
       private MailerInterface $mailer,
       private GotenbergPdfInterface $gotenbergPdf,
       private Environment $twig,
       // more, and more sometimes...
   ) {}
}

Elle implique de nombreuses dépendances, mais seulement une ou deux peuvent être utilisées sur certaines requêtes.

Pire encore :

  • Symfony instancie rapidement tous ces services et toutes leurs propres dépendances, même s'ils ne sont jamais utilisés.

  • Cette classe est difficilement extensible, car chaque enfant doit redéfinir ou transmettre manuellement les arguments du constructor.

La solution : Utiliser les Service Subscribers

🔧 Étape 1: Mettre à jour le service pour charger en différé les services injectés

Mettez à jour votre service en intégrant ServiceSubscriberInterface

La mise en place de cette interface requiert l'implémentation de la méthode publique et statique getSubscribedServices

Cette méthode renvoie un tableau d'objets SubscribedServices ou un tableau ayant une clé de type string qui sert d'identifiant pour obtenir ce service, ainsi qu'une valeur avec une valeur avec le FQCN du service.

Vous aurez aussi besoin d'ajouter une méthode comme setContainer(Psr\Container\ContainerInterface $container) avec l'attribut #[Required]

L'attribut #[Required] permet au container d'injection de dépendance de Symfony d'appeler automatiquement cette méthode pendant l'initialisation du service.

// src/Service/ReportGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Sensiolabs\GotenbergBundle\GotenbergPdfInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;
use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Twig\Environment;


class ReportGenerator implements ServiceSubscriberInterface
{
    private ContainerInterface $container;

    #[Required]
    public function setContainer(ContainerInterface $container): ?ContainerInterface
    {
        $previous = $this->container ?? null;
        $this->container = $container;

        return $previous;
    }


   public static function getSubscribedServices(): array
   {
        return [
            'logger' => LoggerInterface::class,
            'mailer' => MailerInterface::class,
            'sensiolabs_gotenberg.pdf' => GotenbergPdfInterface::class,
            'twig' => Environment::class,
        ];
   }
  
   protected function getLogger(): LoggerInterface
   {
       return $this->container->get('logger');
   }
  
   protected function getMailer(): MailerInterface
   {
       return $this->container->get('mailer');
   }
  
   protected function getPdfGenerator(): GotenbergPdfInterface
   {
       return $this->container->get('sensiolabs_gotenberg.pdf');
   }


   protected function getTwig(): Environment
   {
       return $this->container->get('twig');
   }
  
   /** rest of your code */
}

Il existe une solution alternative en ajoutant ServiceMethodsSubscriberTrait. Cette solution fournit une implémentation pour ServiceSubscriberInterface qui parcourt toutes les méthodes de votre classe qui sont signalées par l'attribut #[SubscribedService]

L'avantage de cette méthode est de décrire les services requis par la classe en fonction du type de retour de chaque méthode.

Elle vous permet également d'intégrer la méthode setContainer(Psr\Container\ContainerInterface $container)

// src/Service/ReportGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Sensiolabs\GotenbergBundle\GotenbergPdfInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;
use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Twig\Environment;


class ReportGenerator implements ServiceSubscriberInterface
{
   use ServiceMethodsSubscriberTrait;
  
   #[SubscribedService('logger')]
   protected function getLogger(): LoggerInterface
   {
       return $this->container->get('logger');
   }
  
   #[SubscribedService('mailer')]
   protected function getMailer(): MailerInterface
   {
       return $this->container->get('mailer');
   }
  
   #[SubscribedService('sensiolabs_gotenberg.pdf')]
   protected function getPdfGenerator(): GotenbergPdfInterface
   {
       return $this->container->get('sensiolabs_gotenberg.pdf');
   }
  
   #[SubscribedService('twig')]
   protected function getTwig(): Environment
   {
       return $this->container->get('twig');
   }
  
   /** rest of your code */
}

🔁 Les services ne sont désormais récupérés que lorsqu'ils sont réellement utilisés, et ne sont jamais instanciés inutilement.

D'accord, c'est mieux pour la performance, mais que se passe-t-il si nous avons d'autres classes qui ont besoin de générer des pages de rapports HTML, des e-mails ou des PDF ?

Nous pouvons résoudre tout ça grâce à une seule classe d'abstraction réutilisable.

🔧 Étape 2: Créer une classe d'abstraction de base

Mettez-y toute la logique du Service Subscriber.

// src/Service/AbstractReport.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Sensiolabs\GotenbergBundle\GotenbergPdfInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;
use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Twig\Environment;

abstract class AbstractReport implements ServiceSubscriberInterface
{
   use ServiceMethodsSubscriberTrait;
  
   #[SubscribedService('logger')]
   protected function getLogger(): LoggerInterface
   {
       return $this->container->get('logger');
   }
  
   #[SubscribedService('mailer')]
   protected function getMailer(): MailerInterface
   {
       return $this->container->get('mailer');
   }
  
   #[SubscribedService('sensiolabs_gotenberg.pdf')]
   protected function getPdfGenerator(): GotenbergPdfInterface
   {
       return $this->container->get('sensiolabs_gotenberg.pdf');
   }
  
   #[SubscribedService('twig')]
   protected function getTwig(): Environment
   {
       return $this->container->get('twig');
   }
}

👶 Étape 3: Étendre la classe d'abstraction de base

Mettons maintenant à jour le service de reporting qui étend cette classe de base.

Cela nous donnera un service plus lisible, dont les services sont récupérés en différé.

Et que se passe-t-il si vous avez besoin du service uniquement pour cette classe ? Vous avez juste besoin d'un constructeur pour ce service, sans avoir à vous soucier d'un constructeur parent.

// src/Service/ReportService.php
namespace App\Service;

use Symfony\Component\HttpFoundation\StreamedResponse;

final class UserReport extends AbstractReport
{
   public function __construct(private readonly CsvGenerator $csvGenerator)
   {}
  
   public function generateMail(string $recipientEmail): void
   {
       /* rest of your code */
       $this->getMailer()->send(/* send email */);
       $this->getLogger()->info('Email report sent to ' . $recipientEmail);
   }
  
   public function generatePdf(): StreamedResponse
   {
       $pdf = $this->getPdfGenerator();
       $this->getLogger()->info('PDF report generated');
      
       return $pdf->html()
           ->content('content.html.twig')
           ->generate()
           ->stream()
       ;
   }
  
   public function generateView(): string
   {
       $view = $this->getTwig()->render('template.html.twig');
       $this->getLogger()->info('Report page generated');
      
       return $view;
   }
  
   public function generateCsv(): string
   {
       $csv = $this->csvGenerator->generate();
      
       /** rest of your code */
   }
}

Vous améliorez ainsi l'expérience développeur (DX)

✅ Pas de modèle de constructeur

🧠 Des accesseurs auto-documentés

🚀 Les nouveaux développeurs n'ont qu'à étendre la classe de base pour coder

⚡ Boost de la performance grâce au chargement différé

🧱 Une structure de service modulaire et respectant le principe DRY

📦 Utiliser les Lazy Service Subscribers dans un bundle Symfony (avec des dépendances optionnelles)

Supposons que votre service fasse désormais partie d'un bundle Symfony réutilisable.

Si le client souhaite uniquement envoyer des rapports par e-mail (sans générer de PDF ni afficher de template Twig), il ne faut pas le forcer à installer des dépendances inutiles comme Twig ou GotenbergBundle.

Avec ServiceSubscriberInterface et l'attribut #[SubscribedService] de Symfony, nous pouvons rendre ces dépendances différées et optionnelles — utilisées uniquement si elles sont présentes.

Voici comment faire :

// src/Service/AbstractReport.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Sensiolabs\GotenbergBundle\GotenbergPdfInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;
use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Twig\Environment;

abstract class AbstractReport implements ServiceSubscriberInterface
{
   use ServiceMethodsSubscriberTrait;
  
   #[SubscribedService('logger')]
   private function getLogger(): LoggerInterface
   {
       return $this->container->get('logger');
   }
  
   #[SubscribedService('mailer')]
   private function getMailer(): MailerInterface
   {
       return $this->container->get('mailer');
   }
  
   #[SubscribedService('sensiolabs_gotenberg.pdf', nullable: true)]
   private function getPdfGenerator(): GotenbergPdfInterface
   {
       if (!$this->container->has('sensiolabs_gotenberg.pdf') || !($pdf = $this->container->get('sensiolabs_gotenberg.pdf')) instanceof GotenbergPdfInterface) {
           throw new \LogicException(\sprintf('Gotenberg is required to use "%s" method. Try to run "composer require sensiolabs/gotenberg-bundle".', __METHOD__));
       }
      
       return $pdf;
   }
  
   #[SubscribedService('twig', nullable: true)]
   private function getTwig(): Environment
   {
       if (!$this->container->has('twig') || !($environment = $this->container->get('twig')) instanceof Environment) {
           throw new \LogicException(\sprintf('Twig is required to use "%s" method. Try to run "composer require symfony/twig-bundle".', __METHOD__));
       }
      
       return $environment;
   }
}

Grâce à ce modèle, vous pouvez créer des services flexibles et extensibles qui :

  • N'utilisent les dépendances que si elles sont installées

  • Échouent correctement grâce à des messages d'erreur utiles

  • Gardent votre bundle léger et modulaire

Nous préférons cette approche pour améliorer l'expérience développeur, la maintenabilité dans GotenbergBundle (un bundle pour générer des PDF -> voir l'article sur le sujet), et rendre votre bundle plus facile à utiliser à une plus grande échelle d'un projet Symfony.

🏁 Conclusion

En combinant ServiceSubscriberInterface avec le trait du service subscriber, vous obtenez le meilleur des deux mondes :
Utilisez uniquement les dépendances si elles sont installées

  • 🔁 Un chargement propre et différé des services

  • ⚡ Pas d'overload du runtime

  • 🧠 Une meilleur expérience développeur (DX) lors de la création des services

Adoptez ce modèle dans vos projets Symfony et vous livrerez plus rapidement avec moins de duplication et un code plus propre.

💡 Le saviez-vous ?

Symfony lui-même utilise ce modèle en interne — notamment dans son AbstractController.

Au lieu d'injecter chaque service dans chaque contrôleur, Symfony utilise ServiceSubscriberInterface.
Cette méthode permet un chargement différé et rend les contrôleurs enfants légers et faciles à étendre, comme nous l'avons fait avec notre AbstractReport.

Vous cherchez à améliorer votre DX avec les Service Subscribers ?

Notre équipe d'experts vous aide à améliorer votre performance et votre expérience développeur. Parlez-nous de votre projet de développement.

Cela pourrait aussi vous intéresser

Fabien Potencier
Elise Hamimi

SymfonyCon Amsterdam 2025 : Notre bilan et les moments forts

Après une première édition emblématique en 2019, SymfonyCon a fait son grand retour à Amsterdam. Dès les premières minutes, on sentait l’énergie d’un rendez-vous très attendu : plus de 1 200 participants, 39 nationalités, les retrouvailles avec la communauté, de belles découvertes… et une ambiance de folie. Cette année, l’événement avait une saveur toute particulière puisqu’il s’agissait de l’édition spéciale anniversaire des 20 ans de Symfony. SensioLabs y était : on vous raconte tout !

En savoir plus
The SensioLabs team celebrating the 20th anniversary of Symfony with balloons
Jules Daunay

L'histoire continue : SensioLabs célèbre les 20 ans de Symfony

Le temps passe vite, surtout quand on écrit le futur du développement ! L’équipe de SensioLabs vient de souffler les 20 bougies du framework Symfony. Nous avons marqué le coup au bureau, mais la fête n'est pas terminée. Le rendez-vous est déjà pris pour une célébration XXL à SymfonyCon Amsterdam 2025 les 27 au 28 novembre.

En savoir plus
PHP 8.5 URI extension
Oskar Stark

La nouvelle extension URI de PHP 8.5 : Une révolution pour l'analyse des URL

PHP 8.5 introduit une nouvelle extension URI puissante qui modernise la gestion des URL. Grâce au support des standards RFC 3986 et WHATWG, la nouvelle classe Uri fournit des objets immuables, des interfaces fluides et une validation appropriée, résolvant ainsi toutes les limites de la fonction historique parse_url(). Cet articl présente des exemples pratiques avant/après et explique quand utiliser chaque standard.

En savoir plus
Open in new tab
Silas Joisten

Le piège des onglets: pourquoi forcer l'ouverture de nouveaux onglets est une mauvaise pratique en UX

Nous l'avons tous fait — ajouter target="_blank" à un lien pour « aider les utilisateurs » à rester sur notre site. Mais ce qui semble être une commodité inoffensive crée souvent de la confusion, diminue l'accessibilité et introduit des risques de sécurité cachés.

En savoir plus
3 dog heads
Mathieu Santostefano

Venez avec votre propre client HTTP

Libérez-vous des dépendances rigides de vos SDK PHP. Dans cet article, apprenez à utiliser les normes PSR-7, PSR-17 et PSR-18, ainsi que la bibliothèque php-http/discovery, pour permettre à vos utilisateurs d'utiliser le client HTTP de leur choix, qu'il s'agisse de Guzzle, de Symfony HttpClient ou d'un autre. Un incontournable pour les développeurs PHP et Symfony.

En savoir plus
Blue sign on a building with several Now What? letters
Thibaut Chieux

Comment prioriser les messages lors du développement d'applications asynchrones avec Symfony Messenger

Le traitement asynchrone offre des avantages tels que la découplage des processus et des temps de réponse plus rapides, mais la gestion des priorités des messages peut s'avérer complexe. Pour traiter des tâches allant de la réinitialisation de mot de passe à des exports complexes, il est essentiel de garantir la livraison rapide des messages critiques. Cet article examine les problèmes fréquents liés au traitement asynchrone et propose des solutions avec Symfony Messenger pour optimiser votre application sans refonte majeure.

En savoir plus
SensioLabs University Courses Annonce La nouvelle formation Master de niveau 3 est disponible sur un fond vert
Jules Daunay

Master Symfony : Devenez un expert de Symfony avec notre nouvelle formation

Améliorez votre maîtrise de Symfony grâce à la nouvelle formation Master de niveau 3 sur Symfony ! Maîtrisez les sujets les plus complexes du framework et développez des compétences approfondies.

En savoir plus
the surface of the earth seen from the space with city lights forming networks
Imen Ezzine

Les verbes HTTP : votre Guide Complet

Apprenez les bases des verbes HTTP : GET, POST, PUT, DELETE, et plus encore. Cet article vous explique leur fonctionnement, leurs utilisations et les conséquences en matière de sécurité.

En savoir plus
Image