Symfony po krůčkách – Event Dispatcher

Dnes se spolu podíváme na EventDispatcher. Jde o komponentu, která dodá tvému kódu flexibilitu. Zároveň je jednou z nejdůležitějších součástek životního cyklu Symfony. Když pochopíš EventDispatcher, budeš zase o kousek blíž k tomu stát se opravdovým mistrem Symfony.
Seriál: Symfony po krůčkách (18 dílů)
- Symfony po krůčkách – Event Dispatcher 30. 11. 2015
- Symfony Console jako první rande se Symfony 7. 12. 2015
Nálepky:
„Cesta dlouhá tisíc mil začíná prvním krokem.“ Confucius
Hlavní pojmy
Event
…neboli událost. Jde o něco, co může nastat při běhu aplikace. Typickým příkladem je objednávka. Když dojde k odeslání objednávky, tak se zavolá Event. Na Event odeslání objednávky pak může slyšet několik EventSubscriberů.
EventSubscriber
…může poslat e-mail adminovi, přičíst kredity za úspěšný nákup, nebo poslat informační sms do skladu s pokynem k zabalení tvých vánočních dárků.
EventDispatcher
…ten se stará o zavolání EventSubscriberů, když nastane určitý Event.
Kde můžeš EventDispatcher najít?
- Symfony\HttpKernel – Controller event
- Symfony\Console – Command event
- Zenify\DoctrineMigrations – SetConsoleOutputEventSubscriber – Doctrine\Migrations pro Nette
Co ti EventDispatcher umožní?
- Dostat se na určité místo v kódu bez nutnosti jeho změny
- Zvýšit flexibilitu a použitelnost tvé aplikace
Jak to aplikovat v kódu?
Symfony\EventDispatcher nainstaluješ pomocí Composeru:
$ composer require symfony/event-dispatcher
Vytvoříš si souborindex.php
<?php
require_once __DIR__ . '/vendor/autoload.php';
$eventDispatcher = new Symfony\Component\EventDispatcher\EventDispatcher;
// dispatchneme event někde v kódu na konci objednávky
$eventDispatcher->dispatch('order.finish');
A spustíš:
$ php index.php
Dispatchneš Event, ale nic se nestane. Aby se něco stalo, bude potřeba ještě EventSubscriber – ten bude naslouchat na order.finish
.
Přidáš tedy EventSubscriber.
<?php
class SendEmailToAdminEventSubscriber implements Symfony\Component\EventDispatcher\EventSubscriberInterface
{
public $signal = 0;
public static function getSubscribedEvents()
{
// tady budeme poslouchat "order.finish" event
// a pokud nastane, použijeme metodu sendEmailToAdmin()
return ['order.finish' => 'sendEmailToAdmin'];
}
public function sendEmailToAdmin()
{
// náš kód, který pošle e-mail adminovi
$this->signal = 1;
}
}
Nakonec přidáš EventSubscriber do EventDispatcheru:
<?php
$sendEmailToAdminEventSubscriber = new SendEmailToAdminEventSubscriber;
$eventDispatcher = new Symfony\Component\EventDispatcher\EventDispatcher;
$eventDispatcher->addSubscriber($sendEmailToAdminEventSubscriber);
var_dump($sendEmailToAdminEventSubscriber->signal);
$eventDispatcher->dispatch('order.finish');
var_dump($sendEmailToAdminEventSubscriber->signal);
A opět spustíš:
$ php index.php
int(0)
int(1)
Teď, když se ti dispatchne order.finish
Event, zavolá se každý EventSubcriber, který se k němu zapsal. V něm se zavolá metoda, která je k němu přiřazena. Dojde tak ke změně $signal
z 0
na 1
.
Pro tip: Metoda getSubscribedEvents() může naslouchat více Eventům, více metodami. Může také určovat jejich pořadí.
Nyní už rozumíš Symfony komponentě EventDispatcher.
Event s argumenty
Při volání události obvykle potřebuješ předat i nějaká data. Například číslo objednávky. Taková třída Event je vlastně pouhý Value object – schránka na data.
<?php
class OrderEvent extends Symfony\Component\EventDispatcher\Event
{
private $orderId;
public function __construct($orderId)
{
$this->orderId = $orderId;
}
public function getOrderId()
{
return $this->orderId;
}
}
Dispatchneš event i s potřebnými daty.
<?php
$orderEvent = new OrderEvent(123);
$eventDispatcher->dispatch('order.finish', $orderEvent);
Rozšíříš EventSubscriber o OrderEvent:
<?php
class SendEmailToAdminEventSubscriber
implements Symfony\Component\EventDispatcher\EventSubscriberInterface
{
public $signal = 0;
public static function getSubscribedEvents()
{
return ['order.finish' => 'sendEmailToAdmin'];
}
public function sendEmailToAdmin(OrderEvent $orderEvent)
{
$this->signal = $orderEvent->getOrderId();
}
}
A doplníš svůj výsledný kód:
$eventDispatcher = new Symfony\Component\EventDispatcher\EventDispatcher;
$sendEmailToAdminEventSubscriber = new SendEmailToAdminEventSubscriber;
$eventDispatcher->addSubscriber($sendEmailToAdminEventSubscriber);
var_dump($sendEmailToAdminEventSubscriber->signal);
$orderEvent = new OrderEvent(123);
$eventDispatcher->dispatch('order.finish', $orderEvent);
var_dump($sendEmailToAdminEventSubscriber->signal);
Výstup pak vypadá takto:
$ event-dispatcher php index.php
int(0)
int(123)
Jsi zase o krok dál
Teď už:
- rozumíš základním workflow událostí
- znáš pojmy Event, EventSubscriber a EventDispatcher
- víš, k čemu využít vlastní Event objekt
- …a umíš použít EventDispatcher prakticky ve svém kódu
Potřebuješ víc?
Pokud bys potřeboval jít ve vysvětlování do větší hloubky, mrkni na oficiální dokumentaci EventDispatcheru.
Už v Symfony děláš léta a chceš posdílet zkušenosti?
Nebo tě Symfony zatím jen láká a rád by se o něm dozvěděl více? Přijd si pokecat na pravidelné měsíční srazy v Praze a Brně.
Díky za článek!
Jen by mě zajímalo, proč jsi začal zrovna s eventy?
Právě eventy totiž přinášejí Symfony největší flexibilitu a efektivitu poměru cena/výkon.
Můžeš rozšířit cizí kód, aniž bys do něj zasahoval.
Ve full-stack frameworku jich najdeš mnoho. Stejně tak v Symfony\Console, Symfony\Security, Composeru, bundlech…
Jestli tě zajímají do hloubky, mrkni na Year with Symfony. Tam jsou rozebrány do hloubky.
Super super, děkuji :)
Proč se to nedělá jednodušeji? Např. nějak podobně jako v Nette:
$order->onFinish = function() { sendMailToAdmin(); }
1) Nemusím do kódu orderu přidávat onFinish. 2) V momentě dispatchování akce nemusím mít vytvořený objekt $order.
Ad 1) No ale musím v kódu orderu zavolat $eventDispatcher->dispatch(‚order.finish‘); ne? Tedy pokud tomu správně rozumím.
Ad 2) V momentě dispatchování snad order vytvořený být musí? Nemusí být vytvořený v momentě subscribování, to ano, v tom je rozdíl.
Mě totiž symfony obecně připadá (podobně jako jiné fw) zbytečně složité a komplikované (což může být můj problém), takže jsem na tomhle případu zajímal, co mi ta větší komplexita přinese.
Hm, no tak v případě symfony musí ten objekt odněkud získat objekt EventDispatcher a použít ho, v případě Nette se musí podědit z Nette/Object. Je to trochu jiný typ závislosti, ale zásadní rozdíl v tom aspoň já nevidím. Jakékoliv události lze v Nette obsloužit taky, jakákoliv data lze předat v eventu taky. Ale ok, už toho nechme.
Hlavní důvod, proč jsem EventDispatcher integroval do Nette, je možnost využít události mimo Nette ekosystém. Můžeš tedy využít eventy v Symfony, Composeru, Consoli apod. a spoustu dalších, které událost definiují jenom jako string, nevyžadují property ani další magii.
Zároveň můžeš použít události životního cyklu Nette aplikace a to obojí pěkně v jedné službě.
Jak by ses třeba dostal k události ‚console.command. v Symfony\Console?
Ok, tento důvod chápu – pokud mám spoustu kódu třetích stran, který využívá symfony EventDispatcher, tak pochopitelně musím používat EventDispatcher, když s ním pracuji.
Symfony je zaměřeno na střední a větší aplikace, kde je mnohem větší důraz na čitelnost a srozumitelnost kódu.
Z tvého jednoduchého kódu lze vyčíst pouze to, že nějaká property má v sobě callback. Co se děje dál není jasné a vyžaduje další znalost (coupling) Nette a/nebo jiného rozšíření.
To je imho největší rozdíl. Syntaxe je implementační detail.
V praxi jsem si osvědčil, že začátečník mnohem lépe chápe třídu, která volá metodu s argumenty, než přířazení callbacku do property. Kolegové v práci tak pochopili EventDispatcher hned.
Tak je pravda, že Nette řešení využívá určitou magii rozšiřující vlastnosti php, ale přiřazení callbacku do onFinish a pak volání $this->onFinish($parametry) ve třídě mi připadá značně intuitivní a v řadě jazyků (javascript) lze něco podobného udělat implicitně.
Ale budiž, co je čitelné a srozumitelné je subjektivní věc, nechci se přit.
Já osobně vidím největší potenciální výhodu toho symfony systému v tom, (už to někdo zmiňoval), že ta obsluha výjjimek je mimo využívající objekt, který ji pouze odněkud převezme a pak použije a teoreticky mu můžu například dodat i nějakou jinou implementaci EventDispatcheru.
Takže shrnuto vidím to tak, že symfony je víc „decoupled“ a potenciálně o něco flexibilnější, nette je (alespoň pro mě) v tomhle jednodušší a stručnější.
Ja tomu asi moc nerozumim. Objekt subscriberu je do dispatcheru pridavan ve chvili, kdy je event „vyhozen“. Ale v tu chvili preci nemuze programator vedet, jake vsechny subscibery budou v budoucnu event odchytavat a novy programator, ktery subsciber vytvari, pak naopak (pravdepodobne) nemuze upravovat misto, kde byl event dispatchnut.