Ž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 deklaruje — SaveTrait 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#
- Rozhodni podle vratnosti: jde efekt vzít zpět, nebo ne? Vratný patří do
afterSave, nevratný docompletedSave. - Deklaruj metodu na manageru s přesnou signaturou — base ji najde sám.
- V
beforeSaveneber data z požadavku jako pravdu — sanitizuj, co přišlo zvenčí (viz níž). - V hoocích nevolej
find()anifindAll()(viz níž). - Vrať
Result::failure(...), když má operace selhat — base udělá rollback sám. - Ověř i cestu selhání: co zůstane v databázi, když
afterSavevrá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#
- Entity se načtou PŘED smazáním a hookům se předají jako pole — po
DELETEuž řádek neexistuje. Hooky se přednačítají jen tehdy, když je manager deklaruje. - 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.php → bumpCacheTags() |
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