Symfony Lazy Services mit Stil – Steigere deine Entwicklererfahrung mit Service Subscribers

· Steven Renaux · 6 Minuten zum Lesen
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

Steigere die Performance und Developer Experience (DX) deiner Symfony-App! Erfahre, wie du Service Subscribers und Traits für das verzögerte Laden von Services verwendest, um die sofortige Instanziierung zu reduzieren, Abhängigkeiten zu vereinfachen und modularen, wartbaren Code zu schreiben.

In Symfony ist es genauso wichtig, deine Services sauber zu strukturieren, wie ihre Performance zu optimieren. Tatsächlich ist wartbarer Code langfristig besser erweiterbar als Mikro-Optimierungen. Ein Service, der stark gekoppelt ist und überladene Abhängigkeiten hat, ist schwieriger zu testen, zu erweitern und sogar zu verstehen.

In diesem Artikel werden wir die Nützlichkeit von Service Subscribers anhand eines charakteristischen Beispiels untersuchen, das ich beobachten konnte.

🚨 Die Herausforderung: Zu schnell überladene Constructors

Stell dir eine klassische Service-Klasse vor:

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

Sie beinhaltet zahlreiche Abhängigkeiten, aber nur ein oder zwei werden im spezifischen Ausführungspfad tatsächlich verwendet.

Schlimmer noch:

  • Symfony instanziiert sofort all diese Services und all ihre eigenen Abhängigkeiten, selbst wenn sie niemals verwendet werden.

  • Diese Klasse ist schwer erweiterbar, da jede Kind-Klasse die Constructor-Argumente manuell neu definieren oder übergeben muss.

Die Lösung: Service Subscribers verwenden

🔧 Schritt 1: Den Service aktualisieren, um injizierte Services verzögert zu laden

Aktualisiere deinen Service, indem du das ServiceSubscriberInterface implementierst.

Die Implementierung erfordert die öffentliche statische Methode getSubscribedServices

Diese Methode gibt ein Array von SubscribedServices-Objekten oder ein Array mit einem String-Schlüssel, der als Bezeichner zum Abrufen dieses Services dient, sowie einem Wert mit dem FQCN des Services zurück.

Du musst auch eine Methode wie setContainer(Psr\Container\ContainerInterface $container) mit dem Attribut #[Required] hinzufügen.

Das Attribut #[Required] ermöglicht es dem Symfony-Dependency-Injection-Container, diese Methode während der Service-Initialisierung automatisch aufzurufen.

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

Es gibt eine alternative Lösung, der ServiceMethodsSubscriberTrait

Diese Lösung bietet eine Implementierung für ServiceSubscriberInterface, die alle Methoden deiner Klasse durchläuft, die mit dem #[SubscribedService] Attribut gekennzeichnet sind.

Der Vorteil dieser Methode ist, dass die von der Klasse benötigten Services basierend auf dem Rückgabetyp jeder Methode beschrieben werden.

Sie ermöglicht es dir auch, die Methode setContainer(Psr\Container\ContainerInterface $container) zu integrieren.

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

🔁 Die Services werden nun nur noch abgerufen, wenn sie tatsächlich verwendet werden, und werden niemals zu früh instanziiert.

Okay, das ist besser für die Performance, aber was passiert, wenn wir andere Klassen haben, die HTML-Berichtsseiten, E-Mails oder PDFs generieren müssen?

Wir können all das mit einer einzigen wiederverwendbaren Abstraktionsklasse lösen.

🔧 Schritt 2: Eine grundlegende Abstraktionsklasse erstellen

Füge dort die gesamte Logik für den Service Subscriber hinzu.

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

👶 Schritt 3: Die grundlegende Abstraktionsklasse erweitern

Aktualisieren wir nun den UserReport-Service, der diese Basisklasse nutzt.

Dies wird uns einen besser lesbaren Service liefern, dessen Services verzögert abgerufen werden.

