Kapitola 18 · Praxe · Migrace z CRUD architektury na DDD

Migrace z CRUD architektury na DDD

Přechod z CRUD aplikace na DDD v Symfony bez přepisování od nuly: Strangler Fig Pattern, vytažení doménové vrstvy, zavedení repozitářů a postupné CQRS. S příklady v PHP.

Autor M. Katuščák
Doba čtení ≈ 34 min
Náročnost pokročilá
Publikováno · Aktualizováno ·
Obsah kapitoly

18.01 Kdy a proč migrovat z CRUD na DDD#

CRUD architektura (Create, Read, Update, Delete) je výchozí volba pro většinu aplikací a dlouho stačí. U správy dat bez komplexní logiky, jako jsou kontakty, katalogy produktů nebo administrace, by vrstvy DDD byly jen zátěží navíc. Problém nastane, když aplikace zesložití a doménová logika se začne usazovat na nevhodných místech.

Kdy DDD přináší hodnotu a kdy je CRUD dostačující

Rozhodnutí o migraci stojí na analýze komplexity domény, ne na trendech. Martin Fowler v knize Patterns of Enterprise Application Architecture označuje Transaction Script za přirozenou volbu pro aplikace s malým množstvím logiky. Se složitějšími pravidly se podle něj takový kód udržuje čím dál hůř a výhodu získává Domain Model [1].

Ekonomika migrace: co stojí a kdy se vrátí

Migrace z CRUD na DDD trvá podle velikosti kódové základny měsíce až roky a zákazníkovi sama o sobě nic nepřinese. Užitek přijde, až tým začne přidávat funkce s menším rizikem regresí.

Inkrementální migrace středně velké CRUD aplikace (50–100 tabulek, 3–5 let vývoje) zabere zpravidla 12 až 24 měsíců. To číslo je zkušenostní řádový odhad, ne měření. Počítá s tím, že migrace běží souběžně s vývojem nových funkcí a nemá vlastní tým na plný úvazek. Dobu prodlužuje špatná testovatelnost stávajícího kódu, slabá znalost domény v týmu a nedostupnost doménových expertů.

Rozhodovací kritérium přitom není technické. Investice se vrátí, jen když přínos za zbývající životnost systému převýší cenu migrace s rezervou. Nákladový model i situace, kdy se investice nikdy nevrátí, rozebírá sekce Migration cost paradox. Management přijme migraci snáz jako průběžnou práci vedle nových funkcí než jako izolovaný refaktoringový projekt.

Kdy migraci nezačínat a kdy je přepis levnější

Tři situace mluví proti migraci vůbec:

  • Systému zbývá kratší životnost než samotná migrace. Aplikace, která má za dva roky skončit, investici nesplatí.
  • Doména se ruší nebo přechází na koupené řešení. Modelovat pravidla, která za rok zaniknou, je práce do koše.
  • V dosahu týmu není nikdo, kdo doméně rozumí. Bez doménového experta vznikne přejmenovaný CRUD, ne doménový model. Tuto past popisuje kapitola Kdy DDD nepoužívat.

Přepis od nuly naopak vyjde levněji tam, kde je systém menší než náklad na zavedení švů a charakterizačních testů, nebo kde ještě nejsou produkční uživatelé a data. Martin Fowler pro to má jméno Sacrificial Architecture [6]: architektura navržená s tím, že se za pár let zahodí, je rozumná volba. Patří k ní ale podmínka: o zahození rozhoduje tým, který systém napsal – ne nový tým, který přijde zvenčí a starý kód chce přepsat, protože se mu nelíbí.

18.02 Strangler Fig Pattern – vzor postupné náhrady#

Strangler Fig Pattern (vzor fíkovníku škrtiče) pojmenoval Martin Fowler [2]. Vzor nahrazuje starý systém po částech, bez „big bang“ přepisu. Název pochází od tropického fíkovníku, který roste kolem hostitelského stromu a postupně ho zardousí.

Původní zápis vyšel 29. června 2004 pod názvem Strangler Application. Dne 29. dubna 2019 Fowler vzor přejmenoval na Strangler Fig Application: zkrácené „strangler“ se odtrhlo od botanické metafory a začalo vyznívat násilně. Starší název se v článcích i v názvech knihoven drží dodnes.

