Události Kdyby/Events v Kdyby/Doctrine
- ondrej256
- Člen | 187
Zdravím,
začal jsem používat doctrinu a chtěl bych využít události postPersist na uložení objednávky.
class OrderSubscriber implements EventSubscriber
{
public function postPersist(LifecycleEventArgs $eventArgs)
{
// odeslat sms
}
public function getSubscribedEvents()
{
return array(Events::postPersist);
}
}
- Jak docílím toho aby listener posloouchal pouze na uložení entity Order? Teď listener přece poslouchá na uložení jakékoliv entitty nebo ne?
- Jak zaregistruju na entityManager daný listener?
Našel jsem, že se to dělá takto:
$em->getEventManager()->addEventSubscriber($helloWorldSubscriber);
Kde mám ale přidání tohoto listeneru dělat?
Předem děkuju za odpověď
Editoval ondrej256 (18. 10. 2016 13:58)
- jiri.pudil
- Nette Blogger | 1032
- Ten
$eventArgs
objekt má metodugetEntity()
, nad kterou si můžeš udělatinstanceof
check. - Nejsnáz přes neon konfiguraci. Uznávám, že v dokumentaci
kdyby/events
je to trochu schované, ale je to tam :)
- David Matějka
- Moderator | 6445
muzes taky pouzit entity listenery, ktere se vazou na konkretni entitu: http://docs.doctrine-project.org/…/events.html#…
v kdyby/doctrine staci takovy listener registrovat jako sluzbu
- Svaťa Šimara
- Člen | 98
fizzy napsal(a):
inak neviem ci je dobre riesenie pouzivat lifecycle eventy v logike aplikacie, snazim sa tomu vyhybat :)
Dobrá připomínka. Jak píše ocramius (člen core Doctrine týmu), doctrine události by se neměly používat pro doménové události http://ocramius.github.io/…t-practices/#/59
Řešení jsem vyzkoušel několik
- eventDispatcher volaný v entitě jako singleton – nejde mockovat, skrytá závislost, fail :-(
- eventDispatcher předávaný do entit – doménová entita závislá na dispatcheru – fail :-(
- eventDispatcher předaný do doménové služby, doménová služba vyvolává eventy – entitu můžu používat i mimo doménovou službu, eventy se potom nevyvolávají – fail :-(
- eventy entita vyvolává sama do sebe a o zpracování se stará infrastruktura – WAT? Entita ukládá události sama do sebe – Zatím používám a nejsem spokojen :-(
Někdo něco lepšího?
Kdyby/Events prosím ne – magic, magic, magic. To už raději cokoliv výše
zmíněného.
- Jan Mikeš
- Člen | 771
@Fafin podělím se o řešení, jak řeším eventy v Entitách já – neříkám, že je to perfektní, ale zatím mi vyhovuje, za jakékoliv návrhy jak to vylepšit budu rád.
Používám kombinaci entity listenerů a subscribery.
Pro ukázku mějme entitu Article a chci stopovat, kdy si ji nějaký uživatel
přečetl (otevřel).
class Article
{
/** @var User[] */
public $readByUsers = []; // pro zjednodušení nechávám public
public function readByUser(User $user)
{
$this->readByUsers[] = $user;
}
}
Sem je to zatím OK a mám pocit, že nemíchám doménu s infrastrukturou a kód bude v pohodě fungovat i bez ORM (Používám XML mapping).
A teď začíná magie :-). Zaregistruji si listener pro article entitu, nastavim entitě v xml tento listener atd..
services:
- Listeners\ArticleListener
class ArticleListener
{
use SmartObject; // Kvůli nette eventům
public $onReadByUser = [];
public function preFlush(Article $article)
{
foreach ($article->readByUsers as $user) {
$this->onReadByUser($this, $user);
}
}
PS. kdyby někdo věděl, jak v PhpStormu na řádku
$this->onReadByUser(..)
zrušit čistě pomocí anotací warning
Method ... not found in ArticleListener
budu rád!
V tomto stavu mám listener, který vyvolává event, ale ještě nemá
nastaven žádný handler. Samozřejmě jsem si přes DI mohl v konstruktoru
předat závislost a rovnou zpracovávat v listeneru, to se mi ale nelíbí,
proto využiju Kdyby\Events
a vytvořím si subscriber.
class ArticleReadSubcriber implements Subscriber
{
private $myService;
// constructor, předání závislostí ..
public function getSubscribedEvents()
{
return ["Listeners\ArticleListener::onReadByUser"]; // Zde by se dala využít konstanta
}
public function onReadByUser(Article $article, User $user)
{
$this->myService->addToLog($article, $user);
}
}
Bohužel je tam tedy ta závislost v listeneru na ORM eventu
preFlush()
, ale toto řešení mi velmi dobře funguje.
TLDR. listenery používám jen jako prostředníka pro vytvoření „opravdového eventu“, který pak zpracuji pomocí Kdyby\Events.
Editoval Jan Mikeš (21. 10. 2016 11:41)
- jiri.pudil
- Nette Blogger | 1032
PS. kdyby někdo věděl, jak v PhpStormu na řádku
$this->onReadByUser(..)
zrušit čistě pomocí anotací warningMethod ... not found in ArticleListener
budu rád!
Přidej si nad třídu patřičnou @method
anotaci :)
- Svaťa Šimara
- Člen | 98
@JanMikeš Tak se dívám, že řešení máš podobné tomu, co aktuálně používám – entita si pamatuje, co se stalo, a o zpracování událostí se stará infrastruktura. Ale když jsi tak pěkně napsal, jak pracuješ, napíšu taky kousek kódu…
interface DomainEvent { }
interface Eventing {
/**
* @return DomainEvent[]
*/
public function popEvents(): array;
}
class CustomerChangedEvent implements DomainEvent {
...
public function getCustomerId(): CustomerId {...}
}
class Customer implements Eventing {
/**
* @var DomainEvent[]
*/
private $events = [];
public function popEvents(): array {
$events = $this->events;
$this->events = [];
return $events;
}
private function raise(DomainEvent $event) {
$this->events[] = $event;
}
public function addAddress(...) {
...
$this->raise(new CustomerChangedEvent($this->customerId));
}
}
Tím je vyřešena doména. V infrastruktuře mám Doctrine EventSubscriber (zdroj http://www.whitewashing.de/…nevents.html )
class DomainEventSubscriber implements EventSubscriber {
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var Eventing[]
*/
private $entities = [];
public function __construct(EventDispatcherInterface $eventDispatcher) {
$this->eventDispatcher = $eventDispatcher;
}
public function getSubscribedEvents() {
return [
Events::postUpdate,
Events::postPersist,
Events::postRemove,
Events::postFlush,
];
}
public function postPersist(LifecycleEventArgs $event) {
$this->keepDomainEvents($event);
}
public function postUpdate(LifecycleEventArgs $event) {
$this->keepDomainEvents($event);
}
public function postRemove(LifecycleEventArgs $event) {
$this->keepDomainEvents($event);
}
public function postFlush() {
foreach ($this->entities as $entity) {
foreach ($entity->popEvents() as $event) {
$this->eventDispatcher->dispatch(get_class($event), new SymfonyEvent($event));
}
}
$this->entities = [];
}
private function keepDomainEvents(LifecycleEventArgs $event) {
$entity = $event->getObject();
if (!($entity instanceof Eventing)) {
return;
}
$this->entities[] = $entity;
}
}
Tento subscriber je zaregistrovaný klasicky do Doctrine.
Subscriber deleguje práci na Dispatcher, který je v mojem případě
Symfoňácký, může být libovolný.
Jak jde taky vidět, obaluju doménové události Symfoňáckými, protože
Symfony dispatcher vyžaduje události jako podomky Event. Taky používám
konvenci – název události = název třídy. Události na doménové úrovni
nejsou potomky Event schválně, doména není závislá na infrastruktuře.
class SymfonyEvent extends Event {
/**
* @var DomainEvent
*/
private $domainEvent;
public function __construct(DomainEvent $domainEvent) {
$this->domainEvent = $domainEvent;
}
public function getDomainEvent(): DomainEvent {
return $this->domainEvent;
}
}
Jak jsem psal, dispatcher je klasický Symfony Dispatcher, tomuto dispatcheru už registruju jednotlivé konkrétní subscribery, např.:
class CustomerElasticExportEventSubscriber implements EventSubscriberInterface {
...
public static function getSubscribedEvents() {
return [
CustomerChangedEvent::class => 'customerChanged',
];
}
public function customerChanged(SymfonyEvent $event) {
$this->processCustomerChanged($event->getDomainEvent());
}
private function processCustomerChanged(CustomerChangedEvent $event) {
...
}
Systém vypadá složitě, ale je vcelku jednoduchý – diagram tříd: https://drive.google.com/…c0s3UVU/view?…
- Jan Mikeš
- Člen | 771
Díky @Fafin ! O tomto způsobu řešení eventů v entitách jsem četl, ale chyběla mi praktická ukázka. Opravdu se mi to líbí a nechám se inspirovat.
Zatím používám a nejsem spokojen :-(
Můžeš ještě zkusit nastínit proč nejsi s aktuální situaci spokojen a jak by se dalo řešení případně ještě vylepšit?
Btw, symfony eventdispatcher zatím nepoužívám, proč musíš obalit
eventy ještě symfoním eventem? Předpokládám, že to je tím, že
eventdispatcher očekává jako parametr instanci SymfonyEvent
?
Editoval Jan Mikeš (24. 10. 2016 8:50)
- Svaťa Šimara
- Člen | 98
@JanMikeš
Nejsem spokojen – entita si pamatuje události. To podle mě není její
zodpovědnost. Podle mě by měla entita vyhazovat události „někam“.
Další problém tohoto systému je mazání – smažu entitu, a nemám kam
zaznamenat událost, že byla entita smazaná. Toto ještě
vyřešené nemám.
Symfony EventDispatcher
očekává potomka
Symfony\...\Event
. Proto ta pomocná
třída SymfonyEvent extends Symfony\...\Event
- Jan Mikeš
- Člen | 771
@Fafin četl jsi jsi DDD in PHP? Je zde zmíněn způsob, jak to udělat tak, jak popisuješ, ale nejsem si jist finální implementací v Nette.
Např. takto by vypadala simulace registrace uživatele (příklad převzat z knihy):
class User
{
public function __construct()
{
// Nelíbí se mi ten statický přístup k něčemu globálnímu, ale existuje čistější řešení?
DomainEventPublisher::instance()->publish(new UserRegistered($this->userId));
}
}
Samotná implementace DomainEventPublisher
pak vypadá
takto:
class DomainEventPublisher
{
private $subscribers;
private static $instance = null;
public static function instance()
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
private function __construct()
{
$this->subscribers = [];
}
public function __clone()
{
throw new \BadMethodCallException('Clone is not supported');
}
public function subscribe(DomainEventSubscriber $aDomainEventSubscriber)
{
$this->subscribers[] = $aDomainEventSubscriber;
}
public function publish(DomainEvent $anEvent)
{
foreach ($this->subscribers as $aSubscriber) {
if ($aSubscriber->isSubscribedTo($anEvent)) {
$aSubscriber->handle($anEvent);
}
}
}
}
Mimochodem tebou zmíněné řešení jsem našel i zde (trait HasEvents
) a právě se rozhoduji
který z těchto 2 přístupů použít, singletony mi jsou ale tak nějak
proti srsti, takže mám trošku vnitřní blok :-).
Co konkrétně mi není jasné u DomainEventPublisher
u je, jak
všechny subscribery přihlásit k odběru? Napadlo mě, že by subscribery
měly být zaregistrované jako služby a při jejich registraci (jak??
v konstruktoru? v configu pomocí - setup()
??) do DI takto:
class SendUserRegistrationEmailSubscriber implements DomainEventSubscriber
{
public function __construct()
{
DomainEventPublisher::instance()->subscribe($this)
}
}
K tomu se váže zároveň i další otázka, šel by ten publisher registrovat taky do DI jako služba? (zatím jsem nezkoušel), abych jej pak mohl vyžadovat v závislostech, případně ručně předávat:
services:
eventPublisher: DomainEventPublisher::instance()
- SendUserRegistrationEmailSubscriber(@eventPublisher)
Pak bych totiž mohl využít něco takového:
services:
- SendUserRegistrationEmailSubscriber
eventPublisher:
class: DomainEventPublisher::instance()
setup:
- subscribe(@SendUserRegistrationEmailSubscriber)
Zatím zkouším modelovat pouze doménu, infrastrukturu a aplikační vrstvu neřeším ještě vůbec, proto budu rád za jakékoliv rady a zkušenosti s tím, jaké nástrahy mě budou ještě čekat a případně jak se jim vyhnout.
edit: částěčně si odpovím na některé otázky sám :-)
Tento zápis se zdá být funkční:
services:
eventPublisher:
class: DomainEventPublisher::instance()
setup:
- subscribe(PersistDomainEventSubscriber)
- PersistDomainEventSubscriber
Zůstávají tedy otázky spíše na to co je čistější a vhodnějším řešením, jestli volání staticky singletonu z entity je čistější než to, aby si entita udržovala eventy v sobě a ty byly zpracovávány infrastrukturou.
Editoval Jan Mikeš (25. 11. 2016 15:13)
- Svaťa Šimara
- Člen | 98
@JanMikeš Četl jsem něco obdobného https://vaughnvernon.co/?… . Pro systém událostí doporučuje statický Publisher, přesně jak píšeš Ty.
Problémy:
Skrytá statická závislost – zvenku o ní nevím. Problém s testováním – entitu nelze jednotkově testovat (závislost nelze mocknout). Jde proti Inversion of control.
Událost se zpropaguje ihned, idkyž se entita nakonec nezapersistuje. Je to správě? V mojem případě ne.
Systém, kde si entita pamatuje své událsti těmito problémy netrpí. Ale je zodpovědností entity si události pamatovat? Podle mě ne :-(