Ž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.
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#
- 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 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
- Chybu položky chyť a zaloguj.
- Zavolej
UnitOfWorkReset::recoverAfterFailure(): otevřený EntityManager vyprázdní, zavřený znovu otevře. Příméclear()nebodetach()neprojde testemUnitOfWorkResetBoundaryTest. - 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 |
- Injektuj manager vlákna konstruktorem (
commentThreadManager,ratingThreadManager,messageThreadManager); traita ho jen čte. - Typ vlákna předej sám (
ThreadTypesekce) — drží se na vláknu kvůli moderaci v administraci a nastavení po typech. - 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#
- 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.
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
- Řádek typu v migraci:
key,module_id,iconacms_admin_system_pages_page_idstránky editace cílového záznamu (akce, která bereid). Bez stránky je položka prostý text. - Sondu, která typ pozná (viz výše).
- Producenta v Api presenteru (
shouldProduceNotifications(), Volání API). - 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#
- Najdi všechny volající (
greppřesapp/,tests/,bin/) a zapiš si je — každý se bude měnit. - 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, …). - Přepiš každého volajícího na
->isSuccess(). VafterSaveselhání vrať dál (return $result;) — base udělá rollback a administrátor uvidí červenou hlášku. - Drátový tvar Api presenteru nech být (
{success},{success, code}…): presenter zResultsloží stejnou odpověď jako dřív (pomocníci*Wire()). Klienti Api se tím nemění. - Přidej metodu do seznamu
CONVERTEDvtests/Unit/System/ResultReturnSweepTest.php. Statický strážní test projdeapp/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