FIG. 18.2-A Strangler Fig: čtyři fáze migrace CRUD → DDD

Výhody oproti přepisu najednou (big bang rewrite)

Přepsat celý systém najednou („big bang rewrite“) patří k největším rizikům softwarového vývoje. Joel Spolsky v článku „Things You Should Never Do“ [3] popisuje, proč firmy ztratily konkurenční výhodu tím, že kompletně přepsaly fungující systémy. Strangler Fig Pattern oproti tomu:

  • Zákazník dostává nové funkce i během migrace.
  • Snižuje riziko: systém nikdy není kompletně „rozbitý“.
  • Dovoluje návrat: když nová implementace selže, stará dál funguje.
  • Tým se učí DDD postupně, na reálném produkčním kódu.
  • Refaktoring lze zastavit kdykoli, systém zůstává v konzistentním, funkčním stavu.

Co vzor neřeší

Původní zápis byl krátká úvaha nad metaforou. V roce 2024 ho Fowler přepsal a převzal od Iana Cartwrighta, Roba Horna a Jamese Lewise čtyři aktivity: ujasnit cílové výsledky, rozdělit problém na menší části, dodávat je postupně a měnit organizaci. Pořadí podle Fowlera neznamená posloupnost. Změna organizace prostupuje všemi ostatními aktivitami: bez ní skončí nový systém stejně křehký jako ten nahrazený, jen na novější technologii [2].

Vzor komplexitu neodstraňuje, rozprostírá ji v čase. Platí se za to přechodovou architekturou: routovací vrstvou, dvojím zápisem, překladovými adaptéry. Ian Cartwright, Rob Horn a James Lewis pro ni mají vlastní jméno: Transitional Architecture [7]. Připojují varování: část práce se nakonec zahodí. Kdo přechodovou vrstvu nezahodí, zdědí ji jako trvalou součást systému.

Datová migrace při Strangler Fig

Kód se dá nahrazovat po částech, data ne – tabulka má v každém okamžiku jeden tvar. Strangler Fig proto potřebuje plán, jak data převést do nového modelu bez výpadku a s možností návratu. Osvědčený postup má čtyři fáze.

1. Dual-write s porovnáním. Aplikace začne zapisovat do starého i nového modelu současně. Primární zůstává starý zápis; nový běží navíc a jeho chyba nesmí shodit požadavek. Asynchronní job oba zdroje porovnává a rozdíly loguje. Každý nalezený rozdíl znamená chybu v mapování; opravit se musí ještě před přepnutím.

Pozor na termín. „Dual-write“ zde znamená zápis do dvou datových modelů v jedné databázové transakci, tedy operaci, která buď proběhne celá, nebo vůbec. Kapitola Outbox Pattern používá tentýž pojem pro zápis do databáze a do brokeru bez společné transakce – tedy pro problém, který outbox řeší.

Dual-write z aplikace předpokládá, že do zápisové cesty starého systému lze zasáhnout. U kódu, kterému nikdo nerozumí, nebo u zápisů obcházejících ORM to neplatí. Náhradou je Event Interception [8]: změny zachytává vrstva pod aplikací (databázový trigger nebo CDC nástroj typu Debezium) a nový model je konzumuje. Volba se řídí jedním kritériem. Existuje-li v kódu jedno místo, kudy teče každý zápis, stačí dual-write. Pokud takové místo není, zbývá vrstva pod aplikací.

2. Backfill. Teprve po zapnutí dual-write naplní jednorázový skript nové tabulky historickými daty. Obrácené pořadí je vadné. UPDATE legacy řádku, který backfill už zpracoval, by se před zapnutím dual-write ztratil, protože checkpoint WHERE id > checkpoint ho podruhé nenačte. Skript musí být idempotentní: opakované spuštění nesmí vytvořit duplicity ani přepsat novější záznam, který mezitím zapsal dual-write. Běží po dávkách podle id nebo updated_at, při konfliktu vyhrává novější záznam, a ukládá si checkpoint posledního zpracovaného řádku, takže po pádu naváže tam, kde skončil.

3. Shadow reads. Čte se z obou zdrojů: odpověď uživateli sestavuje starý model, výsledek nového se jen porovná a neshoda zvedne alert. Teprve nulová míra rozdílů po dnech až týdnech provozu dává jistotu, že nový model je úplný a správný.

