Jak modernizovat legacy PHP aplikaci pomocí postupného refaktoringu
Jak bezpečně refaktorovat legacy PHP aplikaci. Praktické kroky, nástroje, typy v PHP 8 a strategie, které snižují technický dluh a riziko regresí.
Jak bezpečně refaktorovat legacy PHP aplikaci. Praktické kroky, nástroje, typy v PHP 8 a strategie, které snižují technický dluh a riziko regresí.
Většina vývoje v praxi probíhá na existujících systémech. Enterprise aplikace i úspěšné produkty běží roky a jejich codebase nese stopu tehdejších technologií i tlaku na rychlé dodávky. Řada z nich vznikla ještě v době PHP 5.x, kdy chyběl striktní typový systém a architektura se často přizpůsobovala potřebě rychle dodat MVP.
S příchodem PHP 8.x se rozdíl mezi starým a současným způsobem vývoje výrazně prohloubil. Moderní PHP nabízí robustní typovou kontrolu, immutability a nativní Enums. Legacy aplikace přitom zůstávají zdrojem vysoké kognitivní zátěže. což znamená, že vývojáři tráví mnoho času reverzní analýzou kódu místo rozvoje funkcionality.
Typickou reakcí bývá úvaha o kompletním přepisu aplikace (The Big Rewrite). Z pohledu softwarové ekonomiky však jde o velmi rizikový krok. Udržitelnější cesta vede přes postupný refaktoring. Průběžná modernizace umožňuje využít možnosti PHP 8, stabilizovat architekturu a zároveň zachovat provoz i kontinuitu vývoje.
Legacy aplikace mají vrstevnatou strukturu. Každá část vznikala v jiném období, pod jinými požadavky a s jinou mírou disciplíny. Kvalita tak nikdy není rovnoměrná. Efektivní refaktoring proto vychází z pochopení toho, kde kód skutečně brzdí vývoj a kde je technický dluh pouze historický, ale relativně stabilní.
V praxi se jako nejspolehlivější ukazuje kombinace dvou metrik: Cyclomatic Complexity a Code Churn. Komplexita popisuje logickou náročnost, churn pak frekvenci změn. Jejich spojení dokáže odhalit oblasti, kde se riziko reálně koncentruje.
Mezi nejčastější anti-patterny patří:
Refaktoring bez testů je vždy risk a náročný proces. Z mé zkušenosti je situace u legacy projektů ještě náročnější, protože často chybí testy, které mají skutečnou hodnotu. Ty, které existují, bývají pevně navázané na konkrétní implementaci, testují privátní metody, vyžadují rozsáhlé mockování a při změně struktury kódu selhávají, i když výsledné chování systému zůstává správné.
U legacy systémů proto nemá smysl snažit se dopsat Unit testy na všechno. Mnohem efektivnější je zavést Characterization Tests, tedy Golden Master přístup, který zafixuje chování systému zvenčí. Vybereme reprezentativní vstup, například HTTP request nebo importní XML, necháme ho projít aktuální implementací a uložíme výstup jako snapshot. Ten pak slouží jako externí kontrakt a okamžitě upozorní na změnu chování během refaktoringu.
Moderní PHP dokáže přesunout velkou část kontroly ze samotného běhu aplikace do statické analýzy. U legacy systémů jde o zásadní posun, protože velká část chyb nevzniká v byznysové logice, ale v nejasné struktuře dat a v implicitních předpokladech mezi jednotlivými vrstvami aplikace.
Nejčastějším zdrojem chyb v dynamickém PHP je nejistota kolem struktury dat. Asociativní pole působí na první pohled jednoduše, ale při práci s nimi se vývojář nevyhne otázkám typu: Co v tom poli vlastně je? Jaké klíče mám čekat? Jaký typ tam přijde tentokrát? A je tahle hodnota vůbec povinná?
Takové pochybnosti zpomalují práci a chyby se často objeví až v runtime, kdy je jejich dohledávání o to náročnější.
Modernizace proto stojí na nahrazení polí typovanými objekty (DTO). PHP 8.2 nabízí readonly třídy i Constructor Property Promotion, takže jasně definovaná datová struktura nevyžaduje skoro žádný boilerplate. Výsledkem je přehlednější kód, menší prostor pro chyby a mnohem spolehlivější statická analýza.
class UserData {
public function __construct(
public string $email,
public ?int $age = null,
public UserStatus $status,
) {}
}Legacy kód bývá plný „magických hodnot“ (stringy jako 'active', 'deleted' nebo inty 1, 2). Když vývojář narazí na podobný řetězec, často si musí domýšlet, co přesně znamená a kde se ještě používá. To zvyšuje chybovost i kognitivní zátěž.
PHP 8.1 přineslo Backed Enums, které tenhle problém řeší mnohem čistěji. Hodnoty se přesunou do jednoho místa, dostanou jasný význam a mohou mít i vlastní metody.
Kód je díky tomu čitelnější, soudržnější a změny v doméně se dělají na jednom místě místo lovení magických hodnot napříč projektem.
enum UserStatus: string {
case Active = 'active';
case Blocked = 'blocked';
public function isEditable(): bool {
return $this === self::Active;
}
}declare(strict_types=1); eliminuje implicitní přetypování, které je v dynamickém PHP častým zdrojem nejasných chyb. Ještě větší přínos však přináší důsledná práce s hodnotami, které mohou být null. Legacy kód často spoléhá na tichý předpoklad, že proměnná null neobsahuje, a právě tato situace pak vede k obtížně reprodukovatelným chybám.
Použití nullable typů, jako je ?string nebo ?int, nutí programátory s těmito variantami pracovat vědomě. Možné stavy proměnných jsou popsány přímo v rozhraní metod a objektů, což výrazně zvyšuje předvídatelnost aplikace. Výsledkem je stabilnější chování systému, méně skrytých selhání a lepší podpora statické analýzy.
Moderní PHP kód by měl být plně typovaný – od parametrů a návratových hodnot až po interní proměnné – protože typové deklarace zpřesňují kontrakty mezi vrstvami, zvyšují předvídatelnost chování a umožňují statické analýze odhalit chyby včas.
U rozsáhlých legacy aplikací je ruční hlídání kvality kódu nereálné. Code review dokáže zachytit architektonické problémy, ale není vhodné pro systematickou kontrolu typů, chybějících návratových hodnot nebo nedefinovaných struktur. Tyto úkoly musí převzít automatizace.
Statická analýza a CI pipeline společně tvoří kontrolní vrstvu, která odfiltruje většinu chyb ještě před tím, než se dostanou do produkce.
PHPStan (nebo Psalm) je pro moderní PHP ekosystém standard. U legacy projektů však nastává typická situace: při prvním spuštění nástroj nahlásí stovky až tisíce chyb. Opravit je najednou není reálné.
Praktické řešení představuje baseline. Uloží aktuální stav chyb, CI ho ignoruje, ale nový kód musí být v pořádku. Tím se zastaví další degradace projektu a zároveň se vytvoří prostor pro postupné čištění.
Doporučený postup:
Tím se zavede přísnější kontrola bez toho, aby se tým dostal do paralýzy způsobené příliš velkým množstvím problémů najednou.
Rector pracuje nad abstraktním syntaxovým stromem a umožňuje měnit strukturu kódu mechanicky. Hodí se zejména v okamžicích, kdy je potřeba provést zásah ve velkém počtu souborů.
Mezi běžné úlohy patří:
Rector dokáže ušetřit velké množství času a eliminuje chyby způsobené manuálními úpravami. Každý výstup však musí být podložen testy. Nástroj není náhradou za doménovou znalost a jeho úpravy mohou narazit na speciální případy, které analyzátor nerozpozná.
Konzistentní styl kódu má přímý dopad na rychlost práce. Diffy jsou kratší, konflikty při mergích méně časté a kontext se čte rychleji. To je obzvlášť důležité u legacy projektů, kde se střídalo mnoho autorů.
Nástroje jako PHP-CS-Fixer nebo Pint automatizují formátování a udržují jednotný styl. Díky tomu se vývojáři mohou soustředit na podstatné části review místo řešení mezery, závorky nebo konvencí.
Automatizace bez CI pipeline ztrácí smysl. Každý pull request musí projít sadou kontrol, které zahrnují:
Teprve v takovém prostředí je možné přistoupit ke strukturálním změnám bez obavy, že se chyby projeví až po nasazení.
Jakmile jsou typy stabilní a aplikace je pokrytá testy, je možné posunout refaktoring na úroveň architektury. Cílem této fáze není změnit strukturu podle „dokonalého návrhu“, ale odstranit provázanost, zpřehlednit toky dat a zpřístupnit logiku v menších, lépe uchopitelných komponentech.
Strukturální refaktoring je investice, která zlepšuje čitelnost, testovatelnost i odolnost systému při změnách.
V legacy aplikacích se často setkáváme s „Service“ třídami, které se staly odkladištěm nesouvisejících metod. Alternativním přístupem je pattern Action Classes (nebo Single Action Controllers). Každá třída zapouzdřuje právě jeden byznysový případ (Use Case), např. ApproveOrderAction. Taková třída má jasně definované závislosti, jednu veřejnou metodu a je triviálně testovatelná.
V legacy projektech bývá pojem „Service“ často příliš široký. Do jedné třídy se dostávají nesouvisející funkce, které obsluhují různé části domény. Testování i změny jsou pak náročné, protože jedna úprava ovlivní celou třídu.
Praktičtější přístup představují Action Classes. Každá třída zapouzdřuje jeden konkrétní use case, například ApproveOrderAction nebo GenerateInvoiceAction. Výsledkem je:
Action Classes snižují kognitivní zátěž tím, že omezují kontext, se kterým musí vývojář pracovat. Zároveň přirozeně podporují oddělení doménové logiky od infrastruktury.
Skryté závislosti patří mezi nejčastější překážky při refaktoringu. Třída, která sama instancuje své závislosti nebo používá statická volání, znemožňuje izolované testování a často zakrývá skutečný rozsah své odpovědnosti.
Explicitní Dependency Injection řeší oba problémy:
Tento způsob řízení závislostí vede k přirozenému zpřehlednění architektury. Vývojář nemusí uvažovat o tom, „co si třída někde uvnitř vytvoří“, protože vše je viditelné přímo z jejího rozhraní.
V legacy kódu se často nacházejí třídy, které převzaly více rolí, než je dlouhodobě udržitelné. Porušení principu Single Responsibility se v praxi projevuje tím, že třída kombinuje validace, doménová rozhodnutí i práci s infrastrukturou. Změna jedné části pak ovlivní zbytek a tým tráví více času hledáním vedlejších efektů než samotným vývojem.
Refaktoring takových tříd je postupný a navazuje na testy i typový systém:
Tento postup výrazně snižuje riziko regresí. Místo jednoho velkého přepisu vzniká série malých změn, které jsou kontrolovatelné a zpětně dohledatelné.
Technický dluh nelze splatit jednorázově. Je nutné změnit mindset týmu. Osvědčenou praxí je Boy Scout Rule (pravidlo skautů): „Nech kód o něco čistší, než jsi ho našel.“
V praxi to znamená:
Proto není dobré bárt refaktoring jako samostatnou fázi. Patří přímo do procesu vývoje funkcionality. Tým má přehled o konkrétní části systému, kterou rozšiřuje, a právě v tom okamžiku je nejefektivnější provést i menší architektonické úpravy.
Modernizace legacy aplikace není jednorázový úkol. Je to proces, který kombinuje technické kroky s dobrou organizací práce. Typový systém PHP 8, statická analýza, automatizace a postupný refaktoring vytvářejí prostředí, ve kterém je možné vylepšovat architekturu bez ohledu na stáří kódu.
Klíčovým principem je postupovat po malých, bezpečných krocích. Stabilizovat chování pomocí testů, zavést předvídatelné datové kontrakty, zpřesnit hranice systému a odstranit skryté závislosti. Tyto úpravy přinášejí okamžitý přínos a zároveň připravují aplikaci na další rozvoj.
Důležitá je i každodenní práce týmu. Pokud se zlepšování stane součástí běžného vývoje, technický dluh přestane být bariérou a projekt se stane předvídatelnějším i udržitelnějším. Takový přístup neřeší problémy jen krátkodobě. Umožňuje posunout systém do stavu, ve kterém se dá dlouhodobě stavět, rozšiřovat a bezpečně inovovat.
Jsem vývojář, který se zaměřuje na projekty, kde standardní řešení nestačí. Od roku 2018 pomáhám digitálním agenturám a firmám s technicky náročnými výzvami – od návrhu robustní architektury až po optimalizaci a další rozvoj WordPress a Laravel aplikací.
Nemusíme hned začít – stačí se pobavit o tom, co potřebujete. Někdy i krátký rozhovor hodně vyjasní.