Symfony Migration: Von Version 5 auf 7 ohne Code Freeze

#Symfony 5 auf 7 Migration
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Symfony 5.4 hat im November 2025 das Ende des Community-Security-Supports erreicht. Eine Anwendung, die heute noch darauf läuft, sitzt auf einem ungepatchten Framework, und jeder Monat Warten lässt den Berg an Deprecations wachsen, den das spätere Upgrade abtragen muss. Die beruhigende Nachricht: Eine Symfony 5 auf 7 Migration ist ein deutlich kleineres Projekt, als der Versionssprung vermuten lässt. Nichts zwischen 5.4 und 7.x ähnelt einem architektonischen Rewrite. Was dich von einem aktuellen Framework trennt, ist eine lange Liste von Deprecations plus eine PHP-Versionsanforderung, und beides lässt sich abarbeiten, während das Team weiter Features liefert.

Dieser Guide behandelt speziell den Weg von 5.4. Wenn deine Anwendung auf Symfony 2 oder 3 sitzt, ist der Pfad länger und von anderer Art; wir haben ihn in einer separaten mehrstufigen Migrationsstrategie beschrieben.

Was eine Symfony 5 auf 7 Migration tatsächlich beinhaltet

Symfony-Majors folgen einem strikten Deprecation-Vertrag. Symfony 6.0 ist 5.4 ohne den als deprecated markierten Code, und 7.0 ist 6.4 ohne den deprecated Code. An keiner der beiden Grenzen gibt es neue Konzepte zu lernen. Wenn deine 5.4-Codebasis null Deprecation-Warnungen auslöst, ist der Sprung auf 6.4 fast nur eine Änderung am Composer-Constraint. Dasselbe gilt erneut von 6.4 auf 7.x.

Das macht aus der gesamten Symfony 5 auf 7 Migration einen Plan, den du auf ein Board hängen kannst: Deprecations auf 5.4 aufräumen, Sprung auf 6.4 LTS, die 6.x-Deprecations aufräumen, Sprung auf 7.x. Die Zwischen-Minors zu überspringen ist unterstützt und normal; von 5.4 direkt auf 6.4 ist der Standardweg. Das sinnvolle Ziel im Jahr 2026 ist Symfony 7.4, das aktuelle LTS. Symfony 8.0 existiert ebenfalls, ist aber 7.4 minus deprecated Code, sodass eine saubere 7.4-Codebasis den nächsten Major zur selben Routine macht statt zu einem weiteren Rettungsprojekt.

Teams, die von Symfony 2 oder 3 kamen, mussten eine ersetzte Security-Komponente, den Wechsel zu Flex und eine neue Verzeichnisstruktur verdauen. Von 5.4 aus überspringst du all das. Die Arbeit ist real, aber sie ist Hausmeisterarbeit, keine Strukturarbeit.

Kläre die PHP-Anforderung, bevor du das Framework anfasst

Symfony 6.4 verlangt mindestens PHP 8.1, Symfony 7 mindestens PHP 8.2. Viele 5.4-Anwendungen laufen noch auf PHP 7.4 oder 8.0, und das ist der erste Blocker, den du beseitigen musst, denn Framework und Sprache sollten sich niemals im selben Deploy bewegen.

Symfony 5.4 läuft in seinen aktuellen Patch-Releases problemlos auf PHP 8.2 und 8.3, die Reihenfolge ist also klar: Erst PHP auf der Framework-Version upgraden, der du bereits vertraust, in Produktion setzen lassen, dann die Framework-Arbeit beginnen, wenn die Sprachanforderung vom Tisch ist. Geh direkt auf 8.2 oder 8.3 statt bei 8.1 anzuhalten, damit die Anforderung an der 7.0-Grenze nicht zurückkommt. Die Sprachseite haben wir in unserem Guide zum Upgrade von PHP 7.4 auf 8.3 beschrieben; die Kurzfassung: Eine Composer-Platform-Config und eine CI-Versionsmatrix erledigen den Großteil der Risikoreduktion.

Schalte das Deprecation-Log in Produktion ein

Deprecated Code auf 5.4 funktioniert genau wie zuvor. Das macht eine Migration ohne Freeze möglich, bedeutet aber auch, dass du Sichtbarkeit darüber brauchst, was deine Anwendung tatsächlich aufruft. Testsuites decken nur die Pfade ab, an die du beim Testen gedacht hast. Produktionstraffic findet den Rest.

