Blog
Jak bezpiecznie zaktualizować aplikację PHP 5.6 lub starszą aplikację PHP 8
Aktualizacja starszej aplikacji PHP nie jest jednym poleceniem Composer. Bezpieczna migracja chroni reguły biznesowe testami Pest, wprowadza PHPStan etapami, aktualizuje framework i zależności oraz opiera optymalizację na pomiarach.
Migracja PHP w Symfony i Laravel: przewodnik techniczny
Aktualizacja PHP zmienia środowisko wykonania, nie tylko numer wersji. Strona może działać, choć import uruchamiany inną binarką CLI już się nie powiedzie, a worker nie odczyta starszej wiadomości. Ważne jest więc sprawdzenie wymaganego zachowania na rzeczywistych ścieżkach wykonania aplikacji.
Ten przewodnik dotyczy analizy zgodności, ochrony przed regresją i weryfikacji wdrożenia. Przykłady są ilustracyjne, nie opisują migracji wykonanych u klientów GiSoft.
Ustal środowiska migracji
Zinwentaryzuj obsługę HTTP, komendy konsolowe, zadania cykliczne, odbiorców wiadomości, importy, eksporty i skrypty utrzymaniowe. Dla każdej ścieżki zapisz używaną binarkę PHP, rozszerzenia, konfigurację, sterowniki bazy oraz zależności zewnętrzne. Generowanie PDF, przetwarzanie obrazów, SOAP i poczta mogą wymagać czegoś więcej niż pakietów Composer.
Aplikacja PHP 5.6 może potrzebować pośrednich zestawów wersji PHP, frameworka i zależności. W starszej aplikacji PHP 8 różnic może być mniej. Żaden przypadek nie narzuca uniwersalnej drabiny wersji: potrzebne są kombinacje możliwe do sprawdzenia, wynikające z rzeczywistych ograniczeń. Niewspierane wersje pośrednie mogą służyć do migracji w odizolowanym środowisku, ale nie stają się przez to właściwym celem produkcyjnym.
Wybierając cel, sprawdź oficjalną tabelę wsparcia PHP i instrukcję aktualizacji odpowiedniej wersji frameworka. Aktywne wsparcie różni się od samych poprawek bezpieczeństwa. Wybierz utrzymywane wydanie poprawkowe z okresem wsparcia odpowiednim dla projektu; przykłady kodu poniżej nie dokonują tego wyboru.
Środowisko Co pozwala sprawdzić
Obecne Dotychczasowe zachowanie zgodnymi narzędziami
Migracyjne Zgodność wybranego zestawu wersji
Jak docelowe HTTP, CLI, workery i przebieg wdrożenia
Analiza statyczna działa na wspieranym środowisku narzędzi.
Nie zastępuje uruchomienia aplikacji w środowisku docelowym.Aktualnych Pest i PHPStan nie można po prostu zainstalować w PHP 5.6. Zachowaj tam zgodny zestaw testów PHPUnit albo testuj starą aplikację z zewnątrz. Nowoczesną analizę uruchamiaj w osobnym, odpowiednim środowisku, uwzględniając zgodność kodu startowego i autoloadingu oraz konfigurując docelową wersję PHP. Testy uruchamiające aplikację potrzebują zależności działających w ich środowisku testowym.
Przykłady używają nowszej składni: typowane właściwości wymagają PHP 7.4, mixed i argumenty nazwane PHP 8.0, promowane właściwości readonly PHP 8.1, a klasy readonly PHP 8.2. To minima składniowe, nie wymagania konkretnego wydania Pest czy frameworka.
Znajdź blokady w zależnościach Composer
Przed zmianą ograniczeń przejrzyj composer.json, composer.lock, rozszerzenia i wtyczki. Ustaw zmienną powłoki PHP_UPGRADE_TARGET na dokładną wersję PHP, którą sprawdzasz. Dwa ostatnie polecenia są alternatywami, nie osobnymi etapami migracji:
composer show --direct
composer outdated --direct
composer why-not php "${PHP_UPGRADE_TARGET:?}"
composer prohibits php "${PHP_UPGRADE_TARGET:?}"show --direct pokazuje zależności bezpośrednie, a outdated --direct sprawdza dostępność nowszych wydań. why-not i prohibits to aliasy wskazujące zadeklarowane blokady. Nie dowodzą zgodności aplikacji ani nie tworzą planu migracji.
Wersja Composer musi działać w używanym środowisku. Dokumentacja wskazuje linię 2.2 LTS dla starszego PHP; trzeba sprawdzić jej opcje i zgodność wtyczek, zamiast zakładać, że najnowszy plik wykonywalny uruchomi się na PHP 5.6. Analiza obejmuje cały graf zależności, nie tylko pakiety bezpośrednie.
Symulowana wartość config.platform pomaga rozwiązać zależności, ale nie instaluje PHP ani rozszerzeń. Późniejsze composer check-platform-reqs sprawdza rzeczywiste środowisko CLI, pomijając tę symulację. Środowiska WWW i workerów wymagają osobnej kontroli. Ignorowanie wymagań platformy nie naprawia wdrożenia.
Zabezpiecz zachowanie przed zmianą implementacji
Zacznij od kosztownych błędów: uwierzytelniania i uprawnień, zamówień, faktur, wyniku płatności, importów oraz odpowiedzi publicznego API. Mały test może chronić jedną regułę bez odtwarzania całej aplikacji:
it(
'nie publikuje po nieudanej płatności',
function (): void {
$payment = Payment::failed();
$invoice = Invoice::forPayment($payment);
$publisher = new InMemoryInvoicePublisher();
$service = new InvoicePublishingService($publisher);
$service->publish($invoice);
expect($invoice->isPublished())->toBeFalse()
->and($publisher->publishedInvoices())->toBeEmpty();
},
);Payment, Invoice, InMemoryInvoicePublisher i InvoicePublishingService są przykładami klas projektu. Test zakłada, że nieudana płatność blokuje publikację bez zgłaszania wyjątku. Sprawdza tę regułę i testowy mechanizm publikacji, nie zapis do bazy ani rzeczywistą usługę wysyłki.
Testy charakteryzujące utrwalają zachowanie, które trzeba poznać przed zmianą:
it(
'zachowuje wynik zaokrąglenia importu',
function (): void {
$calculator = new LegacyOrderCalculator();
$result = $calculator->calculate(
netAmount: 19.995,
taxRate: 23,
);
expect($result->grossAmount)->toBe(24.59);
},
);Ta hipotetyczna wartość odniesienia zakłada brak wcześniejszego zaokrąglenia kwoty netto i zaokrąglenie brutto do dwóch miejsc. W arytmetyce dziesiętnej 19.995 × 1.23 = 24.59385, co przy zaokrąglaniu do najbliższej wartości z połówkami w górę daje 24.59. Wcześniejsze zaokrąglenie netto do 20.00 dałoby 24.60. Pominięty kalkulator i jego rzeczywiste zasady trzeba sprawdzić w pierwotnym środowisku; liczby nie dokumentują faktycznych rozliczeń.
Liczby zmiennoprzecinkowe celowo odzwierciedlają stare API. Ich binarna reprezentacja nie pozwala dokładnie zapisać wielu kwot dziesiętnych, a ta asercja nie jest zaleceniem projektowania obliczeń finansowych. Reprezentacja dziesiętna lub odpowiednio skalowane liczby całkowite wymagają jawnych reguł precyzji i zaokrąglania. Zmianę tego kontraktu warto oddzielić od utrwalenia dotychczasowego wyniku.
Sprawdź też luźne porównania, liczby zwracane przez sterownik bazy jako tekst, null i pusty tekst, daty i strefy czasowe, sortowanie, JSON oraz serializację. Przejście testu charakteryzującego zachowuje obserwację, niekoniecznie poprawną regułę biznesową.
Zachowaj użyteczne testy i dobierz zgodne narzędzia
Wprowadzenie Pest nie wymaga przepisania istniejących testów PHPUnit. Pest opiera się na PHPUnit, ale jego wydanie, wersja PHPUnit, wtyczki i konfiguracja muszą pasować do wybranego PHP. Stary zestaw testów nie zacznie automatycznie działać pod dowolnym nowym Pest. Pozostanie przy PHPUnit podczas migracji jest rozsądną opcją.
Testy jednostkowe służą izolowanym regułom, integracyjne — repozytoriom i rzeczywistym zależnościom, a funkcjonalne — zachowaniu HTTP i uprawnieniom. Testy architektury sprawdzają tylko skonfigurowane zasady. Smoke testy potwierdzają kilka ważnych ścieżek, ale nie zastępują dokładniejszych kontroli.
Ujawnij założenia typów za pomocą PHPStan
Zacznij od użytecznego poziomu analizy, uwzględnij istotnych wywołujących i zależności, a przed zwiększaniem rygoru usuń problemy wysokiego ryzyka:
vendor/bin/phpstan analyse src --no-progressPolecenie uruchamiaj w przygotowanym środowisku analizy. W razie potrzeby skonfiguruj docelowe PHP, rozszerzenia analizy frameworka i wykrywanie symboli. Przejrzana lista istniejących błędów, czyli baseline, może oddzielić je od nowych. Szerokie wyciszenia lub ciągłe generowanie tej listy od nowa mogą ukrywać regresje. Najwyższy poziom analizy nie jest warunkiem każdej aktualizacji.
Jeżeli pole rzeczywiście przechowuje klienta albo null, rozszerzanie jego typu odbiera użyteczną informację:
private mixed $customer;Węższa deklaracja opisuje ten kontrakt i inicjalizuje pole:
private ?Customer $customer = null;To fragmenty wnętrza klasy. Nie ustawiaj wymaganej relacji na null tylko po to, by usunąć ostrzeżenie. W starszym kodzie można używać odpowiedniego PHPDoc, dopóki środowisko nie obsługuje typowanych właściwości.
Podobnie sygnatura repozytorium obiecująca jedynie tablicę mówi wywołującym niewiele:
/**
* @return array
*/
public function findActiveCustomers(): array;Przydatniejszy kontrakt określa zawartość każdego wiersza:
/**
* @return list<array{
* id: int,
* email: string,
* active: bool
* }>
*/
public function findActiveCustomers(): array;To alternatywne fragmenty deklaracji metody interfejsu. Mapper musi rzeczywiście zwracać opisane liczby całkowite, teksty i wartości logiczne; adnotacje nie konwertują wyników bazy.
Kolekcja Doctrine potrzebuje typu elementów oraz inicjalizacji przy tworzeniu nowej encji:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
// Fragment wnętrza przykładowej encji.
/** @var Collection<int, Order> */
private Collection $orders;
public function __construct()
{
$this->orders = new ArrayCollection();
}
/** @return Collection<int, Order> */
public function getOrders(): Collection
{
return $this->orders;
}To fragment encji, nie kompletne mapowanie. Inicjalizację trzeba włączyć do rzeczywistego konstruktora; typ klucza dostosować, jeśli relacja używa kluczy tekstowych. PHPDoc opisuje kolekcję dla analizy, nie waliduje każdego dodawanego obiektu podczas działania. Mapowania ORM, hydratacja i kolumny dopuszczające null nadal wymagają testów integracyjnych.
Zmieniaj granice frameworka tam, gdzie jest to potrzebne
W Symfony sprawdź właściwą instrukcję aktualizacji dla konfiguracji usług, uwierzytelniania, rozwiązywania argumentów, formularzy, walidacji, mapowań Doctrine i serializacji. Ostrzeżenia o przestarzałych rozwiązaniach grupuj według właściciela i zależności. Aktualizacja pakietu może być konieczna przed zmianą kodu aplikacji; nie należy globalnie ukrywać wszystkich ostrzeżeń.
W Laravelu sprawdź konkretne wersje dostawców usług, middleware, mechanizmów uwierzytelniania, rzutowania Eloquent, serializacji kolejek, poczty, obsługi plików i pakietów społeczności. Wymiana każdej fasady ani przebudowa architektury nie są wymaganiami aktualizacji PHP.
Granica kontrolera pomaga, gdy zmiany obsługi transportu rozchodzą się po logice biznesowej:
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
final class CreateOrderController
{
public function __construct(
private readonly CreateOrderServiceInterface $service,
) {
}
public function __invoke(
CreateOrderRequest $request,
): JsonResponse
{
$result = $this->service->create($request->toDto());
return new JsonResponse(
data: $result->toArray(),
status: Response::HTTP_CREATED,
);
}
}Ten fragment Symfony wymaga PHP 8.1 lub nowszego. Zakłada, że projektowy CreateOrderRequest powstaje z danych HTTP i jest walidowany przed użyciem. Nie jest to wbudowany typ żądania Symfony. Pominięto routing, rejestrację usług, kontrolę uprawnień, mapowanie błędów i transakcje. Serwis aplikacyjny nadal musi egzekwować reguły biznesowe.
W Laravelu Form Request może obsługiwać walidację wejścia i uprawnienia do żądania przed wywołaniem serwisu. W obu frameworkach interfejs powinien odpowiadać rzeczywistemu kontraktowi lub potrzebie wymiany, a nie rozszerzać prac nad zgodnością o domyślną przebudowę architektury.
Wybierz sposób obsługi niezgodnych pakietów
Utrzymywany pakiet może wymagać tylko aktualizacji. Nieużywany można usunąć po sprawdzeniu użyć pośrednich. Porzucony może potrzebować zamiennika, a trudna integracja — czasowej izolacji.
Przy generowaniu dokumentów port należący do projektu może ograniczyć wpływ wymiany na wywołujących:
interface DocumentGeneratorInterface
{
public function generate(DocumentData $data): GeneratedDocument;
}DocumentData i GeneratedDocument są typami projektu. Adapter starego i nowego rozwiązania realizowałby ten port, a dokumenty trzeba porównać pod kątem wymaganej treści i formatu. Interfejs nie uruchomi niezgodnej biblioteki na nowszym PHP ani nie zapewni jej bezpieczeństwa. Osobno utrzymywany stary komponent nadal wymaga planu zabezpieczenia i wycofania.
Sprawdzaj regresje wydajności związane z aktualizacją
W miarę możliwości weryfikuj zgodność i optymalizację osobno, nawet jeśli należą do tego samego projektu. Regresja po aktualizacji ORM lub sterownika może wymagać natychmiastowej poprawki. Niezwiązana z nią przebudowa zapytań czy pamięci podręcznej zwykle może poczekać.
Żądanie GET /api/en/orders zwracające 25 rekordów mogłoby mieć poniższy profil. To wymyślone liczby dydaktyczne, nie pomiary GiSoft:
Cała odpowiedź 780 ms Zapytania SQL 54
Baza danych 180 ms Pamięć szczytowa 110 MB
Hydratacja 120 ms Rozmiar odpowiedzi 1.4 MB
Serializacja 210 ms Zewnętrzne HTTP 190 msSkładowe nie muszą obejmować całego czasu i mogą się nakładać zależnie od sposobu pomiaru. Porównuj reprezentatywne dane, odpowiedzi, stany cache i współbieżność w równoważnych środowiskach. Śledź percentyle opóźnień, błędy, czas bazy, pamięć i oczekiwanie w kolejce, bez zapisywania wrażliwych danych żądań.
Stabilny kontrakt odczytu pomaga sprawdzić, czy aktualizacja ORM zmienia typy, sposób ładowania lub liczbę zapytań:
final readonly class OrderListItem
{
public function __construct(
public int $id,
public string $number,
public string $customerName,
public string $status,
public \DateTimeImmutable $createdAt,
) {
}
}interface OrderReadRepositoryInterface
{
/**
* @return list<OrderListItem>
*/
public function findPage(
int $page,
int $limit,
): array;
}DTO jest jedną z możliwości modelowania odczytu, nie obowiązkowym zamiennikiem encji. Mapper musi dostarczać zadeklarowane typy, w tym DateTimeImmutable. Repozytorium powinno określać dopuszczalny rozmiar strony, stabilną kolejność i granice dostępu. Sam DTO nie zapobiega N+1 i nie gwarantuje szybszej odpowiedzi.
Istniejący test liczby zapytań może wykryć zmianę sposobu ładowania:
it(
'ogranicza liczbę zapytań strony zamówień',
function (): void {
$this->createOrders(count: 100);
$collector = $this->startApplicationQueryCollection();
$items = $this->orderQuery()->findPage(
page: 1,
limit: 50,
);
expect($items)->toHaveCount(50)
->and($collector->queryCount())->toBeLessThanOrEqual(4);
},
);Wszystkie trzy metody pomocnicze należą do przykładowego projektu, nie do Pest ani Doctrine. Dane testowe, uruchomienie aplikacji i wymagane uwierzytelnienie przygotuj przed liczeniem. Kontroluj stan EntityManagera lub modeli i pamięci podręcznej, aby już załadowane relacje nie ukryły zapytań. Po mierzonej operacji zatrzymaj lub wyzeruj kolektor. Limit czterech zapytań jest ilustracyjny i nie mierzy ich kosztu.
Test rozmiaru odpowiedzi chroni inną właściwość:
it(
'ogranicza rozmiar odpowiedzi publicznej',
function (): void {
$client = static::createClient();
$client->request(
'GET',
'/api/en/orders?page=1&limit=25',
);
self::assertResponseIsSuccessful();
$content = (string) $client->getResponse()->getContent();
expect(strlen($content))->toBeLessThan(300_000);
},
);Przykład zakłada konfigurację Pest z Symfony WebTestCase, reprezentatywne dane i wymagane uwierzytelnienie. strlen() liczy bajty treści odpowiedzi testowej, niekoniecznie skompresowane bajty przesłane po sieci. Limit jest przykładowy; mała odpowiedź nadal może zawierać błędne dane, dlatego potrzebne są także asercje kontraktu. Dokładne progi czasowe lepiej sprawdzać w kontrolowanych pomiarach wydajności niż we współdzielonym środowisku zwykłego CI.
Traktuj cache i wiadomości jako kontrakty wdrożenia
Aktualizacja może zmienić serializowane dane, adaptery pamięci podręcznej albo rozszerzenia PHP. Sprawdź zgodność odczytu między wersjami lub wersjonuj format i zaplanuj wygaszenie bądź unieważnienie wpisów. Zarządzane encje Doctrine i modele Eloquent mają różne mechanizmy cyklu życia, ale żadne nie są domyślnie przenośnym formatem cache. Nawet DTO potrzebuje stabilnego kontraktu serializacji.
Takie klucze są jedynie szkicem:
settings.public.{locale}
menu.header.{locale}
orders.summary.{customerId}.{month}Uwzględnij organizację, uprawnienia i wersję danych, jeśli wpływają na wynik. Sprawdź unieważnianie po zatwierdzeniu zmian i równoległe odczyty. Redis, Memcached i pliki nie są wymiennymi automatycznymi mechanizmami awaryjnymi; zmiana backendu cache nie jest warunkiem aktualizacji PHP.
Zaplanuj restart workerów lub opróżnienie kolejki, zgodność starszych wiadomości, ograniczone ponowienia i obsługę błędów. Sam upgrade nie uzasadnia przenoszenia kolejnych operacji do kolejek. Jeśli aplikacja już używa outboxa, dane biznesowe i wpis outboxa muszą być zatwierdzane w tej samej transakcji bazy, aby zapis był atomowy. Publikacja i odbiór nadal mogą się powtarzać, więc operacje wywołujące skutki wymagają obsługi duplikatów.
Nie dołączaj przypadkowo destrukcyjnych zmian schematu do aktualizacji PHP. Gdy zmiana jest konieczna, zgodne dodatki, uzupełnienie danych, weryfikacja i późniejsze usunięcie starych struktur mogą zachować możliwości wycofania, ale trzeba obsłużyć zapisy w okresie przejściowym. Podwójny zapis nie jest obowiązkowy i ma własne scenariusze awarii. Cofnięcie kodu nie przywraca usuniętych danych ani nie odwraca działań zewnętrznych.
Zbuduj i wdróż sprawdzony zestaw
Poniżej jest przykładowe zadanie CI dla Bash i narzędzi GNU, nie zestaw poleceń zweryfikowany w tym projekcie. Zakłada zainstalowane, zgodne zależności deweloperskie i pokazane katalogi testów:
set -euo pipefail
composer validate --strict
composer check-platform-reqs
find src tests -name '*.php' -print0 \
| xargs -0 -r -n 1 php -l
vendor/bin/phpstan analyse --no-progress
vendor/bin/pest tests/Unit
vendor/bin/pest tests/Integration
vendor/bin/pest tests/FunctionalJeśli zgodnym, przyjętym narzędziem pozostaje PHPUnit, użyj vendor/bin/phpunit. Pełne vendor/bin/pest może na etapie odbioru zastąpić trzy wywołania katalogów; wykonywanie obu wariantów nie jest samo w sobie konieczne. Kontrola składni nie wykonuje kodu, a analiza statyczna nie dowodzi poprawności biznesowej.
Plik blokady zależności przygotuj i zweryfikuj w kontrolowanym środowisku. Produkcja powinna instalować sprawdzony zestaw, nie rozwiązywać zależności od nowa. Na rzeczywistym środowisku produkcyjnego zestawu pakietów użyj composer check-platform-reqs --no-dev. Narzędzia deweloperskie mogą mieć wyższe wymagania niż aplikacja.
Przed wdrożeniem sprawdź HTTP, CLI, zadania cykliczne, workery i integracje w warunkach zbliżonych do produkcji. Przygotuj kopie i sprawdź odtwarzanie. Stopniowe kierowanie ruchu lub przełączanie klientów ma sens tylko przy odpowiednim routingu i zgodnych współdzielonych danych; inaczej potrzebne jest przetestowane przełączenie albo okno serwisowe.
Przygotowanie cache aplikacji oraz odświeżenie procesów PHP i OPcache dostosuj do modelu wdrożenia. Reset OPcache w CLI nie odświeża osobnej pamięci procesu PHP-FPM. Obserwuj błędy, opóźnienia, nieudane zadania i kluczowe wyniki biznesowe; ustal warunki przerwania wdrożenia. Odtworzenie działania musi uwzględniać schemat, wiadomości, sesje i nowe zapisy, nie tylko poprzedni obraz aplikacji.
Po odbiorze usuń zbędne warstwy zgodności i zapisz sprawdzony zestaw wersji. Efektem aktualizacji powinna być znana ścieżka wdrożenia i weryfikacji kolejnej zmiany, nie większa lista niewyjaśnionych wyjątków.
Źródła techniczne
Sprawdź te materiały dla wybranych wersji; wymagania zmieniają się niezależnie od artykułu.
- Wsparcie i przewodniki migracji PHP: https://www.php.net/supported-versions.php
- Polecenia Composer i kontrola platformy: https://getcomposer.org/doc/03-cli.md
- Wymagania środowiska Composer: https://getcomposer.org/doc/00-intro.md
- Wymagania instalacyjne Pest: https://pestphp.com/docs/installation
- Uruchomienie i konfiguracja celu PHPStan: https://phpstan.org/user-guide/getting-started oraz https://phpstan.org/config-reference
- Duże aktualizacje Symfony: https://symfony.com/doc/current/setup/upgrade_major.html
- Instrukcje dla wersji Laravel: https://laravel.com/docs
