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

Po smazání vrací getId() smazané entity null

Doctrine při flush() vynuluje ID odstraněné spravované instance, takže v afterDelete a completedDelete už $entity->getId() nevrátí, co entita měla. DeleteTrait::delete() proto ID zachytí ještě před beforeDelete a předá je každému hooku v $meta['__deletedIds'] (konstanta DeleteTrait::DELETED_IDS_META, WeakMap klíčovaný objektem entity). Čti je jen přes $this->getDeletedId($meta, $entity); vrací null pro entitu, kterou toto mazání nenačetlo. Klíč se nastaví vždy, takže hodnota, kterou pod ním pošle volající, se přepíše.

protected function completedDelete(array $entities, Result $result, array $meta): void
{
    foreach ($entities as $entity) {
        $id = $this->getDeletedId($meta, $entity);   // ne $entity->getId()
        …
    }
}

Hook s jinou signaturou vrátí zpět každé uložení

SaveTrait hook najde přes method_exists() a volá ho pozičně v pořadí výše. Chybí-li na prvním místě bool $isInsert, dostane parametr typovaný entitou bool a PHP vyhodí TypeError. save() ho chytí jako každou výjimku, vrátí transakci zpět a odpoví DB_ERROR; příčina je jen v logu save.error. Nespadne tedy jedno uložení, ale každé uložení té entity. Nový hook proto opiš ze signatury výše.

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.

Neúspěšné uložení služby se vrátí celé

Vrátí-li service->save() neúspěch (třeba DB_DUPLICATE_ENTRY z repozitáře), SaveTrait transakci vrátí a afterSave ani completedSave nespustí. Zápisy, které už udělal beforeSave (čítač kuponu, číselná řada objednávek), tak bez záznamu nezůstanou.

afterCommit(): efekt až po skutečném commitu#

Vnořené uložení (manager volaný z jiné operace) „commituje“ jen logicky a jeho completedSave běží ještě uvnitř vnější transakce. Ta se může vrátit — a e-mail poslaný rovnou na SMTP už nevrátíš. Nevratný efekt proto zaregistruj přes $this->service->afterCommit(fn () => …) (TransactionManager::afterCommit()):

Situace Kdy callback běží
mimo transakci hned
uvnitř transakce (i vnořené) po commitu nejvnější transakce
rollback, rollback-only, selhaný commit nikdy, callback se zahodí

Callbacky běží mimo transakci a jsou best-effort jako completedSave: výjimka se jen zapíše do logu transaction.afterCommit a ostatní callbacky doběhnou. Vidí jen transakce TransactionManager; holá DBAL transakce otevřená kolem (beginTransaction() na spojení) pro něj neexistuje a callback se spustí hned. Takhle odchází e-mail o odpovědi na inzerát a upozornění na novou zprávu v konverzaci.

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 načítej cizí data předem#

Hook běží nad hydratovaným grafem v jedné operaci

Čtení (find(), findAll()) identity mapu nevyprazdňuje, mapu čistí jen hranice operace. Hook, který uvnitř volá cizí manager, se k rozběhlé operaci jen připojí. Přesto je bezpečnější číselníky a nastavení načíst před stavbou grafu nebo je vzít přes DBAL: entita, kterou si hook odnese mimo operaci, je po jejím konci odpojená a flush() skončí hláškou A new entity was found through the relationship ….

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.

Rollback na nejvyšší úrovni (TransactionManager::rollbackPhysically()) navíc vyprázdní UnitOfWork ($em->clear()). Hydrátor totiž zapisuje do spravovaných entit ještě před beforeSave(), takže po odmítnutém uložení v paměti zůstávají přepsané kolekce a naplánované orphanRemoval; další flush v témže procesu (další položka cronu nebo importu, jiný manager) by je jinak zapsal. Vnořený rollback UnitOfWork nečistí, jen označí transakci „rollback-only“.

Kde selhalo Co zbude v UnitOfWork
uložení na nejvyšší úrovni nic, identity mapa je prázdná
uložení vnořené do cizí transakce celý rozpracovaný graf až do rollbacku volajícího
odmítnutí přes ProductSetManager::reject() nebo ShipperTariffManager::rejectImmutable() nic, manager sám volá UnitOfWorkReset::abort()

Po neúspěšném uložení entitu načti znovu

