Event Subscriber i Event Dispatcher. Sistem rada sa događajima u Drupalu.
Pregled sistema događaja
Sistemi događaja se koriste u mnogim složenim aplikacijama kao način da ekstenzije mogu menjati rad sistema. Sistem događaja može biti implementiran na različite načine, ali generalno su koncepti i komponente koje čine sistem iste.
- Pretplatnici na događaje (Event Subscribers) - ponekad nazivani „slušačima“, su metode ili funkcije koje se pozivaju kao odgovor na događaj koji se širi kroz registar događaja.
- Registar događaja (Event Registry) - mesto gde se prikupljaju i sortiraju pretplatnici događaja.
- Dispatcher događaja (Event Dispatcher) - mehanizam putem kojeg se događaj inicira ili „šalje“ kroz sistem.
- Kontekst događaja (Event Context) - za mnoge događaje je potreban određeni skup podataka važan za pretplatnike na događaj. To može biti jednostavna vrednost prosleđena pretplatniku ili kompleksan objekat, na primer specijalno kreirana klasa koja sadrži relevantne podatke.
Drupal Hook-ovi
Veći deo svog postojanja Drupal je imao rudimentarni sistem događaja preko „hook-ova“. Pogledajmo kako se pojam „hook-ova“ razlaže na ovih 4 elementa sistema događaja.
- Pretplatnici na događaje - Drupal hook-ovi se registruju definisanjem funkcije sa određenim imenom. Na primer, ako želite da se pretplatite na događaj „hook_my_event_name“, morate definisati funkciju myprefix_my_event_name(), gde je „myprefix“ ime vašeg modula ili teme.
- Registar događaja - Drupal hook-ovi se čuvaju u kešu „cache_bootstrap“ pod identifikatorom „module_implements“. To je niz modula koji implementiraju hook označen imenom tog hook-a.
- Dispatcher događaja - hook-ovi se pozivaju drugačije u Drupal 7 i Drupal 8:
1) Drupal 7: hook-ovi se šalju pomoću funkcije module_invoke_all()
2) Drupal 8: hook-ovi se šalju preko servisnog metoda \Drupal::moduleHandler()->invokeAll().
- Kontekst događaja - kontekst se prosleđuje pretplatniku kroz parametre. Na primer, dispatcher će izvršiti sve implementacije "hook_my_event_name" i proslediti parametar $some_arbitrary_parameter:
1) Drupal 7: module_invoke_all('my_event_name', $some_arbitrary_parameter);
2) Drupal 8: \Drupal::moduleHandler()->invokeAll('my_event_name', [$some_arbitrary_parameter]);
Neki nedostaci „hook“ pristupa događajima:
- Registruju događaje samo tokom ponovnog građenja keša.
Generalno, Drupal traži nove hook-ove samo prilikom izgradnje određenih keševa. To znači da ako želite da implementirate novi hook na sajtu, moraćete da rebuildujete različite keševe u zavisnosti od hook-a koji implementirate.
- Mogu reagovati na svaki događaj samo jednom po modulu.
Budući da se ovi događaji implementiraju definisanjem vrlo specifičnih funkcija, u svakom modulu ili temi može postojati samo jedna implementacija događaja. To je arbitrarno ograničenje u poređenju sa drugim sistemima događaja.
- Nije moguće lako odrediti redosled izvršavanja događaja.
Drupal određuje redosled pretplatnika kroz težine modula u sistemu. Moduli i teme imaju „težinu“ koja definiše redosled učitavanja modula i shodno tome redosled izvršavanja događaja. Zaobilaženje ovog problema dodato je kasnije u Drupalu 7 pomoću hook_module_implements_alter, drugog hook-a na koji modul može da se pretplati ako želi da menja redosled izvršavanja hook-ova bez menjanja težine modula.
Sa Symfony osnovom u Drupalu 8 sada postoji novi sistem događaja. Bolji sistem događaja u većini slučajeva. Iako u jezgru Drupala 8 nema mnogo događaja, mnogi moduli su počeli da koriste ovaj sistem.
Drupal 8 događaji
Događaji u Drupalu 8 su veoma slični Symfony događajima. Pogledajmo kako se oni uklapaju u naš spisak komponenti sistema događaja.
- Pretplatnici na događaje - klase koje implementiraju \Symfony\Component\EventDispatcher\EventSubscriberInterface.
- Dispatcher događaja - klasa koja implementira \Symfony\Component\EventDispatcher\EventDispatcherInterface. Obično postoji barem jedna instanca dispatcher-a kao servis, ali po potrebi mogu se kreirati i drugi dispatcher-i.
- Registar događaja - registar pretplatnika se čuva u objektu „Dispatcher događaja“ kao niz koji sadrži ime događaja i prioritet (redosled). Prilikom registracije događaja kao servisa, on se registruje u globalno dostupnom dispatcher-u.
- Kontekst događaja - klasa koja nasleđuje \Symfony\Component\EventDispatcher\Event. Obično svako proširenje koje šalje sopstveni događaj kreira novi tip Event klase koji sadrži relevantne podatke za pretplatnike.
Učenje kako koristiti Drupal 8 događaje pomoći će vam da bolje razumete razvoj korisničkih modula i pripremiće vas za budućnost gde će, nadamo se, događaji zameniti hook-ove. Pa, hajde da napravimo prilagođeni modul koji pokazuje kako koristiti svaku od ovih komponenti događaja u Drupalu 8.
Moj prvi Drupal 8 pretplatnik na događaje
Napravimo naš prvi pretplatnik na događaje u Drupalu 8 koristeći neke osnovne događaje. Volim da započnem sa nečim jednostavnim, pa ćemo napraviti pretplatnika koji prikazuje poruku korisniku kada se Config objekat sačuva ili obriše.
Prvo što nam treba je modul u kojem ćemo raditi. Ja sam ga nazvao custom_events.
name: Custom Events type: module description: Custom/Example event work. core: 8.x package: Custom
Sledeći korak je da registrujemo novog pretplatnika na događaje u Drupalu. Za to nam treba fajl custom_events.services.yml. Ako dolazite iz Drupala 7 i poznajete hook sistem, ovaj korak je kao pisanje funkcije "hook_my_event_name" u vašem modulu ili temi.
services: # Ime ovog servisa. my_config_events_subscriber: # Klasa pretplatnika na događaje koja će slušati događaje. class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber' # Obeležen kao event_subscriber da se registruje kod event_dispatch servisa. tags: - { name: 'event_subscriber' }
Prilično jednostavno, ali da razjasnimo:
1) Definišemo novi servis sa imenom "my_config_events_subscriber".
2) Postavljamo svojstvo "class" na globalno ime nove PHP klase koju ćemo kreirati.
3) Definišemo svojstvo „tags“ i dodajemo tag „event_subscriber“. To registruje servis kao pretplatnika na događaje.
Alternativno, možete koristiti PHP klasu pretplatnika kao ime servisa i preskočiti svojstvo „class“, na primer:
services: # Ime servisa koristeći klasu pretplatnika. Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber: tags: - { name: 'event_subscriber' }
Sada treba samo da napišemo klasu pretplatnika na događaje. Ova klasa mora da:
1. Implementira interfejs EventSubscriberInterface.
2. Ima metodu getSubscribedEvents() koja vraća niz gde su ključevi imena događaja na koje se pretplaćuje, a vrednosti su imena metoda za te događaje.
Ovo je naš pretplatnik koji se pretplaćuje na događaje klase ConfigEvents i izvršava lokalne metode za svaki događaj.
src/EventSubscriber/ConfigEventsSubscriber.php
<?php namespace Drupal\custom_events\EventSubscriber; use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Class ConfigEventsSubscriber. * * @package Drupal\custom_events\EventSubscriber */ class ConfigEventsSubscriber implements EventSubscriberInterface { /** * {@inheritdoc} * * @return array * Imena događaja za koje se sluša i metode koje treba izvršiti. */ public static function getSubscribedEvents() { return [ ConfigEvents::SAVE => 'configSave', ConfigEvents::DELETE => 'configDelete', ]; } /** * Reaguje na čuvanje Config objekta. * * @param \Drupal\Core\Config\ConfigCrudEvent $event * Config CRUD događaj. */ public function configSave(ConfigCrudEvent $event) { $config = $event->getConfig(); \Drupal::messenger()->addStatus('Sačuvana konfiguracija: ' . $config->getName()); } /** * Reaguje na brisanje Config objekta. * * @param \Drupal\Core\Config\ConfigCrudEvent $event * Config CRUD događaj. */ public function configDelete(ConfigCrudEvent $event) { $config = $event->getConfig(); \Drupal::messenger()->addStatus('Obrisana konfiguracija: ' . $config->getName()); } }
To je to! Izgleda prilično jednostavno, ali hajde da istaknemo važne detalje:
- Implementiramo EventSubscriberInterface.
- Implementiramo metodu getSubscribedEvents(), koja vraća niz parova ime događaja => ime metode.
- U metodama configSave() i configDelete() očekujemo objekat tipa ConfigCrudEvent, koji ima metodu getConfig() za dobijanje Config objekta.
Moguća pitanja koja se javljaju:
- Šta je ConfigEvents::SAVE i odakle dolazi?
Obično, pri definisanju novih događaja, kreira se globalno dostupna konstanta sa imenom događaja. Ovde \Drupal\Core\Config\ConfigEvents ima konstantu SAVE, koja je vrednost „config.save“.
- Zašto očekujemo objekat ConfigCrudEvent i kako smo to znali?
Prilikom definisanja događaja, obično se kreira poseban tip objekta događaja koji sadrži potrebne podatke i ima jasan API za pristup tim podacima. U ovom slučaju smo saznali očekivani objekat pregledom koda i dokumentacije.
Sada možemo da uključimo modul i testiramo događaj. Očekujemo da kad god se Config objekat sačuva ili obriše u Drupalu, korisnik vidi poruku sa imenom Config objekta.
Budući da su Config objekti veoma česti u Drupalu 8, lako je isprobati ovo. Većina modula koristi Config objekte za svoje postavke, tako da možemo instalirati i ukloniti modul i videti koje Config objekte čuvaju i brišu.
1. Instalirajte modul "custom_events".
2. Instalirajte modul "statistics".
Poruka nakon instalacije modula statistics.
Izgleda da su dva Config objekta sačuvana! Prvi je core.extension koji upravlja instaliranim modulima i temama, a drugi statistics.settings.
3. Deinstalirajte modul "statistics".
Poruka nakon deinstalacije modula statistics.
Sada vidimo oba događaja SAVE i DELETE. Vidimo da je Config objekat statistics.settings obrisan, a core.extension sačuvan.
To bih nazvao uspehom! Uspešno smo se pretplatili na dva osnovna Drupal događaja.
Sada hajde da pogledamo kako da kreiramo sopstvene događaje i šaljemo ih drugim modulima na korišćenje.
Moj prvi Drupal 8 događaj i slanje događaja
Prvo treba da odlučimo kakav tip događaja ćemo slati i kada ćemo ga poslati. Napravićemo događaj za Drupal hook koji u jezgru nema svoj događaj - "hook_user_login".
Počnimo kreiranjem nove klase koja nasleđuje Event, nazvaćemo je UserLoginEvent. Takođe ćemo definisati globalno dostupno ime događaja za pretplatnike.
src/Event/UserLoginEvent.php
<?php namespace Drupal\custom_events\Event; use Drupal\user\UserInterface; use Symfony\Component\EventDispatcher\Event; /** * Događaj koji se aktivira kada se korisnik prijavi. */ class UserLoginEvent extends Event { const EVENT_NAME = 'custom_events_user_login'; /** * Korisnički nalog. * * @var \Drupal\user\UserInterface */ public $account; /** * Konstruktor objekta. * * @param \Drupal\user\UserInterface $account * Nalog korisnika koji se prijavio. */ public function __construct(UserInterface $account) { $this->account = $account; } }
- UserLoginEvent::EVENT_NAME je konstanta sa vrednošću „custom_events_user_login“. To je ime našeg novog prilagođenog događaja.
- Konstruktor prima objekat UserInterface i čuva ga kao svojstvo događaja. Ovo omogućava pretplatnicima da pristupe korisničkom nalogu preko $account.
To je to!
Sada samo treba da pošaljemo ovaj događaj. Uradićemo to u implementaciji "hook_user_login". Kreirajte fajl custom_events.module.
<?php /** * @file * Sadrži custom_events.module. */ use Drupal\custom_events\Event\UserLoginEvent; /** * Implementira hook_user_login(). */ function custom_events_user_login($account) { // Kreiraj događaj. $event = new UserLoginEvent($account); // Dobij servis event_dispatcher i pošalji događaj. $event_dispatcher = \Drupal::service('event_dispatcher'); $event_dispatcher->dispatch(UserLoginEvent::EVENT_NAME, $event); }
U našoj implementaciji "hook_user_login" potrebno je samo nekoliko koraka da pošaljemo novi događaj:
1. Kreiramo novi objekat UserLoginEvent i prosleđujemo mu $account.
2. Dohvatimo servis event_dispatcher.
3. Pozovemo metodu dispatch() servisa event_dispatcher sa imenom događaja i objektom događaja.
To je to! Sada šaljemo naš prilagođeni događaj svaki put kada se korisnik prijavi.
Zatim ćemo završiti primer kreiranjem pretplatnika na ovaj događaj. Prvo treba da dodamo pretplatnika u services.yml.
services: # Servis pretplatnika na događaje za config. my_config_events_subscriber: class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber' tags: - { name: 'event_subscriber' } # Pretplatnik na događaj koji šaljemo u hook_user_login. custom_events_user_login: class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriber' tags: - { name: 'event_subscriber' }
Poput ranije, definišemo novi servis i označavamo ga kao event_subscriber. Sada treba da napišemo ovu klasu pretplatnika.
src/EventSubscriber/UserLoginSubscriber.php
<?php namespace Drupal\custom_events\EventSubscriber; use Drupal\custom_events\Event\UserLoginEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Klasa UserLoginSubscriber. * * @package Drupal\custom_events\EventSubscriber */ class UserLoginSubscriber implements EventSubscriberInterface { /** * Konekcija ka bazi podataka. * * @var \Drupal\Core\Database\Connection */ protected $database; /** * Formatter za datum. * * @var \Drupal\Core\Datetime\DateFormatterInterface */ protected $dateFormatter; /** * {@inheritdoc} */ public static function getSubscribedEvents() { return [ // Staticka konstanta događaja => metoda u ovoj klasi. UserLoginEvent::EVENT_NAME => 'onUserLogin', ]; } /** * Pretplata na događaj prijave korisnika. * * @param \Drupal\custom_events\Event\UserLoginEvent $event * Objekat događaja. */ public function onUserLogin(UserLoginEvent $event) { $database = \Drupal::database(); $dateFormatter = \Drupal::service('date.formatter'); $account_created = $database->select('users_field_data', 'ud') ->fields('ud', ['created']) ->condition('ud.uid', $event->account->id()) ->execute() ->fetchField(); \Drupal::messenger()->addStatus(t('Dobrodošli, vaš nalog je kreiran %created_date.', [ '%created_date' => $dateFormatter->format($account_created, 'short'), ])); } }
Objašnjenje:
1. Pretplaćujemo se na događaj UserLoginEvent::EVENT_NAME metodom onUserLogin() koju smo kreirali.
2. U onUserLogin pristupamo svojstvu $account objekta $event i radimo neke radnje.
3. Kada se korisnik prijavi, vidiće poruku sa datumom kreiranja naloga.
Poruka nakon prijave.
Vuala! Poslali smo novi prilagođeni događaj i pretplatili se na njega. Sjajno!
Prioriteti pretplatnika na događaje
Još jedna sjajna osobina sistema događaja je da pretplatnik može da postavi sopstveni prioritet unutar sebe, umesto da menja težinu modula ili koristi dodatne hook-ove za promenu prioriteta (kao što je to bilo kod hook-ova).
To je lako učiniti, ali da bismo to najbolje demonstrirali, napišimo drugog pretplatnika na događaje kada već imamo jednog. Napisaćemo „AnotherConfigEventSubscriber“ i postaviti prioritete za njegove slušače.
Prvo ćemo registrovati novog pretplatnika u services.yml:
services: # Servis pretplatnika za config događaje. my_config_events_subscriber: class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber' tags: - { name: 'event_subscriber' } # Pretplatnik na događaj iz hook_user_login. custom_events_user_login: class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriber' tags: - { name: 'event_subscriber' } another_config_events_subscriber: class: '\Drupal\custom_events\EventSubscriber\AnotherConfigEventsSubscriber' tags: - { name: 'event_subscriber' }
Zatim ćemo napisati fajl AnotherConfigEventSubscriber.php:
<?php namespace Drupal\custom_events\EventSubscriber; use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Class AnotherConfigEventsSubscriber. * * @package Drupal\custom_events\EventSubscriber */ class AnotherConfigEventsSubscriber implements EventSubscriberInterface { /** * {@inheritdoc} * * @return array * Imena događaja na koje sluša i metode koje treba izvršiti. */ public static function getSubscribedEvents() { return [ ConfigEvents::SAVE => ['configSave', 100], ConfigEvents::DELETE => ['configDelete', -100], ]; } /** * Reaguje na čuvanje Config objekta. * * @param \Drupal\Core\Config\ConfigCrudEvent $event * Config CRUD događaj. */ public function configSave(ConfigCrudEvent $event) { $config = $event->getConfig(); \Drupal::messenger()->addStatus('(Another) Sačuvana konfiguracija: ' . $config->getName()); } /** * Reaguje na brisanje Config objekta. * * @param \Drupal\Core\Config\ConfigCrudEvent $event * Config CRUD događaj. */ public function configDelete(ConfigCrudEvent $event) { $config = $event->getConfig(); \Drupal::messenger()->addStatus('(Another) Obrišana konfiguracija: ' . $config->getName()); } }
Jedina važna razlika je što smo promenili niz koji vraća metoda getSubscribedEvents(). Umesto da kao vrednost ima samo ime metode, sada je niz gde je prvi element ime metode, a drugi prioritet pretplatnika.
Promenili smo ovo:
public static function getSubscribedEvents() { return [ ConfigEvents::SAVE => 'configSave', ConfigEvents::DELETE => 'configDelete', ]; }
U ovo:
public static function getSubscribedEvents() { return [ ConfigEvents::SAVE => ['configSave', 100], ConfigEvents::DELETE => ['configDelete', -100], ]; }
Rezultati koje očekujemo:
- AnotherConfigEventSubscriber::configSave() ima veoma visok prioritet i treba da se izvrši pre ConfigEventsSubscriber::configSave().
- AnotherConfigEventSubscriber::configDelete() ima nizak prioritet i treba da se izvrši posle ConfigEventsSubscriber::configDelete().
Pogledajmo događaj SAVE u praksi ponovo uključivanjem modula statistics.
Instalacija modula Statistics i prikaz poruka.
Odlično! Naš novi pretplatnik na ConfigEvents::SAVE se izvršio pre onog koji smo ranije napisali. Sada uklonimo modul statistics i pogledajmo događaj DELETE.
Deinstalacija modula statistics i prikaz poruka.
Takođe odlično! Naš novi pretplatnik na ConfigEvents::DELETE se izvršio nakon onog drugog jer ima nizak prioritet.
Napomena: Kada se pretplatnik na događaj registruje bez navedenog prioriteta, podrazumevana vrednost je 0.
Reference:
- GitHub repozitorijum - sadrži sav radni kod iz ovog vodiča.
- Symfony dokumentacija: Event Listeners i Subscribers - imajte u vidu da Drupal 8 ne koristi „event listeners“ u smislu Symfony, fokus je na event subscribers.
- Symfony dokumentacija: Event Dispatcher
- Originalni blog post - veoma sličan ovom vodiču, sadrži dodatne informacije o budućnosti događaja u Drupalu.
Drupal’s online documentation is © 2000-2020 by the individual contributors and can be used in accordance with the Creative Commons License, Attribution-ShareAlike 2.0. PHP code is distributed under the GNU General Public License.