Zwischen 2012 und 2017 war ich als angestellter Entwickler bei einem IT-Dienstleister für deutsche Banken. Eine der Anwendungen, die ich dort übernommen habe, war einmal Bugzilla gewesen. Irgendwann davor hatte jemand angefangen, sie als PHP-Anwendung weiterzuentwickeln, und als ich sie bekam, war vom Original nichts mehr übrig außer dem Namen, den die Nutzer ihr noch gaben. Sie lief auf PHP 5.1, also auf einer Version, die seit 2006 keine Updates mehr bekam. Rund 100 PHP-Dateien.
Ich schreibe das hier auf, weil die Anfragen, die ich heute bekomme, in erstaunlich vielen Punkten genau so aussehen.
Der Zustand
Der Entwickler vor mir hatte eine Arbeitsweise, die man in gewachsenen Systemen oft findet: Jede Datei wurde von Zeile 1 bis Zeile x geschrieben, ohne Wiederverwendung. Keine Funktionen, keine Klassen, keine Includes für gemeinsame Logik. Wenn ein Report für das neue Jahr gebraucht wurde, wurde das Report-Skript des Vorjahres kopiert und in der Kopie die Jahreszahl geändert. Das ist kein Vorwurf, es funktionierte, und die Fachabteilung bekam ihre Reports. Aber es bedeutete, dass eine Änderung an der Report-Logik in jeder Jahreskopie einzeln nachgezogen werden musste, und dass niemand mehr sicher sagen konnte, welche Kopie die aktuelle Wahrheit enthielt.
Dazu kamen die üblichen Merkmale der Zeit: mysql_*-Aufrufe überall, Deployment durch Kopieren der Dateien ans Ziel, und Feature-Flags als PHP-Konstanten in den Dateien selbst. Auf die Flags komme ich zurück.
Warum nicht neu
Die Frage stellte sich nicht. Die Anwendung lief, die Nutzer kannten sie, und die Logik darin bildete Abläufe ab, die niemand aufgeschrieben hatte. Ein Neubau hätte diese Abläufe aus dem Gedächtnis der Anwender rekonstruieren müssen. Das ist bei einem System mit 100 Dateien schon eine Wette, und bei einem Bank-Dienstleister, wo jede Abweichung erklärt werden muss, keine gute.
Erster Schritt: Struktur statt Kopien
Bevor ich an die PHP-Version ging, habe ich die Kopien zusammengeführt. Die Jahres-Reports wurden zu einer Funktion mit dem Jahr als Parameter. Gemeinsame Abläufe, die in einem Dutzend Dateien identisch standen, wurden zu Klassen. Das hat die Zahl der PHP-Dateien deutlich reduziert und, wichtiger, die Zahl der Stellen, an denen die Migration später etwas ändern musste.
Das ist der Teil, den ich heute in jedem Befund zuerst anschaue: Wie viel des Codes ist Kopie? Denn jede Kopie ist eine Bruchstelle, die beim Versionssprung einzeln gefunden und einzeln repariert werden muss. Wer die Kopien vorher zusammenführt, migriert weniger Code.
Der Sprung: 5.1 direkt auf 7.0
Als PHP 7.0 erschien, bin ich direkt darauf gegangen, ohne Zwischenstufe über 5.6. Bei rund 100 Dateien, die ich zu diesem Zeitpunkt gut kannte, war das vertretbar. Bei einem größeren System, das ich nicht selbst umgebaut habe, empfehle ich heute die Stufen, weil sich sonst die Fehlerquellen mehrerer Versionen überlagern und die Fehlersuche länger dauert als die Zwischenstufe gekostet hätte. Wie das aussieht, steht im Beitrag zur Migration von PHP 5.6 auf 8.
Das größte bekannte Hindernis war mysql_*, das PHP 7 ersatzlos entfernt hat. Ich habe es damals mit einem Drittanbieter-Paket gelöst, das die alten Aufrufe auf die neue Erweiterung umleitet. Das war ein pragmatischer Schritt, kein sauberer: Die Abfragen blieben, wie sie waren, inklusive der manuellen Maskierung. Heute würde ich denselben Schritt als Brücke nehmen und die kritischen Abfragen direkt danach auf Prepared Statements umstellen, weil der Umleitungs-Layer die SQL-Injection-Frage nicht beantwortet. Mehr dazu im Beitrag von mysql_* zu mysqli und PDO.
Was unerwartet gebrochen ist
Die Feature-Flags. Sie lebten als Konstanten in PHP-Dateien, und der Prozess war: Datei ans Ziel kopieren, dann die Konstante von Hand umstellen. Solange dieselbe Person deployte und dieselbe Person die Flags kannte, ging das gut. Bei der Migration, mit vielen Dateien in Bewegung und am Ende einem kompletten Umzug von Server A auf Server B, gingen genau diese Handgriffe verloren. Flags standen nach dem Kopieren auf dem Stand der Quelle, nicht auf dem Stand, den die Zielumgebung brauchte.
Niemand hatte das auf der Liste, weil es kein Code-Problem war. Es war ein Prozess, der in einem Kopf steckte. Seitdem gehört „Wie wird deployt, und was passiert nach dem Kopieren von Hand?“ zu den ersten Fragen, die ich stelle, bevor ich eine Migration plane. Die Antwort steht nie im Code.
Wie die Umschaltung lief
Ohne Ausfall, weil sie kein Termin war, sondern ein Zeitraum. Die Umstellungen kamen über Monate, Datei für Datei, jede für sich lauffähig. Am Ende stand der Umzug von Server A auf Server B als ein großer Schritt, aber zu dem Zeitpunkt war der Code bereits auf dem neuen Stand und getestet. Der Umzug hat nur noch die Umgebung gewechselt, nicht die Anwendung.
Eine Zahl für „das Ergebnis“ habe ich nicht, und ich erfinde keine. Es war ein Langzeitprozess neben dem Tagesgeschäft, und sein Ergebnis war, dass die Anwendung auf einer unterstützten Version lief, weniger Dateien hatte und eine Änderung an einem Report nicht mehr in fünf Kopien nachgezogen werden musste.
Was davon heute noch gilt
Drei Dinge, und sie stehen in dieser Reihenfolge in jedem Befund, den ich schreibe:
- Kopien zusammenführen, bevor migriert wird. Weniger Code, weniger Bruchstellen.
- Den Deployment-Prozess aufschreiben, inklusive der Handgriffe danach. Dort brechen Migrationen, nicht im Code.
- Umleitungs-Pakete für
mysql_*sind eine Brücke. Prepared Statements sind das Ziel, und sie gehören in dieselbe Etappe.
Wenn Ihr System so aussieht, wie das hier beschriebene, ist es fast sicher kein Fall für einen Neubau. Es ist ein Fall für einen Befund und danach für eine Minimal-Migration. Beides hat einen Festpreis, beides steht unten.