Instance, kterou volající držel, je po vyprázdnění odpojená. Další save() nebo flush s ní pracuje s grafem, o kterém Doctrine neví, a změny se tiše ztratí nebo uložení spadne. Chceš-li to zkusit znovu, načti entitu znovu z Api.

Vlastní operace přes víc řádků: inAtomicTransaction()#

Operace mimo save()/delete(), která mění víc řádků (matice, přeskládání, hromadné nastavení), patří do AtomicTransactionTrait::inAtomicTransaction($connection, $work). Closure vrací Result: neúspěch i výjimka znamenají rollback, výjimka se zaloguje a vrátí jako DB_ERROR, úspěch commit.

return $this->inAtomicTransaction($this->em->getConnection(), function (): Result {
    // několik zápisů; první neúspěch vrať jako Result::failure(...)
    return Result::success(ResultCode::OK);
}, 'invoicer.pdfVariantMatrix');

Vlastní transakci otevře jen tehdy, když žádná neběží (isTransactionActive() na DBAL spojení). Volaná uvnitř cizí transakce jen vrátí výsledek a commit i rollback nechá volajícímu.

Vnořený rollback nevrátí jen vnitřní část

TransactionManager nepoužívá savepointy. Rollback na vnořené úrovni jen označí celou transakci „rollback-only“ a poslední commit volajícího pak vrátí všechno a vyhodí RollbackOnlyException. Proto inAtomicTransaction() ve vnořeném běhu nic nevrací zpátky sama a jen předá Result.

Zavřený EntityManager po pádu flushe#

Doctrine ORM 3 nemá reopen(). Když flush selže, UnitOfWork::commit() EntityManager zavře a každý další persist()/flush() v témže procesu hodí „The EntityManager is closed.“ — další položka cronu nebo importu, uložení objednávky po závodě o zákazníka. App\Core\Doctrine\EntityManagerReopener::reopenIfClosed() ho otevře znovu a zahodí persistery UnitOfWork, které si Doctrine po zavření nechá. Mimo TransactionManager se reopener ani clear()/detach() přímo nevolají (hlídá to UnitOfWorkResetBoundaryTest), ale přes App\Core\Doctrine\Operations\UnitOfWorkReset:

Kdo Co volá
TransactionManager sám po nejvyšším rollbacku — v transakci manageru nic psát nemusíte
místa, která chybu flushe řeší sama (import produktů a sad) UnitOfWorkReset::recoverAfterFailure($reason, $logChannel) — otevřený EntityManager vyprázdní, zavřený znovu otevře
místo, které má jen znovu otevřít zavřený EntityManager (OrderCustomerManager) UnitOfWorkReset::reopenIfClosed($reason, $logChannel)
fáze, která po zápisech přes DBAL začíná s čistou mapou (FeedManager, StockDocumentManager) UnitOfWorkReset::resetBatch($reason)

Postup: smyčka, která má po chybě jedné položky pokračovat

  1. Chybu položky chyť a zaloguj.
  2. Zavolej UnitOfWorkReset::recoverAfterFailure(): otevřený EntityManager vyprázdní, zavřený znovu otevře. Přímé clear() nebo detach() neprojde testem UnitOfWorkResetBoundaryTest.
  3. Pokračuj další položkou — a ověř, že se po chybě opravdu uloží.

Otevírej jen po rollbacku a s prázdnou UnitOfWork

Persister entity, jejíž INSERT selhal, ji drží ve frontě $queuedInserts. Kdyby se neodhodil, další flush by ji přehrál s prázdným changesetem — tedy INSERT samých NULL. Proto reopener persistery zahazuje a otevírá se až po vrácené transakci.

Reopener sahá reflexí do vnitřků Doctrine

Vlastnosti closed, persisters a collectionPersisters hlídá tests/Unit/Core/TransactionSequencesTest.php; po aktualizaci ORM, která je přejmenuje, helper přepiš. Dekorovaný EntityManager otevřít neumí — vrátí false a zapíše to do logu, další zápisy v procesu pak selžou.

Vlákno komentářů, hodnocení nebo konverzace při založení#

Entita, ke které se dá hned po založení psát nebo ji hodnotit, dostane vlákno v afterSave() při INSERTu. Manager k tomu použije jednu ze tří traitů; každá vlákno uloží a jeho id zapíše do sloupce entity přes updateColumn():

