Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Jádro / Životní cyklus manageru

Životní cyklus manageru#

Kam patří vedlejší efekty zápisu a mazání — přepočty, notifikace, faktury, indexace. Zdroj: app/Core/Traits/Api/Models/Managers/{SaveTrait,DeleteTrait}.php.

Hooky jsou opt-in#

Api manager nabízí pro save() i delete() sadu hooků, které se zapojí jen když je manager deklarujeSaveTrait je hledá přes method_exists(). Manager, který žádný nemá, běží úplně stejně jako dřív.

Hook Vrací Kdy běží Selhání znamená
beforeSave / beforeDelete ?Result před operací, v transakci rollback celé operace
afterSave / afterDelete ?Result po operaci, ještě v transakci rollback celé operace
completedSave / completedDelete void po commitu, mimo transakci jen se zaloguje
beforeSave(bool $isInsert, T &$entity, array &$meta): ?Result
afterSave(bool $isInsert, T &$entity, Result $result, array $meta): ?Result
completedSave(bool $isInsert, T $entity, Result $result, array $meta): void

beforeDelete(array $entities, array $meta): ?Result
afterDelete(array $entities, Result $result, array $meta): ?Result
completedDelete(array $entities, Result $result, array $meta): void

Data spočítaná v before* protečou do after* v $meta['__beforeSave'] (resp. __beforeDelete) — ne dalším argumentem, aby se signatura neměnila a existující managery se nerozbily.

Který hook použít#

Efekt Hook Proč
validace, zachycení kontextu, doplnění odvozeného pole beforeSave ještě se dá zabránit uložení
přepočet odvozeného stavu z uložených dat afterSave / afterDelete data už existují, ale jde to vrátit
e-mail, notifikace, PDF, indexace, platba completedSave / completedDelete nesmí shodit už uložená data

Aditivní efekty do afterDelete NEPATŘÍ

Připsané kredity, odeslané e-maily, vystavené faktury. Po commitu se nedají dopočítat a naivní odečet rozbije saldo. Do afterDelete patří jen přepočet odvozeného stavu ze zbylých řádků — vratný a idempotentní.

Proto je completed* best-effort

SaveTrait ho obaluje try/catch a výjimku jen zaloguje. Selhání odeslání e-mailu nesmí shodit už uloženou objednávku.

Postup: přidávám vedlejší efekt zápisu#

  1. Rozhodni podle vratnosti: jde efekt vzít zpět, nebo ne? Vratný patří do afterSave, nevratný do completedSave.
  2. Deklaruj metodu na manageru s přesnou signaturou — base ji najde sám.
  3. V beforeSave neber data z požadavku jako pravdu — sanitizuj, co přišlo zvenčí (viz níž).
  4. V hoocích nevolej find() ani findAll() (viz níž).
  5. Vrať Result::failure(...), když má operace selhat — base udělá rollback sám.
  6. Ověř i cestu selhání: co zůstane v databázi, když afterSave vrátí neúspěch.
protected function beforeSave(bool $isInsert, Invoice &$entity, array &$meta = []): ?Result
{
    $this->calculator->recalculate($entity);          // odvozená pole ze vstupů

    if ($isInsert && $entity->getBillingLineId() === null) {
        return Result::failure(ResultCode::VALIDATION_FAILED, 'Invoice needs a billing line.');
    }

    return null;                                       // null = pokračuj
}

🔴 V hoocích nikdy find() ani findAll()#

Cizí Api čtení odpojí celý objektový graf

Hooky běží nad už hydratovaným grafem. Každé Api čtení volá clearDoctrine(), tedy vyprázdní identity mapu — a nejbližší flush() skončí hláškou A new entity was found through the relationship …. Platí to i pro manager cizího modulu volaný „jen kvůli jednomu číselníku“.

Potřebuji v hooku Bezpečná náhrada
jeden řádek číselníku raw DBAL dotaz
nastavení SettingContext
cizí klíč $em->getReference()
ověřit existenci SELECT COUNT přes DBAL

Podrobně Identity mapa.

Transakce#

Save i delete obalí hooky transakcí (useTransactionForSave + beginTransaction() na service). Neúspěšný Result i vyhozená výjimka v before* / after* vrátí celou operaci; completed* běží až po commitu.

Po úspěšném commitu base ještě zvedne verze cache tagů (bumpCacheTags()) — generická invalidace, kterou manager nepíše. Při rollbacku se nic nebumpne, což je konzistentní: nic se neuložilo, tak nemá co zneplatnit. Podrobně Cache.

Mazání má dvě zvláštnosti#

  1. Entity se načtou PŘED smazáním a hookům se předají jako pole — po DELETE už řádek neexistuje. Hooky se přednačítají jen tehdy, když je manager deklaruje.
  2. Hromadné a stromové mazání jde jinou cestou.

deleteAll(filters) a deleteNode() hooky NESPOUŠTÍ

Kdo na hook spoléhá, zjistí to až tím, že se po hromadném mazání nic nepřepočítalo — počty v kategoriích zůstanou staré a nikde není chyba.

Monotonní versus absolutní přepočet#

Denormalizovaný sloupec plněný z plateb má dvě varianty a rozlišuje je příznak v $meta:

Varianta Kdo ji volá Chování
update* platební brána, front jen prodlužuje — nikdy nezkrátí
recalc* administrace umí i zkrátit, i na prázdno

afterDelete přepočítává vždy absolutně — po smazání platby musí hodnota klesnout.

Monotonní varianta je ochrana proti pořadí callbacků

Callbacky z brány nechodí zaručeně v pořadí. Kdyby prodloužení uměla zkrátit, opožděný callback ze starší platby by odebral platnost, kterou přidala novější.

Databázové triggery ne#

Denormalizovaný stav patří do PHP hooků, ne do triggerů

Trigger se nedá verzovat, testovat ani přečíst z kódu. Nový vývojář o něm neví a hledá chybu tam, kde není. Co dřív dělaly triggery, je dnes v hoocích — a mělo by to tak zůstat.

Externí zápis#

Na skutečné externí cestě je v $meta['__external'] === true.

Hook musí citlivá pole sanitizovat na serveru

Bez toho si klient nastaví třeba „zaplaceno“ sám. Deklarativní ochrana jsou atributy pro zápis sloupců; hook je druhá vrstva pro to, co se atributem vyjádřit nedá.

Kam sáhnout#

Chci Kde
pořadí hooků a transakci app/Core/Traits/Api/Models/Managers/SaveTrait.php
přednačtení entit před mazáním app/Core/Traits/Api/Models/Managers/DeleteTrait.php
invalidaci cache po commitu app/Core/Base/BaseManager.phpbumpCacheTags()
hotový vzor s beforeSave i completedSave app/UI/Api/Invoicer/Models/Managers/InvoiceManager.php

Navazující kapitoly: Identity mapa · Hydrátory · Atributy pro zápis sloupců · Cache