4. Cutover. Přepnutí na nový model řídí feature flag, ne deploy. Provoz se převádí postupně: 1 %, 10 %, 50 %, vše. Metriky z fáze shadow reads přitom zůstávají zapnuté. Rollback znamená přepnout flag zpět; starý model je díky dual-write stále aktuální. To platí jen tehdy, když po přepnutí primáru dual-write pokračuje v obráceném směru, tedy když nový model zapisuje zpět do starého. Starý zápis se vypíná jako úplně poslední krok, po několika týdnech klidného provozu. Symfony pro feature flagy vlastní komponentu nemá; v PHP se používají knihovny třetích stran, typicky bundle flagception/flagception-bundle nebo klient unleash/client.

Schéma se mění stejným způsobem. Sloupec ani tabulka se nepřejmenovává jedním ALTER TABLE. Pramod Sadalage a Martin Fowler pro to popisují přechodnou fázi [9]. Nová struktura vznikne vedle staré, po dobu migrace se plní obě a původní název zůstane dostupný jako pohled (view) nebo přes trigger. Závislé systémy tak migrují vlastním tempem a DROP přijde až na konci. V Symfony tuto posloupnost nese DoctrineMigrationsBundle: každá fáze je vlastní verzovaná migrace, ne ruční zásah do produkční databáze.

18.03 Anti-Corruption Layer mezi legacy a novým modelem#

Slabým místem koexistence dvou architektur jsou data. Nový model si sáhne pro uživatele do legacy tabulky, převezme její sloupce jeden ku jedné a za měsíc vypadá stejně jako to, co měl nahradit. Anti-Corruption Layer je vrstva, která tomu brání: překládá cizí model na doménový a zpět, a nic jiného nedělá. Plný výklad vzoru včetně jeho místa na Context Mapě je v kapitole Bounded Context a Context Mapping, zde jde o jeho migrační podobu.

Cartwright, Horn a Lewis popisují tutéž věc pod jménem Legacy Mimic [10]: nový systém mluví se starým tak, aby starý o změně nevěděl. Rozdíl je v úhlu pohledu. Legacy Mimic chrání starý systém před novým, ACL chrání nový model před tvarem toho starého. Při migraci z CRUD potřebujete obojí, obvykle ve stejné třídě.

Tři pravidla drží ACL na uzdě:

  1. Vrstva patří do infrastruktury nového Bounded Contextu, nikdy do jeho domény. Doménový kód o existenci legacy tabulky neví.
  2. Překlad je explicitní kontrakt. Co legacy model neumí vyjádřit, se doplní výchozí hodnotou nebo hlasitě selže. Doménový model se kvůli tomu neohýbá.
  3. ACL má datum expirace. Až legacy zápisy skončí, vrstva se maže.

Hranici je rozumné hlídat i staticky: pravidlo, které zakáže jmennému prostoru App\<BC>\Domain\* odkazovat na App\<BC>\Infrastructure\Legacy\*, odhalí prosáknutí dřív než code review. Postup je v kapitole Architektonické testy.

18.04 Techniky bezpečné postupné změny#

Strangler Fig říká, co dělat v měřítku systému. K jednomu konkrétnímu refaktoringu ale mlčí: jak vyměnit implementaci třídy, kterou volá čtyřicet míst, a přitom nasazovat každý den. K tomu slouží tři pojmenované techniky.

Branch by Abstraction

Fowler ji popsal v roce 2014 [11]; termín zavedl Paul Hammant a autorství připisuje Stacy Curlovi. Postup má pět kroků:

  1. Nad rozhraním mezi volajícím a starou implementací vytvořte abstrakci, v PHP zpravidla interface.
  2. Převeďte na ni všechny volající. Chování se nemění, systém zůstává nasaditelný.
  3. Za stejnou abstrakcí postavte novou implementaci (doménový repozitář místo přímého EntityManager).
  4. Přepínejte volající po částech, přes feature flag nebo alias v services.yaml.
  5. Starou implementaci smažte.

Ve verzovacím systému přitom žádná větev nevznikne. Obě implementace žijí v hlavní větvi vedle sebe a rozdíl mezi nimi drží abstrakce, ne merge.

Parallel Change: expand – migrate – contract

