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.
Přehled komentářů