Doctrine 2: událostní handlery

V dnešním dílu budeme pokračovat v navěšování vlastních funkcí na události v rámci životního cyklu entity. Projdeme 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.

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

Vedle očividného faktu, že se každý handler volá v jiný okamžik životního cyklu, se handlery pro jednotlivé události navzájem zásadně liší v dalších dvou ohledech. Prvním je, že některé z nich se volají pro každou instanci entity zvlášť, jiné jen pro každou třídu entity, jiné dokonce jen jednorázově napříč všemi entitami dohromady. Druhým rozdílem jsou různé parametry, které dostávají různé typy handlerů od Doctrine 2 na svůj vstup.

Než se budeme moci bavit o konkrétních událostech, musíme k některým z nich přijít poněkud zeširoka.

Metadata entit

Aby mohla Doctrine 2 pracovat s jednotlivými entitami, musí si někde evidovat přehled o jejich struktuře a vlastnostech, které jsme jim přisoudili pomocí anotací. Interně si systém tyto metainformace eviduje v instancích třídy DoctrineORMMappingClassMetadata.

Mezi zajímavá metadata patří například název entity, seznam jejích polí, identifikátorů a asociací. Tady je také to správné místo, pokud k některé entitě či jejím jednotlivým polím potřebujete přistupovat přes reflexe.

Poměrně zajímavé možnosti se nabízejí, když si uvědomíte, že kromě čtení můžete existující metadata i modifikovat. Můžete tedy například k již evidované entitě přidat ještě další pole, které bude mapované na některý databázový sloupec.

// továrna pro získávání metadat pro jednotlivé entity
$factory = new DoctrineORMMappingClassMetadataFactory($em);
// získáme metadata pro entitu s článkem
$metadata = $factory->getMetadataFor('Article');
// přidáme sloupec s datumem
$metadata->mapField(array(
    'fieldName' => 'timestamp',
    'type' => 'datetimetz',
));

Pokud něco takového potřebujete dělat, nejvhodnějším okamžikem je chvíle hned poté, kdy si Doctrine 2 takovou metadata reprezentaci vytvoří. Neboli v rámci události  loadClassMetadata.

Událost 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í.

class EventListener
{
    public function loadClassMetadata(DoctrineORMEventLoadClassMetadataEventArgs $eventArgs)
    {
        // ...
    }
}

Na vstup příslušné metody listeneru vám Doctrine 2 předá všechny informace, které byste mohli o dané třídě potřebovat, a to zabalené v přepravce DoctrineORMEventLoadClassMetadataEventArgs. Ve skutečnosti je v přepravce jen jediná věc, a to právě metadata dostupná přes metodu  getClassMetadata().

Výše uvedené přidání pole timestamp do každé entity v aplikaci bychom tedy mohli udělat například takto:

class EventListener
{
    public function loadClassMetadata(DoctrineORMEventLoadClassMetadataEventArgs $eventArgs)
    {
        $metadata = $eventArgs->getClassMetadata();
        $metadata->mapField(array(
            'fieldName' => 'timestamp',
            'type' => 'datetimetz',
        ));
    }
}

Událost dále využijete třeba v případě, kdy chcete mít ve své sdílené knihovně znovupoužitelné entity. Těm pak v loadClassMetadata handleru na základě aktuální konfigurace dané aplikace určíte, na které konkrétní navazující třídy mají vlastně jejich asociace odkazovat. Nemusíte tak zbytečně v každé aplikaci dědit a přepisovat úplně všechny entity. K tomu se ale dostaneme někdy jindy, až se budeme bavit o dědičnosti entit.

Událost 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.

class EventListener
{
    public function postLoad(DoctrineORMEventLifecycleEventArgs $eventArgs)
    {
        $entity = $eventArgs->getEntity();
        $em = $eventArgs->getEntityManager();
        // ...
    }
}

Na svém parametru očekává handler přepravku DoctrineORMEventLifecycleEventArgs. Skrze ni máme uvnitř handleru k dispozici nejen dotčenou entitu přes metodu getEntity(), ale navíc i celý Entity Manager přes metodu getEntityManager(). Můžete tedy přistupovat i k ostatním entiám, načítat, upravovat či mazat je.

Událost využijete pro případné úpravy dat v načtené entitě předtím, než je předána k používání do aplikace. Například pokud máte vícejazyčnou aplikaci a po načtení chcete příslušná pole entity naplnit správnou jazykovou mutací.

Události prePersist a preRemove

Událost prePersist 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.

Obdobně událost preRemove se volá uvnitř $em->remove(), tedy před tím, než se z databáze vymaže daná instance entity.

I do těchto dvou handlerů se na parametru předává přepravka DoctrineORMEventLifecycleEventArgs s příslušnou entitou a Entity Managerem.

Changeset

Dovolím si teď krátkou odbočku k tématu, které s událostmi souvisí jen velice okrajově a přitom je pro některé z nich naprosto klíčové.

Poměrně zajímavý je vnitřek metody $em->flush() a na ni navazujících metod v UnitOfWork. Rozhodně doporučuji podívat se dovnitř do zdrojáků, co vlastně Doctrine 2 všechno dělá a jak zpracovává všechny naplánované změny. Pomůže vám to zásadně pochopit, jak vlastně Doctrine 2 funguje.