Zápis na Fowlerově webu je od Danila Sata [12]; samotnou techniku popsal Joshua Kerievsky už v roce 2006. Týká se změny jednoho rozhraní, ne celého systému:

  • Expand – rozhraní se rozšíří tak, aby uneslo starou i novou variantu. Metoda setEmail(string $email) dostane sourozence changeEmail(Email $email).
  • Migrate – volající se převádějí. Uvnitř aplikace je to práce na hodiny, u externích klientů API na měsíce; tato fáze bývá nejdelší.
  • Contract – stará varianta se odstraní. Odchod se ohlásí atributem #[\Deprecated] (PHP 8.4) nebo trigger_error(..., E_USER_DEPRECATED).

Hodnota vzoru je v tom, že kód jde nasadit v každé ze tří fází. Stejná trojice platí i pro databázové schéma; popisuje ji čtyřfázová migrace dat v sekci Strangler Fig Pattern.

Mikado Method

Metodu vytvořili Ola Ellnestam a Daniel Brolund, v PHP ji zpopularizoval Matthias Noback [13]. Jde shora dolů: cílová změna se provede rovnou a nechá se selhat. Z chybových hlášek a padlých testů se odvodí prerekvizity, změna se zahodí (git checkout .) a řeší se nejdřív listy vzniklého grafu. Graf tak vzniká z reality kompilátoru a testů, ne z odhadu u tabule.

Noback k ní přidává pravidlo: refaktoring musí jít kdykoli zastavit a projekt musí být v tu chvíli v lepším stavu než na začátku [14]. Migrace, která je použitelná až po dokončení, je big bang rozložený do sprintů.

18.05 Krok 1: Analýza existující domény#

Přesouvání kódu předchází pochopení domény. Nejčastější chybou je skok rovnou do refaktoringu. Výsledkem je DDD architektura, která kopíruje strukturu starých databázových tabulek a skutečný doménový model neodráží.

Identifikace Bounded Contexts z existujícího CRUD kódu

Bounded Contexts se v existující CRUD aplikaci poznají podle přirozených hranic:

  • Skupiny entit a tabulek, které jsou silně provázané navzájem, ale slabě propojené s ostatními skupinami. To jsou kandidáti na jeden Bounded Context.
  • God Services – velké service třídy jsou paradoxně dobrým vodítkem. Obsahuje-li OrderService logiku objednávky, platby i doručení, skrývá jedna třída tři Bounded Contexts.
  • Stejné slovo s různým významem. Znamená-li „zákazník“ v prodeji něco jiného než v zákaznické podpoře, vede tudy hranice dvou Bounded Contexts.

Event Storming jako nástroj pro analýzu

Event Storming vymyslel Alberto Brandolini [4]. Workshopová technika modeluje doménu přes doménové události a přivádí k návrhu i lidi mimo vývojový tým, hlavně doménové experty. Při migraci z CRUD odhalí logiku ukrytou v kontrolerech a service třídách, pojmenuje přechody stavů z pohledu domény, ne databáze, a ukáže přirozené hranice Bounded Contexts. Notaci, průběh workshopu a jeho tři úrovně rozebírá kapitola Event Storming a Domain Storytelling.

Co nemigrovat

Analýza má přinést dva seznamy: co existuje a co skončí. Feature parity, tedy požadavek, aby nový systém uměl přesně totéž co starý, Cartwright, Horn a Lewis nedoporučují [15]. Náklad na replikaci existující funkcionality se soustavně podceňuje a část replikovaného chování stejně nikdo nepoužívá. V CRUD aplikaci s deseti lety historie bývá typickým nálezem exportní obrazovka, kterou obsluhovali dva lidé a jeden z nich je tři roky pryč. Takovou funkci nemá smysl modelovat. Ruší se.

Verdikt ale nevydává tým sám. O seznamu funkcí ke zrušení rozhoduje produkt s doménovým expertem; vývojáři k němu dodají data o skutečném užití z logů a metrik.

18.06 Krok 2: Extrakce doménové vrstvy#

Extrakce přesouvá doménová pravidla z kontrolerů a service tříd do objektů, kterým patří. Ty si invarianty hlídají samy a zvenčí je nikdo neobejde.

Přesunutí doménových pravidel do doménových objektů

Refaktoring má dva kroky. Nejdřív vzniknou Value Objects pro primitivy s doménovými pravidly. Pak se logika přesune do entit a doménových služeb.

