Symfony Migration: From Version 5 to 7 Without a Code Freeze
Symfony 5.4 reached the end of community security support in November 2025. An application still running on it today sits on an unpatched framework, and every month of waiting grows the pile of deprecations the eventual upgrade has to clear. The reassuring part is that a Symfony 5 to 7 migration is a much smaller project than the version gap suggests. Nothing between 5.4 and 7.x resembles an architectural rewrite. What separates you from a current framework is a long list of deprecations plus one PHP version constraint, and both can be cleared while the team keeps shipping features.
This guide covers the 5.4 route specifically. If your application sits on Symfony 2 or 3, the path is longer and different in kind, and we described it in a separate multi-step migration strategy.
What a Symfony 5 to 7 Migration Actually Involves
Symfony majors follow a strict deprecation contract. Symfony 6.0 is 5.4 with the deprecated code deleted, and 7.0 is 6.4 with the deprecated code deleted. There are no new concepts to learn at either boundary. If your 5.4 codebase triggers zero deprecation warnings, the jump to 6.4 is close to a composer constraint change. The same holds again from 6.4 to 7.x.
That turns the whole Symfony 5 to 7 migration into a plan you can put on a board: clean up deprecations on 5.4, jump to 6.4 LTS, clean up the 6.x deprecations, jump to 7.x. Skipping the intermediate minors is supported and normal; going from 5.4 straight to 6.4 is the standard route. The sensible landing spot in 2026 is Symfony 7.4, the current LTS. Symfony 8.0 exists as well, but it is 7.4 minus the deprecated code, so a clean 7.4 codebase turns the next major into the same routine instead of another rescue project.
Teams coming from Symfony 2 or 3 had to absorb a replaced security component, the switch to Flex, and a new directory structure. Coming from 5.4, you skip all of that. The work is real, but it is janitorial rather than structural.
Resolve the PHP Constraint Before Touching the Framework
Symfony 6.4 requires at least PHP 8.1, and Symfony 7 requires at least PHP 8.2. Plenty of 5.4 applications still run PHP 7.4 or 8.0, and that is the first blocker to clear, because framework and language should never move in the same deploy.
Symfony 5.4 runs fine on PHP 8.2 and 8.3 in its current patch releases, so the order is straightforward: upgrade PHP first on the framework version you already trust, let it settle in production, then start the framework work with the language constraint out of the way. Go directly to 8.2 or 8.3 rather than stopping at 8.1, so the constraint does not come back at the 7.0 boundary. We wrote up the language side in our PHP 7.4 to 8.3 upgrade guide; the short version is that a composer platform config and a CI version matrix do most of the risk reduction.
Turn On the Deprecation Log in Production
Deprecated code on 5.4 works exactly as before. That is what makes a migration without a freeze possible, but it also means you need visibility into what your application actually calls. Test suites only exercise the paths you thought to test. Production traffic finds the rest.
Symfony routes every deprecation into a dedicated deprecation log channel. Give it a handler in production:
# config/packages/prod/monolog.yaml
monolog:
channels: ['deprecation']
handlers:
deprecation:
type: stream
channels: [deprecation]
path: '%kernel.logs_dir%/deprecations.log'
A week of real traffic gives you an inventory, with call counts, of everything that will break in 6.0. Add bin/console debug:container --deprecations for the container side, and wire the PHPUnit bridge into CI with SYMFONY_DEPRECATIONS_HELPER set to a baseline, so the count can only go down. The inventory splits into deprecations in your own code, which you fix directly, and deprecations coming out of vendor packages, which you fix by upgrading the bundle that triggers them.
Fix Deprecations Continuously Instead of in a Big Bang
This is what makes the code freeze unnecessary. Every deprecation fix is valid 5.4 code, so each one can be its own small PR, reviewed normally and deployed with the next regular release. The migration never needs a long-running branch that drifts away from main while everyone else keeps merging.
Feature work and deprecation work interleave in the same trunk, and the deprecation count becomes a number the team watches trend toward zero over a few sprints. A budget that has worked on our projects: each developer clears a few deprecations per week alongside normal tickets, and one person owns the trend line so it does not quietly stall.
When the production log stays quiet for a week and CI enforces zero new deprecations from your own code, the version bump itself is a small, boring PR: raise extra.symfony.require in composer.json from 5.4.* to 6.4.*, run composer update "symfony/*", fix whatever the cleanup missed. Boring is the goal.
Let Rector Do the Mechanical Work
A large share of deprecation fixes are mechanical: changed method signatures, renamed classes, moved namespaces, annotations that become attributes. This is Rector territory. Rector ships rule sets for the Symfony 5 to 6 and 6 to 7 boundaries, plus sets that convert Doctrine and routing annotations to native PHP attributes. Running them turns weeks of typing into a day or two of reviewing diffs.
Two habits keep Rector useful rather than noisy. Run one rule set at a time and commit each run separately, so a surprising diff is easy to bisect. And treat the output as a draft: Rector updates signatures reliably, but it cannot answer design questions, such as what your custom guard authenticator should look like as a modern authenticator. We covered setup and rule selection in Rector for legacy PHP, and the same workflow applies here unchanged.
The Configuration and Flex Recipe Updates You Cannot Skip
Code is only half the migration. The other half lives in config/, and it is the half that surprises teams still running defaults that Flex recipes wrote in 2020.
Flex can update those files for you. composer recipes lists every recipe with a newer version, and composer recipes:update applies the changes as a diff you review like any other PR. Expect the biggest diffs in two files.
In security.yaml, the encoders section became password_hashers in 5.3, guard authenticators are gone in 6.0, and enable_authenticator_manager: true must be set on 5.4 to opt into the new security system. The option then disappears again in 7.0, because the new system is the only one left. If you still have custom guard authenticators, rewriting them against AbstractAuthenticator is the one piece of real design work in the whole migration, so schedule it early rather than last.
In framework.yaml, http_method_override needs an explicit value, the session configuration changed shape, and the annotations section is removed in 7.0 because attribute support replaced doctrine/annotations across the framework. Routes written as @Route annotations must become #[Route] attributes before 7.0. Rector converts them wholesale, and the same applies to Doctrine mapping annotations.
Third-Party Bundles Decide Your Critical Path
Your own code follows a predictable deprecation contract. Your vendor directory does not, and in most 5.4 applications the bundle audit produces the only real bad news. Run composer why-not symfony/framework-bundle 6.4 in week one; the output is the honest list of what blocks the jump.
The usual suspects from the 5.4 era are well known. SensioFrameworkExtraBundle is abandoned, and its features moved into the framework itself, so @ParamConverter becomes #[MapEntity] and @IsGranted becomes the core #[IsGranted] attribute. EasyAdmin 3 does not support Symfony 6, so the admin panel needs the move to EasyAdmin 4, which is more involved than a constraint bump and deserves its own ticket. API Platform 2.6 stops at Symfony 5.4 as well; the supported route goes through 2.7, which lets you adopt the version 3 resource metadata while still on the old major, before landing on 3.x. FOSRestBundle is in maintenance mode, and many teams replace it with plain controllers or API Platform during this migration rather than carrying it forward. Old JWT and OAuth bundles built on guard need their current authenticator-based majors.
Upgrade each of these as an isolated PR while still on 5.4, wherever the bundle supports both majors. Every bundle you modernize before the bump shrinks the bump.
A Realistic Timeline for a Mid-Size 5.4 Application
As a concrete anchor: an application around 120,000 lines, a team of five that keeps shipping features the whole time, test coverage good enough to trust.
| Phase | Calendar time | What happens |
|---|---|---|
| PHP to 8.2 or 8.3 | 1 to 2 weeks | Platform bump, CI matrix, deployed on Symfony 5.4 |
| Deprecation cleanup on 5.4 | 3 to 5 weeks | Small PRs, Rector rule sets, bundle upgrades in parallel |
| Jump to 6.4 LTS | 2 to 4 days | Constraint bump, composer update "symfony/*", recipe updates |
| Cleanup on 6.4 | 2 to 3 weeks | Fresh deprecation inventory, attributes everywhere, guard leftovers |
| Jump to 7.x | 2 to 4 days | Constraint bump, recipe updates, final config removals |
Call it eight to twelve calendar weeks at part-time intensity, with zero weeks of frozen feature work. The biggest schedule risk is a blocking bundle discovered late, which is why the composer why-not audit belongs in week one, not week six.
If the deprecation inventory looks overwhelming, or nobody has the bandwidth to own the trend line, this is a project shape we know well from legacy code optimization work: an assessment of your actual deprecation log and bundle list first, in the same spirit as our code quality consulting audits, then a fixed plan, then execution alongside your team while releases continue. A short description of your setup sent to hello@wolf-tech.io is enough for a first read on effort, and there is more background on how we work at wolf-tech.io.

