Doctrine 2: události

V předchozích dílech seriálu jsme si představili základní možnosti Doctrine 2. S nimi dokážete zajistit jednoduché mapování objektů na databázi. Dnes a příště se podíváme na první z pokročilejších témat, a to na životní cyklus entity, události, listenery a subscribery.
Seriál: Doctrine 2 (12 dílů)
- Doctrine 2: úvod do systému 21. 7. 2010
- Doctrine 2: základní definice entit 4. 8. 2010
- Doctrine 2: pokročilá definice entit 11. 8. 2010
- Doctrine 2: načítání, ukládání a mazání 26. 8. 2010
- Doctrine 2:stavy entit a transakce 9. 9. 2010
- Doctrine 2: asociace 23. 9. 2010
- Doctrine 2: práce s asociacemi 14. 10. 2010
- Doctrine 2: DQL 3. 11. 2010
- Doctrine 2: Query Builder a nativní SQL 18. 11. 2010
- Doctrine 2: události 9. 12. 2010
- Doctrine 2: událostní handlery 13. 1. 2011
- Architektura aplikace nad Doctrine 2 23. 2. 2012
Jednoduché mapování nestačí
V seriálu jsme si dosud představili základní možnosti Doctrine 2. Brzy ale narazíte na řadu úkolů, pro které už si se základním jednoduchým mapováním nevystačíte. Případně zjistíte, že už podvacáté píšete do další entity zase tentýž kód. Velkou část podobných problémů lze elegantně vyřešit s pomocí událostí a listenerů.
Události a listenery vám zpočátku nejspíš přijdou jako vyšší dívčí, jako něco hodně pokročilého, do čeho nemusíte vidět a co je dobré jenom pro specialisty na vývoj Doctrine 2 rozšíření (v poslední době už se mimochodem také objevují první prakticky použitelné extenze).
Ve skutečnosti by ale práci s událostmi a listenery měl ovládat každý, kdo to myslí s Doctrine 2 alespoň trochu vážně. Bez nich je práce s Doctrine 2 často velice neohrabaná a neelegantní. Případně vede ke značně nečistým postupům, kdy se například budete snažit Entity Manager dostat dovnitř entity, kam vůbec nepatří.
Životní cyklus entity
Připomeňme si nejprve, jakými stavy může také procházet entita během svého života:
// vytvoříme novou instanci entity $article = new Article; // persistujeme článek $em->persist($article); // uložíme článek do databáze $em->flush(); // načteme článek z databáze $article = $em->find('Article', 123); // něco v článku změníme $article->setTitle('Lorem ipsum'); // uložíme změny do databáze $em->flush(); // smažeme článek $em->remove($article); // promítneme smazání do databáze $em->flush();
Události v rámci životního cyklu
Během životního cyklu zavádí Doctrine 2 celou řadu událostí, na které pak můžeme navěsit nějaké další akce. Pojďme si nejprve ukázat, jaké události vlastně Doctrine 2 rozeznává:
loadClassMetadata
- K události dochází v okamžiku, kdy si systém zjišťuje metainformace o libovolné entitní třídě, například z nám již známých anotací. Tedy když v rámci jednoho requestu přistupuje Doctrine 2 poprvé k nějaké třídě entity. Nevolá se tedy pro každou instanci zvlášť, ale volá se pro každou třídu. Hodí se, pokud potřebujete provést nějaké změny či dodatečná nastavení metadat po jejich načtení.
postLoad
- Událost je volaná pro každou instanci entity bezprostředně poté, co byla načtená z databáze. Tedy zejména uvnitř zavolané metody
$em->find()
, ale také třeba při pozdním načtení asociovaných entit uvnitř proxy tříd. Využijete ji pro případné úpravy dat v načtené entitě předtím, než je předána k používání do aplikace. prePersist
- Událost je volaná pro každou instanci entity před tím, než ji EntityManager persistuje. Tedy jednak při zavolání
$em->persist()
, dále pak uvnitř$em->flush()
, pokud během něj dochází ke kaskádovému persistování asociovaných entit. Pokud máte v entitě definovaný automaticky generovaný klíč (sekvence či auto increment), nemusí být ještě v tomto okamžiku jeho hodnota naplněná. postPersist
- Událost je volaná pro každou instanci entity poté, co ji EntityManager skutečně persistoval. Tedy uvnitř
$em->flush()
hned po provedení INSERTů nad databází. Příkladem použití jsou operace, které potřebujete provést uvnitř entity na základě přiděleného primárního klíče –postPersist
je první okamžik, kdy se můžete spolehnout na to, že bude generovaný primární klíč skutečně naplněný. preUpdate
- Volá se uvnitř
$em->flush()
bezprostředně před tím, než jsou do databáze promítnuty všechny změny v dané instanci entity. postUpdate
- Volá se uvnitř
$em->flush()
bezprostředně poté, co byly do databáze promítnuty všechny změny v dané instanci entity. preRemove
- Volá se uvnitř
$em->remove()
, tedy před tím, než se z databáze vymaže daná instance entity. postRemove
- Volá se uvnitř
$em->flush()
bezprostředně poté, co se z databáze vymazala daná instance entity. onFlush
- Událost se volá při zavolání
flush()
. Na rozdíl od všech předchozích událostí je trochu zvláštní. Není vázána na konkrétní entitu, neprovádí se pro každou instanci zvlášť, ale provádí se jednorázově pro celý Entity Manager najednou. Umožňuje ještě na poslední chvíli zasahovat do změn naplánovaných k poslání do databáze.
Potřebné akce můžete na události navěsit dvěma různými způsoby. Buďto uvnitř samotných entit s pomocí příslušných anotací, anebo přes event listenery.
Události uvnitř entit
Uvnitř entity můžeme u její jakékoliv metody určit, že se má automaticky volat, pokud nastane některá z uvedených událostí. Nejprve musíte pro celou entitu nastavit anotaci @hasLifecycleCallbacks
– pozor, na to se velice snadno zapomíná! Následně pak stačí pro příslušné metody nastavit jednu či více z následujících anotací:
@postLoad
@prePersist
@postPersist
@preUpdate
@postUpdate
@preRemove
@postRemove
Pro ilustraci konkrétní příklad, jak s pomocí událostí zajistit automatickou aktualizaci sloupce s datem a časem poslední modifikace záznamu:
/** * @entity * @hasLifecycleCallbacks */ class Article { /** * @column(type="DateTime") */ private $modified; /** * @prePersist * @preUpdate */ public function update() { $this->modified = new DateTime('now'); } }
Výhodou tohoto přístupu je jeho jednoduchost a přímočarost. Bohužel stále neřeší většinu problémů, se kterými se budete potýkat. Uvnitř entity třeba nemáte přístup k Entity Manageru. Velice špatná je také znovupoužitelnost takového kódu, která je v nejlepším případě omezená jen na prostou dědičnost, a to je opravdu málo.
Event listener
Druhou možností, jak navěsit vlastní akce na události, je využití event listenerů. Princip je takový, že všechny akce si nadefinujete vedle, mimo jakékoliv entity, a navěsíte je zvenku v rámci konfigurace Doctrine 2 na jednotlivé události. Realizace je sice trochu složitější, zato je celé řešení čistší a zejména plně znovupoužitelné.
Pro demonstraci způsobu, jak se konkrétní handler navěsí na nějakou událost, si teď ukážeme velice jednoduchý listener, který vypíše Hello world!
pokaždé, když se persistuje nebo maže nějaká entita.
class HelloWorldListener { public function prePersist(LifecycleEventArgs $eventArgs) { echo 'Hello world!'; } public function preRemove(LifecycleEventArgs $eventArgs) { echo 'Hello world!'; } }
Jedná se o běžnou třídu, která nemusí ani nic dědit či implementovat. Měla by mít ale metody pojmenované podle názvů událostí, na které ji chceme navěsit.
Takto vytvořený listener pak už jen stačí přidat do Entity Manageru respektive v něm uchovávaném Event Manageru. Musíme ale zároveň říct, pro které všechny události se má daný listener volat. K tomu můžeme využít předdefinované příslušné konstanty uvnitř třídy DoctrineORMEvents
:
// vytvoříme instanci našeho listeneru $helloWorldListener = new HelloWorldListener; // přidáme ji do entity manageru $em->getEventManager()->addEventListener( array(Events::perPersist, Events::preRemove), $helloWorldListener );
Event subscriber
Pokud vám nesedí, že výčet podporovaných událostí musíte zadávat takhle explicitně do prvního parametru metody addEventListener()
, máte pravdu. Tahle informace logicky patří přímo dovnitř HelloWorldListeneru, on sám by měl vědět a s sebou si nést informaci, které události vlastně obsluhuje.
Doctrine 2 nabízí i takovou elegantnější variantu, pak se tomu ovšem neříká listener, ale subscriber. Ten je pak oproti původnímu listeneru nutné postavit jako implementaci rozhraní DoctrineCommonEventSubscriber
a implementovat v něm navíc metodu getSubscribedEvents()
:
class HelloWorldSubscriber implements EventSubscriber { public function prePersist(LifecycleEventArgs $eventArgs) { echo 'Hello world!'; } public function preRemove(LifecycleEventArgs $eventArgs) { echo 'Hello world!'; } public function getSubscribedEvents() { return array(Events::perPersist, Events::preRemove); } }
Drobný rozdíl je pak i v samotném připojování k Event Manageru, kde se místo metody addEventListener()
volá metoda addEventSubscriber()
:
// vytvoříme instanci našeho subscriberu $helloWorldSubscriber = new HelloWorldSubscriber; // přidáme ji do entity manageru $em->getEventManager()->addEventSubscriber($helloWorldSubscriber);
Metoda addEventSubscriber()
pak neudělá vůbec nic jiného, než že pro všechny události vrácené z getSubscribedEvents()
zaregistruje naši instanci jako jakýkoliv jiný listener. Oba příklady výše tedy v důsledku dělají úplně totéž.
Navěšujeme na správném místě
Když se vrátíte zpátky do prvních dílů seriálu, všimnete si, že při počátečním instancování samotného Entity Manageru se mu do konstruktoru jako třetí parametr předává právě Event Manager.
Pokud tedy chceme v aplikaci používat nějaké listenery či subscribery, ideálním místem pro jejich registraci do Event Manageru je bootstrap, a to ještě předtím, než vůbec instancujeme Entity Manager:
// vytvoříme instanci Event Manageru $eventManager = new EventManager; // vytvoříme instanci našeho subscriberu $helloWorldSubscriber = new HelloWorldSubscriber; // přidíme subscriber do Event Manageru $eventManager->addEventSubscriber($helloWorldSubscriber); // instancujeme samotný entity manager $em = EntityManager::create($database, $config, $eventManager);
Event Manager a na něj navazující třídy je sám o sobě mimochodem hodně obecný a samostatně použitelný i beze zbytku Doctrine 2. Také je definovaný v nejnižší Common vrstvě. Takže po něm můžete sáhnout i v případě, kdy vás třeba jinak Doctrine 2 a její konkrétní události vůbec nezajímají a jenom potřebujete ve své aplikaci využít nějaký obecný systém vlastních událostí, listenerů a subscriberů.
Pokračování příště
Příště budeme v načatém tématu pokračovat, projdeme si podrobněji práci s jednotlivými handlery pro různé typy událostí, ukážeme si jejich specifika a konkrétní praktické příklady.
Nevim, z javy jsem na anotace zvykly, ale jejich pouziti v php se mi prici – zejmena kvuli nepodpore v IDE.
Udalosti (triggery) jsou samozrejme potreba, ale zrovna 2x se mi nelibi jejich pouziti v Doctrine2. Osobne pouzivam zpusob, kdy mam vytvorenu hlavni tridu, ktera implementuje vsechny metody udalosti. Podedena trida si je pak akorat pretizi a dela si svoje.
Mam tak v kodu vetsi prehled a zarucene stejne nazvy pro jednotlive udalosti.
Taky mohli dat prefixy before a after, nez pre a post …
„Jedná se o běžnou třídu, která nemusí ani nic dědit či implementovat. Měla by mít ale metody pojmenované podle názvů událostí, na které ji chceme navěsit.“
Mela by mit … nemam rad tento pristup, uprednostnuju interface + pripadne adapter …
array(Events::perPersist, Events::preRemove),
melo by byt
array(Events::prePersist, Events::preRemove),
Osobne pouzivam zpusob, kdy mam vytvorenu hlavni tridu, ktera implementuje vsechny metody udalosti. Podedena trida si je pak akorat pretizi a dela si svoje.
Tedy zneužívání dědičnosti k něčemu, k čemu by se dědičnost vůbec používat neměla?
Mam tak … zarucene stejne nazvy pro jednotlive udalosti.
Soulasím s tím, že tohle není v Doctrine 2 úplně čisté a že by rozhraní jednotlivých listenerů mělo být vyžadováno jako implementace nějakých daných interfaces. Zkusím v tomto smyslu poslat vývojářům návrh do diskuze.
Mela by mit … nemam rad tento pristup, uprednostnuju interface + pripadne adapter …
Jak jsem už napsal výše, s tím souhlasím a také se mi to nelíbí.
U toho sjednocování názvu listenerů bych si nebyl tak jistý. Znamenalo by to, že by entita měla metody jako prePersist či preUpdate, případně nějaké variace na toto téma? Takováhle metoda by v entitě přeci neměla strašit. Z mého pohledu to do entit zatahává části náležící persistanční vrstvě.
Čistější se mi zdá aktuální přístup, tedy že se naslouchání řídí „značkováním“ metod. Ano, nepříjemné je to, že má pak člověk bordel v tom, jaká metoda je volána event handlerem (zde by se opravdu hodila podpora ze strany IDE), ale z akademického pohledu mi to přijde smysluplnější.
„Tedy zneužívání dědičnosti k něčemu, k čemu by se dědičnost vůbec používat neměla?“
Smim se zeptat, proc by se takto nemela pouzivat dedicnost, resp. cim ji zneuzivam? Jaky zpusob je tedy ten spravny?
Osobne mam takovyto pristup zazity a nevidim na nem nic spatneho.
Nelibi se mi totiz tento pristup:
$article = new Article;
$em->persist($article);
je mi milejsi pouzivat neco takoveho:
$article->persist();
Mozna k tomu pristupuju spatne, ale takto mi entita „predstavuje i ten persistentni objekt“ – nekde uvnitr je schovany EM.
Aha, tak to se dopouštíte dokonce dvou chyb zároveň ;). I když slovo „chyba“ zde nechápejte nějak tvrdě jako něco, co je vysloveně špatně, spíš něco, co se v teorii i praxi neukázalo být jako ta nejlepší cesta, ale spíš jako cesta, po které se jde docela dlouho celkem pohodlně, ale na které pak dříve či později strašlivě narazíte a vymlátíte si všechny zuby.
Prvním bodem je design pattern nazývaný ActiveRecord, tedy to, co Vy označujete jako $article->persist(). Oproti tomu stojí jiné univerzálnější a systémově správnější přístupy, například Data Mapper. Pro více informací viz google, byly o tom napsány stohy pojednání. Z vlastní zahrádky například úvod u článku Doctrine 2: načítání, ukládání a mazání nebo lehce zavádějící, lehce pomýlený a zastaralý, ale v tomto smyslu stále užitečný článek Pět vrstev modelu.
Druhým bodem je pak dědičnost v OOP. Ta je určena výhradně ke specializaci obecnější třídy – ovšem ve smyslu věcné podstaty třídy, její domény, nikoliv obecné funkčnosti. Zneužívání dědičnosti pro znovupoužitelnost obecných funkcí nesouvisejících s doménou třídy je bad practice, vede k chybným programátorským postupům a naprosto vadným návrhům aplikace. Oproti tomu jako best practice stojí tzv. kompozice, tedy skládání požadované funkčnosti z více různých samostatných tříd, jako je tomu například právě zde v případě samostatných registrovaných listenerů. Opět viz google a hromada dalších zdrojů, například nějaká literatura o návrhových vzorech.
Diky za reakci.
ad. 1.
jasne, zatim jsem nenarazil, takze mi takovyto zpusob vyhovoval
ad. 2.
ActiveRecord … no, jak se to vezme … V pozadi se porad pouziva nejaky mapper, ktery se o entity stara. Metody entit (napr. persist()) jsou jen prostrednici, zajistujici komunikaci s mapperem. Zmena mapperu je porad mozna. V tomto pripade vidim kolizi hlavne v pojmu entita – kdy neni jenom schranka ale uz neco i dela, resp zprostredkovava.
Hlavni duvod proc jsem zvolil tento pristup je predavani instance entity manageru do jinych trid (zejmena do kontroleru), kdy neni vhodne (a ani nemusi byt vzdy potreba) jej predavat v konstruktoru ci v inicializacni metodou.
Dalsi moznost by bylo ziskani EM z nejakeho singletonu – ale toto reseni se mi taky nelibi.
Jaky postup byste mi tady doporucil?
ad.3
Jasne, kompozice je ok, ale ne vzdy a vsude jsem ji schopen nasadit. Registrovat samostatne vlastni listenery je samozrejme v poradku. Ja narazel na definici jednotlivych udalosti.
Pouzivam udalosti ve smyslu trigeru, takze ocekavam stejne chovani v pripade jakekoliv manipulace s entitou, kdykoliv a kdekoliv. Takze v tomto pripade mi dedicnost nepripada jako vylozene spatne reseni.
Nejpis skoncime diskuzi o tom co vsechno ma umet a co ma obsahovat entita a jak se na ni vlastne divat. Jestli z pohledu domeny ci z pohledu perzistentniho prvku (tak se na ni divam ja a vidim to tedy jako vhodny pristup). Z pohledu domeny a tedy vyznamu objektu to vidim taky jinak.
Diky za komentar.
Hlavni duvod proc jsem zvolil tento pristup je predavani instance entity manageru do jinych trid (zejmena do kontroleru), kdy neni vhodne (a ani nemusi byt vzdy potreba) jej predavat v konstruktoru ci v inicializacni metodou. Dalsi moznost by bylo ziskani EM z nejakeho singletonu – ale toto reseni se mi taky nelibi. Jaky postup byste mi tady doporucil?
Podle mě nejlepším řešením na tohle je nějaké pokročilé DI/IOC/ServiceLocator/Context/nazvětesitojakchcete – do jiných tříd si pak nemusíte nutně předávat přímo instanci EntityManageru, ale jenom ServiceLocator, ze kterého si případně vhodný EntityManager vyžádáte.
ServiceLocator přitom nemusí (a rozhodně by neměl) být globálním singletonem, měl by být v různých částech a úrovních aplikace předávatelný a modifikovatelný.
Zároveň v sobě nemusí nutně nést už hotový instancovaný EntityManager, ale například pouze továrničku nebo obecně jiný nápovědný způsob, jak v případě potřeby EntityManager instancovat na vyžádání. To navíc při pokročilé práci s Doctrine 2 stejně budete potřebovat – občas se vám totiž EntityManager uzavře v polovině práce a vy si potřebujete někde uvnitř aplikace najednou otevřít další nový.
Vy jste neuvěřitelný. Ale sranda musí být.
Nemůžu si pomoct, ale je to trochu maglajs. Když to srovnám třeba s tímhle: http://mongoid.org/docs/callbacks/
Když to srovnám s uvedeným odkazem, tak na něm vidím něco jako
set_callback(:save, :before)
nebobefore_create
, v Doctrine 2 pak pro stejný účel vidím něco jako@prePersist
. To je vše, jedna jediná anotace snad není žádný maglajz, ne?Nevíte, zda nějaká firma nenabízí školení Doctrine 2?
Díky Petr
Slyšel jsem, že prý v http://medio.cz nabízejí nějaké kurzy, ale teď k tomu nemohu nic nalézt.
Díky, napsal jsem jim, tak uvidíme.