Blog
PHP-5.6- und ältere PHP-8-Anwendungen sicher aktualisieren
Die Aktualisierung einer älteren PHP-Anwendung ist kein einzelner Composer-Befehl. Eine sichere Migration schützt Verhalten mit Pest, führt PHPStan schrittweise ein, aktualisiert Framework und Abhängigkeiten in Phasen und misst vor der Optimierung.
PHP-Migration in Symfony und Laravel: ein technischer Leitfaden
Ein PHP-Upgrade verändert die Ausführungsumgebung, nicht nur eine Versionsnummer. Eine Seite kann funktionieren, während ein Import mit einer anderen CLI-Binary scheitert oder ein Worker ältere Nachrichten nicht mehr lesen kann. Entscheidend ist, ob das benötigte Verhalten auf den tatsächlichen Ausführungspfaden der Anwendung erhalten bleibt.
Dieser Leitfaden behandelt Kompatibilitätsprüfung, Regressionstests und die Überprüfung des Deployments. Die Beispiele sind illustrativ und schildern keine Kundenmigrationen von GiSoft.
Die Migrationsumgebungen festlegen
Erfassen Sie HTTP-Einstiegspunkte, Konsolenbefehle, geplante Aufgaben, Nachrichtenempfänger, Importe, Exporte und Wartungsskripte. Zu jedem Pfad gehören PHP-Binary, Erweiterungen, Konfiguration, Datenbanktreiber und externe Abhängigkeiten. PDF-Erzeugung, Bildverarbeitung, SOAP und E-Mail können mehr als Composer-Pakete voraussetzen.
Eine PHP-5.6-Anwendung benötigt möglicherweise Zwischenstände aus PHP, Framework und Bibliotheken. Bei einer älteren PHP-8-Anwendung können die Lücken kleiner sein. Eine allgemeingültige Versionsleiter gibt es nicht: Maßgeblich sind überprüfbare Kombinationen der tatsächlichen Abhängigkeiten. Nicht mehr unterstützte Zwischenversionen sind Hilfsmittel in isolierten Migrationsumgebungen, nicht automatisch geeignete Produktionsziele.
Prüfen Sie bei der Zielauswahl die offizielle PHP-Supporttabelle und die versionsbezogene Upgrade-Anleitung des Frameworks. Aktive Unterstützung und reine Sicherheitswartung unterscheiden sich. Wählen Sie einen gepflegten Patchstand mit ausreichender verbleibender Unterstützung; die folgenden Codebeispiele legen dieses Ziel nicht fest.
Umgebung Was sie klärt
Bestehend Bisheriges Verhalten mit passenden Werkzeugen
Migration Kompatibilität einer Versionskombination
Produktionsnah HTTP, CLI, Worker und Deployment-Ablauf
Statische Analyse läuft auf einer unterstützten Tool-Laufzeit.
Sie ersetzt keine Ausführung in der Zielumgebung.Aktuelle Versionen von Pest und PHPStan lassen sich nicht einfach unter PHP 5.6 installieren. Dort bleibt eine kompatible PHPUnit-Suite erhalten, oder die alte Anwendung wird von außen getestet. Moderne Analysewerkzeuge brauchen eine geeignete separate Umgebung, einschließlich kompatiblem Bootstrap und Autoloading sowie einer konfigurierten PHP-Zielversion. Tests, die die Anwendung starten, benötigen dort lauffähige Abhängigkeiten.
Die Beispiele verwenden neuere Syntax: typisierte Eigenschaften ab PHP 7.4, mixed und benannte Argumente ab PHP 8.0, Constructor Promotion mit readonly-Eigenschaften ab PHP 8.1 sowie readonly-Klassen ab PHP 8.2. Das sind Syntaxgrenzen, nicht die Anforderungen einer bestimmten Pest- oder Framework-Version.
Mit Composer die Blockaden erkennen
Prüfen Sie composer.json, composer.lock, Erweiterungen und Plugins, bevor Sie Versionsvorgaben ändern. Die Shell-Variable PHP_UPGRADE_TARGET muss die genaue untersuchte PHP-Version enthalten. Die letzten beiden Befehle sind Alternativen, keine getrennten Migrationsschritte:
composer show --direct
composer outdated --direct
composer why-not php "${PHP_UPGRADE_TARGET:?}"
composer prohibits php "${PHP_UPGRADE_TARGET:?}"show --direct zeigt direkte Abhängigkeiten, outdated --direct sucht neuere Versionen. why-not und prohibits sind Aliase für die Prüfung deklarierter Blockaden. Sie belegen weder die Anwendungskompatibilität noch erstellen sie einen Migrationsplan.
Composer selbst muss in seiner Ausführungsumgebung funktionieren. Seine Dokumentation nennt die LTS-Reihe 2.2 für älteres PHP. Prüfen Sie deren Optionen und Plugin-Kompatibilität, statt die neueste Binary unter PHP 5.6 vorauszusetzen. Relevant ist der gesamte Abhängigkeitsgraph, nicht nur die direkt eingebundenen Pakete.
Ein simulierter config.platform-Wert hilft bei der Auflösung, installiert aber weder PHP noch Erweiterungen. composer check-platform-reqs prüft später die tatsächliche CLI-Umgebung und ignoriert diese Simulation. Web- und Worker-Umgebungen brauchen eigene Kontrollen. Plattformanforderungen zu ignorieren, ist keine Lösung für das Deployment.
Verhalten vor der Implementierungsänderung absichern
Beginnen Sie mit teuren Fehlerfällen: Anmeldung und Berechtigungen, Bestellungen, Rechnungen, Zahlungsergebnisse, Importe und öffentliche API-Antworten. Ein kleiner Test kann eine Regel schützen, ohne die ganze Anwendung nachzubauen:
it(
'veröffentlicht nicht bei fehlgeschlagener Zahlung',
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 und InvoicePublishingService sind projektspezifische Beispiele. Der Test setzt voraus, dass eine fehlgeschlagene Zahlung die Veröffentlichung ohne Ausnahme verhindert. Er prüft diese Regel und den Test-Publisher, nicht die Datenbankpersistenz oder einen echten Versanddienst.
Charakterisierungstests halten Verhalten fest, das vor einer Änderung verstanden werden muss:
it(
'bewahrt die Rundung des importierten Auftrags',
function (): void {
$calculator = new LegacyOrderCalculator();
$result = $calculator->calculate(
netAmount: 19.995,
taxRate: 23,
);
expect($result->grossAmount)->toBe(24.59);
},
);Dieser hypothetische Vergleichswert setzt voraus, dass der Nettobetrag nicht vorab gerundet wird und das Bruttoergebnis auf zwei Nachkommastellen gerundet wird. Dezimal gerechnet ergibt 19.995 × 1.23 = 24.59385 bei üblicher Half-up-Rundung 24.59. Würde zunächst netto auf 20.00 gerundet, ergäbe sich 24.60. Der ausgelassene Rechner und seine tatsächliche Rundungsregel müssen in der ursprünglichen Umgebung überprüft werden; die Zahlen dokumentieren keine reale Buchhaltungspraxis.
Die Gleitkommazahlen bilden bewusst eine alte API ab. Viele Dezimalbeträge sind binär nicht exakt darstellbar; die Assertion ist keine Empfehlung für die Gestaltung finanzieller Berechnungen. Dezimalzahlen oder passend skalierte Ganzzahlen brauchen ausdrückliche Genauigkeits- und Rundungsregeln. Eine solche Vertragsänderung sollte von der Sicherung des bisherigen Ergebnisses getrennt werden.
Prüfen Sie außerdem lose Vergleiche, numerische Zeichenketten aus Datenbanktreibern, null gegenüber leeren Zeichenketten, Datums- und Zeitzonenverhalten, Sortierung, JSON und Serialisierung. Ein erfolgreicher Charakterisierungstest bewahrt eine Beobachtung, nicht zwangsläufig eine fachlich richtige Regel.
Nützliche Tests behalten und passende Werkzeuge wählen
Für Pest müssen bestehende PHPUnit-Tests nicht neu geschrieben werden. Pest baut auf PHPUnit auf; seine Version, die zugehörige PHPUnit-Version, Plugins und Konfiguration müssen aber zur gewählten PHP-Umgebung passen. Eine alte Testsuite funktioniert nicht automatisch mit einer beliebigen neuen Pest-Version. Während der Migration bei PHPUnit zu bleiben, ist eine legitime Entscheidung.
Unit-Tests prüfen isolierte Regeln, Integrationstests Repositories und reale Abhängigkeiten, funktionale Tests HTTP-Verhalten und Berechtigungen. Architekturtests kontrollieren nur konfigurierte Regeln. Smoke-Tests bestätigen wenige wichtige Pfade und ersetzen diese tieferen Prüfungen nicht.
Typannahmen mit PHPStan sichtbar machen
Beginnen Sie auf einer sinnvollen Analysestufe, beziehen Sie relevante Aufrufer und Abhängigkeiten ein und beheben Sie riskante Befunde vor der nächsten Verschärfung:
vendor/bin/phpstan analyse src --no-progressDer Befehl gehört in die vorbereitete Analyseumgebung. PHP-Zielversion, Framework-Erweiterungen und Symbolerkennung müssen gegebenenfalls konfiguriert werden. Eine geprüfte Baseline kann bestehende von neuen Befunden trennen. Breite Ausnahmen oder fortlaufend neu erzeugte Baselines können dagegen Regressionen verdecken. Nicht jedes Upgrade setzt die höchste Analysestufe voraus.
Enthält eine Eigenschaft tatsächlich einen Kunden oder null, geht durch einen breiteren Typ hilfreiche Information verloren:
private mixed $customer;Die engere Deklaration beschreibt den Vertrag und initialisiert die Eigenschaft:
private ?Customer $customer = null;Das sind Ausschnitte aus einer Klasse. Eine erforderliche Beziehung sollte nicht auf null gesetzt werden, nur um eine Warnung zu beseitigen. Älterer Code kann passende PHPDoc-Angaben nutzen, bis seine Laufzeit typisierte Eigenschaften unterstützt.
Auch eine Repository-Signatur, die lediglich ein Array verspricht, sagt Aufrufern wenig:
/**
* @return array
*/
public function findActiveCustomers(): array;Ein genauerer Vertrag beschreibt die einzelnen Zeilen:
/**
* @return list<array{
* id: int,
* email: string,
* active: bool
* }>
*/
public function findActiveCustomers(): array;Dies sind alternative Methodendeklarationen in einer Schnittstelle. Der Mapper muss die angegebenen Ganzzahlen, Zeichenketten und booleschen Werte tatsächlich liefern; Annotationen konvertieren keine Datenbankwerte.
Eine Doctrine-Collection braucht einen Elementtyp und eine Initialisierung für neu erzeugte Entitäten:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
// Ausschnitt aus einer beispielhaften Entität.
/** @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;
}Der Ausschnitt ist kein vollständiges Entity-Mapping. Integrieren Sie die Initialisierung in den tatsächlichen Konstruktor und passen Sie den Schlüsseltyp bei textbasierten Schlüsseln an. PHPDoc beschreibt die Collection für die Analyse, validiert aber nicht jedes eingefügte Objekt zur Laufzeit. ORM-Mappings, Hydrierung und nullable Datenbankspalten benötigen weiterhin Integrationstests.
Framework-Grenzen nur bei Bedarf ändern
Für Symfony sind je nach Upgrade Service-Konfiguration, Authentifizierung, Argumentauflösung, Formulare, Validierung, Doctrine-Mappings und Serialisierung zu prüfen. Gruppieren Sie Deprecations nach Verantwortlichkeit und Abhängigkeit. Ein Paket-Upgrade kann Voraussetzung für eine Änderung im Anwendungscode sein; globale Warnungsunterdrückung hilft dabei nicht.
Für Laravel gehören die konkreten Versionen von Service Providern, Middleware, Authentifizierungsmechanismen, Eloquent-Casts, Queue-Serialisierung, Mail- und Dateisystemanbindung sowie Community-Paketen in die Prüfung. Weder der Ersatz sämtlicher Facades noch eine neue Architektur ist eine Voraussetzung für das PHP-Upgrade.
Eine Controller-Grenze hilft, wenn Transportänderungen andernfalls in die Geschäftslogik hineinreichen:
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,
);
}
}Dieser Symfony-Ausschnitt setzt PHP 8.1 oder neuer voraus. Der projektspezifische CreateOrderRequest muss aus HTTP-Eingaben erzeugt und vor Verwendung validiert werden; er ist kein eingebauter Symfony-Request-Typ. Routing, Service-Registrierung, Berechtigungsprüfung, Fehlerabbildung und Transaktionsführung sind ausgelassen. Der Anwendungsdienst muss weiterhin seine fachlichen Regeln durchsetzen.
In Laravel kann ein Form Request Eingaben validieren und die Berechtigung für die Anfrage prüfen, bevor ein Dienst aufgerufen wird. In beiden Frameworks sollte eine Schnittstelle einem echten Vertrag oder Austauschbedarf dienen. Eine umfassende Architekturänderung gehört nicht automatisch zur Kompatibilitätsarbeit.
Über inkompatible Pakete entscheiden
Eine gepflegte Abhängigkeit braucht möglicherweise nur ein Upgrade. Ein ungenutztes Paket lässt sich nach Prüfung indirekter Nutzung entfernen. Ein aufgegebenes Paket benötigt eventuell Ersatz, eine schwierige Integration vorübergehend eine Isolation.
Bei der Dokumenterzeugung kann ein projekteigener Port die Auswirkungen des Austauschs auf die Aufrufer begrenzen:
interface DocumentGeneratorInterface
{
public function generate(DocumentData $data): GeneratedDocument;
}DocumentData und GeneratedDocument sind Projekttypen. Alte und neue Adapter würden diesen Port implementieren; ihre Dokumente wären auf geforderten Inhalt und Format zu prüfen. Die Schnittstelle macht weder eine inkompatible Bibliothek unter neuerem PHP lauffähig noch eine unsichere Bibliothek sicher. Auch eine separat betriebene Altkomponente braucht einen ausdrücklichen Sicherheits- und Ablöseplan.
Upgrade-bedingte Performance-Regressions prüfen
Überprüfen Sie Kompatibilität und Optimierung möglichst getrennt, auch wenn beides zum selben Projekt gehört. Eine Regression durch ein ORM- oder Treiber-Upgrade kann eine unmittelbare Korrektur erfordern. Eine davon unabhängige Neugestaltung von Abfragen oder Cache kann meist warten.
Eine Anfrage an GET /api/en/orders mit 25 Datensätzen könnte beispielsweise dieses Profil aufweisen. Die Zahlen sind erfundenes Lehrmaterial, keine GiSoft-Messwerte:
Gesamtantwort 780 ms SQL-Abfragen 54
Datenbank 180 ms Spitzenspeicher 110 MB
Hydrierung 120 ms Antwortgröße 1.4 MB
Serialisierung 210 ms Externes HTTP 190 msDie Teilzeiten müssen nicht die gesamte Dauer abdecken und können sich je nach Instrumentierung überschneiden. Vergleichen Sie repräsentative Daten, Antworten, Cache-Zustände und Nebenläufigkeit in gleichwertigen Umgebungen. Beobachten Sie Latenzperzentile, Fehler, Datenbankzeit, Speicher und Queue-Wartezeit, ohne sensible Nutzdaten zu protokollieren.
Ein stabiler Lesevertrag hilft dabei, veränderte Typen, Ladestrategien oder Abfragezahlen nach einem ORM-Upgrade zu erkennen:
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;
}Das DTO ist ein mögliches Lesemodell, kein vorgeschriebener Entity-Ersatz. Sein Mapper muss die deklarierten Typen einschließlich DateTimeImmutable liefern. Das Repository muss Seitengrößen, stabile Sortierung und Zugriffsgrenzen festlegen. Ein DTO allein verhindert weder N+1-Abfragen noch garantiert es schnellere Antworten.
Ein vorhandener Abfragezähltest kann verändertes Ladeverhalten aufdecken:
it(
'begrenzt Abfragen einer Bestellseite',
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);
},
);Alle drei Hilfsmethoden gehören zum Beispielprojekt, nicht zu Pest oder Doctrine. Testdaten, Bootstrap und gegebenenfalls Authentifizierung werden vor der Zählung vorbereitet. Kontrollieren Sie EntityManager- beziehungsweise Modellzustand und Cache, damit bereits geladene Beziehungen keine Abfragen verbergen. Nach der Messung wird der Collector gestoppt oder zurückgesetzt. Die Grenze von vier Abfragen ist illustrativ und misst nicht deren Kosten.
Ein Test der Antwortgröße schützt eine andere Eigenschaft:
it(
'begrenzt die öffentliche Antwortgröße',
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);
},
);Er setzt Pest mit Symfony WebTestCase, repräsentative Testdaten und gegebenenfalls Authentifizierung voraus. strlen() zählt Bytes des Antwortinhalts im Test, nicht unbedingt komprimierte Netzwerkbytes. Der Grenzwert ist ein Beispiel; auch eine kleine Antwort kann fachlich falsch sein. Assertions zum Antwortvertrag bleiben notwendig. Exakte Zeitgrenzen gehören eher in kontrollierte Performancemessungen als in eine gewöhnliche geteilte CI-Umgebung.
Cache und Nachrichten als Deployment-Verträge behandeln
Ein Upgrade kann serialisierte Daten, Cache-Adapter oder PHP-Erweiterungen verändern. Prüfen Sie, ob alte und neue Leser gemeinsame Daten verstehen, oder versionieren Sie das Format und planen Ablauf beziehungsweise Invalidierung. Verwaltete Doctrine-Entitäten und Eloquent-Modelle haben unterschiedliche Lebenszyklen; keines von beiden ist automatisch ein portables Cache-Format. Auch DTOs benötigen stabile Serialisierungsverträge.
Diese Schlüssel sind nur Skizzen:
settings.public.{locale}
menu.header.{locale}
orders.summary.{customerId}.{month}Berücksichtigen Sie Mandant, Berechtigungen und Datenversion, soweit sie das Ergebnis beeinflussen. Testen Sie Invalidierung nach erfolgreichen Commits sowie parallele Lesezugriffe. Redis, Memcached und Dateispeicherung sind keine automatisch austauschbaren Rückfalllösungen. Ein PHP-Upgrade verlangt keinen Wechsel des Cache-Backends.
Planen Sie Worker-Neustarts oder das Leeren der Warteschlange, die Lesbarkeit alter Nachrichten, begrenzte Wiederholungen und den Umgang mit fehlgeschlagenen Nachrichten. Zusätzliche Arbeit muss nicht allein wegen des Upgrades asynchron werden. Nutzt die Anwendung bereits eine Outbox, müssen Geschäftsdaten und Outbox-Eintrag für eine atomare Erfassung in derselben Datenbanktransaktion bestätigt werden. Veröffentlichung und Verarbeitung können trotzdem mehrfach erfolgen; folgenreiche Aktionen brauchen daher Duplikatbehandlung.
Destruktive Schemaänderungen sollten nicht beiläufig mit dem Laufzeitwechsel ausgeliefert werden. Ist ein Schemaübergang nötig, können kompatible Ergänzungen, Datenübernahme, Validierung und spätere Entfernung Rückkehroptionen erhalten. Schreibzugriffe währenddessen müssen berücksichtigt werden. Doppeltes Schreiben ist nicht verpflichtend und hat eigene Fehlerszenarien. Ein Code-Rollback stellt weder gelöschte Daten wieder her noch nimmt es externe Aktionen zurück.
Die geprüfte Kombination bauen und ausliefern
Das folgende CI-Beispiel setzt Bash und GNU-Werkzeuge voraus. Es wurde nicht gegen dieses Projekt ausgeführt und nimmt installierte, kompatible Entwicklungsabhängigkeiten sowie die gezeigten Testverzeichnisse an:
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/FunctionalIst PHPUnit der etablierte kompatible Runner, verwenden Sie vendor/bin/phpunit. Ein vollständiger Lauf mit vendor/bin/pest kann bei der Abnahme die drei Verzeichnisaufrufe ersetzen; beides auszuführen ist nicht grundsätzlich nötig. Syntaxprüfungen führen den Code nicht aus, statische Analyse beweist kein Geschäftsverhalten.
Erstellen und prüfen Sie die Lock-Datei in einer kontrollierten Umgebung. Produktion sollte die getesteten Abhängigkeiten installieren, nicht einen neuen Satz auflösen. composer check-platform-reqs --no-dev prüft die Produktionsabhängigkeiten auf ihrer tatsächlichen Laufzeit. Entwicklungswerkzeuge können höhere Anforderungen stellen als die Anwendung.
Vor dem Release sind HTTP, CLI, geplante Aufgaben, Worker und Integrationen unter produktionsnahen Bedingungen zu prüfen. Bereiten Sie Backups vor und testen Sie die Wiederherstellung. Mandanten- oder prozentbasierte Rollouts passen nur, wenn Routing und gemeinsame Daten das ermöglichen; andernfalls braucht es eine getestete Umschaltung oder ein Wartungsfenster.
Stimmen Sie Cache-Aufwärmung und Aktualisierung der PHP-Prozesse beziehungsweise des OPcache auf das Deployment ab. Ein OPcache-Reset über CLI erneuert keinen separaten PHP-FPM-Prozesscache. Beobachten Sie Fehler, Latenz, fehlgeschlagene Jobs und kritische Geschäftsergebnisse; definieren Sie Abbruchkriterien. Wiederherstellung betrifft Schema, Nachrichten, Sitzungen und neue Schreibvorgänge, nicht nur das alte Image.
Nach der Abnahme werden entbehrliche Übergangsschichten entfernt und die getestete Versionskombination dokumentiert. Das Ergebnis sollte ein bekannter Prüf- und Auslieferungsweg für die nächste Änderung sein, keine längere Liste unerklärter Ausnahmen.
Technische Quellen
Prüfen Sie diese Quellen für die gewählten Versionen; Anforderungen ändern sich unabhängig vom Artikel.
- PHP-Support und Migrationshinweise: https://www.php.net/supported-versions.php
- Composer-Befehle und Plattformprüfung: https://getcomposer.org/doc/03-cli.md
- Composer-Laufzeitanforderungen: https://getcomposer.org/doc/00-intro.md
- Pest-Installationsanforderungen: https://pestphp.com/docs/installation
- PHPStan-Einrichtung und Zielkonfiguration: https://phpstan.org/user-guide/getting-started sowie https://phpstan.org/config-reference
- Symfony-Hauptversionswechsel: https://symfony.com/doc/current/setup/upgrade_major.html
- Versionsbezogene Laravel-Anleitungen: https://laravel.com/docs
