Event Subscriber w Drupal – kiedy i jak stosować

drupal

Event Subscriber w Drupal to potężny mechanizm pozwalający reagować na zdarzenia zachodzące w systemie – od zapisu encji, przez logowanie użytkownika, aż po obsługę własnych, biznesowych eventów. Dobrze zaprojektowany subscriber porządkuje logikę, zmniejsza liczbę hooków i ułatwia testowanie. Wymaga jednak świadomości, kiedy jest najlepszym wyborem, jak go zarejestrować w kontenerze usług i jak nie wpaść w pułapki zależności czy problemów z wydajnością.

Podstawy mechanizmu Event Subscriber w Drupal

Czym jest zdarzenie i event subscriber

W architekturze opartej na wzorcu *observer* moduły nie wywołują się bezpośrednio, lecz reagują na zdarzenia, jakie emituje system. W Drupal rolę obserwatora pełni Event Subscriber – zwykła klasa PHP, która implementuje interfejs EventSubscriberInterface. Każde zdarzenie jest obiektem klasy dziedziczącej po Symfony\Contracts\EventDispatcher\Event (lub podobnej, w zależności od wersji), a za ich dystrybucję odpowiada Event Dispatcher.

W praktyce wygląda to tak: gdzieś w kodzie (np. w module core) wywoływana jest metoda dispatch na usłudze event_dispatcher, a wszystkie zarejestrowane subskrybery otrzymują ten sam obiekt eventu. Każdy subscriber może go odczytać, zmodyfikować lub na jego podstawie wykonać dodatkowe akcje (np. logowanie, wysyłkę maili, aktualizację encji).

Różnica między event subscriber a hookami

Drupal od lat opierał się na hookach. Mechanizm eventów jest nowszy i pochodzi z ekosystemu Symfony. Najważniejsze różnice to:

  • hooki to funkcje proceduralne, subskrybery to klasy usług, co sprzyja programowaniu obiektowemu;
  • eventy lepiej integrują się z kontenerem DI – w subscriberze można wstrzykiwać serwisy, konfigurację, logger itp.;
  • priorytetyzacja jest czytelniejsza – kolejność wykonania nadawana jest liczbowo przy rejestracji zdarzeń;
  • nazwa eventu jest jednym, spójnym identyfikatorem, często stałą z klas core (np. KernelEvents::REQUEST).

Hooki nadal są użyteczne, zwłaszcza w miejscach ściśle związanych z API Drupala (np. hook_entity_type_build), ale w nowym kodzie opartym na Symfony i kontenerze warto preferować Event Subscriber, szczególnie gdy logika ma być modularna, testowalna i łatwo rozszerzalna.

Rodzaje zdarzeń w Drupal

W Drupal spotkasz kilka głównych kategorii eventów:

  • Eventy jądra Symfony – np. KernelEvents::REQUEST, KernelEvents::RESPONSE, KernelEvents::EXCEPTION; pozwalają reagować na poziomie cyklu życia żądania HTTP;
  • Eventy Drupala – generowane przez core, np. zdarzenia entity, user, routingu czy konfiguracji;
  • Eventy modułów – wiele popularnych modułów (np. Commerce, Paragraphs, Rules) wystawia własne eventy;
  • Własne eventy biznesowe – definiowane w Twoim module do obsługi niestandardowej logiki domenowej (np. OrderCompletedEvent).

Zrozumienie, z jakim typem eventu pracujesz, pozwala dobrać odpowiednie miejsce subskrypcji, poprawny typ klasy eventu oraz właściwy priorytet, aby nie kolidować z innymi modułami.

Zalety używania Event Subscriber

Najważniejsze korzyści z użycia subskryberów:

  • lepsza separacja logiki – kod reakcji na zdarzenie można odseparować od kontrolerów, formularzy czy hooków;
  • możliwość wstrzykiwania zależności przez kontener – logika staje się bardziej testowalna i łatwiejsza w utrzymaniu;
  • centralizacja reakcji na dany typ eventu – zamiast wielu rozproszonych ifów i hooków;
  • elastyczność – łatwo dodawać kolejne subskrybery reagujące na to samo zdarzenie, bez modyfikacji istniejącego kodu;
  • lepsze dopasowanie do współczesnych praktyk PHP i frameworków opartych na Symfony.

Kiedy stosować Event Subscriber w projektach Drupal