Symfony leitet jede Deprecation in einen eigenen deprecation-Log-Channel. Gib ihm in Produktion einen Handler:

# config/packages/prod/monolog.yaml
monolog:
    channels: ['deprecation']
    handlers:
        deprecation:
            type: stream
            channels: [deprecation]
            path: '%kernel.logs_dir%/deprecations.log'

Eine Woche echter Traffic liefert dir ein Inventar, mit Aufrufzahlen, von allem, was in 6.0 brechen wird. Ergänze bin/console debug:container --deprecations für die Container-Seite und binde die PHPUnit Bridge mit SYMFONY_DEPRECATIONS_HELPER auf eine Baseline gesetzt in die CI ein, damit die Zahl nur noch sinken kann. Das Inventar teilt sich in Deprecations in deinem eigenen Code, die du direkt behebst, und Deprecations aus Vendor-Paketen, die du behebst, indem du das auslösende Bundle upgradest.

Behebe Deprecations kontinuierlich statt im Big Bang

Das ist es, was den Code Freeze unnötig macht. Jeder Deprecation-Fix ist gültiger 5.4-Code, also kann jeder ein eigener kleiner PR sein, normal reviewt und mit dem nächsten regulären Release deployt. Die Migration braucht nie einen langlebigen Branch, der von main wegdriftet, während alle anderen weiter mergen.

Feature-Arbeit und Deprecation-Arbeit verzahnen sich im selben Trunk, und die Deprecation-Zahl wird zu einer Kennzahl, die das Team über ein paar Sprints gegen null laufen sieht. Ein Budget, das in unseren Projekten funktioniert hat: Jeder Entwickler räumt neben den normalen Tickets ein paar Deprecations pro Woche ab, und eine Person besitzt die Trendlinie, damit sie nicht stillschweigend stehen bleibt.

Wenn das Produktionslog eine Woche lang ruhig bleibt und die CI null neue Deprecations aus eigenem Code erzwingt, ist der Versionssprung selbst ein kleiner, langweiliger PR: extra.symfony.require in der composer.json von 5.4.* auf 6.4.* heben, composer update "symfony/*" ausführen, beheben, was das Aufräumen übersehen hat. Langweilig ist das Ziel.

Lass Rector die mechanische Arbeit machen

Ein großer Teil der Deprecation-Fixes ist mechanisch: geänderte Methodensignaturen, umbenannte Klassen, verschobene Namespaces, Annotations, die zu Attributes werden. Das ist Rector-Territorium. Rector liefert Regelsets für die Grenzen Symfony 5 auf 6 und 6 auf 7, plus Sets, die Doctrine- und Routing-Annotations in native PHP-Attributes umwandeln. Sie auszuführen verwandelt Wochen Tipparbeit in ein oder zwei Tage Diff-Review.

Zwei Gewohnheiten halten Rector nützlich statt laut. Führe ein Regelset nach dem anderen aus und committe jeden Lauf separat, damit ein überraschender Diff leicht zu bisecten ist. Und behandle die Ausgabe als Entwurf: Rector aktualisiert Signaturen zuverlässig, kann aber keine Designfragen beantworten, etwa wie dein eigener Guard Authenticator als moderner Authenticator aussehen sollte. Setup und Regelauswahl haben wir in Rector für Legacy-PHP behandelt, und derselbe Workflow gilt hier unverändert.

Die Konfigurations- und Flex-Recipe-Updates, die du nicht überspringen kannst

Code ist nur die Hälfte der Migration. Die andere Hälfte lebt in config/, und das ist die Hälfte, die Teams überrascht, die noch mit Defaults laufen, die Flex-Recipes 2020 geschrieben haben.

Flex kann diese Dateien für dich aktualisieren. composer recipes listet jedes Recipe mit einer neueren Version, und composer recipes:update wendet die Änderungen als Diff an, den du wie jeden anderen PR reviewst. Erwarte die größten Diffs in zwei Dateien.

In security.yaml wurde der Abschnitt encoders in 5.3 zu password_hashers, Guard Authenticators sind in 6.0 weg, und enable_authenticator_manager: true muss auf 5.4 gesetzt sein, um in das neue Security-System einzusteigen. Die Option verschwindet in 7.0 wieder, weil das neue System das einzige verbleibende ist. Wenn du noch eigene Guard Authenticators hast, ist ihr Umschreiben gegen AbstractAuthenticator das eine Stück echter Designarbeit in der gesamten Migration; plane es also früh statt zuletzt.

