Symfony hat für Upgrades einen Weg vorgesehen, der besser ist als der der meisten Frameworks: Was in der nächsten Hauptversion wegfällt, meldet sich in der aktuellen vorher als Deprecation. Wer alle Meldungen beseitigt hat, kann die Hauptversion wechseln.
So steht es in der Anleitung, und so funktioniert es auch. Der Grund, warum Symfony-Upgrades trotzdem ins Stocken geraten, liegt woanders: Der erste Lauf auf einem gewachsenen Projekt bringt vier- bis fünfstellige Zahlen von Meldungen, die meisten davon aus Bibliotheken, und niemand weiß, wo er anfangen soll.
Dieser Artikel beschreibt, wie man aus dieser Liste eine Reihenfolge macht, und was der Deprecation-Layer nicht meldet. Der zweite Teil ist der wichtigere, weil dort die Aufwände liegen, die Angebote sprengen.
Was der Deprecation-Layer leistet
Die Zusage von Symfony ist eng und deshalb belastbar: Innerhalb einer Hauptversion bleibt die öffentliche Schnittstelle stabil. Was in der nächsten Hauptversion verschwindet, wird vorher als veraltet markiert und meldet sich zur Laufzeit.
Daraus folgt der Weg, der für jedes Symfony-Upgrade gilt: Zuerst auf die letzte Nebenversion der aktuellen Hauptversion, dort alle Meldungen beseitigen, dann die Hauptversion wechseln. Wer versucht, von 4.4 direkt auf 6.4 zu gehen, verzichtet auf genau die Warnungen, für die der Weg gebaut ist.
Die Meldungen sammelt man nicht im Browser, sondern in der Testsuite oder, wenn es keine gibt, in einem Durchlauf über die wichtigsten Seiten. Wichtig ist nur, dass der Lauf reproduzierbar ist, sonst lässt sich der Fortschritt nicht messen.
# Meldungen sammeln, aber den Lauf nicht daran scheitern lassen
SYMFONY_DEPRECATIONS_HELPER='disabled=1' \
vendor/bin/phpunit --log-junit=/dev/null 2>deprecations.log
wc -l deprecations.logDie Zahl am Ende ist die Ausgangsgröße. Sie ist immer erschreckend und sie ist immer irreführend, weil dieselbe Meldung tausendfach auftaucht.
Das Log lesbar machen
Der erste nützliche Schritt ist nicht Arbeit am Code, sondern eine Auswertung. Aus zwölftausend Zeilen werden so regelmäßig vierzig verschiedene Meldungen, und aus vierzig wird eine Liste, die sich planen lässt.
# Gleiche Meldungen zusammenfassen, häufigste zuerst
sed -E 's/"[^"]*"/"X"/g; s/[0-9]+/N/g' deprecations.log \
| sort | uniq -c | sort -rn | head -40Das Ersetzen von Zeichenketten und Zahlen ist der Trick daran: Ohne das zählt jede Meldung als eigene, weil ein Klassenname oder eine Zeilennummer darin steht.
Interessanter als die Häufigkeit ist die Herkunft. Die Meldung nennt fast immer die aufrufende Stelle, und die entscheidet, wer die Arbeit macht:
grep -oE '(src|vendor)/[a-zA-Z0-9_./-]+' deprecations.log \
| cut -d/ -f1-3 | sort | uniq -c | sort -rn | head -20In der Praxis stehen danach achtzig Prozent der Meldungen unter vendor/, und das ist eine gute Nachricht: Für den Großteil der Arbeit reicht ein Versionssprung eines Pakets.
Drei Sorten Meldung, drei Antworten
Jede Zeile im Log fällt in eine von drei Kategorien, und die Verwechslung dieser Kategorien ist der Grund, warum Aufwände falsch geschätzt werden.
Erstens: eigener Code. Eine Stelle unter src/ ruft etwas Veraltetes auf. Das ist die Sorte, die man sich vorstellt, wenn man an ein Upgrade denkt, und sie ist meist der kleinste Teil. Die Anleitung nennt den Ersatz, die Änderung ist lokal.
Zweitens: ein Paket, das nachziehen muss. Der Aufruf steht unter vendor/. Sie ändern hier nichts am eigenen Code, sondern heben die Version des Pakets an. Der Aufwand liegt nicht im Beheben, sondern im Ordnen: Welche Version des Pakets verträgt sich mit welcher Symfony-Version, und welche zieht wieder andere Pakete mit.
Drittens: ein Paket, das nicht mehr nachzieht. Das ist der teure Fall. Das Paket ist aufgegeben, die letzte Fassung unterstützt die neue Hauptversion nicht, und es gibt keinen Nachfolger. Jeder dieser Funde ist eine eigene Entscheidung: ersetzen, selbst übernehmen, oder die Funktion aufgeben.
Diese dritte Sorte gehört als Erstes gesucht, nicht als Letztes. Sie bestimmt, ob das Upgrade drei Wochen oder drei Monate dauert, und sie steht nicht in der Meldungszahl:
composer outdated --direct --format=json \
| php -r '$d=json_decode(file_get_contents("php://stdin"),true);
foreach($d["installed"] as $p) if(!empty($p["abandoned"]))
echo $p["name"], " -> ", is_string($p["abandoned"]) ? $p["abandoned"] : "kein Nachfolger", PHP_EOL;'Dass dieser Sprung überhaupt zu einem Termin wird und nicht zum Dauerthema, entscheidet ein Kalender über den ganzen Stapel: Ein EOL-Kalender für den ganzen Stack.
Die Reihenfolge: von unten nach oben
Mit den drei Kategorien ergibt sich eine Reihenfolge, die den Aufwand deutlich senkt, weil sie doppelte Arbeit vermeidet.
- Aufgegebene Pakete klären. Erst wenn feststeht, was aus ihnen wird, ist der Umfang bekannt. Ein Ersatz ändert oft mehr Code als alle Deprecations zusammen.
- Pakete anheben, eines nach dem anderen. Nach jedem Sprung das Log neu erzeugen. Die Zahl fällt in Stufen, und jeder Schritt ist einzeln zurückdrehbar.
- Erst danach den eigenen Code. Was jetzt noch übrig ist, ist wirklich Ihres. Wer damit anfängt, behebt Meldungen, die ein Paketsprung sowieso erledigt hätte.
- Die Hauptversion wechseln, wenn das Log leer ist und nicht vorher.
Der zweite Punkt ist der, an dem Geduld sich auszahlt. Alle Pakete gleichzeitig anzuheben führt zu einem Zustand, in dem etwas kaputt ist und niemand weiß, welcher der vierzehn Sprünge es war.
Welche Pakete überhaupt installiert sind und welche davon von Hand verändert wurden, klärt Composer als Archäologie.
Was der Deprecation-Layer nicht meldet
Und hier liegt der Teil, wegen dem Symfony-Upgrades trotz sauberem Log länger dauern als geplant. Drei Umbauten melden sich nicht, weil sie nicht in Aufrufen stecken, sondern in Struktur.
Die Projektstruktur. Ein Projekt, das vor Symfony Flex angelegt wurde, hat ein anderes Verzeichnislayout, eine andere Konfigurationsablage und eine andere Art, Bundles zu registrieren. Nichts davon ist veraltet im Sinne des Layers, es ist nur nicht das, womit die heutige Anleitung rechnet. Der Umbau ist mechanisch und kostet trotzdem Tage.
Die Dienstdefinitionen. Der Wechsel von ausdrücklich konfigurierten Diensten zu Autowiring und Autoconfiguration ist kein Deprecation, sondern eine andere Denkweise. Man kann ihn aufschieben, und viele tun das. Die Rechnung kommt beim übernächsten Upgrade, wenn die Anleitung nur noch den heutigen Weg beschreibt.
Die Sicherheitsschicht. Der Umbau des Security-Bundles ist der Punkt, an dem die meisten Projekte stehen bleiben. Authentikatoren, Firewalls, Zugriffsregeln: Das ist kein Suchen und Ersetzen, sondern ein Neuschreiben eines Teils, den niemand gern anfasst, weil ein Fehler dort sofort sicherheitsrelevant ist.
Für die Schätzung heißt das: Die Meldungszahl beschreibt den kleineren Teil der Arbeit. Wer ein Angebot allein darauf stützt, liegt vorhersehbar daneben.
Rector, und wo er aufhört
Für die mechanischen Teile gibt es Regelsätze, die den Code automatisch umschreiben. Für Symfony sind sie nach Hauptversionen geschnitten, und sie erledigen einen großen Teil der ersten Kategorie.
// rector.php
return RectorConfig::configure()
->withPaths([__DIR__ . '/src'])
->withSets([
SymfonySetList::SYMFONY_54,
SymfonySetList::SYMFONY_60,
SymfonySetList::SYMFONY_CODE_QUALITY,
]);Zwei Regeln dazu, beide aus Erfahrung.
Ein Regelsatz je Durchlauf, ein Commit je Regelsatz. Wer alle Sätze auf einmal laufen lässt, bekommt einen Diff über achthundert Dateien, den niemand liest. Getrennt bleibt jeder Schritt nachvollziehbar, und der eine, der etwas kaputt macht, ist auffindbar.
Der Diff wird gelesen, nicht überflogen. Rector schreibt korrekt um, was er versteht. Bei Code, der zur Laufzeit Klassennamen zusammensetzt oder magische Methoden benutzt, versteht er weniger, als er zu verstehen glaubt. Genau dort passieren die Änderungen, die erst in Produktion auffallen.
Was Rector nicht anfasst, ist der ganze Abschnitt darüber: Struktur, Dienste, Sicherheit. Das bleibt Handarbeit, und es ist der Teil, der die Laufzeit des Vorhabens bestimmt.
Wie das Werkzeug im Bestand zugeschnitten wird, ohne bei viertausend geänderten Dateien zu enden, steht in Rector im Altprojekt.
Warum Sie keine Hauptversion überspringen
Die Frage kommt in jedem zweiten Projekt: Wenn wir sowieso auf 4.4 stehen und 7 das Ziel ist, können wir die Zwischenschritte nicht auslassen?
Technisch ja, praktisch nein, und der Grund ist der Layer selbst. Jede Hauptversion meldet nur die Änderungen zur nächsten. Wer zwei Schritte auf einmal geht, hat für den zweiten keine Warnungen und findet die Stellen dort, wo niemand sie finden will.
Dazu kommt ein wirtschaftliches Argument, das schwerer wiegt als das technische: Die Zwischenschritte sind einzeln ausrollbar. Ein Projekt, das nach drei Wochen auf 5.4 steht und produktiv läuft, hat einen Wert gesichert, auch wenn danach ein Quartal lang nichts weitergeht. Ein Projekt, das nach drei Monaten „fast auf 7" ist, hat nichts.
Dieselbe Logik gilt für die PHP-Version darunter: Sie hat ihren eigenen Takt und ihre eigenen Brüche, und die beiden Sprünge gehören nicht in dieselbe Auslieferung. Was beim PHP-Sprung wirklich auf Sie zukommt, steht in einem eigenen Artikel.
Wenn Sie den Weg nicht selbst gehen wollen: wie ein Symfony-Upgrade bei mir abläuft, steht auf einer eigenen Seite, inklusive des Teils, in dem Rector läuft und danach gemessen wird, was übrig bleibt.
Dieser Artikel gehört zu einer Reihe über Systeme, die es schon gibt. Der Rückblick ordnet alle Artikel der Reihe nach Anlass.

