Special Anniversary Black Friday: Get 30% off all training and 10% off all services Get a Quote


Symfony Lazy Services with Style: Boost DX using Service Subscribers

· Steven Renaux · 5 minutes to read
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

Boost your Symfony app's performance and developer experience! Learn how to use Service Subscribers and traits for lazy service loading to reduce eager instantiation, simplify dependencies, and create modular, maintainable code.

In Symfony, structuring your services cleanly is just as important as optimizing performance. Because maintainable code scales better in the long run than micro-optimizations. A tightly coupled service with bloated dependencies is harder to test, extend, or even understand.

In this article, we’re going to see how useful service subscribers are through a typical example I have experienced.

🚨 The Problem: Eager-Loaded Constructors

Imagine a typical service class:

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

That’s a lot of dependencies — and maybe only one or two are actually used during a specific execution path.

Even worse:

  • Symfony eagerly instantiates all these services and all of their own dependencies too, even if they’re never used.

  • This class is hardly extensible because every child must manually redefine or forward constructor arguments.

The Solution: Let’s use Service Subscribers

🔧 Step 1: Update the service to lazily load injected services

Update your service to implement ServiceSubscriberInterface

This interface requires the public static method getSubscribedServices to be implemented.

This method returns an array of SubscribedServices objects or an array with a key string as the id to get the service and a value with the FQCN of the service.

You also need to add a method like setContainer(Psr\Container\ContainerInterface $container) with a #[Required] attribute.

The #[Required] attribute tells the Symfony dependency injection container to automatically call this method during service initialization.

// 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 */
}

There is an alternative solution by adding ServiceMethodsSubscriberTrait, it provides an implementation for ServiceSubscriberInterface that looks through all methods in your class that are marked with the #[SubscribedService] attribute.

It describes the services needed by the class based on each method’s return type.

It also adds the method setContainer(Psr\Container\ContainerInterface $container) for you.

// 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 */
}

🔁 Services are now only fetched when actually used — and never eagerly instantiated.

Okay, it’s better about performance, but if we have other classes that need to generate report HTML pages, mail or PDF… We can solve all of that with one reusable abstract class.

🔧 Step 2: Create the Abstract Base Class

Put all the logic about Service Subscriber into it.

// 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');
   }
}

👶 Step 3: Let’s extend this Abstract Base Class

Now let’s update the reporting service that extends this base class.

This will give us a more readable service, whose services are fetched lazily.

And if you need a service only for this class. You only need a constructor with this service, without dealing with a parent constructor.

// 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 */
   }
}

It Improves Developer Experience (DX)

✅ No constructor boilerplate

🧠 Self-documenting accessors

🚀 New devs just extend and go

⚡ Performance boost via lazy loading

🧱 Modular and DRY service structure

📦 Using Lazy Service Subscribers in a Symfony Bundle (with Optional Dependencies)

Let’s say your service is now part of a reusable Symfony bundle.

If the consumer only wants to send reports via email (without generating PDFs or rendering Twig templates), we shouldn’t force them to install unnecessary dependencies like Twig or GotenbergBundle.

With ServiceSubscriberInterface and Symfony’s #[SubscribedService] attribute, we can make those dependencies lazy and optional — only used if present.

Here’s how:

// 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;
   }
}

With this pattern, you can build flexible and extendable services that:

  • Only use dependencies if they’re installed

  • Fail gracefully with helpful error messages

  • Keep your bundle lightweight and modular

We prefer this approach to improve developer experience, maintainability in GotenbergBundle (Bundle to generate PDF -> see the article about talks at SymfonyCon Vienna 2024), and make your bundle easier to adopt on a broader scale of Symfony project.

🏁 Conclusion

By combining ServiceSubscriberInterface with the service subscriber trait, you get the best of both worlds:
Only use dependencies if they’re installed

  • 🔁 Clean and lazy service loading

  • ⚡ No runtime overhead

  • 🧠 Better developer experience (DX) when creating services

Adopt this pattern in your Symfony projects, and you’ll ship faster with less duplication, and cleaner code.

💡 Did you know?

Symfony itself uses this pattern internally — most notably in its AbstractController.

Instead of injecting every service into every controller, Symfony uses ServiceSubscriberInterface.
This enables lazy loading and makes child controllers lightweight and easy to extend, just like we did with our AbstractReport.

Are you looking to take your DX to new heights with Service Subscribers?

Our team of experts is ready to help you improve your performance and developer experience. Tell us more about your development project.

This might also interest you

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

The Story Continues: SensioLabs Celebrates Symfony's 20th Anniversary

Time flies, especially when you're busy shaping the future of development! The SensioLabs team has just reached a milestone with the anniversary of the Symfony framework. We marked the occasion at the office, but the party isn't over yet. The date is already set for an XXL celebration at SymfonyCon Amsterdam 2025, from November 27 to 28.

Read more
PHP 8.5 URI extension
Oskar Stark

PHP 8.5's New URI Extension: A Game-Changer for URL Parsing