Value Objects místo primitivních typů

Doménový koncept schovaný do string nebo int je příznak zvaný Primitive Obsession. Value Object nahradí primitiv objektem, který drží validaci i chování pohromadě.

18.07 Krok 3: Zavedení repozitářů#

CRUD aplikace typicky volá EntityManagerInterface nebo Doctrine repozitáře přímo z kontrolerů a service tříd. DDD staví mezi doménu a persistenci doménové rozhraní repozitáře. Doménový kód nezná EntityManager ani SQL a implementace se dá vyměnit bez jeho úprav.

Vytvoření doménového rozhraní repozitáře

Rozhraní repozitáře patří do doménové vrstvy a popisuje operace tak, jak je potřebuje doména.

18.08 Krok 4: Postupné zavedení CQRS#

Command Query Responsibility Segregation (CQRS) na DDD navazuje, ale přichází na řadu až poté, co se doménový model usadí. Když přijde dřív, přesune komplexitu z domény do handleru, kde je neviditelná a hůř se testuje.

Začít s Command stranou (write side)

Na write side, u operací měnících stav systému, už doménový model existuje a Command jen pojmenuje záměr. Čtení může zpočátku zůstat na přímých Doctrine dotazech; optimalizované SQL jako read model je v DDD systému plnohodnotný trvalý stav, ne provizorium.

Read model nad legacy schématem jako první krok

Z toho, že query side smí zpočátku zůstat, neplyne, že se čtení odkládá na konec. Opačné pořadí bývá dokonce bezpečnější. Read model postavený nad starým schématem nezmění ani jeden zápis, takže nemůže poškodit data. Ověří se porovnáním výstupu se starou obrazovkou. A projekce, která pro něj vznikne, je později připravená konzumovat doménové události z nového modelu.

Konkrétně: dotaz z OrderRepository::findAllForListing() se přesune do OrderListQuery s vlastním handlerem a read DTO, samotné SQL zůstane beze změny. Až se write model překlopí, mění se jen tělo handleru; kontroler ani šablona o tom nevědí. Read modely, jejich projekce a eventual consistency rozebírá kapitola CQRS.

18.09 Testování při migraci#

O úspěchu migrace rozhodují testy. Bez nich refaktoring zavleče regrese, které se ukážou až v produkci. Migrace z CRUD na DDD potřebuje dvě techniky: charakterizační testy, které zachytí stávající chování, a unit testy nové doménové vrstvy.

Charakterizační testy (Characterization Tests)

Pojem „charakterizační testy“ pochází z knihy Michaela Featherse Working Effectively with Legacy Code [5]. Charakterizační test nepopisuje, jak by se systém měl chovat, ale jak se aktuálně chová. Při refaktoringu zachytí nechtěné změny chování. Feathers zároveň definuje legacy code jako kód bez testů – ne jako kód starý nebo ošklivý.

Švy: kudy se test do legacy kódu dostane

Charakterizační test je výsledek, šev (seam) je jeho předpoklad. Podle Featherse je šev místo, kde lze změnit chování programu, aniž se edituje kód na tomtéž místě. Ke každému patří enabling point: místo, kde se rozhoduje, která varianta chování se použije [16]. Zavést švy do zaběhnutého systému stojí značné úsilí. Je to investice, ne vedlejší produkt refaktoringu.

V Symfony CRUD kódu se opakují tři:

  • Konstruktorová injekce místo new. Řádek $mailer = new SmtpMailer(); uvnitř metody šev nemá. Závislost předaná konstruktorem ho vytvoří a enabling point tím skončí v konfiguraci služeb.
  • Interface místo konkrétní třídy. Type hint na UserRepository místo na DoctrineUserRepository posune enabling point do services.yaml, kde ho test přepíše.
  • Event listener místo inline kódu. Vedlejší efekt vytažený z metody do listeneru jde v testovacím prostředí odpojit; enabling point je registrace listeneru.

Kdy charakterizační test být nemusí