Reagowanie na zdarzenia systemowe (core i Symfony)

Event Subscriber sprawdza się szczególnie wtedy, gdy Twoja logika wiąże się z cyklem życia żądania HTTP lub operacjami wykonywanymi przez kernela. Przykładowe scenariusze:

  • modyfikacja odpowiedzi HTTP w ostatnim etapie (np. dodanie nagłówków bezpieczeństwa w KernelEvents::RESPONSE);
  • przekierowanie użytkownika przed obsługą kontrolera (np. wymuszenie akceptacji regulaminu przy pierwszym logowaniu, w KernelEvents::REQUEST);
  • niestandardowa obsługa błędów i wyjątków (np. logowanie rozszerzonych danych w KernelEvents::EXCEPTION);
  • dołączanie danych globalnych do wszystkich odpowiedzi HTML (np. banery, feature flags, tracking).

Hooki nie mają bezpośredniego dostępu do wszystkich etapów obsługi żądania. Wykorzystanie eventów Symfony pozwala wpływać na zachowanie aplikacji na bardzo niskim, ale bezpiecznym poziomie, bez modyfikacji core.

Integracja z zewnętrznymi systemami i mikroserwisami

W projektach, gdzie Drupal pełni rolę frontu lub centrum orkiestracji, Event Subscriber doskonale nadaje się do integracji z zewnętrznymi usługami:

  • po zapisaniu encji (np. artykułu, produktu) wyślij asynchroniczne żądanie do API (np. indeksacja w wyszukiwarce, notyfikacja w CRM);
  • po zalogowaniu użytkownika wywołaj synchronizację profilu z systemem SSO lub HR;
  • po złożeniu zamówienia w module Commerce wyślij dane do systemu fakturującego lub magazynowego.

Dzięki subskryberom integracja staje się niezależną warstwą – łatwo ją wyłączyć, zastąpić lub rozbudować, nie ruszając logiki core ani formularzy.

Implementacja logiki biznesowej i domenowej

W bardziej złożonych aplikacjach Drupal staje się pełnoprawnym backendem. W takim kontekście eventy są naturalnym sposobem modelowania logiki domenowej. Przykłady:

  • po utworzeniu konta użytkownika (np. UserCreateEvent) uruchom zestaw akcji: przydzielenie ról, zapis w CRM, wysyłka maili powitalnych;
  • po zmianie statusu zamówienia (OrderStatusChangedEvent) wywołaj zdarzenia finansowe, generuj dokumenty, odpal workflow powiadomień;
  • po publikacji treści (np. ArticlePublishedEvent) oznacz powiązane encje jako wymagające ponownego przeliczenia (cache, statystyki).

Tego typu zdarzenia mogą być również emitowane w Twoim module, a różne subskrybery – tworzone przez inne zespoły czy moduły – mogą na nie reagować, zachowując wysoką spójność i luźne powiązanie komponentów.

Optymalizacja, logowanie i monitorowanie

Eventy są świetnym miejscem na wdrożenie mechanizmów obserwowalności i optymalizacji:

  • logowanie statystyk wydajności na etapie KernelEvents::TERMINATE – po wysłaniu odpowiedzi do klienta;
  • dodawanie niestandardowych nagłówków diagnostycznych (np. wersja deployu, identyfikator requestu);
  • monitorowanie zachowań użytkowników (we współpracy z modułami emitującymi zdarzenia kliknięć, pobrań, przejść);
  • włączanie lub wyłączanie funkcji na podstawie warunków (feature toggles, A/B testing) obsługiwanych w subscriberach.

Odpowiednio napisany Event Subscriber może stać się centralnym elementem monitoringu, nie ingerując przy tym w poszczególne kontrolery ani szablony.

Jak zdefiniować i zarejestrować Event Subscriber

Struktura plików i nazewnictwo

W typowym module własnym (np. my_module) klasy subskryberów umieszcza się w katalogu:

my_module/src/EventSubscriber

Przykładowa nazwa pliku: MyModuleRequestSubscriber.php. Taka konwencja ułatwia automatyczne ładowanie klas oraz orientację w kodzie. W dużych projektach warto pogrupować subskrybery w podkatalogi (np. Http, Entity, Commerce), aby uniknąć przeładowania jednego katalogu.

Tworzenie klasy subskrybera

