Legacy-PHP modernisieren, ohne alles neu zu schreiben
Andreas Kunisch, , 6 Min. Lesezeit
Viele PHP-Anwendungen, die über Jahre gewachsen sind, funktionieren im Kern zuverlässig, lassen sich aber immer schwerer erweitern. Neue Features brauchen länger, Bugfixes reißen an unerwarteten Stellen neue Probleme auf, und im Team traut sich niemand mehr, bestimmte Dateien anzufassen.
Eine komplette Neuentwicklung wirkt dann verlockend, ist aber selten die richtige Antwort: Sie bindet über Monate Kapazität, stoppt die Weiterentwicklung des bestehenden Systems und liefert erst am Ende einen Gegenwert. Der praktikablere Weg ist eine schrittweise Modernisierung, bei der die Anwendung während des Umbaus weiterläuft und sich jeder Schritt einzeln testen und zurückrollen lässt.
Was bedeutet schrittweise Modernisierung konkret?
Schrittweise Modernisierung heißt, die bestehende Anwendung in kleinen, einzeln auslieferbaren Schritten zu verändern. Ersetzt wird sie dabei nicht auf einen Schlag. Jeder Schritt verbessert einen klar abgegrenzten Teil (eine Klasse, ein Modul, eine Schnittstelle), und jeder wird für sich getestet, ausgerollt und beobachtet.
Der bekannteste Rahmen dafür ist der Strangler-Fig-Ansatz, benannt nach dem Würgefeigenbaum, der einen Wirtsbaum langsam umwächst, bis dieser ersetzt ist. Übertragen auf Software bedeutet das: Neue oder modernisierte Teile entstehen neben dem Altsystem und übernehmen Stück für Stück dessen Zuständigkeiten, bis es irgendwann nicht mehr gebraucht wird.
Wichtig ist, dass die Anwendung in jedem Zwischenzustand funktionsfähig und auslieferbar bleibt. Es gibt also nie einen Punkt, an dem Alt- und Neusystem gleichzeitig halb fertig nebeneinanderstehen und nichts mehr zuverlässig läuft.
Warum sind Tests die Voraussetzung für jeden weiteren Schritt?
Tests sind die Voraussetzung, weil sie der einzige verlässliche Nachweis sind, dass eine Änderung das bestehende Verhalten nicht kaputt macht. Ohne sie verlässt man sich auf manuelles Nachprüfen, das bei einer gewachsenen Anwendung mit vielen Sonderfällen schnell lückenhaft wird.
Bei Legacy-Code, der keine oder kaum Tests hat, beginnt man mit Charakterisierungstests: Tests, die nicht das gewünschte, sondern das tatsächliche Verhalten des Codes festhalten, inklusive der Eigenheiten und Sonderfälle, die über die Jahre entstanden sind. Sie bilden ein Sicherheitsnetz, bevor überhaupt etwas umgebaut wird.
Erst mit diesem Netz lassen sich Zuständigkeiten entflechten, Klassen aufteilen oder Abhängigkeiten tauschen, ohne bei jedem Schritt zu riskieren, dass sich im Hintergrund unbemerkt etwas ändert. Die Tests wachsen mit jedem Umbauschritt mit und werden nach und nach durch aussagekräftigere Unit- und Integrationstests ersetzt.
Wie bringt man Abhängigkeiten und PHP-Version wieder auf einen aktuellen Stand?
Veraltete PHP-Version und Abhängigkeiten bringt man am besten in kleinen, isolierten Schritten auf einen aktuellen Stand. Ein einziger großer Sprung macht Fehler schwer auffindbar. Zuerst prüft man mit Tools wie PHPStan oder Rector, welche Sprachversion der Code tatsächlich verträgt, und behebt Inkompatibilitäten paketweise.
Composer-Abhängigkeiten aktualisiert man über Major-Versionen hinweg nacheinander und liest dabei jeweils das Changelog. So lässt sich bei Problemen genau zuordnen, welche Änderung die Ursache war.
Erst wenn PHP-Version und Kernabhängigkeiten tragfähig sind, lohnt es sich, über einen Rahmenwechsel wie einen Umzug nach Symfony nachzudenken. Ein modernes Fundament ohne saubere Verantwortlichkeiten im Code bringt sonst wenig.
Wie entflicht man Zuständigkeiten, bevor man etwas verschiebt?
Zuständigkeiten entflicht man, indem man Geschäftslogik, Datenzugriff und Ausgabe (HTML, JSON, E-Mail) innerhalb des Altsystems trennt, bevor irgendetwas in eine neue Struktur wandert. Ein typisches Muster in gewachsenem PHP-Code ist, dass eine einzelne Datei Validierung, Datenbankzugriff und Ausgabe in einer einzigen Funktion vermischt.
Der erste Schritt ist, diese Verantwortlichkeiten in eigene Klassen oder Funktionen mit klaren Ein- und Ausgaben zu ziehen, ohne das Verhalten zu verändern. Das macht jeden Teil einzeln testbar und zeigt oft erst, wie viele Sonderfälle und Abhängigkeiten tatsächlich dahinterstecken.
Erst entflochtener Code lässt sich sinnvoll in neue Module oder Services verschieben. Verschiebt man unentflochtenen Code direkt, wandert nur die Unordnung mit um.
Wie läuft der Umzug nach Symfony in der Praxis ab?
Der Umzug nach Symfony läuft typischerweise über eine Übergangsphase, in der Alt- und Neusystem über klar definierte Schnittstellen zusammenarbeiten. Das kann zum Beispiel ein gemeinsamer Reverse Proxy sein, der Anfragen je nach Route an das alte oder das neue System weiterleitet. Zuerst wandern einzelne, gut abgegrenzte Bereiche: ein neues Modul, eine neue API-Route, ein neuer administrativer Bereich.
In einem Projekt, an dem ich aktuell mitarbeite, geht es um einen PHP-Shop, der über mehr als 15 Jahre gewachsen ist. Dort bereiten wir gemeinsam mit dem Team einen solchen schrittweisen Umzug nach Symfony vor, und der Shop läuft währenddessen im Betrieb weiter.
Die Reihenfolge richtet sich danach, wo neue Anforderungen ohnehin anstehen und wo das Altsystem den meisten Wartungsaufwand verursacht. So liefert jeder Schritt auch unabhängig vom Gesamtumzug bereits einen Nutzen.
Wie bleibt der Betrieb während der Modernisierung stabil?
Der Betrieb bleibt stabil, wenn jeder Umbauschritt klein genug ist, um einzeln überwacht und im Fehlerfall zurückgerollt zu werden. Feature-Flags oder Routing-Regeln erlauben, einen neuen Teil zunächst nur für einen kleinen Anteil der Anfragen zu aktivieren. Alle anderen Anfragen laufen vorerst weiter über den alten Pfad.
Monitoring und Logging müssen schon vor dem jeweiligen Schritt stehen. Fehlerraten, Antwortzeiten und Geschäftskennzahlen wie abgeschlossene Bestellungen zeigen dann, ob ein neuer Teil tatsächlich gleichwertig arbeitet. Erst wenn diese Werte über einen angemessenen Zeitraum stabil sind, wird der alte Pfad abgeschaltet.
Für jeden Schritt muss vorab ein Rollback-Weg feststehen. Im Zweifel reicht es, die Route auf das Altsystem zurückzuschalten. Ohne diese Möglichkeit wird aus einem kontrollierten Umbau schnell ein riskanter Alles-oder-nichts-Schritt.
Wann ist eine Neuentwicklung doch die bessere Wahl?
Eine Neuentwicklung lohnt sich, wenn sich die fachlichen Anforderungen grundlegend geändert haben und das Altsystem nur noch wenig von dem abbildet, was das Geschäft heute braucht. In diesem Fall verlängert eine Modernisierung lediglich die Lebensdauer einer Struktur, die ohnehin ersetzt werden muss.
Ebenso sinnvoll ist ein Neubau, wenn die Codebasis so klein oder so isoliert ist, dass eine schrittweise Ablösung mehr Aufwand verursacht als ein überschaubarer Neuaufbau von Grund auf, etwa bei einem einzelnen, klar abgegrenzten Tool ohne viele Abhängigkeiten. Manchmal ist die technische Basis auch so veraltet, dass schon das Verstehen des Bestands mehr kostet als ein Neuaufbau: PHP-Version ohne Support, keine Tests, keine Dokumentation, kein Zugriff auf den Quellcode. Dann ist ein Neubau oft der realistischere Weg.
In den meisten gewachsenen Anwendungen überwiegt jedoch der schrittweise Weg: Er liefert laufend Verbesserungen, hält das System im Betrieb und senkt das Risiko im Vergleich zu einem großen Neubauprojekt, dessen Erfolg erst am Ende feststeht.
Häufige Fragen
- Wie lange dauert eine schrittweise Modernisierung?
- Das hängt von Größe und Zustand der Anwendung ab und lässt sich seriös nicht in Wochen angeben. Realistisch ist ein kontinuierlicher Prozess über mehrere Monate bis Jahre. Er läuft parallel zur regulären Weiterentwicklung und bringt unterwegs laufend Nutzen. Einen Stichtag, an dem alles fertig ist, gibt es dabei nicht.
- Muss die Zielarchitektur zwingend Symfony sein?
- Nein. Symfony bietet sich an, wenn die Anwendung bereits in PHP geschrieben ist und von einer etablierten, gut gewarteten Struktur mit klarer Trennung von Routing, Geschäftslogik und Persistenz profitiert. Entscheidend ist aber der schrittweise Weg an sich. Er funktioniert grundsätzlich auch mit anderen modernen PHP-Frameworks.
- Was unterscheidet Refactoring von Modernisierung?
- Refactoring verändert die interne Struktur von Code, ohne sein Verhalten nach außen zu ändern, zum Beispiel beim Aufteilen einer Klasse. Modernisierung ist umfassender: Sie schließt Refactoring ein, umfasst aber auch Versions- und Abhängigkeits-Updates, Architekturwechsel und begleitende Schritte wie Tests und Monitoring.
- Lohnt sich der Aufwand auch für ein kleineres Projekt?
- Ja, in reduziertem Umfang. Auch bei einer kleineren Anwendung zahlen sich Charakterisierungstests und kleine, rollbare Schritte aus, sobald mehrere Personen am Code arbeiten oder die Anwendung produktiv genutzt wird. Bei sehr kleinen, isolierten Tools kann ein Neubau allerdings schneller sein als ein formaler Modernisierungsprozess.
- Wo fängt man an, wenn unklar ist, wo man anfangen soll?
- Am Bereich mit dem höchsten Verhältnis aus Risiko und Änderungshäufigkeit: Code, der häufig angefasst wird, aber kaum Tests hat. Dort zuerst Charakterisierungstests schreiben, dann in kleinen Schritten entflechten. Das bringt den schnellsten Sicherheitsgewinn.