Und was passiert, wenn du den Service nur für diese Klasse benötigst? Du brauchst nur einen Constructor für diesen Service, ohne dich um einen übergeordneten Constructor kümmern zu müssen.

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

So verbesserst so die Developer Experience (DX)

✅ Kein Constructor-Boilerplate

🧠 Selbst-dokumentierende Accessoren

🚀 Neue Entwickler müssen das Projekt nur erweitern

⚡ Performance-Boost durch verzögertes Laden

🧱 Eine modulare und DRY-Service-Struktur

📦 Lazy Service Subscribers in einem Symfony Bundle verwenden (mit optionalen Abhängigkeiten)

Nehmen wir an, dein Service ist nun Teil eines wiederverwendbaren Symfony Bundles.

Wenn der Kunde nur Berichte per E-Mail versenden möchte (ohne PDFs zu generieren oder Twig-Templates anzuzeigen), sollte er nicht gezwungen werden, unnötige Abhängigkeiten wie Twig oder GotenbergBundle zu installieren.

Mit dem ServiceSubscriberInterface und dem #[SubscribedService] Attribut von Symfony können wir diese Abhängigkeiten verzögert und optional machen – sie werden nur verwendet, wenn sie vorhanden sind:

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

Mit diesem Muster kannst du flexible und erweiterbare Services erstellen, die:

  • Abhängigkeiten nur verwenden, wenn sie installiert sind

  • Korrekte Fehlerbehandlung mit nützlichen Fehlermeldungen bieten

  • Dein Bundle leicht und modular halten

Wir bevorzugen diesen Ansatz, um die Entwicklererfahrung und Wartbarkeit im GotenbergBundle (ein Bundle zur PDF-Generierung -> siehe den Artikel über die Vorträge auf der SymfonyCon Vienna 2024) zu verbessern und dein Bundle in größerem Umfang eines Symfony-Projekts einfacher nutzbar zu machen.

🏁 Fazit

Durch die Kombination von ServiceSubscriberInterface mit dem ServiceSubscriberTrait erhältst du das Beste aus beiden Welten: Abhängigkeiten nur verwenden, wenn sie installiert sind.

🔁 Sauberes und verzögertes Laden von Services

⚡ Keine Überlastung der Laufzeit

🧠 Eine bessere Developer Experience (DX) beim Erstellen von Services

Übernehme dieses Pattern in deinen Symfony-Projekten und du wirst schneller liefern, mit weniger Duplikation und saubererem Code.

💡 Wusstest du das schon?

Symfony selbst verwendet dieses Muster intern – insbesondere in seinem AbstractController.

Anstatt jeden Service in jeden Controller zu injizieren, verwendet Symfony das ServiceSubscriberInterface. Diese Methode ermöglicht ein verzögertes Laden und macht die untergeordneten Controller leicht und einfach erweiterbar, genau wie wir es mit unserem AbstractReport gemacht haben.

Möchtest du deine Developer Experience auf ein neues Level heben?

Unser Expertenteam ist bereit, dir dabei zu helfen, deine Performance und Entwicklererfahrung zu verbessern. Erzähl uns mehr über dein Entwicklungsprojekt!

Das könnte dich auch interessieren

Fabien Potencier
Elise Hamimi

SymfonyCon Amsterdam 2025: Unser Rückblick und die Highlights

Nach einer legendären ersten Ausgabe im Jahr 2019 feierte die SymfonyCon ihr großes Comeback in Amsterdam. Von Anfang an war die Energie einer mit Spannung erwarteten Konferenz zu spüren: mehr als 1.200 Teilnehmer, 39 Nationalitäten, das größte Treffen der Symfony-Community des Jahres, großartige Entdeckungen ... und eine ausgelassene Atmosphäre. Dieses Jahr war etwas ganz Besonderes, denn es war das 20-jährige Jubiläum von Symfony. SensioLabs war dabei: Wir berichten Ihnen ausführlich über unsere Erfahrungen dort!

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

Die Geschichte geht weiter: SensioLabs feiert 20 Jahre Symfony