Klasa subskrybera zazwyczaj:

  • implementuje interfejs EventSubscriberInterface;
  • definiuje metodę static getSubscribedEvents, która zwraca tablicę mapującą nazwy eventów na metody;
  • zawiera metody obsługujące konkretne zdarzenia, przyjmujące obiekt eventu jako argument;
  • może przyjmować serwisy w konstruktorze (wstrzykiwane przez kontener usług).

Dzięki temu subscriber jest w pełni konfigurowalny, może korzystać z loggera, serwisu mailowego, usług domenowych itp., a jego implementacja pozostaje przejrzysta i spójna.

Rejestracja w kontenerze usług

Event Subscriber musi być zarejestrowany jako usługa z odpowiednim tagiem. W pliku my_module.services.yml dodajesz wpis, który określa:

  • id usługi (np. my_module.request_subscriber);
  • klasę PHP;
  • argumenty konstruktora (jeśli są potrzebne);
  • tag event_subscriber, informujący Drupala, że to subskryber.

Przykład (bez użycia cudzysłowów w tekście):

my_module.services.yml

services:
my_module.request_subscriber:
class: Drupal\my_module\EventSubscriber\MyModuleRequestSubscriber
arguments: [@logger.factory]
tags:
– { name: event_subscriber }

Dzięki tagowi event_subscriber Drupal automatycznie podłączy klasę do mechanizmu dispatchera i wywoła jej metody dla zadeklarowanych eventów.

Przykład prostego subskrybera

Załóżmy, że chcesz zalogować każdą wizytę na stronie, zanim kontroler zostanie wykonany. Tworzysz klasę:

src/EventSubscriber/MyModuleRequestSubscriber.php

namespace Drupal\my_module\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Psr\Log\LoggerInterface;

class MyModuleRequestSubscriber implements EventSubscriberInterface {
protected $logger;

public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}

public static function getSubscribedEvents() {
return [
KernelEvents::REQUEST => [onRequest, 0],
];
}

public function onRequest(RequestEvent $event) {
$request = $event->getRequest();
$this->logger->info(Request path: @path, [path => $request->getPathInfo()]);
}
}

Metoda getSubscribedEvents zwraca tablicę, gdzie kluczem jest nazwa eventu, a wartością – para nazwa_metody, priorytet. Im wyższa liczba priorytetu, tym wcześniej subscriber zostanie wywołany.

Praktyczne wzorce i dobre praktyki przy tworzeniu Event Subscriber

Wstrzykiwanie zależności i testowalność

Jedną z największych zalet subskryberów jest ścisła integracja z kontenerem DI. Zamiast używać statycznych wywołań (np. \Drupal::service), wstrzykuj w konstruktorze tylko te serwisy, których naprawdę potrzebujesz. Przykładowe zależności:

  • logger.factory – do logowania;
  • mailer – do wysyłki wiadomości;
  • entity_type.manager – do operacji na encjach;
  • config.factory – do pobierania konfiguracji modułu.

Taka konstrukcja ułatwia pisanie testów jednostkowych – możesz zastąpić prawdziwe serwisy atrapami, a subskryber testować w izolacji, nie uruchamiając całego Drupala.

Priorytety i kolejność wykonywania

Gdy kilka modułów subskrybuje ten sam event, istotna staje się kolejność ich wywoływania. Priorytet ustawiasz w getSubscribedEvents. Przykład:

  • KernelEvents::REQUEST => [onRequestEarly, 100];
  • KernelEvents::REQUEST => [onRequestLate, -100];

Wyższy priorytet (100) wykona się przed niższym (0, -100). Ustawiaj priorytety świadomie, zwłaszcza gdy Twój kod zależy od efektów innych subskryberów lub ma ingerować w kontroler, redirect czy dostęp do stron.

Unikanie logiki biznesowej w eventach core

Choć kuszące jest implementowanie pełnej logiki biznesowej bezpośrednio w subscriberach reagujących na eventy kernela, lepiej traktować je jako cienką warstwę transportową. Dobra praktyka to:

  • subscriber odbiera event i wyciąga z niego dane;
  • dane przekazuje do serwisu domenowego (np. OrderService, UserOnboardingManager);
  • serwis wykonuje właściwą, skomplikowaną logikę, niezależną od konkretnego typu eventu.

Taki podział sprawia, że logika biznesowa pozostaje re-używalna i łatwiejsza w testowaniu, a subskrybery są jedynie adapterami pomiędzy eventami a warstwą domenową.