Plné pokrytí legacy systému charakterizačními testy nikdo nenapíše a kdo na ně čeká, nezačne. Matthias Noback argumentuje, že strukturální transformace zachovávající chování jsou bezpečné i bez testů, pokud je jistí statická analýza a párové programování [17]. Na jeho úvahu navazuje gradace, kterou Noback v článku takto nerozepisuje:

  1. Automatizovaný refaktoring z IDE (rename, extract method, move class) proběhne bez testu.
  2. Ruční strukturální změna pod PHPStan na úrovni 8 a s jedním vysokoúrovňovým smoke testem.
  3. Zásah do rozhodovací logiky – teprve zde charakterizační test, a jen na dotčené cestě.

Hranice mezi druhým a třetím bodem je odhad, ne pravidlo. Když si tým není jistý, patří změna do třetí kategorie.

Při extrakci logiky z legacy kódu pomáhají i jazykové modely: vygenerují první sadu charakterizačních testů nebo popíší, co nepřehledná metoda dělá. Souvislosti přibližuje kapitola DDD a umělá inteligence.

Unit testy doménové vrstvy

Doménový objekt, který nezná databázi ani HTTP, se testuje bez nich. Test běží v milisekundách, na okolí nezávisí a čte se jako zápis doménového pravidla. Techniky, které tuto vrstvu pokrývají do hloubky, rozebírá kapitola Testování DDD: InMemory repozitáře, testování doménových událostí a architektonické testy.

18.10 Rizika a doporučení#

Nejčastější chyby při migraci

  • Anémický doménový model – Nejčastější past. Třídy dostanou názvy jako v DDD (User, Order), obsahují ale jen gettery a settery a logika zůstane v service třídách. Vznikne DDD terminologie s CRUD implementací.
  • Přílišná granularita Bounded Contexts – Příliš mnoho malých kontextů vede k distribuované komplexitě, protože každá integrace mezi nimi něco stojí. Bezpečnější start jsou větší kontexty; dělí se, až když je důvod jasný.
  • ORM diktující tvar modelu – Anti-vzorem není atributové mapování samo o sobě; sekce 18.06 i Recept 2 ho přijímají jako pragmatickou volbu. Problém začíná, když ORM určuje tvar modelu: public settery kvůli hydrataci, anémická entita, flush() volaný z kontroleru. Projekty, které potřebují striktní oddělení domény od persistence, řeší tutéž potřebu přes Persisted Object Pattern.
  • CQRS bez doménového modelu – Zavedení CommandBusu a QueryBusu bez refaktorovaného doménového modelu přidá vrstvy komplexity bez přínosu. CQRS je amplifikátor: zesílí jak výhody, tak problémy stávající architektury.
  • Chybějící Anti-Corruption Layer – Integrace nové DDD vrstvy se starým CRUD kódem potřebuje překladovou vrstvu. Bez ní pronikají pojmy starého modelu do nového.

Proč migrace selhávají

Seznam výše je technický, důvody selhání bývají organizační. Čtyři se opakují:

  • Migrace bez mandátu. Tým ji dělá „při tom“, nikdo z vedení o ní neví, a první tlak na termín ji zastaví v půli. Argumentaci v jazyce obchodní hodnoty rozebírá sekce Business case pro DDD refaktoring.
  • Tým bez doménového experta. Model pak kopíruje databázi, protože jiný zdroj pravdy není k dispozici.
  • Chybějící rozhodnutí, co nemigrovat. Bez něj se rozsah rovná celému legacy systému a konec se vzdaluje rychleji, než se pracuje.
  • Migrace nesená jedním člověkem. Odejde-li, zůstane polovina rozdělané práce, které nikdo nerozumí. Bus factor a knowledge silos rozebírá sekce Knowledge silos.

Pátý důvod je tišší než ostatní: přechodová architektura, kterou nikdo nezahodil. Routovací vrstva a dvojí zápis měly žít měsíce. Po dvou letech jsou z nich součásti systému a migrace skončila tím, že přibyla třetí architektura vedle dvou původních.

DDD koncepty a jejich implementaci v Symfony rozebírají navazující kapitoly Implementace v Symfony 8 a CQRS. Konkrétní třecí plochy, na které migrace naráží v produkci, shrnuje kapitola DDD v praxi – kde to bolí: Doctrine, asynchronní infrastruktura a tým.

18.11 Refaktoringová kuchařka – krátké recepty#

Strangler Fig je strategický pohled na celou migraci. V denní praxi se opakují drobné situace; kuchařka jich shrnuje devět nejčastějších, každou ve formátu „symptomy → krok 1, 2, 3“. Recepty jsou záměrně krátké, důkladnější rozbor nesou odkazované kapitoly.