Pro další práci s událostmi je důležité zejména vědět, že aby mohla Doctrine 2 v rámci flush() promítnout všechny nové, změněné a smazané entity do databáze efektivně, musí si nejprve připravit seznamy všech změn, takzvané changesety. Pro každý typ operace si počítá samostatný changeset – jeden pro vložení nově vytvořených entit, další pro změnu již persistovaných entit, další pro smazání odstraněných entit, další changesety pak eviduje pro hromadné operace hromadných aktualizací či mazání.

Aktuální podobu jednotlivých changesetů najdete v Unit Of Work:

// z Entity Manageru získáme Unit Of Work
$uow = $em->getUnitOfWork();
// získáme jednotlivé changesety, tedy pole entit
$inserts = $uow->getScheduledEntityInsertions();
$updates = $uow->getScheduledEntityUpdates();
$deletes = $uow->getScheduledEntityDeletions();
$bulkDeletes = $uow->getScheduledCollectionDeletions();
$bulkUpdates = $uow->getScheduledCollectionUpdates();

Changesety pro nově vytvořené či smazané entity se jakoby vytváří průběžně společně s tím, jak se volají metody $em->persist() a $em->remove(). Naproti tomu changeset pro změněné entity se počítá až uvnitř $em->flush() těsně před provedením samotných aktualizací. Doctrine 2 při tomto výpočtu porovnává původní hodnoty jednotlivých proměnných v jednotlivých instancích entit s jejich aktuálními hodnotami a pokud se některé liší, naplánuje je k uložení. Databázový UPDATE se tak volá efektivně jen pro opravdu změněné entity a v rámci nich navíc jen pro jejich opravdu změněné sloupce.

Každopádně v určitém okamžiku jsou již všechny changesety přepočítané a připravené pro provedení nad databází. To je okamžik, kdy Doctrine 2 volá událost  onFlush.

Událost onFlush

Událost onFlush je tedy jako dělaná pro situace, kdy potřebujete ještě před skutečným provedením v databázi zasahovat do fronty připravených změn nebo do zařazených entit.

Spouští se při každém zavolání metody flush() jednorázově pro celý Entity Manager najednou.

class EventListener
{
    public function onFlush(DoctrineORMEventOnFlushEventArgs $eventArgs)
    {
        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();
        // ...
    }
}

Na svém parametru očekává handler přepravku DoctrineORMEventOnFlushEventArgs. Přes její metodu getEntityManager() máme uvnitř handleru k dispozici Entity Manager, ze kterého můžeme v dalších krocích získat UnitOfWork i se všemi changesety.

class EventListener
{
    public function onFlush(DoctrineORMEventOnFlushEventArgs $eventArgs)
    {
        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();
        foreach ($uow->getScheduledEntityInsertions() AS $entity) {
            //...
        }
        foreach ($uow->getScheduledEntityUpdates() AS $entity) {
            //...
        }
        foreach ($uow->getScheduledEntityDeletions() AS $entity) {
            //...
        }
        foreach ($uow->getScheduledCollectionDeletions() AS $list) {
            //...
        }
        foreach ($uow->getScheduledCollectionUpdates() AS $list) {
            //...
        }
    }
}

Pokud ale během této události uděláte jakoukoliv změnu v nějaké entitě, musíte si nakonec ručně vynutit přepočítání changesetů. V opačném případě by se už změna do databáze nepromítla.

Changeset pro konkrétní entitu můžete přepočítat zavoláním $uow->computeChangeSet($classMetadata, $entity) nebo $uow->recomputeSingleEntityChangeSet($classMetadata, $entity). Všechny changesety pak znovu přepočítáte metodou  $uow->computeChangeSets().

Událost 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. Z logiky věci se volá pro každou měněnou entitu zvlášť.

Protože se událost volá poměrně pozdě, až po spočítání všech changesetů a dalších přípravách na aktualizaci databáze, nikdy u entity neměňte žádné její navazující asociace.

Na svém parametru očekává handler speciální přepravku DoctrineORMEventPreUpdateEventArgs. Ta vedle samotné entity nabízí navíc její spočítaný changeset, a funkce pro získávání a nastavování hodnot jednotlivých polí entity.

class EventListener
{
    public function preUpdate(DoctrineORMEventPreUpdateEventArgs $eventArgs)
    {
        // vrátí entitu
        $entity = $eventArgs->getEntity();
        // vrátí changeset dané entity
        $changeset = $eventArgs->getEntityChangeSet();
        // byla zadaná proměnná změněná?
        $isChanged = $eventArgs->hasChangedField('title');
        // z přepravky získáme starou a novou hodnotu proměnné
        $oldValue = $eventArgs->getOldValue('title');
        $newValue = $eventArgs->getNewValue('title');
        // můžeme nastavit jinou novou hodnotu
        $eventArgs->setNewValue('title', 'Lorem ipsum');
    }
}

Jakékoliv případné změny v entitě už teď musíte dělat přes $eventArgs->setNewValue(). Kdybyste místo toho použili klasické $entity->setTitle('Lorem ipsum');, tak už se taková změna do databáze nepromítne.

Události postUpdate, postRemove a postPersist

Všechny tři události jsou volané uvnitř $em->flush() bezprostředně poté, co se nad databází provedou všechny naplánované změny.

Do všech tří handlerů se na parametru předává přepravka DoctrineORMEventLifecycleEventArgs s příslušnou entitou a Entity Managerem, úplně stejně, jako je tomu i u postLoad, prePersistpreRemove.

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

Zdroj: https://www.zdrojak.cz/?p=3403