Modernizacja legacy code nie wymaga przepisywania systemu od zera. Sprawdzony plan działania to audyt i mapa zależności, testy regresji na krytycznych ścieżkach biznesowych, refaktoryzacja moduł po module i wdrożenie CI/CD — dzięki temu system działa przez cały czas, a ryzyko jest ograniczone do jednego modułu. Ten przewodnik zbiera pełny plan modernizacji starego kodu PHP: od rozpoznania długu technicznego, przez wybór strategii, po konkretne narzędzia, czas i koszty.
Najważniejsze: Modernizacja legacy code to proces, nie jednorazowy projekt. Najbezpieczniejsza kolejność to audyt → testy zabezpieczające → refaktoryzacja modułowa → CI/CD → monitoring. Przepisywanie od zera jest ostatecznością, nie punktem wyjścia — w praktyce kończy się dłużej, drożej i z mniejszą liczbą funkcji niż zakładano.
Czym jest legacy code i kiedy staje się problemem
Legacy code to działający kod źródłowy, który powstał w przeszłości i pozostaje w użyciu, choć nikt nie chce go już rozwijać. Kluczowe słowo to „działający" — kod legacy zwykle zarabia na siebie, obsługuje realne procesy i ma w sobie lata wiedzy biznesowej zapisanej w warunkach brzegowych, których nie ma nigdzie indziej. Definicję i typowe objawy rozkładam szerzej w artykule legacy code — co to jest.
Problem zaczyna się, gdy kod przestaje być zmieniany bezpiecznie. Sygnały, że masz do czynienia z legacy code w fazie problemowej:
- każda zmiana wymaga ręcznego testowania całego systemu, bo testów automatycznych nie ma,
- wdrożenie na produkcję odbywa się przez FTP albo ręczne komendy na serwerze,
- tylko jedna osoba (często już nie pracuje w firmie) rozumie, jak działa kluczowy moduł,
- biblioteki i wersja PHP są poza wsparciem, a znane podatności nikt nie łata,
- nowa funkcja, która powinna zająć dzień, zajmuje dwa tygodnie.
Kod w PHP 5.6 z 2014 roku nie jest problemem sam w sobie. Problemem jest to, że jego utrzymanie pochłania budżet, którego nikt nie widzi w fakturach — i blokuje rozwój, którego firma nie może odłożyć. Ten niewidzialny koszt ma nazwę — dług techniczny.
Dług technologiczny — ukryty koszt, który rośnie z odsetkami
Dług techniczny (technical debt) to metafora Warda Cunninghama: każdy skrót w kodzie to pożyczka, od której płacisz odsetki w postaci wolniejszych zmian, większej liczby błędów i trudniejszego wdrożenia. Sam dług nie jest złem — każdy system go ma. Złem jest dług, którym nikt nie zarządza.
W praktyce rozróżniam dług celowy („wiemy, że to skrót, zaplanujmy poprawkę") od długu przypadkowego („nie wiedzieliśmy, że to skrót, dopóki coś się nie zepsuło"). Ten drugi rośnie najszybciej, bo nikt nie planuje jego spłaty. Jak go zmierzyć, gdzie go najczęściej znajdziesz i jak zaplanować spłatę etapami — piszę w osobnym przewodniku o długu technologicznym.
Praktyczna zasada z projektów, które przejmuję: dopóki zespół potrafi wdrażać zmiany w godzinach, a nie tygodniach, dług techniczny jest do opanowania. Gdy wdrożenie nowej funkcji zajmuje dłużej niż jej napisanie — czas na plan modernizacji.
Przepisywanie od zera czy modernizacja?
To najczęstsze pytanie, jakie słyszę od klientów, którzy odziedziczyli stary projekt PHP. Odpowiedź brzmi: prawie nigdy nie przepisujesz od zera. Przepisywanie aplikacji to ogromne ryzyko i koszt, a historia branży jest nim usiana — najgłośniejszy przykład to Netscape, który przepisał działający produkt i przestał się liczyć w przeglądarkowej wojnie. Joel Spolsky nazwał to „najgłupszą rzeczą, jaką może zrobić firma programistyczna", i po dwudziestu latach ta ocena się nie zmieniła.
Wiele firm traciło miesiące na przepisywanie systemu, który działał dobrze, tylko po to, by odkryć, że nowa wersja nie obsługuje wszystkich edge case'ów wypracowanych przez lata. Stary kod, choć brzydki, zawiera decyzje biznesowe w warunkach brzegowych, które są warte więcej niż elegancka architektura nowego projektu. Szczegółowe porównanie obu dróg — kosztów, ryzyka i kryteriów wyboru — rozkładam w artykule refaktoryzacja czy przepisanie od zera.
Zamiast tego proponuję plan modernizacji krok po kroku, który pozwala stopniowo poprawiać jakość kodu bez zatrzymywania działalności operacyjnej.
Cztery strategie modernizacji — porównanie
Modernizacja legacy code to nie jedna metoda, lecz cztery strategie, które można łączyć. Wybór zależy od stanu systemu, budżetu i tego, jak krytyczny jest dla przychodów.
| Strategia | Zakres | Ryzyko | Czas typowo | Kiedy wybierać |
|---|---|---|---|---|
| Patchowanie | Łatanie błędów, aktualizacje bezpieczeństwa, minimalne zmiany | Niskie | dni do tygodni | System stabilny, niski budżet, potrzebujesz czasu na decyzję |
| Refaktoryzacja modułowa | Poprawa jakości i architektury kawałek po kawałku, bez zmiany technologii | Średnie, kontrolowane | miesiące | System działa, ale spowalnia rozwój; chcesz modernizować w tle |
| Wymiana modułowa (Strangler Fig) | Nowe moduły obok starych, przejmowanie funkcji pojedynczo | Średnie, stopniowe | kwartały | Chcesz wymienić technologię bez wielkiego przełączenia i przestoju |
| Przepisanie od zera | Budowa nowego systemu obok starego, przełączenie w jednym momencie | Wysokie | kwartały i więcej | Technologia nie do uratowania, zespół tego samego zdania, budżet i czas zabezpieczone |
W praktyce większość projektów kończy się hybrydą: refaktoryzacja modułowa jako tryb domyślny, przepisanie pojedynczych modułów tam, gdzie architektura jest nie do uratowania, i patchowanie bezpieczeństwa jako działanie ciągłe. Tylko pełne przepisanie systemu biznesowego w jednym skoku jest strategią, której w większości przypadków odradzam.
Krok 1 — audyt i mapa zależności
Zanim cokolwiek zmienisz, zrób mapę istniejącego systemu. Sprawdź, które moduły są krytyczne dla biznesu, które generują najwięcej błędów, a które są martwym kodem, którego nikt już nie używa. Stwórz mapę zależności między modułami — to pokaże, które części systemu można modernizować niezależnie.
Dobry audyt odpowiada na pięć pytań: co się zepsuje pierwsze, co można wyłączyć bez żalu, które biblioteki mają znane podatności, gdzie są najwolniejsze fragmenty i które moduły zmieniają się najczęściej. Wynikiem nie jest raport na półce, lecz lista priorytetów z roadmapą — co robimy w tym kwartale, co w następnym, a co świadomie odkładamy. Jak wygląda taki audyt od środka i ile trwa, opisuję w przewodniku o audycie kodu legacy.
W tym etapie warto też wykonać audyt bezpieczeństwa. Starsze systemy często mają przestarzałe biblioteki ze znanymi podatnościami. Nawet jeśli nie planujesz pełnej modernizacji, zacznij od łatania krytycznych dziur bezpieczeństwa — wzorzec OWASP Top 10 to dobry punkt odniesienia (najczęstsze podatności aplikacji Laravel).
Krok 2 — testy regresji i testy charakterystyczne
Dodaj testy dla krytycznych ścieżek biznesowych. Nawet jeśli nie masz testów jednostkowych, testy end-to-end, na przykład z użyciem Laravel Dusk, dadzą Ci bezpieczeństwo przy zmianach. Testy regresji to Twoja siatka bezpieczeństwa — pozwalają szybko wykryć, czy wprowadzona zmiana nie zepsuła czegoś innego.
W starym kodzie, w którym testy pisze się trudno, sięgnij po testy charakterystyczne (characterization tests) — technikę opisaną przez Michaela Feathersa w książce „Working Effectively with Legacy Code". Zamiast zakładać, jak system powinien działać, opisujesz testami, jak działa obecnie. Nawet jeśli utrwalasz w ten sposób błąd, zyskujesz coś cenniejszego — punkt odniesienia, który pozwala bezpiecznie refaktoryzować. Praktyczny warsztat z tej techniki przeprowadzam w artykule o testach charakterystycznych.
Skup się na testowaniu najważniejszych procesów biznesowych: składanie zamówienia, generowanie faktury, logowanie użytkownika, obsługa płatności. Nie musisz testować wszystkiego — zacznij od tego, co ma bezpośredni wpływ na przychody. W projektach, które przejmuję, typowo pokrywamy najpierw 5–10 ścieżek krytycznych, a resztę dopisujemy przy okazji refaktoryzacji modułów.
Krok 3 — modernizacja modułowa i wzorzec Strangler Fig
Wybieraj jeden moduł na sprint. Zamknij stary moduł testami, przepisz lub zrefaktoruj, uruchom w środowisku staging, a potem wdróż na produkcję i monitoruj przez kilka dni. Dopiero gdy masz pewność, że nowa wersja działa stabilnie, przechodź do kolejnego modułu.
To praktyczna realizacja wzorca Strangler Fig (figowiec dusiciel) — nowy kod wyrasta wokół starego, przejmuje jego ruch, a stary moduł zostaje wyłączony dopiero wtedy, gdy nowy udowodni stabilność. Wariant dla dużych monolitów to Branch by Abstraction: między starą i nową implementację wstawiasz warstwę pośrednią (interfejs), dzięki której przełączenie modułu to zmiana konfiguracji, a nie wielkie wdrożenie. Oba wzorce — z przykładami i kryteriami wyboru — opisuję w artykule o wzorcu Strangler Fig.
Taka strategia ma trzy zalety. Po pierwsze, ryzyko jest ograniczone do jednego modułu — awaria dotyka jednej funkcji, nie całego systemu. Po drugie, zespół stopniowo poznaje nowy kod i uczy się na bieżąco, zamiast miesiącami budować coś, czego nikt nie widział w działaniu. Po trzecie, biznes nie odczuwa przerwy w działaniu systemu — a to często warunek, bez którego modernizacja w ogóle nie dochodzi do skutku.
Krok 4 — CI/CD i automatyzacja wdrożeń
Skonfiguruj automatyczne wdrożenia, jeśli jeszcze ich nie masz. Jeśli wdrożenie wymaga ręcznych komend na serwerze, prędzej czy później ktoś zrobi to źle — a w starym systemie bez testów każdy błąd wdrożenia to potencjalna awaria produkcji. Continuous Integration i Continuous Delivery to standard, który znacząco podnosi bezpieczeństwo i szybkość dostarczania zmian.
Automatyczne testy, code review, deployment na staging i promocja na produkcję po zatwierdzeniu — to przepis na spokojną pracę z kodem, niezależnie od jego wieku. Konkretną konfigurację pipeline'u (GitHub Actions, testy, migracje bazy, wdrożenie zero-downtime) pokazuję w artykule o CI/CD i DevOps dla Laravel.
W systemach legacy pipeline wdrażasz stopniowo: najpierw automatyczne testy na pull requestach, potem automatyczne wdrożenia na staging, na końcu — na produkcję. Każdy z tych kroków działa niezależnie i już sam w sobie zmniejsza ryzyko.
Narzędzia wspierające modernizację kodu
Na rynku dostępnych jest wiele narzędzi, które mogą wesprzeć proces modernizacji — i w praktyce dzielą się na cztery grupy.
Statyczna analiza kodu. PHPStan i Psalm analizują kod bez uruchamiania go i wykrywają błędy typów, martwy kod i podejrzane wzorce jeszcze przed wdrożeniem. W starym kodzie zaczynasz od najniższego poziomu reguł i podnosisz poprzeczkę moduł po module — to tani sposób na zatrzymanie pogarszania jakości.
Automatyczna refaktoryzacja i aktualizacje. Rector automatyzuje zmiany składni i refaktoryzacje: potrafi zaktualizować kod do nowszej wersji PHP, podmienić przestarzałe konstrukcje i wygenerować dziesiątki tysięcy zmian, które ręcznie zajęłyby miesiące. Dla projektów Laravel dochodzi Laravel Shift, który automatyzuje aktualizacje frameworka między wersjami — łącznie z testami i pull requestami do przeglądu. Aktualizacje samego PHP — od audytu zgodności po wdrożenie — rozkładam w przewodniku o migracji PHP na 8.x.
Testy. PHPUnit i Pest to fundament. Testy end-to-end (Laravel Dusk, Playwright) pokrywają ścieżki krytyczne od strony użytkownika. W legacy najważniejsza jest dyscyplina: każdy refaktoryzowany moduł dostaje testy zanim zostanie tknięty.
Monitoring i wydajność. Blackfire.io i Tideways pokazują, które fragmenty kodu są najwolniejsze i gdzie optymalizacja przyniesie największe korzyści. New Relic lub Sentry domykają obraz — widzisz błędy produkcji zanim zgłosi je klient. Docker i powtarzalne środowiska lokalne skracają czas wdrożenia nowych osób w projekt.
Nie kupuj wszystkich narzędzi naraz. Kolejność, która działa: statyczna analiza → testy → CI/CD → monitoring wydajności → automatyczna refaktoryzacja na większą skalę.
AI w modernizacji legacy code
Asystenci AI zmieniają ekonomię pracy ze starym kodem — nie dlatego, że „przepiszą system za Ciebie", ale dlatego, że skracają najbardziej kosztowny etap: zrozumienie, co ten kod właściwie robi. Modele językowe dobrze radzą sobie z wyjaśnianiem nieznanych funkcji, generowaniem brakującej dokumentacji, pisaniem pierwszych wersji testów charakterystycznych i tłumaczeniem konstrukcji między wersjami języka.
W praktyce używam AI w trzech miejscach: przy mapowaniu zależności w module, który pierwszy raz widzę; przy generowaniu szkiców testów, które potem weryfikuję uruchomieniem; i przy masowych refaktoryzacjach, gdzie Rector wykonuje zmianę, a AI pomaga przygotować reguły. Granica jest stała — decyzje architektoniczne i ocena ryzyka zostają po stronie człowieka, bo model nie zna kontekstu biznesowego, który w legacy code jest najważniejszy.
Ile trwa i ile kosztuje modernizacja
Dla średniej wielkości aplikacji PHP (50–100 modułów) modernizacja modułowa zajmuje zwykle od 3 do 12 miesięcy przy pracy ciągłej. Audyt techniczny to 2 000 zł netto jednorazowo, a program modernizacji rozliczany etapami mieści się w widełkach 2 000–25 000 zł. Pełne widełki — od audytu po przebudowę dużych systemów — rozkładam w artykule o kosztach modernizacji legacy.
Modernizacja to inwestycja z konkretnym zwrotem. Przykład z mojej praktyki: klient modernizował system napisany w PHP 5.6 do Laravel 11. Proces trwał 8 miesięcy, ale po zakończeniu koszty utrzymania spadły o 60%, a czas wdrożenia nowej funkcji skrócił się z 2 tygodni do 2 dni. Podobny projekt — z zerem krytycznych awarii przez 12 miesięcy od zakończenia — opisuję w case study modernizacji aplikacji PHP.
Po drugiej stronie równania jest koszt zaniechania. Utrzymanie legacy systemu jest typowo 2–3 razy droższe niż nowoczesnego odpowiednika — nie z powodu serwerów, ale z powodu trudności znalezienia specjalistów znających stare technologie i ryzyka błędów przy każdej zmianie. Do tego dochodzą koszty przestojów i utraconych szans, których nie widać w żadnym budżecie.
Plan działania — podsumowanie w 6 krokach
- Audyt i mapa zależności — co jest krytyczne, co martwe, co ma podatności. Wynik: lista priorytetów z roadmapą.
- Testy regresji i charakterystyczne na ścieżkach krytycznych — zanim tknisz kod, zabezpiecz zachowanie systemu.
- Modernizacja modułowa (Strangler Fig) — jeden moduł na sprint, wdrożenie, monitoring, dopiero potem następny.
- CI/CD — automatyczne testy, staging i wdrożenia bez ręcznych komend na serwerze.
- Narzędzia — PHPStan/PHPUnit na start, Rector i monitoring wydajności w kolejnych etapach.
- Przegląd i decyzja — po kilku modułach wiesz, czy kończysz refaktoryzacją, czy wydzielasz pojedyncze moduły do przepisania.
Modernizacja legacy code to proces, nie jednorazowe zadanie. Działaj małymi krokami, zabezpieczaj testami i monitoruj każdą zmianę — a system, który dziś spowalnia firmę, stanie się znowu przewidywalnym narzędziem. Jeśli potrzebujesz wsparcia w audycie lub zaplanowaniu modernizacji swojego projektu, sprawdź usługę modernizacji legacy PHP i napisz do mnie przez formularz kontaktowy.
Najczęściej zadawane pytania
Od czego zacząć modernizację legacy code?
Od audytu i mapy zależności, a nie od zmian w kodzie. Audyt pokazuje, które moduły są krytyczne dla biznesu, gdzie są podatności bezpieczeństwa i co można wyłączyć bez żalu. Dopiero na tej podstawie powstaje kolejność prac — zwykle zaczyna się od łatania krytycznych dziur bezpieczeństwa i pokrycia testami najważniejszych ścieżek biznesowych.
Czy zawsze trzeba przepisywać cały system?
Nie, wręcz przeciwnie. Przepisywanie od zera powinno być ostatecznością. Zazwyczaj wystarczy stopniowa refaktoryzacja najważniejszych modułów, a pojedyncze moduły o nie do uratowania architekturze wydziela się do przepisania osobno — bez zatrzymywania reszty systemu.
Jak długo trwa typowa modernizacja?
To zależy od wielkości i złożoności systemu. Dla średniej wielkości aplikacji PHP (50–100 modułów) proces może zająć od 3 do 12 miesięcy przy pracy ciągłej. Pierwsze efekty — bezpieczeństwo i szybsze wdrożenia — widać już po kilku tygodniach, po pierwszym zrefaktoryzowanym module i uruchomieniu CI/CD.
Czy nowy kod zawsze jest lepszy od starego?
Nie. Nowy kod pisany w pośpiechu może być gorszy jakościowo niż stary, sprawdzony kod. Dlatego tak ważne są testy i code review — a przy przepisywaniu modułów zasada jest jedna: nowa wersja musi przejść te same testy co stara, zanim trafi na produkcję.
Ile kosztuje utrzymanie legacy systemu?
Koszty utrzymania legacy systemu są zazwyczaj 2–3 razy wyższe niż nowoczesnego odpowiednika, głównie ze względu na trudność znalezienia specjalistów znających stare technologie i ryzyko błędów przy każdej zmianie. W praktyce dochodzi do tego najdroższy element — spowolniony rozwój, który blokuje przychody.
Kiedy modernizacja legacy code się nie opłaca?
Gdy system jest stabilny, rzadko się zmienia i nikt nie planuje nowych funkcji — wtedy wystarczy patchowanie bezpieczeństwa i aktualizacje infrastruktury. Modernizacja ma sens wtedy, gdy koszt zaniechania przewyższa koszt pracy: tempo zmian spada, koszty utrzymania rosną, a znalezienie osób znających technologię graniczy z cudem.
Źródła i narzędzia
- Michael C. Feathers — Working Effectively with Legacy Code
- Martin Fowler — StranglerFigApplication
- Joel Spolsky — Things You Should Never Do (przepisywanie od zera)
- Rector — automatyczna refaktoryzacja PHP
- PHPStan — statyczna analiza PHP
- Laravel Shift — automatyczne aktualizacje Laravel
- OWASP Top 10