KwikTech Symfony · API Platform · Doctrine upgrades
Upgrade-Leitfaden · 11 Min. Lesezeit

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&amp;max[direct]=0&amp;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:

DeprecationErsatz
Symfony\Component\Security\Core\SecuritySymfony\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 AnnotationsDieselben 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 TypenReturn- und Parametertypen ergänzen
framework.http_method_override: true stillschweigend angenommenExplizit 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.0 in ihrer composer.json festhalten. Auf ^6.4 || ^7.0 erweitern 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:update zeigt den Diff pro Paket. Übernehmen, dann eigene Werte wieder eintragen.
  • Twig: 7.x zieht Twig 3.10+, das {% filter %} zugunsten von {% apply %} und spaceless deprecated. lint:twig meldet sie.
  • symfony/phpunit-bridge und PHPUnit: 7.4 funktioniert mit PHPUnit 9 bis 13; wer auf PHPUnit 12+ wechselt, macht auch dort Annotations zu Attributes (Rectors PHPUnitSetList ü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, mit doctrine:migrations:diff erzeugen.

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

  1. PHP 8.3 oder 8.4 auf 6.4
  2. Rector-7.x-Sets und PHP-Sets
  3. Return Types gepatcht, direkte Deprecations auf null
  4. Bundle-Majors, die 7.x blockieren (jeweils eigener PR, wo sich die API geändert hat)
  5. 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.

Mein Projekt scannen Assessment anfragen (€750)

Weitere Leitfäden