Recept 1: Anémická Doctrine entita

Symptomy: entita má jen gettery/settery, veškerá logika je v Service třídě.

  1. Identifikujte invarianty entity (co se nesmí porušit).
  2. Pro každý invariant najděte metodu v *Service, která ho dnes drží.
  3. Přesuňte metodu do entity, getter/setter zúžte na private nebo zrušte.
  4. Service se zúží na tenký koordinátor (Application Service): načte entitu, zavolá její metodu a předá události.
  5. Souvisí: Anti-vzor: Anemic Domain Model · Domain Services vs. Application Services.

Recept 2: Doctrine atributy v doménové třídě – kdy je to problém

Symptomy: App\Domain\Order má #[ORM\Entity], doména závisí na Doctrine.

Pragmatická výchozí volba v tomto průvodci atributy přijímá: jsou to metadata, ne chování, a Symfony ekosystém s nimi pracuje idiomaticky (viz rozhodnutí o mappingu). Když projekt striktní oddělení opravdu potřebuje (Hexagonal, dlouhodobá výměna ORM, core doména s vysokou hodnotou), postup je:

  1. Zaveďte Persisted Object Pattern: doménová třída zůstane POPO, persistence model + mapper jdou do App\<BC>\Infrastructure\Persistence\Doctrine\.
  2. Mapper hydratujte z perzistence přes rekonstituční továrnu (jako User::reconstitute() v sekci 18.06), která neemituje doménové události.
  3. Hlídejte hranici staticky: composer require --dev phparkitect/phparkitect + pravidlo App\<BC>\Domain\* nesmí závisět na Doctrine\* (kniha používá phparkitect ve více kapitolách, alternativou je deptrac).

Recept 3: Primitivní ID jako string / int

Symptomy: Order::$id: string, kdekoli se předává jen string.

  1. Zaveďte VO OrderId (final readonly class OrderId { public function __construct(public string $value) {} }, generování přes Uuid::v7()).
  2. Doctrine custom type pro OrderId (mapping z DB string ↔ VO). VO musí implementovat __toString(); UnitOfWork předpokládá, že identifikátor je převeditelný na řetězec, a bez toho mapování nefunguje.
  3. Postupně upravte signatury v handlerech. PHPStan na úrovni 8 odhalí každý zapomenutý string.

Recept 4: Doctrine tabulka sdílená více BC

Symptomy: tabulka users se používá v Ordering BC i Billing BC; oba do ní zapisují.

  1. Určete BC, kterému tabulka patří (typicky Identity).
  2. Ostatní BC do ní nesmí zapisovat – jen číst. Čtení přesuňte do read modelů (každý BC má vlastní projekci).
  3. Zápisy nahraďte voláním Identity API (sync HTTP nebo async event publishing s outboxem).
  4. Souvisí: Outbox Pattern.

Recept 5: Doménová logika v kontroleru

Symptomy: 200řádkový kontroler s if-else stromem doménových rozhodnutí.

  1. Vytvořte Command DTO + CommandHandler v Application vrstvě.
  2. Kontroler se zúží na tři kroky: validace vstupu → dispatch commandu → odpověď.
  3. Autorizaci přesuňte do Voteru (souvisí Autorizace).

Recept 6: Agregát bobtná (1000+ řádků)

Symptomy: Order má 30 metod a 15 polí.

  1. Najděte pole, která se mění nezávisle (různé invarianty, různé use cases).
  2. Zvažte rozdělení na dva agregáty (např. Order + OrderShipment). Spojí je sdílené OrderId, žádná silná reference.
  3. Specifikační logiku vyextrahujte do Specification tříd (souvisí Specifications).

Recept 7: eventDispatcher->dispatch() uvnitř doménové metody

Symptomy: Agregát volá Symfony EventDispatcher přímo.

  1. Agregát dědí z AggregateRoot a eventy zaznamenává voláním record($event).
  2. Aplikační handler po repository->save() volá $order->releaseEvents() a události zapíše do outboxu ve stejné transakci.
  3. Doména ztratí závislost na Symfony EventDispatcheru a test se obejde bez frameworku.
  4. Souvisí: Outbox – Aggregate publikuje.

Recept 8: Stav je sloupec string $status