Traita Metoda Vlákno Výchozí sloupec
CommentThreadCreatorTrait createCommentThread($id, ThreadType) prázdné vlákno komentářů commentThreadId
RatingThreadCreatorTrait createRatingThread($id, ThreadType) prázdné vlákno hodnocení ratingThreadId
MessageThreadCreatorTrait openMessageThreadFor($id, MessageThreadType, …) soukromá konverzace s dvěma účastníky a první zprávou messageThreadId
  1. Injektuj manager vlákna konstruktorem (commentThreadManager, ratingThreadManager, messageThreadManager); traita ho jen čte.
  2. Typ vlákna předej sám (ThreadType sekce) — drží se na vláknu kvůli moderaci v administraci a nastavení po typech.
  3. Neúspěch vrať z afterSave(): traita vlastní transakci neotevírá, rollback udělá save().

Konverzace bez účastníka s účtem nevznikne

openMessageThreadFor() vrátí null a messageThreadId zůstane prázdné, když ani jedna strana nemá účet (host píše na anonymní inzerát). Sloupec je nullable a není to chyba: takové vlákno by nikdo nikdy neotevřel. Účastníky předávej bez threadId a role, doplní je MessageThreadManager::openThread().

updateColumns() s kolekcí nahrazuje#

Kolekce (ToMany) v updateColumns() se zapisuje jako úplné nahrazení: smažou se všechny vazby rodiče a vloží se předané. Prázdné pole tedy smaže všechno. Hodnoty musí být objekty vazebních entit; skalár skončí INVALID_ARGUMENT ještě před zápisem. Sloupce i kolekce jdou v jedné transakci mapperu, takže pád uprostřed vkládání nenechá rodiče s polovinou řádků (cenové snímky sad).

Uvnitř cizí transakce mapper vrací jen svou úroveň

Mapper vrací zpět jen úroveň, kterou sám otevřel, a selhání rollbacku jen zaloguje. Kdyby vracel naslepo, po nepovedeném commitu by vyhodil NoActiveTransaction a zakryl původní chybu, nebo by vrátil i úroveň volajícího.

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.

Hromadné mazání (delete([…]) bez hooků, deleteAll()) jede jako jedna transakce: při technické chybě se vrátí celá dávka a Result nese deletedCount = 0 a rolledBack = true. Chybějící id nejsou chyba, zbytek dávky se smaže a commitne.

Úklid obrázků smazané entity#

DeleteTrait po commitu volá vedle completedDelete() ještě completedAssetCleanup() z AssetProcessingTrait. Rozjede se jen u manageru, který definuje ownedAssetIds(object $entity): array (které obrázky entita vlastní). Postup a pravidla sdílených obrázků: Varianty obrázků.

Metoda třídy přebije metodu traitu bez varování

completedDelete() si definuje sama řada Api managerů. Kdyby sdílený úklid bydlel v traitu pod stejným jménem, v těch managerech by tiše neběžel. Proto má vlastní jméno — a ze stejného důvodu AdvertHardDeleteTrait volá svůj post-commit krok completedAdvertHardDelete() z completedDelete() manageru výslovně. Když v traitu přidáváš hook, který už některý manager definuje, musí ho volat ručně.

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ší.

Položky plateb uživatele (UserPaymentManager) ukazují obě varianty i třetí případ — kredity, které se nepřepočítávají vůbec:

Hodnota Brána (updateStatusForPayment()) Uložení z administrace Smazání položky
member_until (tarify s duration > 0) monotonně prodlouží formulář (UserPaymentFormTrait) posílá meta.recalcMemberUntil → absolutní přepočet; jiné uložení aktivní položky jen prodlouží absolutní přepočet; bez aktivní členské položky NULL
kredity (tarify s credits > 0) připíše jednou za platbu připíše jen při čerstvé aktivaci: insert s active = 1 nebo přechod active 0 → 1 neodečítá

Přechod 0 → 1 zjišťuje beforeSave() čtením uloženého active přímo z DB se zámkem FOR UPDATE (spravovaná entita už nese novou hodnotu) a předá ho do afterSave() přes data Result. Dva správci, kteří aktivují tutéž položku najednou, se tak seřadí a kredity se připíšou jednou.

Kredity jsou nevratné

Utracený zůstatek nejde zpětně dopočítat, proto smazání ani deaktivace položky kredity neubere; storno je samostatná operace. Kdo by do afterDelete() přidal odečet, mohl by uživateli srazit zůstatek pod nulu za kredity, které už utratil.

