Identity mapa a clearDoctrine()#
Nejzávažnější landmina celého systému. Kdo ji nezná, narazí do týdne — a stráví den hledáním chyby v místě, kde není.
Mechanismus#
Api repository vyprazdňuje Doctrine identity mapu na začátku každého čtení:
// app/Core/Traits/Api/Models/Repositories/GetAllTrait.php
public function getAll(…): array
{
$this->clearDoctrine(); // ← ÚPLNĚ PRVNÍ řádek
…
}
protected function clearDoctrine(): void
{
$this->db->getUnitOfWork()->clear(); // celá identity mapa pryč
$cache = $this->db->getConfiguration()->getResultCache();
if ($cache) { $cache->clear(); }
}
getAll() je jediné hrdlo celého čtecího stacku. get() i getBy() na něj jen
delegují (GetTrait a GetByTrait volají getAll() s limitem 1), takže find(),
findBy() i findAll() mapu vyprázdní úplně stejně.
Platí to i pro cizí managery a pro Admin i Front
Admin i Front jdou přes most do téhož Api stacku. Manager, který si zavolá jiný
manager kvůli číselníku nebo nastavení, spustí clearDoctrine() úplně stejně.
„Jen si přečtu jedno id“ není nevinná operace.
Objekty žijí dál, jen přestanou být spravované
Právě proto to není vidět. Kód po clearDoctrine() čte i zapisuje gettery bez
chyby a rozbije se až nejbližší flush() — často o několik vrstev jinde.
Tři projevy#
1. Odpojená reference#
Doctrine\ORM\ORMInvalidArgumentException:
A new entity was found through the relationship 'X#owner' that was not
configured to cascade persist operations
Hook zavolal cizí findAll() a odpojil referenci. Při flush() vypadá jako nová
entita bez kaskády.
Náprava: znovupřipojit přes $em->getReference() až za všemi cizími voláními,
nebo cizí volání nahradit raw DBAL.
2. Líná kolekce inicializovaná až za clearDoctrine()#
Zákeřnější, protože nestačí „nevolat find() v hoocích“:
- Služba natáhne entitu s
includes— kolekce, která v nich není, zůstane líná. - Cestou se zavolá cizí manager →
clearDoctrine()→ vlastník je odpojený. - Teprve teď se iteruje líná kolekce → děti jsou spravované, ale jejich zpětná reference ukazuje na odpojeného vlastníka.
flush()→ stejná hláška.
includes musí pokrývat každou kolekci, které se dotkne cokoli spuštěné z ní
Ne jen to, co čte orchestrační služba sama. Kde to nejde, dohydratujte kolekce explicitním „touch“, dokud je vlastník ještě spravovaný.
3. Duplicitní instance#
While adding an entity of class …\OrderShipper with an ID hash of "1" to the
identity map, another object of class …\OrderShipper was already present for
the same ID.
Graf se načetl, mapa se vyprázdnila, a tatáž entita se do ní dostala podruhé jinou cestou.
Postup: píšu hook, který potřebuje cizí data#
- Načti všechno PŘED tím, než začneš skládat graf. Číselníky, nastavení, cizí klíče — všechno na začátku, ne uprostřed.
- Uvnitř hooku nevolej
find()anifindAll()— ani na vlastním manageru. - Použij bezpečnou náhradu:
| Potřebuji | Náhrada |
|---|---|
| jeden řádek číselníku | raw DBAL dotaz přes $em->getConnection() |
| nastavení | SettingContext (in-memory, nesahá na DB) |
| cizí klíč do asociace | $em->getReference() |
| ověřit existenci | SELECT COUNT přes DBAL |
- Když se cizímu volání nedá vyhnout, udělej ho první a teprve potom načti entitu, kterou budeš ukládat.
- Ověř uložením, ne čtením. Chyba se projeví až při
flush().
Raw DBAL v hooku není obcházka vrstev, je to nutnost
Proto to dělá i InvoiceNumberGenerator: čte řadu i MAX(order) čistě přes
getConnection(), aby nesáhl na identity mapu uprostřed rozestavěného grafu
faktury.
Postup: dostal jsem „A new entity was found through the relationship“#
Od nejlevnějšího:
- Nedívej se na místo, kde spadl
flush(). Výjimka vzniká až při ukládání; viník je čtení, které proběhlo předtím — někdy o několik vrstev výš. - Projdi cestu od načtení entity ke
flush()a hledej každé volánífind()/findBy()/findAll(), včetně cizích managerů. - Podívej se na
includes. Chybí tam kolekce, kterou někdo iteruje až později? - Zkontroluj lifecycle hooky manageru — a taky hooky manažerů, které z nich voláš.
- Změř to, nehádej: Doctrine umí událost
onClear. Zaregistruj posluchače, který vypíše zásobník volání, a uvidíš přesně, které volání mapu vyprázdnilo. - Teprve pak sahej na
getReference()jako na náplast — nejdřív musíš vědět, co mapu vyčistilo.
Hláška ukazuje na flush(), viník je jinde
Tohle je hlavní důvod, proč se na to hledá tak dlouho. Zásobník volání ukazuje ukládání, ne čtení — a čtení může být v úplně jiném modulu.
Postup: přidávám čtení do už fungujícího procesu#
Typicky „potřebuju sem doplnit ještě jméno uživatele“:
- Zjisti, jestli jsi uvnitř rozestavěného grafu. Jsi-li v
beforeSave,afterSave, v generátoru nebo v orchestrační službě mezi načtením a uložením — ano. - Když ano, vezmi data raw DBAL dotazem, nebo je natáhni před začátek procesu a předej si je parametrem.
- Když ne (presenter, komponenta, čtecí cesta), je běžné
findAll()v pořádku. - Nikdy nespoléhej na to, že „to je jen jedno id“ — velikost dotazu s tím nemá
nic společného, mapu vyprázdní i
SELECTna jeden řádek.
Pravidlo#
Nejdřív všechna čtení, pak teprve stavba grafu
Jediná věta, která tuhle landminu shrnuje. Potřebujete-li číselník, nastavení nebo cizí data, načtěte je před tím, než začnete skládat objektový graf k uložení. Ne uprostřed.
Kam sáhnout#
| Chci | Kde |
|---|---|
| samotné vyprázdnění | app/Core/Traits/Api/Models/Repositories/GetAllTrait.php → clearDoctrine() |
proč to get() a getBy() dělá taky |
…/Repositories/{Get,GetBy}Trait.php — delegují na getAll() |
| hotový příklad obejití přes DBAL | app/UI/Api/Invoicer/Models/Generators/InvoiceNumberGenerator.php |
| lifecycle hooky, kterých se to týká | app/Core/Traits/Api/Models/Managers/SaveTrait.php |
Navazující kapitoly: Životní cyklus manageru · Hydrátory · Volání API · Pětivrstvý model