Symptomy: Order::$status: string, podmínky všude if ($order->status === 'PLACED').

  1. Zaveďte enum se stavy, které zná doména: enum OrderStatus: string { case Draft = 'draft'; case Confirmed = 'confirmed'; /* … */ case Cancelled = 'cancelled'; }. Legacy hodnotu 'PLACED' přeloží ACL translator na Confirmed.
  2. Přechody stavů provádějí metody agregátu: $this->status = OrderStatus::Cancelled.
  3. Pro složitá pravidla přechodů zvažte stavový automat (Symfony Workflow nebo doménová reprezentace).

Recept 9: Legacy tabulka, kterou nelze změnit

Symptomy: do tabulky users zapisuje i skript mimo aplikaci nebo cizí systém; ALTER TABLE je vyloučený.

  1. Nový model si tabulku nepřivlastňuje. Čte přes ACL translator, který legacy sloupce překládá na doménové pojmy.
  2. Chybějící atributy uložte do vlastní tabulky nového kontextu, spojené cizím klíčem. Legacy schéma zůstává nedotčené.
  3. Pokud jsou názvy sloupců neúnosné, vytvořte nad tabulkou pohled (view) s doménovým pojmenováním a mapujte entitu na něj.
  4. Souvisí: Mapping složitých Value Objects.

Časté otázky

Jaké příznaky ukazují, že CRUD aplikace je zralá na migraci?

Typickými signály jsou God Services o stovkách řádků a kontrolery obsahující doménová pravidla. Dále doménová logika zamíchaná v Doctrine repozitářích, opakované regresní chyby při drobných změnách a rostoucí čas potřebný pro onboarding nových vývojářů. Pokud aplikace tyto příznaky nevykazuje a zůstává prostým mapováním formulářů na tabulky, migrace odpovídající hodnotu nepřinese. Obecnější otázku, pro jaké projekty je DDD vhodné, řeší samostatná kapitola Kdy DDD nepoužívat. Viz také sekci Kdy a proč migrovat.

Co je Strangler Fig Pattern?

Strangler Fig (fíkovník škrtič) je migrační vzor popsaný Martinem Fowlerem, při kterém nová architektura postupně „obroste“ starý systém a nahradí ho po částech. Nová funkcionalita vzniká od začátku v DDD stylu, zatímco stará CRUD část zůstává v provozu a s každou iterací ubývá. Obě části existují paralelně a propojují se přes Anti-Corruption Layer. Podrobný rozbor v sekci Strangler Fig Pattern.

Jak začít s analýzou existující domény?

Začíná se Event Stormingem nebo obdobnou kolaborativní technikou s doménovými experty: zmapují se hlavní události, příkazy a aktéři. Z této mapy vyplývá návrh Bounded Contexts a Ubiquitous Language. Paralelně se v existujícím kódu hledají implicitní hranice modelu: moduly, tabulky nebo funkční celky, které jsou málo propojené. Cílem první iterace je hrubá mapa, ne úplný model. Praktický postup v sekci Analýza existující domény.

Jak extrahovat doménovou vrstvu z existujícího CRUD kódu?

Migrace začíná u jednoho vybraného Bounded Contextu, pro který vzniká nová doménová vrstva oddělená od Doctrine entit. Doménová logika ze service tříd a kontrolerů se přesouvá do metod agregátu, zatímco původní CRUD kód zůstává jako adaptér pro API a persistenci. Nejprve se zavede Anti-Corruption Layer, pak se refaktorují jednotlivé use casy. Charakterizační testy proti původnímu chování minimalizují regrese. Detailní rozbor v sekci Extrakce doménové vrstvy.

Jaká jsou hlavní rizika migrace z CRUD na DDD a jak je zmírnit?

Nejčastější pastí je anémický model: nové třídy mají DDD názvy, ale logika zůstává v service třídách. Dále hrozí nadměrná granularita Bounded Contexts, ORM diktující tvar modelu (veřejné settery kvůli hydrataci, flush z kontroleru) a zavádění CQRS bez přepracovaného modelu. Největším rizikem je Big Bang Rewrite, který se zřídka dotáhne do konce. Migrace má probíhat inkrementálně přes Strangler Fig, u středně velké aplikace s realistickým odhadem 12–24 měsíců. Rozbor rizik a zmírňujících opatření v sekci Rizika a doporučení.