In framework.yaml braucht http_method_override einen expliziten Wert, die Session-Konfiguration hat ihre Form geändert, und der Abschnitt annotations ist in 7.0 entfernt, weil Attribute-Support doctrine/annotations im gesamten Framework ersetzt hat. Routen, die als @Route-Annotations geschrieben sind, müssen vor 7.0 zu #[Route]-Attributes werden. Rector konvertiert sie im Ganzen, und dasselbe gilt für Doctrine-Mapping-Annotations.

Drittanbieter-Bundles bestimmen deinen kritischen Pfad

Dein eigener Code folgt einem vorhersehbaren Deprecation-Vertrag. Dein Vendor-Verzeichnis nicht, und in den meisten 5.4-Anwendungen liefert das Bundle-Audit die einzigen wirklich schlechten Nachrichten. Führe in Woche eins composer why-not symfony/framework-bundle 6.4 aus; die Ausgabe ist die ehrliche Liste dessen, was den Sprung blockiert.

Die üblichen Verdächtigen aus der 5.4-Ära sind bekannt. SensioFrameworkExtraBundle ist aufgegeben, und seine Features sind ins Framework selbst gewandert, also wird @ParamConverter zu #[MapEntity] und @IsGranted zum Core-Attribute #[IsGranted]. EasyAdmin 3 unterstützt Symfony 6 nicht, das Admin-Panel braucht also den Umzug auf EasyAdmin 4, was aufwändiger ist als ein Constraint-Bump und ein eigenes Ticket verdient. API Platform 2.6 endet ebenfalls bei Symfony 5.4; der unterstützte Weg führt über 2.7, womit du die Resource-Metadaten von Version 3 noch auf dem alten Major übernehmen kannst, bevor du auf 3.x landest. FOSRestBundle ist im Wartungsmodus, und viele Teams ersetzen es während dieser Migration durch schlichte Controller oder API Platform, statt es weiterzutragen. Alte JWT- und OAuth-Bundles, die auf Guard aufbauen, brauchen ihre aktuellen Authenticator-basierten Majors.

Upgrade jedes davon als isolierten PR noch auf 5.4, wo immer das Bundle beide Majors unterstützt. Jedes Bundle, das du vor dem Sprung modernisierst, verkleinert den Sprung.

Ein realistischer Zeitplan für eine mittelgroße 5.4-Anwendung

Als konkreter Anker: eine Anwendung mit rund 120.000 Zeilen, ein Team von fünf, das die ganze Zeit weiter Features liefert, Testabdeckung gut genug, um ihr zu vertrauen.

PhaseKalenderzeitWas passiert
PHP auf 8.2 oder 8.31 bis 2 WochenPlatform-Bump, CI-Matrix, deployt auf Symfony 5.4
Deprecation-Aufräumen auf 5.43 bis 5 WochenKleine PRs, Rector-Regelsets, Bundle-Upgrades parallel
Sprung auf 6.4 LTS2 bis 4 TageConstraint-Bump, composer update "symfony/*", Recipe-Updates
Aufräumen auf 6.42 bis 3 WochenFrisches Deprecation-Inventar, überall Attributes, Guard-Reste
Sprung auf 7.x2 bis 4 TageConstraint-Bump, Recipe-Updates, letzte Config-Entfernungen

Rechne mit acht bis zwölf Kalenderwochen bei Teilzeit-Intensität, mit null Wochen eingefrorener Feature-Arbeit. Das größte Terminrisiko ist ein spät entdecktes blockierendes Bundle, weshalb das composer why-not-Audit in Woche eins gehört, nicht in Woche sechs.

Wenn das Deprecation-Inventar erdrückend wirkt oder niemand die Kapazität hat, die Trendlinie zu besitzen, ist das eine Projektform, die wir aus der Arbeit in Legacy-Code-Optimierung gut kennen: zuerst eine Bewertung deines tatsächlichen Deprecation-Logs und deiner Bundle-Liste, im selben Geist wie unsere Audits im Code Quality Consulting, dann ein fester Plan, dann Umsetzung neben deinem Team, während die Releases weiterlaufen. Eine kurze Beschreibung deines Setups an hello@wolf-tech.io reicht für eine erste Einschätzung des Aufwands, und mehr Hintergrund zu unserer Arbeitsweise findest du auf wolf-tech.io.