Přejít k navigační liště

Zdroják » Databáze » Doctrine 2: události

Doctrine 2: události

Články Databáze

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.

Nálepky:

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ř HelloWorldLis­teneru, 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.

Komentáře

Subscribe
Upozornit na
guest
14 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Ghost

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 …

drobna chyba

array(Events::per­Persist, Events::preRemove),
melo by byt
array(Events::pre­Persist, Events::preRemove),

drevolution

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ší.

Ghost

„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.

Ghost

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.

.

Vy jste neuvěřitelný. Ale sranda musí být.

Pepa

Nemůžu si pomoct, ale je to trochu maglajs. Když to srovnám třeba s tímhle: http://mongoid.org/docs/callbacks/

Lábus

Nevíte, zda nějaká firma nenabízí školení Doctrine 2?

Díky Petr

drevolution

Slyšel jsem, že prý v http://medio.cz nabízejí nějaké kurzy, ale teď k tomu nemohu nic nalézt.

Lábus

Díky, napsal jsem jim, tak uvidíme.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.