Ostrożność w kontekście wydajności i błędów

Subskrybery wywoływane są bardzo często – np. event request może odpalać się przy każdym żądaniu. Nieodpowiedzialne dodanie kosztownych operacji (złożone zapytania, zewnętrzne API synchroniczne) szybko odbije się na wydajności. Zwróć uwagę na:

  • buforowanie wyników – wykorzystuj cache, jeśli ta sama logika wykonywana jest wielokrotnie;
  • wykonywanie ciężkich zadań asynchronicznie (kolejki, cron, worker) zamiast w trakcie requestu;
  • odporność na błędy – wyjątek w subscriberze może przerwać obsługę całego żądania; obsługuj błędy lokalnie, loguj je i w razie potrzeby degraduj funkcjonalność w kontrolowany sposób.

Warto też regularnie profilować kluczowe eventy (REQUEST, RESPONSE) i sprawdzać, które subskrybery zajmują najwięcej czasu, aby uniknąć niekontrolowanego spadku wydajności.

Tworzenie własnych eventów i rozszerzanie modułów

Definiowanie własnej klasy eventu

Aby wprowadzić własne eventy domenowe, definiujesz klasę, która przechowuje dane związane ze zdarzeniem. Przykład (uproszczony):

src/Event/OrderCompletedEvent.php

namespace Drupal\my_module\Event;
use Symfony\Contracts\EventDispatcher\Event;

class OrderCompletedEvent extends Event {
public const EVENT_NAME = my_module.order_completed;
protected $order;

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

public function getOrder() {
return $this->order;
}
}

Stała EVENT_NAME ułatwia odwoływanie się do eventu w kodzie. W klasie umieszczasz tylko te dane, które są potrzebne subskryberom.

Wywoływanie (dispatch) własnych eventów

Gdy chcesz wywołać event, korzystasz z usługi event_dispatcher. Najczęściej robisz to w serwisie, który zarządza logiką domenową, np. po pomyślnym zakończeniu procesu zakupu:

$event = new OrderCompletedEvent($order);
$dispatcher = \Drupal::service(event_dispatcher);
$dispatcher->dispatch($event, OrderCompletedEvent::EVENT_NAME);

W kodzie produkcyjnym lepiej unikać statycznych helperów i wstrzykiwać event_dispatcher przez konstruktor serwisu. Dzięki temu miejsce dispatchowania jest jeszcze bardziej spójne z architekturą DI.

Rozszerzanie modułów z pomocą eventów

Wiele modułów wystawia eventy specjalnie po to, by umożliwić innym modułom ingerencję w ich zachowanie bez konieczności modyfikowania kodu źródłowego. Przykładowe sytuacje:

  • moduł Commerce emituje event po złożeniu zamówienia – inny moduł może w tym momencie wygenerować kupon rabatowy;
  • moduł odpowiedzialny za import danych emituje event po wczytaniu encji – inne moduły mogą wzbogacić dane o dodatkowe informacje;
  • moduł workflow emituje event po zmianie stanu – subskrybery uruchamiają powiadomienia, integracje czy dodatkowe walidacje.

Dzięki takiemu podejściu ekosystem Drupala staje się bardziej modularny: moduły nie muszą wiedzieć o sobie nawzajem, wystarczy, że uzgodnią kontrakt w postaci nazw eventów i struktur danych przekazywanych przez klasy zdarzeń.

Wersjonowanie i stabilność własnych eventów

Jeśli tworzysz moduł, który będzie używany przez inne zespoły lub opublikowany jako open source, traktuj własne eventy jak część publicznego API. Oznacza to, że:

  • unikaj nagłych zmian w strukturze eventu (np. usuwania metod bez wersjonowania);
  • dokumentuj dostępne właściwości i gwarantowane zachowanie (kiedy event jest wywoływany, jakie dane są zawsze dostępne);
  • jeśli musisz wprowadzić zmiany niezgodne wstecz, rozważ nowe nazwy eventów lub nowe klasy zamiast łamania istniejącego kontraktu.

Takie podejście podnosi stabilność modułu i ułatwia innym programistom bezpieczne rozszerzanie jego funkcjonalności bez ryzyka, że aktualizacja nagle przerwie działanie ich subskryberów.

< Powrót

Zapisz się do newslettera


Zadzwoń Napisz