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ů)

  1. Doctrine 2: úvod do systému 21.7.2010
  2. Doctrine 2: základní definice entit 4.8.2010
  3. Doctrine 2: pokročilá definice entit 11.8.2010
  4. Doctrine 2: načítání, ukládání a mazání 26.8.2010
  5. Doctrine 2:stavy entit a transakce 9.9.2010
  6. Doctrine 2: asociace 23.9.2010
  7. Doctrine 2: práce s asociacemi 14.10.2010
  8. Doctrine 2: DQL 3.11.2010
  9. Doctrine 2: Query Builder a nativní SQL 18.11.2010
  10. Doctrine 2: události 9.12.2010
  11. Doctrine 2: událostní handlery 13.1.2011
  12. 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ř 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.

Provozuje vývojářskou firmu Medio Interactive. Vystudoval informační a znalostní inženýrství na VŠE, kde stále příležitostně přednáší o tvorbě webů.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 14

Přehled komentářů

Ghost Re: Doctrine 2: události
drobna chyba Re: Doctrine 2: události
Jan Tichý Re: Doctrine 2: události
drevolution Re: Doctrine 2: události
Ghost Re: Doctrine 2: události
Jan Tichý Re: Doctrine 2: události
Ghost Re: Doctrine 2: události
Jan Tichý Re: Doctrine 2: události
. Re: Doctrine 2: události
Pepa nevypadá to moc hezky
Jan Tichý Re: nevypadá to moc hezky
Lábus Školení Doctrine 2
drevolution Re: Školení Doctrine 2
Lábus Re: Školení Doctrine 2
Zdroj: https://www.zdrojak.cz/?p=3384