Migracja PHP 7.4 na 8.x to najczęstsza migracja w polskich projektach — na 7.4 stoi większość aplikacji pisanych w latach 2019–2022. Dobra wiadomość: to również migracja najlepiej udokumentowana i najbardziej zautomatyzowana. Zła: między 7.4 a 8.4 narosły cztery generacje zmian, a każda ma własną listę pułapek. Ten artykuł rozkłada przejście wersja po wersji — co dokładnie się psuje, jak to naprawić i w jakiej kolejności iść.
Najważniejsze: Kolejność jest sztywna: 7.4 → 8.0 → 8.1 → 8.2 → 8.3 → 8.4. Nie musisz wdrażać każdego kroku na produkcję, ale musisz przejść przez każdy na poziomie kodu i testów. Rector prowadzi przez wszystkie przejścia automatycznie (
withPhpSets(php84: true)), a ręczne poprawki skupiają się na zmianach zachowania, nie składni.
Dlaczego warto iść krokami, a nie skokiem
Technicznie da się przepisać kod z 7.4 od razu na 8.4 — Rector ma zestaw kumulatywny. Praktycznie skok przez cztery wersje naraz ma dwie wady.
-
Pierwsza: diagnoza błędów. Gdy testy sypią się po masowej zmianie, nie wiesz, które zachowanie za to odpowiada. Przy krokach wersja po wersji każdy commit odpowiada jednemu przejściu — błąd znajdujesz w minutach, nie w dniach.
-
Druga: zależności. Frameworki i biblioteki mają własne wymagania wersyjne. Laravel 8 wspiera PHP 8.0, Laravel 9 — 8.0/8.1, Laravel 10 — 8.1/8.2, Laravel 11 — 8.2/8.3. Idąc krokami, aktualizujesz framework równolegle z PHP, wersja po wersji — jak po schodach. Skok naraz oznacza aktualizację frameworka przez cztery wersje w jednym kroku, co jest osobnym, dużym projektem.
W małych projektach (kilkanaście tysięcy linii, framework w wersji wspierającej 8.4) skok bezpośredni jest sensowny — Rector i testy wystarczą. W średnich i dużych — kroki.
7.4 → 8.0 — największa porcja zmian
- To przejście generacyjne: PHP 8.0 zmieniło model błędów, system typów i usunęło konstrukcje z 5.x/7.0. Najważniejsze zmiany z punktu widzenia typowego projektu.
Wyjątki zamiast warningów. Wiele funkcji, które w 7.4 zwracały false i logowały warning, w 8.0 rzuca wyjątki (ValueError, TypeError). Dotyczy m.in. strpos() z niepoprawnymi argumentami, funkcji arytmetycznych, json_decode() z głębokością. Kod, który „działał mimo warningów", przestaje działać.
// 7.4: zwraca false + warning
$pos = strpos('abc', '');
// 8.0: rzuca ValueError — trzeba walidować argumenty
if ($needle === '') {
// obsłuż przypadek brzegowy
}
Surowsze porównania stringów z liczbami. 0 == "abc" w 7.4 dawało true, w 8.0 — false. To zmiana, która cicho psuje logikę warunków w starym kodzie — testy na ścieżkach decyzyjnych (uprawnienia, koszyki, rabaty) są tu obowiązkowe.
Usunięte funkcje i konstrukcje. each() (od lat deprecjonowane), create_function(), money_format(), rozszerzenie mysql_* (usunięte już w 7.0), stare API preg_replace z modifierem /e. Rector podmienia większość automatycznie.
Zmiany w sortowaniu i funkcjach tablicowych. usort() i rodzina stabilnych sortowań — w 8.0 sortowania są stabilne, co zmienia kolejność równych elementów. Jeśli kod polegał na niestabilności (a część starego kodu polegała), wyniki się zmienią.
Konstruktor promotion, atrybuty, named arguments — nowe możliwości, które Rector częściowo wprowadza automatycznie (np. promotion), ale nie musisz ich używać. Migracja to nie refaktoryzacja do nowego stylu.
8.0 → 8.1 — deprecation nulli i nowe typy
Łagodniejsze przejście, ale z jedną zmianą, która dotyka niemal każdego projektu.
Deprecation null w parametrach nie-nullowalnych. Przekazywanie null do funkcji wewnętrznych oczekujących stringa/int (np. strlen(null), htmlspecialchars(null)) daje od 8.1 deprecation. Kod działa dalej, ale zaśmieca logi — a w przyszłych wersjach to będzie błąd.
// 7.4/8.0: działa bez ostrzeżenia
htmlspecialchars($value); // $value może być null
// 8.1+: deprecation — napraw jawnie
htmlspecialchars($value ?? '');
Nowości, które warto wykorzystać przy okazji migracji: enums (enum Status: string), readonly properties, never jako typ zwracany, fibers (dla bibliotek asynchronicznych), array_is_list().
Zmiany w zachowaniu: $GLOBALS działa inaczej (pełny dostęp do zapisu), część ostrzeżeń o parametrach opcjonalnych przed wymaganymi stała się deprecation. Rector ma reguły dla większości przypadków.
8.1 → 8.2 — dynamiczne właściwości i readonly klasy
Kolejne przejście „deprecation-focused" — kod działa, ale logi się zapełniają.
Deprecation dynamicznych właściwości. Tworzenie właściwości „w locie" ($obj->newField = 1 na obiekcie, który nie zadeklarował pola) jest od 8.2 deprecjonowane. Dotyczy to szczególnie starych ORM-ów, obiektów DTO i kodu, który „doklejał" pola do modeli. Rozwiązania: deklaracja pól, #[AllowDynamicProperties] jako bufor, albo refaktoryzacja na tablice/typy.
class UserDto
{
// 8.2+ wymaga jawnej deklaracji albo atrybutu
public string $name;
public ?string $email = null;
}
Deprecation części interpolacji stringów. Konstrukcje typu "${var}" (poza {$var}) są deprecjonowane — prosta zamiana składni, Rector robi to automatycznie.
Nowości: readonly klasy, typy DNF ((A&B)|C), usunięcie części dynamicznych właściwości z biblioteki standardowej. Pełne wsparcie dla 8.2 mają Laravel 10/11, Symfony 6.2+.
8.2 → 8.3 i 8.3 → 8.4 — kosmetyka i domknięcie
Dwa ostatnie kroki są najlżejsze — dla większości projektów to dni pracy, nie tygodnie.
8.3: typowane stałe klas (kod nadpisujący stałe ze zmienionym typem dostaje błąd — dotyczy głównie bibliotek), deprecation get_class() bez argumentu (zamiana na static::class), losowe zmiany w Random\Randomizer, nowa funkcja json_validate().
8.4: deprecation niejawnych parametrów nullowalnych (string $x = null → ?string $x = null), property hooks (gettery/settery na właściwościach), asymetryczna widoczność, nowe funkcje tablicowe (array_find, array_any, array_all).
Po przejściu na 8.4 projekt jest na wersji z pełnym wsparciem do końca 2028 roku — i gotowy na 8.5 (kolejne kosmetyczne przejście, gdy będzie potrzebne).
Tabela zbiorcza — co sprawdzić w każdym kroku
| Przejście | Najważniejsze ryzyko | Typowa naprawa | Czas dla średniego projektu |
|---|---|---|---|
| 7.4 → 8.0 | wyjątki zamiast warningów, porównania stringów | walidacja argumentów, poprawki warunków | 1–3 tygodnie |
| 8.0 → 8.1 | deprecation nulli | ?? '' i jawne typy nullowalne |
2–5 dni |
| 8.1 → 8.2 | dynamiczne właściwości | deklaracje pól / atrybut | 3–7 dni |
| 8.2 → 8.3 | typowane stałe (biblioteki) | aktualizacja zależności | 1–3 dni |
| 8.3 → 8.4 | niejawne nullowalne parametry | Rector + przegląd sygnatur | 1–2 dni |
Praktyczny plan przejścia 7.4 → 8.4
Kolejność pracy, która sprawdza się w projektach.
- Audyt na 8.4 — PHPCompatibility z
testVersion 8.4-i PHPStan od razu pokażą pełną listę blokad wszystkich przejść. Listę porządkujesz potem według kroków. - Testy ścieżek krytycznych — bez nich nie zaczynaj. 15–20 testów end-to-end wystarczy na start.
- Rector krokami — najpierw
withPhpSets(php80: true), commit, testy. Potemphp81,php82,php83,php84. Każdy krok to osobny commit i osobne uruchomienie testów. - Framework równolegle — aktualizuj Laravel/Symfony o jedną wersję na krok PHP (mapę wersji frameworka do PHP znajdziesz w dokumentacji frameworka).
- Staging po każdych dwóch krokach — wdrażaj na staging i przechodź testy manualne; produkcję aktualizuj, gdy cały łańcuch jest zielony.
- Deprecation do zera — po każdym kroku uruchom aplikację i wyczyść logi z ostrzeżeń. Zaległe deprecation z 8.1/8.2 to najczęstszy powód niespodzianek przy kolejnych krokach.
Szczegóły techniczne workflow z Rectorem — konfiguracja, tryby, CI — są w artykule o automatycznej migracji z Rector. Pełny proces z audytem, stagingiem i wdrożeniem opisuje przewodnik o migracji PHP na 8.x. Jeśli zaczynasz od jeszcze starszej wersji (5.6), dochodzi krok przez 7.x — opisany w tym samym przewodniku.
Najczęściej zadawane pytania
Czy z PHP 7.4 mogę przejść od razu na 8.4?
Technicznie tak — Rector ma zestaw kumulatywny (withPhpSets(php84: true)), który stosuje reguły wszystkich przejść. W praktyce dla średnich i dużych projektów lepiej iść krokami: łatwiej przypisać regresje do konkretnej zmiany, a framework aktualizujesz w rytmie zgodnym z jego wymaganiami wersyjnymi.
Co się najczęściej psuje przy przejściu z 7.4 na 8.0?
- Trzy rzeczy: wyjątki tam, gdzie wcześniej były warningi (funkcje stringowe i arytmetyczne), zmienione porównania stringów z liczbami (
0 == "abc"przestało być prawdziwe) oraz usunięte funkcje (each(),create_function(),preg_replacez/e). Dwie pierwsze wymagają przeglądu logiki, trzecia jest w większości automatyczna.
Ile trwa migracja z PHP 7.4 na 8.4?
- Mały projekt: 1–2 tygodnie. Średni system z frameworkiem: 4–10 tygodni (łącznie z aktualizacją frameworka i bibliotek). Duży system bez testów: 2–4 miesiące etapami. Samą refaktoryzację kodu Rector wykonuje w godziny — reszta czasu to testy, poprawki ręczne i wdrożenia.
Czy po migracji PHP muszę od razu aktualizować framework?
Nie „od razu", ale w tej samej podróży. Frameworki mają własne przedziały wsparcia PHP — np. Laravel 8 działa na PHP 8.0, ale nie na 8.2+. Jeśli zostaniesz na starym frameworku na nowym PHP, stracisz wsparcie frameworka. Najlepszy rytm: przy każdym kroku PHP podnieś framework o wersję, którą obsługuje.
Czy dynamiczne właściwości to blokada migracji na 8.2?
Nie — to deprecation, kod działa dalej. Ale zaśmieca logi i stanie się błędem w przyszłej wersji. Najczęstsze źródła to stare ORM-y i kod „doklejający" pola do obiektów. Naprawa: jawne deklaracje pól albo #[AllowDynamicProperties] jako świadomy bufor z datą przeglądu.
Czy migracja z 7.4 wpłynie na wydajność aplikacji?
- Zwykle pozytywnie: PHP 8.x jest szybsze od 7.4 o kilkanaście–kilkadziesiąt procent w zależności od profilu (JIT, optymalizacje silnika, szybsze funkcje). Największy zysk widzą aplikacje obciążone CPU; aplikacje ograniczone bazą danych zyskają mniej. Pełny efekt daje też aktualizacja frameworka i bibliotek.