Die Zeit vergeht wie im Flug – besonders, wenn man an der Zukunft der Entwicklung schreibt! Das SensioLabs-Team hat gerade die 20 Kerzen des Symfony-Frameworks ausgeblasen. Wir haben den Anlass im Büro gefeiert, doch die Party ist noch nicht vorbei. Das Datum für eine XXL-Feier steht bereits fest: die SymfonyCon Amsterdam vom 27. bis 28. November 2025.

Mehr erfahren
PHP 8.5 URI extension
Oskar Stark

PHP 8.5's neue URI-Erweiterung: Ein Game-Changer für URL-Parsing

PHP 8.5 führt eine leistungsstarke neue URI-Erweiterung ein, die die URL-Verarbeitung modernisiert. Mit Unterstützung für RFC 3986 und WHATWG-Standards bietet die neue Uri-Klasse unveränderliche Objekte, fluent Interfaces und korrekte Validierung - und behebt alle Einschränkungen der veralteten parse_url()-Funktion. Dieser Leitfaden zeigt praktische Vorher/Nachher-Beispiele und erklärt, wann welcher Standard zu verwenden ist.

Mehr erfahren
Open in new tab
Silas Joisten

Die Tab-Falle: Warum das Erzwingen neuer Tabs eine schlechte UX ist

Wir haben es alle schon getan — target="_blank" zu einem Link hinzugefügt, um „Benutzern zu helfen", auf unserer Website zu bleiben. Aber was sich wie eine harmlose Bequemlichkeit anfühlt, führt oft zu Verwirrung, beeinträchtigt die Barrierefreiheit und birgt versteckte Sicherheitsrisiken.

Mehr erfahren
3 dog heads
Mathieu Santostefano

Lass die Nutzer des SDK ihren eigenen HTTP-Client nutzen

Befreie dich von starren Abhängigkeiten in deinen PHP-SDKs. Erfahre, wie du die Standards PSR-7, PSR-17 und PSR-18 zusammen mit PHP-HTTP/Discovery nutzt, um deinen Benutzern die Verwendung ihres bevorzugten HTTP-Clients zu ermöglichen – sei es Guzzle, Symfony HttpClient oder ein anderes Tool. Ein Muss für PHP- und Symfony-Entwickler.

Mehr erfahren
Blue sign on a building with several Now What? letters
Thibaut Chieux

Wie man Nachrichten beim Aufbau asynchroner Anwendungen mit dem Symfony-Messenger priorisiert

Die asynchrone Verarbeitung bietet Vorteile wie entkoppelte Prozesse und schnellere Reaktionszeiten. Die Verwaltung von Nachrichtenprioritäten kann jedoch zu einer Herausforderung werden. Bei Aufgaben, die vom Zurücksetzen von Passwörtern bis hin zu komplexen Exporten reichen, ist die rechtzeitige Zustellung kritischer Nachrichten unerlässlich. Dieser Artikel befasst sich mit häufigen Problemen bei der asynchronen Verarbeitung und zeigt Lösungen mit Symfony Messenger auf, mit denen Sie Ihre Anwendung ohne umfangreiches Refactoring optimieren können.

Mehr erfahren
SensioLabs University Courses announcing the new level 3 Master training course now available
Jules Daunay

Wir stellen vor: Mastering Symfony 7

Wenn du deine Symfony-Kenntnisse verbessern möchtest, ist der neue Level-3 Trainingskurs bei SensioLabs vielleicht das Richtige für dich! Du meisterst komplexe Themen, optimierst die Leistung und wirst zum Symfony-Experten.

Mehr erfahren
the surface of the earth seen from the space with city lights forming networks
Imen Ezzine

HTTP-Verben: Der ultimative Leitfaden

Hier erklären wir dir die Grundlagen von GET, POST, PUT, DELETE und mehr. In diesem Artikel erfährst du alles über die Funktionsweise, die Anwendungsmöglichkeiten und die Sicherheitsauswirkungen.

Mehr erfahren
Image