Subsrybowanie usług w Symfony

Jest wiele sposobów rejestrowania usług i dostępu do nich w Symfony Framework. W dzisiejszym wpisie przedstawię mój ulubiony oparty o tagi, ale przy wykorzystaniu dobrodziejstw z PHP 8 czyli atrybutów. Zanim jednak o TaggedLocator, który robi taką magię słów kilka o ServiceSubscriber.

ServiceSubscriber w Symfony służy do wstrzykiwania zależności w obiekty, które nie są usługami. Oznacza to, że ServiceSubscriber pozwala na wstrzyknięcie usług do obiektów, które same w sobie nie są usługami, ale wymagają dostępu do innych usług w systemie.

Aby skorzystać z ServiceSubscribera, klasa musi zaimplementować interfejs ServiceSubscriberInterface i zdefiniować metodę “getSubscribedServices“, która zwraca tablicę z nazwami usług wymaganych przez klasę.

Przykładowo, jeśli klasa potrzebuje dostępu do obiektu EntityManager, można wstrzyknąć tę usługę, korzystając z metody get() z obiektu Kontenera Serwisów Symfony:

use Symfony\Contracts\Service\{ServiceSubscriberInterface, ServiceLocatorTrait};

class MyClass implements ServiceSubscriberInterface
{
    use ServiceLocatorTrait;

    public static function getSubscribedServices()
    {
        return [
            'doctrine.orm.entity_manager' => EntityManagerInterface::class,
        ];
    }

    public function myMethod()
    {
        $entityManager = $this->get('doctrine.orm.entity_manager');
        // ...
    }
}

Dzięki temu wstrzykiwaniu zależności za pomocą ServiceSubscribera klasa MyClass może skorzystać z usługi EntityManager bez potrzeby wstrzykiwania jej w konstruktorze lub metodzie setEntityManager().

Jeszcze inną motywacją w używaniu ServiceSubscribera jest sytuacja, gdy chcemy zainicjalizować tylko niektóre serwisy, bo wiemy że tylko one będą nam potrzebne. Takie zarządzanie może odbywać się więc “w locie”, przez nasz serwis pełniący rolę w tym przypadku takiego menadżera / planisty / proxy.

Przejdźmy do TaggedLocator, bo to kolejny sposób na wstrzykiwanie zależności w Symfony, który jest podobny do ServiceSubscribera, ale zapewnia większą elastyczność i dostępność dla usług. W odróżnieniu od ServiceSubscribera, TaggedLocator pozwala na dynamiczne wstrzykiwanie wielu usług o określonym tagu, który jest definiowany w plikach konfiguracyjnych aplikacji. Dzięki temu TaggedLocator umożliwia wstrzykiwanie różnych usług w zależności od potrzeb i konfiguracji, co jest szczególnie przydatne w przypadku aplikacji z dużą liczbą usług.

Przykładowo, można zdefiniować kilka usług implementujących interfejs NotificationInterface, o różnych tagach:

# services.yaml
services:
    app.email_notification:
        class: App\Notification\EmailNotification
        tags: ['app.notification']

    app.sms_notification:
        class: App\Notification\SmsNotification
        tags: ['app.notification']

    app.push_notification:
        class: App\Notification\PushNotification
        tags: ['app.notification']

Następnie, można wstrzyknąć te usługi w innej klasie za pomocą TaggedLocatora:

use Symfony\Component\DependencyInjection\ServiceLocator;

class NotificationManager
{
    private $notificationLocator;

    public function __construct(ServiceLocator $notificationLocator)
    {
        $this->notificationLocator = $notificationLocator;
    }

    public function notify(string $message, string $type)
    {
        $notifications = $this->notificationLocator->getTaggedServices('app.notification');
        foreach ($notifications as $notification) {
            $notification->send($message, $type);
        }
    }
}

Wszystko fajnie, ale jak to uprościć by nie pisać tyle kodu? Tutaj przychodzą właśnie wspomniane atrybuty. Jeśli stworzymy wspólny interfejs dla notyfikacji, nie będziemy musieli ich tagować (tagiem może stać się interfejs). Przykładowo:

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag]
interface NotificationInterface
{
    public function send(string $message, string $type): void;
}

Wstrzyknięcie również możemy ułatwić sobie przez atrybuty. Albo wybieramy konkretny serwis wstrzykując sobie ServiceLocator w konstruktorze

#[TaggedLocator(NotificationInterface::class)]
        private ServiceLocator $notificationLocator,

albo bezpośrednio wstrzykujemy sobie wszystkie usługi:

use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;

class NotificationManager
{
    public function __construct( 
#[TaggedIterator(NotificationInterface::class)] private readonly iterable $notifications)
    {
    }

    public function notify(string $message, string $type)
    {
       foreach ($this->notifications as $notification) {
            $notification->send($message, $type);
        }
    }
}

normalnie czary ;-)…