Smazání prostřední členské položky zkrátí platnost jen přibližně

Absolutní přepočet bere maximum z uložených validity_datetime a platnost zbylých navazujících položek nepřepočítává. Přesné je smazání poslední položky.

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.

Notifikace: příjemce a sondy#

Notifikace administrátorů nese receiverUserId (příjemce; NULL = společná pro všechny správce) a itemId cizího záznamu, na který zvonek vede. Pole se dřív jmenovalo userId; starý název už neexistuje (sloupec v DB je stejný). Producent (NotificationProducerTrait) nastavuje setReceiverUserId(null) pro společné a setReceiverUserId($userId) pro cílené (import produktů míří na toho, kdo ho spustil).

Smaže-li se cílový záznam, notifikace se nemaže, jen odpojí: vynuluje se jí odkaz na cíl (id) a ve zvonku i v přehledu zůstane jako text bez odkazu, místo aby vedla na 404. Smazání přes ORM ji odpojí hned (NotificationTargetDetachSubscriber); smazání mimo ORM (raw SQL, kaskáda v DB) dočistí NotificationOrphanCleaner. Oba poznají, kam typ notifikace míří, ze sond:

Část Úkol
NotificationTargetProbeInterface::existingIds(string $typeKey, array $ids): ?array modul sám odpoví, které z ID ještě existují; null = tuhle sondu typ nepatří
ManagerNotificationTargetProbe základ: mapa klíč typu → admin manager, jeden dotaz id IN (…)
sondy modulů Models/Helpers/NotificationTargetProbe v modulech Eshop, System, Base, Blog, Company (autowire pole NotificationTargetProbeInterface[])
NotificationOrphanCleaner detachOrphans() (500 nejnovějších) — jen z tlačítka „Odpojit osiřelé“ v přehledu (POST, Api clean-orphans); detachOrphansResuming() (dalších 500 starších, pozice v cache, po nejstarších znovu od nejnovějších) — z cronu /cron/system/notification/clean-orphans; nikdy při vykreslení stránky, protože GET, který mění data, spouštějí i roboti
final class NotificationTargetProbe extends ManagerNotificationTargetProbe
{
    protected function managers(): array
    {
        return ['ORDER_NEW' => $this->orderManager, 'COMPLAINT_NEW' => $this->complaintManager];
    }
}

Nový typ notifikace = doplnit sondu

Typ, ke kterému se žádná sonda nehlásí, se neodpojí nikdy — po smazání záznamu zůstane ve zvonku odkaz na 404. Stejně se chová typ, jehož ověření selže (fail-safe: raději mrtvý odkaz než odpojená platná notifikace).

Kam odkaz notifikace vede, je v datech, ne v kódu. Typ (cms_system_notification_types) ukazuje sloupcem cms_admin_system_pages_page_id na admin stránku; NotificationTargetResolver z ní složí presenter:action a odkaz je vždy destinace + ['id' => itemId]. Sloupec icon nese třídu Font Awesome (prázdný = zástupná ikona). Resolver sdílí zvonek i přehled notifikací.

Postup: přidávám typ notifikace

  1. Řádek typu v migraci: key, module_id, icon a cms_admin_system_pages_page_id stránky editace cílového záznamu (akce, která bere id). Bez stránky je položka prostý text.
  2. Sondu, která typ pozná (viz výše).
  3. Producenta v Api presenteru (shouldProduceNotifications(), Volání API).
  4. Ověřte ve zvonku proklik s rolí, která na cílovou stránku právo má, i s rolí bez něj.

Stránka typu musí mít existující presenter

Resolver ho ověří přes class_exists() a jinak vrátí null; bez té kontroly by link() v hlavičce shodil každou admin stránku. Překlep v presenteru stránky tedy nic neshodí, jen notifikace zůstane bez odkazu.

Zvonek (NotificationBell) filtruje receiverUserId IS NULL OR receiverUserId = <přihlášený> (jeden skupinový OrFilter, takže read = 0 platí pro obě větve); bez identity je prázdný. Odkaz na cíl se nabídne jen s právem na cílovou stránku. Provozní popis: Notifikace.

Operace vrací Result, volající čte isSuccess()#

Operace, která něco mění (zápis, synchronizace vazeb, mazání, přepočet, úklid), vrací Result — ne bool a ne pole, ve kterém úspěch kóduje klíč nebo řetězec. false zahodí důvod selhání a volající ho navíc snadno přehlédne; Result nese kód i zprávu.