PHP 8.5 introduces a powerful new URI extension that modernizes URL handling. With support for both RFC 3986 and WHATWG standards, the new Uri class provides immutable objects, fluent interfaces, and proper validation - addressing all the limitations of the legacy parse_url() function. This guide shows practical before/after examples and explains when to use each standard.

Read more
Open in new tab
Silas Joisten

The Tab Trap: Why Forcing New Tabs Is Bad UX

We’ve all done it — added target="_blank" to a link to “help users” stay on our site. But what feels like a harmless convenience often creates confusion, breaks accessibility, and introduces hidden security risks.

Read more
3 dog heads
Mathieu Santostefano

Bring Your Own HTTP client

Break free from rigid dependencies in your PHP SDKs. Learn how to use PSR-7, PSR-17, and PSR-18 standards along with php-http/discovery to allow users to bring their favorite HTTP client, whether it's Guzzle, Symfony HttpClient, or another. A must-read for PHP and Symfony developers.

Read more
Blue sign on a building with several Now What? letters
Thibaut Chieux

How To Prioritize Messages When Building Asynchronous Applications With Symfony Messenger

Asynchronous processing offers benefits like decoupled processes and faster response times, but managing message priorities can become a challenge. When dealing with tasks ranging from password resets to complex exports, ensuring timely delivery of critical messages is essential. This article explores common asynchronous processing issues and provides solutions using Symfony Messenger, allowing you to optimize your application without extensive refactoring.

Read more
SensioLabs University Courses announcing the new level 3 Master training course now available
Jules Daunay

Master Symfony: Unlock Expert Skills with Our Training

Take your Symfony proficiency from good to great with the new Level 3 training course at SensioLabs! Master complex topics, optimize performance, and become a Symfony expert.

Read more
the surface of the earth seen from the space with city lights forming networks
Imen Ezzine

HTTP Verbs: Your Ultimate Guide

HTTP Verbs Explained: Learn the basics of GET, POST, PUT, DELETE, and more. This article explains how they work, their applications, and their security implications.

Read more
Poster of Guillaume Loulier presentation
Salsabile El-Khatouri

A Symfony Training at SensioLabs: Behind The Scenes

What does Symfony training at SensioLabs look like? Find out in this interview with Guillaume Loulier, a passionate developer and trainer, who tells us all about the official Symfony training courses.

Read more
Toy factory production line
Silas Joisten

Supercharging Symfony Testing with Zenstruck Foundry

Zenstruck Foundry has revolutionized the way we write tests in Symfony. In this post, you’ll learn how expressive factories, isolated test data, and a smoother developer experience helped us streamline our testing workflow and boost productivity.

Read more
Domain Driven Design practical approach
Silas Joisten

Applying Domain-Driven Design in PHP and Symfony: A Hands-On Guide

Learn how to apply Domain-Driven Design (DDD) principles in Symfony with practical examples. Discover the power of value objects, repositories, and bounded contexts.

Read more
Photo speaker meetup AI Symfony
Jules Daunay

Symfony and AI: the video is now available

What about Symfony and Artificial Intelligence (AI)? This was the theme of the exclusive event organized by SensioLabs in partnership with Codéin on October 3rd. With the added bonus of feedback from a development project combining Symfony and AI. If you missed the event, check out the video now available for free on our Youtube channel.

Read more
2025 a year of celebrations for PHP with windows about API Platform, PHP, AFUP and Symfony
Jules Daunay

2025: a year of anniversaries for PHP, AFUP, Symfony and API Platform

2025 is going to be a big year for anniversaries. We will be celebrating the 20th anniversary of Symfony, the 30th anniversary of PHP, the 25th anniversary of AFUP and the 10th anniversary of API Platform. For SensioLabs, this is a major milestone that proves the longevity of the technologies in our ecosystem. We are proud to celebrate these anniversaries with the community all year long.

Read more
SymfonyDay Chicago 2025
Simon André

SymfonyDay Chicago 2025: A Celebration of Community

On March 17th, the Symfony community met in Chicago for SymfonyDay Chicago 2025. The event, held on St. Patrick's Day, was both a celebration of Symfony and a moment to support Ryan Weaver in his fight against cancer. It was more than just a conference — it was a gathering around a valued member of the community.

Read more
Blue ElePHPant on a computer
Imen Ezzine

Optimize Your PHP Code: 8 Functions You Need for Efficient Table Handling

If you want to become a good PHP developer, you must learn to work with arrays. Arrays are used a lot in PHP: temporarily store, organize, and process data before it's saved in a database. Knowing how to work with them efficiently will help you manage and process data more effectively.

Read more
type-safety-uuid
Oskar Stark

Type-Safe Identifiers with Symfony and Doctrine: Using Dedicated ID Classes

Learn how to enhance type safety in Symfony and Doctrine by using dedicated ID classes like BookId and UserId instead of raw UUIDs. This approach prevents identifier mix-ups, improves code clarity, and ensures better integration with Symfony Messenger and repository methods. Explore practical examples and best practices for implementing type-safe identifiers in your Symfony applications.

Read more
Image