Jedes Jahr rufen mich eine Handvoll Teams mit derselben Bitte an. Sie sind auf Symfony 5.4 oder 6.2. Sie wissen, dass sie dort nicht bleiben können. Die letzte Person, die das Upgrade versucht hat, hat das Unternehmen verlassen, ausgebrannt, mittendrin. Der Vorstand beginnt zu fragen, wann das Upgrade ausgeliefert wird, und die Antwort, die alle geben, lautet “nächstes Quartal”. Seit drei Quartalen ist es “nächstes Quartal”.
Das ist kein Symfony-Problem. Es ist ein Deprecation-Schuldenproblem, das Symfony sichtbar macht, nach Fahrplan, alle zwei Jahre. Und die gute Nachricht, wenn Sie sie ertragen können, ist: Das Upgrade selbst ist fast nie der schwere Teil. Das Upgrade ist eine mechanische Operation, die Rector an einem Nachmittag erledigen kann. Schwer ist die Arbeit, die Sie drei Jahre lang übersprungen haben, weil Sie Features ausgeliefert haben, und das Upgrade verlangt sie nun auf einen Schlag zurück.
Dieser Essay handelt davon, wie Sie aufhören, Symfony-Major-Upgrades als Projekte zu behandeln, und damit anfangen, sie als Nebenprodukt kontinuierlicher Hygiene zu behandeln. Wenn Sie die Hygiene richtig machen, hören Upgrades auf, Ereignisse zu sein. Wenn nicht, rettet kein Playbook der Welt das nächste Upgrade vor dem Todesmarsch.
Warum Symfony-Upgrades verschleppt werden
Das Muster ist in jedem Einsatz dasselbe. Es sieht so aus:
- Eine Major-Version erscheint. Ihr Team liest die Release Notes, überfliegt die Deprecations und vereinbart, “das nächsten Sprint anzuschauen”.
- Der nächste Sprint wird zum nächsten Quartal. Deprecation-Warnungen sammeln sich in den Logs und werden dann aus dem Dashboard ausgefiltert, weil sie echte Fehler übertönen.
- Zwei oder drei Jahre vergehen. Das aktuelle LTS ist jetzt zwei Majors vor Ihnen. Eine Dependency hat den Support für Ihre Version eingestellt. Ein Security Advisory betrifft ein Bundle, das Sie nicht upgraden können, weil die neue Version das neue Symfony verlangt.
- Engineering schlägt das Upgrade als eigenständiges Projekt vor. Die Aufwandsschätzung liegt bei sechs Monaten. Der Vorstand fragt, warum die Schätzung sechs Monate beträgt. Engineering kann es nicht so erklären, dass der Vorstand es überzeugend findet.
- Das Upgrade wird nicht finanziert. Das Team umgeht das Problem noch ein Jahr. Der Umfang wächst auf neun Monate.
Ich habe diese Schleife inzwischen vier oder fünf Mal gesehen. Das Team ist nicht inkompetent. Die Aufwandsschätzung ist vermutlich ehrlich. Die Schleife entsteht, weil an jedem Entscheidungspunkt die günstigst aussehende Handlung das Aufschieben ist, und die Kosten des Aufschiebens sind unsichtbar, bis sie es plötzlich nicht mehr sind.
Das Erste, was ich in einem solchen Einsatz tue, ist, die Kosten sichtbar zu machen. Nicht in Personenmonaten (der Vorstand interessiert sich nicht für Personenmonate). Sondern darin, was blockiert ist: “Das SDK des Zahlungsanbieters hat seine minimale Symfony-Version angehoben, und wir können den Security-Patch nicht übernehmen, also betreiben wir einen ungepatchten Webhook-Handler gegen ein acht Monate altes Advisory der Library.” Das ist ein Satz, auf den ein Vorstand reagieren kann.
Das Upgrade ist nicht die Arbeit
Das Zweite, was ich Teams sage: Das Upgrade selbst, eng gefasst, ist ein bis drei Arbeitstage. Also die symfony/*-Constraints in der composer.json anheben, composer update laufen lassen, Rector mit dem Symfony-Set ausführen und den mechanischen Bruch beheben. Rectors Symfony-Sets sind inzwischen so weit, dass die mechanische Migration für die meisten geradlinigen Anwendungen automatisiert ist.
Die Arbeit, die tatsächlich sechs Monate dauert, ist alles drumherum:
- Aufgeschobener Deprecation-Abbau. Jedes
@trigger_error, das Sie stummgeschaltet haben, jedes “das reparieren wir vor 6.0”-TODO, jede “Kompatibilitätsschicht”, die eigentlich temporär sein sollte. All das wird am Upgrade-Tag fällig. - Bundles, die nicht Schritt gehalten haben. Ein langer Schwanz kleiner Bundles mit einem Maintainer, der weitergezogen ist. Jedes einzelne wird zur Entscheidung: upgraden, forken, ersetzen, Feature entfernen.
- Eigene Compiler Passes und Container Extensions, geschrieben gegen interne APIs, die inzwischen refaktoriert wurden. Rector weiß davon nicht immer.
- Tests, die nur durch Deprecation-Nachsicht grün sind. Ein Test, der behauptet, ein Service habe ein bestimmtes Tag, wobei sich das Tag-Format geändert hat. Ein Test, der eine deprecated Assertion-Methode verwendet. Ein Test, der auf einen Request-Flow setzt, der nicht mehr so funktioniert.
- Infrastruktur, die im Gleichschritt mitziehen muss. Ein PHP-Minor-Sprung, der eine Docker-Base-Image-Änderung verlangt, die einen CI-Runner-Update verlangt, der einen neuen OpenSSL-Build verlangt. Jeder einzelne dieser Schritte kann zur eigenen Woche werden.
Der Fehler, den die meisten Teams machen: Sie schätzen das Upgrade nur nach Punkt 1, vielleicht nach Punkt 4, und nehmen an, der Rest sei Rauschen. Der Rest ist kein Rauschen. Der Rest ist das Upgrade.
Die Disziplin des kontinuierlichen Upgrades
Sobald Sie erkennen, dass die Arbeit aus Deprecation-Abbau und Bundle-Hygiene besteht, lautet die Frage nicht mehr “wie machen wir das Upgrade?”, sondern “wie machen wir den Abbau kontinuierlich, damit das Upgrade ein Nicht-Ereignis ist?” Die Antwort ist ein kleines Set an Praktiken, die, wenn Sie sie ehrlich übernehmen, die nächsten fünf Jahre an Upgrades jeweils eine Woche statt jeweils ein Quartal kosten lassen.
1. Jede Deprecation als echten Fehler behandeln
Symfony gibt Ihnen eine reichhaltige Deprecation-Schicht. Sie können daraus Rauschen machen, oder Sie können daraus eine Lint-Regel machen. Wählen Sie Option zwei.
In jeder phpunit.dist.xml, die ich einrichte, konfiguriere ich den Deprecation Handler so, dass der Build bei jeder von Ihrem eigenen Code ausgelösten Deprecation scheitert. Vendor-Deprecations bekommen ein separates Budget, weil Sie sie nicht immer sofort beheben können, aber sie werden geloggt und gezählt.
<phpunit>
<php>
<env name="SYMFONY_DEPRECATIONS_HELPER"
value="max[self]=0&max[direct]=0&quiet[]=indirect&quiet[]=other"/>
</php>
</phpunit>
Die Semantik: null selbst ausgelöste Deprecations, null direkte Deprecations aus Vendors, die Sie aufrufen (das ist Ihr Problem), indirekte Deprecations stumm schalten (das ist das Problem des Library-Autors). Diese Einstellung macht Ihre Testsuite zum Deprecation-Dashboard. Ist CI grün, haben Sie keine Deprecation-Schulden im First-Party-Code. Ist CI rot, wissen Sie genau, was zu beheben ist.
Der psychologische Effekt zählt mehr als der technische. Sobald eine Deprecation den Build bricht, beheben Engineers sie in dem Pull Request, der sie eingeführt hat, statt ein Ticket zu schreiben, das nie jemand aufnimmt. Der Abbau wird kontinuierlich und klein, nicht aufgeschoben und gewaltig.
2. Rector an jedem Pull Request dranhalten
Rector ist das zweite Bein des Hockers. Lassen Sie ihn in CI auf jedem Pull Request laufen, konfiguriert auf die Symfony-Sets Ihrer aktuellen Version plus die nächste. Das bringt zwei Vorteile: Ihr Code-Stil bleibt aktuell, und Sie bekommen Vorwarnung, wenn eine Rector-Regel einen Breaking Change einführen würde, weil das Diff im Pull Request es vor dem Merge zeigt.
Eine minimale rector.php für eine Symfony-App auf dem aktuellen Minor:
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Symfony\Set\SymfonySetList;
use Rector\ValueObject\PhpVersion;
return RectorConfig::configure()
->withPaths([__DIR__.'/src', __DIR__.'/tests'])
->withPhpVersion(PhpVersion::PHP_83)
->withSets([
SymfonySetList::SYMFONY_74,
SymfonySetList::SYMFONY_CODE_QUALITY,
SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
])
->withImportNames(importShortClasses: false);
Wenn Symfony 8.0 erscheint, fügen Sie das 8.0-Set in einem Branch hinzu, lassen es laufen und sehen, was bricht. Dieser Branch wird noch nicht gemerget, aber das Diff ist jetzt konkret. Sie wissen Monate vor dem Release, wie die tatsächliche Migrationsoberfläche Ihrer App aussieht.
3. Bundle-Churn in jeden Sprint einplanen
Das dritte Bein ist das am wenigsten technische und das am häufigsten übersprungene. In jedem Sprint einen kleinen Anteil reservieren (ich empfehle meist 10 Prozent) für Bundle-Updates. Keine Notfall-Security-Patches. Routine-Bumps. Den Output von Composer Audit durchgehen. Die drei Bundles mit dem kleinsten Diff upgraden. Das CHANGELOG von jedem lesen.
Das ist langweilige Arbeit. Junior-Engineers werden sich darüber beschweren. Es ist auch die Arbeit, die die Überraschung “Bundle X unterstützt unsere Symfony-Version nicht mehr” verhindert. Waren Sie schon auf dem aktuellen Minor von Bundle X, ist der nächste Major von Bundle X ein Diff, den Sie reviewen, keine Überraschung, über die Sie in Panik geraten.
Ich habe noch nie ein Team, das das konsequent gemacht hat, ein hartes Symfony-Upgrade erleben sehen. Ich habe viele Teams gesehen, die das nicht gemacht haben und drei Monate damit zubrachten, Bundle-Versionen im Rahmen eines Upgrade-Projekts aufzuholen.
Der Monat-für-Monat-Plan
Wenn Sie das hier lesen und bereits drei Majors hinterher sind, sind die obigen kontinuierlichen Praktiken das Ziel, nicht der Ausgangspunkt. Hier ist der Plan, den ich mit Teams in dieser Situation fahre.
Monat 1: Audit und Deprecation-Freeze
Woche 1 dient dem Messen. Nicht dem Beheben. Nur dem Messen. Sie produzieren drei Artefakte:
- Ein Deprecation-Report. Lassen Sie die aktuelle Testsuite mit dem Deprecation Helper im “Laut”-Modus laufen. Kategorisieren Sie jede Deprecation nach Quelle (eigener Code, direkter Vendor, indirekt), nach Symfony-Version, die sie eingeführt hat, und nach grober Anzahl. Das ist Ihre Baseline.
- Ein Bundle-Inventar. Für jedes Bundle in der
composer.jsonnotieren: installierte Version, neueste Version, Wartungsstatus des Repos, und ob die neueste Version die angestrebte Symfony-Version unterstützt. Markieren Sie farblich die, die es nicht tun. - Ein Rector-Dry-Run. Lassen Sie Rector mit dem Ziel-Symfony-Set und
--dry-runlaufen und inspizieren Sie den Output. Sie achten auf Menge und Form der automatisierten Änderungen. Das setzt die Erwartungen.
Woche 2 beginnt den Freeze. Ab dieser Woche darf keine neue Deprecation mehr eingeführt werden. CI erzwingt das über die Testsuite-Konfiguration von oben. Das repariert die bestehenden Deprecations nicht. Es stoppt nur, dass das Loch tiefer wird, während Sie es flicken.
Wochen 3 und 4 starten den Deprecation-Abbau am oberen Ende der Liste. Die Deprecation-Klasse mit der höchsten Anzahl auswählen und durcharbeiten. Die Aufrufe beheben. Kleine, reviewbare Pull Requests einreichen. Markieren Sie sie mit einem Label wie deprecation-cleanup, damit der Vorstand die Geschwindigkeit sieht.
Monat 2: Normalisierung des Bundle-Baums
Während die Deprecations abnehmen, wenden Sie sich dem Bundle-Inventar zu. Top-down von den schwersten Dependencies (typischerweise api-platform/core, doctrine/doctrine-bundle, in manchen Häusern sonata-project/*) zu den Blättern. Für jedes Bundle:
- Auf die neueste Version anheben, die mit Ihrer aktuellen Symfony-Version kompatibel ist.
- Das CHANGELOG lesen. Jeden durch den Bump eingeführten Bruch beheben.
- In die Produktion ausliefern.
- Zum nächsten Bundle.
Die Faustregel: ein Bundle nach dem anderen, jedes im eigenen Pull Request, jedes separat ausrollbar. Das klingt langsam. Es ist langsam. Bricht aber ein Bundle-Bump etwas in der Produktion, ist der Revert chirurgisch. Haben Sie zehn Bundles in einem Pull Request gebumpt, ist der Revert katastrophal und die Behebung eine Woche Forensik.
Am Ende von Monat zwei ist Ihr Bundle-Baum auf dem aktuellen Stand innerhalb Ihrer Symfony-Version. Ihre Deprecation-Zahl ist um mindestens die Hälfte gesunken. Sie haben das Upgrade noch nicht gemacht. Sie bereiten den Boden.
Monat 3: Das Upgrade selbst
Jetzt heben Sie Symfony an. In einem einzigen Branch:
symfony/*-Constraints in dercomposer.jsonauf die Zielversion aktualisieren.- Die minimale PHP-Version anheben, falls nötig.
composer updateausführen und den Dependency-Baum auflösen.- Rector mit den Ziel-Symfony-Sets ausführen.
- Die vollständige Testsuite laufen lassen. Was bricht, beheben.
- Die Anwendung lokal starten und die wichtigsten Happy Paths manuell durchgehen.
- Auf Staging deployen. Lasttests laufen lassen, falls vorhanden.
- In die Produktion deployen, außerhalb der Kernzeit, mit angekündigtem Deploy-Fenster.
Dieser Schritt dauert ein bis zwei Wochen, wenn die vorherigen beiden Monate richtig gemacht wurden. Wurden die vorherigen beiden Monate übersprungen, dauert dieser Schritt zwei bis drei Monate, und Sie werden es bereuen.
Monat 4: Aufräumen und der erste kontinuierliche Zyklus
Nach dem Upgrade eine kleine Runde Aufräumen: die @deprecated-Punkte, die in der alten Version noch als Rauschen durchgingen, Rector-Regeln speziell für die neue Version, Code-Stil-Abgleich. Zwei oder drei Wochen, eine Handvoll Pull Requests.
Dann werden die kontinuierlichen Praktiken aus dem vorherigen Abschnitt zur Routine. Der nächste Major, zwei Jahre später, ist nun eine Zwei-Wochen-Übung. Der Todesmarsch ist vorbei.
Die Fallen, die Projektpläne auffressen
Ein paar konkrete Fallen, die ich immer wieder im Feld sehe:
PHP und Symfony im gleichen Pull Request upgraden. Tun Sie das nicht. Ein PHP-Minor-Sprung hat seine eigenen Fehlermodi (typisierte Properties, Readonly-Semantik, neue String/Int-Edge-Cases), und sie mit Symfony-Änderungen zu verschränken, verdoppelt Ihre Debug-Fläche. Zuerst PHP upgraden, das ausliefern, eine Woche einwirken lassen, dann Symfony upgraden.
Config-Drift in der FrameworkBundle ignorieren. Zwischen Majors ändert sich die Form der framework.yaml. Die alte Form ist oft deprecated und wird später entfernt. Ein frisches symfony new und das Diffen der generierten Config gegen die eigene bringt eine Menge stiller Deprecations ans Licht, die die Testsuite allein nicht zeigt. Ich mache diesen Durchlauf bei jedem Major.
Darauf vertrauen, dass der Container nach dem Upgrade gleich ist. Wird er nicht sein. Die Autoconfiguration ändert sich, Tagging-Defaults ändern sich, Service-Sichtbarkeit ändert sich. Den Container vorher und nachher dumpen (symfony console debug:container > before.txt, nachher dasselbe) und diffen. Das Diff enthält Überraschungen. Lösen Sie sie auf, bevor Sie ausliefern, nicht in der Produktion.
Asset-Pipelines vergessen. Symfonys Asset-Geschichte hat sich über die letzten Majors verändert (Webpack Encore, AssetMapper, Stimulus-Verschiebungen). War Ihr Frontend auf Encore und Sie wechseln zu AssetMapper, ist das ein separates Projekt mit eigenem Zeitplan. Schmuggeln Sie es nicht ins Symfony-Upgrade. Planen Sie es als Schwesterprojekt.
Den Deprecation Helper als optional behandeln. Er ist das mit Abstand wichtigste Werkzeug in diesem Prozess. Eine Testsuite, die bei Deprecations nicht scheitert, ist eine Lint-Regel, die Sie nicht laufen lassen.
Wann der Plan nicht gilt
Zwei Ausnahmen, beide selten.
Sind Sie mehr als drei Majors hinterher (etwa 4.4 und wollen auf 7.x), gilt die Regel “ein Upgrade nach dem anderen” weiter, aber der Plan läuft für jeden Major Rücken an Rücken. Sie überspringen nichts. Sie gehen von 4.4 auf 5.4, liefern aus, stabilisieren zwei Wochen, gehen von 5.4 auf 6.4, liefern aus, stabilisieren, gehen von 6.4 auf 7.x. Jede Etappe ist die Monat-drei-Arbeit aus dem obigen Plan. Die gesamte Laufzeit liegt bei sechs bis neun Monaten. Das ist tatsächlich ein Projekt; der Rest dieses Essays tut das nicht weg.
Wird die Anwendung innerhalb von zwölf Monaten abgeschaltet, überspringen Sie das Upgrade vollständig. Halten Sie die aktuelle Version auf LTS, übernehmen Sie Security-Backports, und stecken Sie den Engineering-Aufwand in den Nachfolger. Ein System zu upgraden, das kurz vor der Löschung steht, ist ein Ressourcenallokationsfehler, wie befriedigend der Abschluss auch wäre.
Die Kosten des Bleibens
Der Grund, irgendetwas davon zu tun, trotz wie langweilig die Arbeit ist, ist nicht ästhetisch. Es ist, dass das Verharren auf einer alten Symfony-Version kumulierende Kosten hat, die in Planungsgesprächen meist zu niedrig angesetzt werden. Drei konkrete Kosten lohnt es sich, dem Business in dieser Reihenfolge zu nennen:
- Security-Exposure. Jede Woche, in der Sie auf einer nicht mehr oder bald nicht mehr unterstützten Version bleiben, ist eine Woche, in der Sie das Risiko eingehen, dass eine CVE in einer Komponente landet, die Sie nicht patchen können, außer durch ein Upgrade.
- Hiring-Kosten. Kandidaten lesen Ihre Stack-Liste. Symfony 5.4 im Jahr 2026 signalisiert bestimmte Dinge. Die Talente, die Sie einstellen wollen, lesen dieses Signal so, wie Sie es lesen würden, wenn Sie selbst auf Jobsuche wären.
- Zugang zum Bundle-Ökosystem. Die neuesten Bundles, die neuesten Muster, die neuesten API-Platform- und Live-Components-Versionen zielen nur auf das aktuelle Symfony. Jedes Jahr, das Sie zurückbleiben, wächst der Abstand zwischen Ihnen und dem Ökosystem, und die Decke dessen, was Sie mit Ihrer eigenen Codebasis tun können, sinkt.
Keines davon ist an einem beliebigen Dienstag dringend. Alle drei kumulieren wöchentlich. Aufgabe der Upgrade-Praxis ist es, zu verhindern, dass sie jemals dringend werden.
Wenn Sie auf eine Symfony-Version blicken, die zwei oder drei Majors zurückliegt, und den Weg nach vorn planen wollen, startet mein Monolith-Modernisierungseinsatz mit genau diesem Audit. Eine zweiwöchige Diagnose zu Deprecations und Bundle-Gesundheit, ein durchkosteter Plan bis zum aktuellen Stand, und die kontinuierlichen Praktiken so eingerichtet, dass Sie nie wieder zurückfallen.
Referenzen
- Symfony Releases & Maintenance: offizieller Kalender für Minor- und Major-Releases sowie LTS-Support-Fenster.
- Rector: das automatisierte Refactoring-Werkzeug hinter den Symfony-Migration-Sets.
- Symfony PHPUnit Bridge (SYMFONY_DEPRECATIONS_HELPER): Referenz für die Testsuite-Deprecation-Schwellen, die im Abschnitt “jede Deprecation als echten Fehler behandeln” verwendet werden.
- Webpack Encore: Dokumentation zum Legacy-Frontend-Bundler, der in der Asset-Pipelines-Falle genannt wird.
- AssetMapper: Dokumentation zur modernen Importmap-basierten Frontend-Pipeline, die Encore in vielen Projekten ablöst.