Co Návratový typ
operace (syncRoles, deletePrivilege, createFolder, recountAdverts, …) Result
statistika operace (kolik se přepočetlo, smazalo) Result s plochým shrnutím v data: ['updated' => 12]; entita do data nepatří
otázka (isOwnedBy, exists, hasVoted, canRespondTo, …) bool — Result by byl jen šum
zámek (acquire, claimExecution) bool — „zámek drží někdo jiný“ není chyba
čtení pro komponentu (availability(), summary()) pole — kontrakt konzumentů se nemění

Takto se 2. 10. 2026 převedly tyto operace: přiřazení rolí a skupin uživateli (UserRelationManager::syncRoles, syncGroups, removeExternalLogin), operace matice oprávnění (UserRolePrivilegeMatrixManager::setRelation, deletePrivilege, setPrivilegeGroup, saveGroupPrivileges), správce souborů (FileSystemManager::createFolder, writeFile, sanitizeAll), přepočty a úklidy inzerce a katalogu (CategoryManager::recountAdverts, AdvertManager::deleteLongExpired) a ceníky e-shopu (ShipperTariffManager::saveBranch, ProductSetManager::savePriceSnapshot, deletePriceSnapshot).

Objekt Result je vždy pravdivý

if ($manager->syncRoles($id, $roles)) { … } projde i při selhání — a nikdo to nepozná. Volající vždy čte ->isSuccess(). Dřív se výsledek syncRoles dokonce zahazoval a administrátor dostal „Uloženo“, i když role v databázi zůstaly staré.

Postup: převedu bool operaci na Result#

  1. Najdi všechny volající (grep přes app/, tests/, bin/) a zapiš si je — každý se bude měnit.
  2. Změň návratový typ v celém řetězu vrstev (repository → mapper → service → manager); důvody selhání jsou ResultCode (INVALID_VALUE, ITEM_NOT_FOUND, FILE_WRITE_FAILED, …).
  3. Přepiš každého volajícího na ->isSuccess(). V afterSave selhání vrať dál (return $result;) — base udělá rollback a administrátor uvidí červenou hlášku.
  4. Drátový tvar Api presenteru nech být ({success}, {success, code} …): presenter z Result složí stejnou odpověď jako dřív (pomocníci *Wire()). Klienti Api se tím nemění.
  5. Přidej metodu do seznamu CONVERTED v tests/Unit/System/ResultReturnSweepTest.php. Statický strážní test projde app/ a shodí build, najde-li volání převedené metody použité jako holá podmínka (if (, !, &&, ||, (bool)) bez ->isSuccess().

Strážní test hlídá jen metody ze seznamu

Seznam CONVERTED se udržuje ručně a dnes v něm nejsou setRelation, createFolder, writeFile a saveBranch. Nová převedená metoda, kterou do něj nepřidáš, může mít volajícího s holou podmínkou a test o tom nebude vědět.

Admin vrstvy téhož zůstávají na drátovém tvaru

Administrační managery jsou klienti Api přes ApiBridge a vracejí drátový tvar ($result['success']). Čtou explicitní klíče, ne pravdivost, takže past výš tam nehrozí. Převést je jde jen společně s kontraktem Api.

Cron při selhání ohlásí chybu

Cron presenter, jehož operace vrátí neúspěšný Result, pošle chybu (sendError) místo „ok“. Hromadné mazání prošlých inzerátů, které selže, proto nezačne reindexovat nad nezměněnými daty; nenakonfigurovaný kořen nahraných souborů je selhání, ne tichá nula.

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
znovuotevření EntityManageru po pádu flushe app/Core/Doctrine/EntityManagerReopener.php, app/Core/Doctrine/Transactions/TransactionManager.php
přednačtení entit před mazáním app/Core/Traits/Api/Models/Managers/DeleteTrait.php
atomickou operaci přes víc řádků app/Core/Traits/Api/Models/Managers/AtomicTransactionTrait.php
vlákno při založení app/Core/Traits/Api/Models/Managers/{CommentThreadCreatorTrait,RatingThreadCreatorTrait,MessageThreadCreatorTrait}.php
updateColumns() a nahrazení kolekce app/Core/Traits/Api/Models/Mappers/SaveTrait.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

Nastavení cookies