Upgrade von Symfony 6.4 auf 7.4: Deprecations, Return Types und der Sprung
Wie eine 6.4-Anwendung in einem Release auf 7.4 LTS kommt: Deprecations mit der PHPUnit-Bridge aufspüren, Return Types patchen, was 7.0 entfernt hat, und das composer update.
Veröffentlicht 2026-09-12. Geschrieben für Teams, die das Upgrade selbst machen; wer es lieber machen lässt, findet hier Upgrades zum Festpreis. Auch verfügbar auf English.
Was 7.0 tatsächlich geändert hat
Symfony 7.0 ist 6.4 ohne die deprecated Code-Pfade. Es gibt keine neue Architektur zu lernen. Jede Entfernung wurde in einem 6.x-Release als Deprecation angekündigt, also wechselt eine 6.4-Anwendung, deren Testlauf null Deprecations meldet, mit einem Composer-Befehl auf 7.x. Diese Aussage ist präzise: Die Arbeit ist nicht "auf 7 upgraden", sondern "6.4 auf null Deprecations bringen". 7.4 ist das LTS-Ziel (Bugfixes bis November 2028) und setzt PHP 8.2 oder neuer voraus.
Schritt 1: PHP 8.2, besser 8.3 oder 8.4
Symfony 6.4 läuft auf PHP 8.1 bis 8.5, also zuerst PHP anheben, auf 6.4, in einem eigenen PR. Die PHP-Änderung, die den meisten Code berührt, ist die Deprecation impliziter Nullable-Parameter in 8.4:
// deprecated in PHP 8.4
public function __construct(LoggerInterface $logger = null) {}
// korrekt
public function __construct(?LoggerInterface $logger = null) {}
Rectors LevelSetList::UP_TO_PHP_84 (oder withPhpSets()) schreibt diese um. Dynamische Properties (deprecated in 8.2) tauchen im Testlauf als "Creation of dynamic property" auf; deklarieren Sie die Property, sonst tut die Klasse nicht, was Sie denken.
Schritt 2: der Deprecation-Report ist die Checkliste
# phpunit.xml.dist
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&max[direct]=0&max[indirect]=999999"/>
Suite ausführen. Alles unter "direct" ist Ihr Code, der etwas aufruft, das 7.0 entfernt. Häufige Einträge in einer 6.4-Codebasis:
| Deprecation | Ersatz |
|---|---|
Symfony\Component\Security\Core\Security | Symfony\Bundle\SecurityBundle\Security (gleiche Methoden, in 6.2 umgezogen) |
Request::getContentType() | Request::getContentTypeFormat() |
Annotationsbasiertes Routing (type: annotation) | type: attribute, #[Route] |
Symfony\Component\Serializer\Annotation\* | Symfony\Component\Serializer\Attribute\* (Groups, Ignore, MaxDepth, SerializedName) |
Symfony\Component\Validator\Constraints\* als Annotations | Dieselben Klassen als Attributes: #[Assert\NotBlank] |
Symfony\Component\HttpKernel\Event\KernelEvent::isMasterRequest() | isMainRequest() |
Doctrine\Common\Annotations\AnnotationRegistry::registerLoader() | Löschen; mit Attributes braucht es niemand |
Form getBlockPrefix() ohne Return Type, DataTransformerInterface ohne Typen | Return- und Parametertypen ergänzen |
framework.http_method_override: true stillschweigend angenommen | Explizit setzen; der Default hat sich geändert |
UploadedFile::getClientSize(), Response::create(), Yaml::PARSE_KEYS_AS_STRINGS … | Bereits in 6.0 entfernt; wer das sieht, ist nicht auf 6.4 |
Der Bereich "indirect" ist Drittcode, der gegen anderen Drittcode deprecated. Er blockiert 7.4 nicht von sich aus, aber jeder Eintrag zeigt auf ein Bundle, das seine nächste Major-Version braucht.
Schritt 3: Rector, noch einmal
// rector.php
return RectorConfig::configure()
->withPaths([__DIR__.'/src', __DIR__.'/tests'])
->withPhpSets(php84: true)
->withSets([
SymfonySetList::SYMFONY_70,
SymfonySetList::SYMFONY_71,
SymfonySetList::SYMFONY_72,
SymfonySetList::SYMFONY_73,
SymfonySetList::SYMFONY_74,
SymfonySetList::SYMFONY_CODE_QUALITY,
SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
])
->withSymfonyContainerXml(__DIR__.'/var/cache/dev/App_KernelDevDebugContainer.xml');
Die 7.x-Sets sind klein im Vergleich zu den 6.x-Sets. Der Großteil des Diffs wird der Serializer-Attribute-Namespace, der Umzug der Security-Klasse und ein paar umbenannte Konstanten sein. Mit --dry-run ausführen, anwenden, testen, als eigenen PR committen.
Schritt 4: Return Types, letzter Aufruf
Falls das Projekt patch-type-declarations beim 6.x-Upgrade übersprungen hat, jetzt nachholen. In 7.0 stehen die Typen wirklich auf den Interfaces, und ein fehlender Return Type in einer Implementierung ist beim Kompilieren des Containers ein Fatal Error, keine Deprecation:
SYMFONY_PATCH_TYPE_DECLARATIONS='force=2&php=8.2' vendor/bin/patch-type-declarations
bin/console lint:container
Schritt 5: Abhängigkeiten, die bei 6.4 deckeln
Ein Dry-Run-Update zeigt, wer sich weigert:
composer config extra.symfony.require "7.4.*"
composer update "symfony/*" --with-all-dependencies --dry-run
Composer nennt das Paket, das symfony/* auf ^6.x festhält. Die üblichen Verdächtigen und was zu tun ist:
- doctrine/doctrine-bundle 2.x unterstützt 7.x ab 2.11; DoctrineBundle 3 braucht ORM 3. Siehe den ORM-Leitfaden.
- api-platform/core 3.2+ unterstützt 7.x; 4.x ist, wo Sie hinwollen. Siehe API Platform 3 auf 4.
- lexik/jwt-authentication-bundle 3.x, knplabs/knp-paginator-bundle 6.x, nelmio/cors-bundle 2.4+, easycorp/easyadmin-bundle 4.8+ (5.x für 8.x-Support). Die Kompatibilitätsseiten listen das erste Release jedes Bundles, das 7.4 erlaubt.
- Alles Verwaiste (SensioFrameworkExtra, Guard, Swiftmailer-Bundle) hat kein 7.x-Release und muss vor dem Sprung raus. Der Leitfaden zu verwaisten Bundles behandelt jedes einzeln.
- Ihre eigenen privaten Packages, die
symfony/http-kernel: ^6.0in ihrer composer.json festhalten. Auf^6.4 || ^7.0erweitern und zuerst taggen.
Schritt 6: der Sprung
composer update "symfony/*" --with-all-dependencies
composer recipes:update
bin/console cache:clear
bin/console lint:container && bin/console lint:twig templates && bin/console lint:yaml config
bin/console debug:router | head # Plausibilitätscheck
vendor/bin/phpunit
Punkte nach dem Sprung, die keine Deprecations sind und deshalb nie im Report auftauchten:
config/packages/*.yaml: Die Recipes für 7.x haben ein paar Schlüssel verschoben;recipes:updatezeigt den Diff pro Paket. Übernehmen, dann eigene Werte wieder eintragen.Twig: 7.x zieht Twig 3.10+, das{% filter %}zugunsten von{% apply %}undspacelessdeprecated.lint:twigmeldet sie.symfony/phpunit-bridgeund PHPUnit: 7.4 funktioniert mit PHPUnit 9 bis 13; wer auf PHPUnit 12+ wechselt, macht auch dort Annotations zu Attributes (RectorsPHPUnitSetListübernimmt das).- Messenger-Transports: Das Konfigurationsformat für den
failed-Transport und die Retry-Strategie hat sich nicht geändert; die Doctrine-Transport-Tabelle bekommt einen zusätzlichen Index in einer Migration, mitdoctrine:migrations:differzeugen.
Nach 7.4: die Option auf 8.x offenhalten
Symfony 8.0 ist zusammen mit 7.4 erschienen und entfernt wiederum nur, was 7.4 deprecated. Behalten Sie max[self]=0 in der Bridge-Konfiguration und beheben Sie neue Deprecations, sobald 7.x-Minors erscheinen. 8.x wird dann ein Versionssprung, wenn ein Bundle danach verlangt, kein Projekt. Standard-Releases wie 8.x erhalten nur acht Monate Fixes; für eine Anwendung, die Sie nicht monatlich anfassen, ist 7.4 LTS die vernünftige Wahl.
Reihenfolge der Pull Requests
- PHP 8.3 oder 8.4 auf 6.4
- Rector-7.x-Sets und PHP-Sets
- Return Types gepatcht, direkte Deprecations auf null
- Bundle-Majors, die 7.x blockieren (jeweils eigener PR, wo sich die API geändert hat)
- Composer-Sprung auf 7.4, Recipes, Lint, Tests
Für eine saubere 6.4-Codebasis sind das typischerweise ein bis zwei Wochen konzentrierte Arbeit, woraus der Festpreis von €3.500 bis €6.000 entsteht. Was ihn ans obere Ende schiebt: ORM 2, API Platform 3 und eine dünne Testsuite.
Finden Sie heraus, welche Pakete Sie an 6.4 binden
Der Scan liest Ihre composer.lock und zeigt pro Paket, ob das installierte und das neueste Release Symfony 7.4 erlauben, plus die Cliffs (ORM 2, API Platform 3, PHPUnit unter 12), die den Sprung zu einem Projekt machen.
Weitere Leitfäden
- Upgrade von Symfony 5.4 auf 6.4: der vollständige Leitfaden
- API Platform 2.6 auf 3: Resources, Data Provider und Persister migrieren
- API Platform 3 auf 4: der State-basierte Ablauf, RFC-7807-Fehler und Hydra-Änderungen
- Upgrade von Doctrine ORM 2 auf 3 (und DBAL 3 auf 4) in einer Symfony-Anwendung
- Verwaiste Symfony-Bundles ersetzen: SensioFrameworkExtra, Guard, Swiftmailer, FOSRest, FOSUser