From Symfony 2/3 to Symfony 7: A Multi-Step Migration Strategy

#Symfony 2 to 7 migration
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Founder & Lead Developer

Expert in software development and legacy code optimization

A Symfony 2 or Symfony 3 application running in production today has typically survived years of feature additions, team changes, and accumulated workarounds. Attempting a Symfony 2 to 7 migration in one leap is not a migration — it is a rewrite with a familiar directory structure. The gap between those versions spans five major releases, four PHP minimum requirements, a completely replaced security layer, the introduction of Symfony Flex, and the removal of dozens of APIs that your codebase almost certainly depends on.

The teams that succeed at this kind of legacy Symfony upgrade do so incrementally. They treat each LTS release as a stable stopping point, bank the gains, and continue. The teams that fail attempt the full jump and discover that their 40,000-line codebase has no clean seam to cut along.

This guide explains the realistic path: why you stop at 4.4 LTS and 6.4 LTS, what breaks at each boundary, and how to structure the work so your product keeps shipping while the migration is in progress.

Why You Cannot Jump Directly to Symfony 7

Symfony's deprecation model is the core reason. Each major version removes everything deprecated in the preceding minor series. Symfony 3.4 is the deprecation mirror of Symfony 4.0: everything deprecated in 3.x will be removed in 4.0. Symfony 4.4 is the deprecation mirror of 5.0, and Symfony 6.4 is the mirror of 7.0.

If you are on Symfony 2.8 or 3.4, your codebase is almost certainly using APIs removed in Symfony 4.0. The service container, the security firewall configuration, the form extension architecture, the event dispatcher, the routing YAML/XML format — these all changed meaningfully between major versions. You cannot fix these at Symfony 7.0; you fix them at each intermediate boundary, where the framework still runs but tells you loudly what will break in the next major.

The practical consequence is that the full migration path looks like this:

  • Symfony 2.x or 3.x → 3.4 LTS (clean up all deprecations while still on 3.x)
  • Symfony 3.4 → 4.4 LTS (PHP 7.1+ required; introduce Flex, handle bundle removal, fix DI configuration)
  • Symfony 4.4 → 6.4 LTS (PHP 8.1+ required; new security system, typed properties, attribute-based routing)
  • Symfony 6.4 → 7.x (PHP 8.2+ required; remove all 6.x deprecations, tighten type signatures)

Each step has a well-defined scope. Each LTS release is a stable platform where you can stop, run in production, and let the team recover before the next leg.

Step One: Reach Symfony 3.4 LTS First

If you are on any 2.x or early 3.x release, the first task is reaching 3.4 LTS — the last release before the 4.x boundary. Symfony 3.4 runs with PHP 5.5.9 at minimum, though production deployments should be on PHP 7.x by now for performance and security reasons alone.

At 3.4, enable the deprecation handler in your test environment. Symfony ships a DebugClassLoader and the SYMFONY_DEPRECATIONS_HELPER environment variable for PHPUnit. Set it to max[total]=0 and run your full test suite. Every deprecation notice is a work item for the 4.0 boundary.

The most common Symfony 3.x deprecations that will block the 4.0 upgrade are: use of the deprecated AbstractController parent chain, bundle inheritance (a pattern removed in 4.0), controllers as services using the old container shortcut, form event listeners using removed option names, and custom security voters that have not been updated to the modern supports() and voteOnAttribute() signature. Fix all of these before touching the composer.json version constraint.

Step Two: The Symfony 4.4 LTS Jump (The Hardest Step)

The 3.4 to 4.4 jump is typically the most disruptive leg of a Symfony 2 to 7 migration. Three changes create the most friction: the move to Symfony Flex, the removal of bundle inheritance, and the introduction of autowiring and autoconfiguration as defaults.

Symfony Flex and the new directory structure. Symfony 4 introduced a new application skeleton with a significantly different layout. The app/ directory is gone; configuration moves to config/, the kernel moves to src/, and bundle registration is handled by config/bundles.php rather than AppKernel.php. If you are upgrading an existing application rather than starting a fresh skeleton, you will need to migrate this structure manually. The Flex upgrade guide in the official documentation covers the directory mapping step by step.

Bundle removal. The Symfony Standard Edition bundled SwiftmailerBundle, MonologBundle, TwigBundle, and others. By 4.x, several of these either became standalone packages, were replaced, or required explicit installation. More importantly, if your application had its own bundle hierarchy using bundle inheritance to override templates and controllers, that pattern no longer exists in 4.x. You will need to move overridden templates to templates/bundles/ and restructure any controller overrides.

PHP version requirement. Symfony 4.4 requires PHP 7.1.3 minimum. If your server is still running PHP 5.6 or 7.0, the PHP upgrade must happen before or alongside the Symfony 4.4 migration. This is usually an infra task that runs in parallel with the deprecation-fixing work.

The security component changes. Symfony 4.x began deprecating the old authentication providers in favor of the new authenticator-based system that became default in 5.x. If your application has a custom authentication system — common in Symfony 2/3 applications — plan for at least one sprint of security refactoring during this leg.

After reaching 4.4, stop, deploy to staging, and soak in production for at least a sprint cycle before continuing.

Step Three: Symfony 4.4 to Symfony 6.4 LTS

The 4.4 to 6.4 jump is smoother than the previous one, but the PHP requirement is a significant hurdle. Symfony 6.x requires PHP 8.0 minimum; Symfony 6.4 LTS recommends PHP 8.1 for the full feature set including enums and fibers. If your application is still on PHP 7.x, the PHP upgrade is the first milestone on this leg.

The most impactful change between Symfony 4.x and 6.x is the security system. The authenticator-based security component is now the default in Symfony 6.x, and the old form_login, http_basic, and custom provider approach is removed. If you were running a legacy security configuration, this is the step where you will need to rewrite it. The new architecture is cleaner and more testable, but the migration is not mechanical — it requires understanding the application's authentication flows.

Symfony 6.x also embraces PHP 8 attributes for routing and dependency injection. You are not forced to use them, but if your controllers still carry @Route annotations, you will encounter deprecation warnings in 6.4 pointing toward the attribute-based alternatives. This is a good time to make the switch, particularly if your codebase is already running on PHP 8.1.

Doctrine ORM is another area to check. Symfony 6.4 works with both ORM 2.x and 3.x, but the mapping configuration and query builder API have evolved. If your entities use XML, YAML, or annotation mapping, validate their compatibility with the target doctrine/orm version before upgrading Symfony.

After reaching 6.4 LTS, run the full deprecation check again:

bin/console debug:container --deprecations

Every item in this output is a concrete task for the final step. Symfony 6.4 is the designated bridge to 7.0 for exactly this reason: it exposes all 7.0 removals as warnings while the application remains fully functional.

Step Four: Symfony 6.4 to Symfony 7.x

The 6.4 to 7.0 jump is the most mechanical of the four legs, provided you have cleaned up the 6.4 deprecation log. PHP 8.2 is required; PHP 8.3 is recommended. If you have been fixing deprecations throughout the 6.4 LTS lifecycle, this final step is typically a composer version bump followed by a focused triage of type signature mismatches.

The most common issues at this boundary are: stricter method signatures on extended framework classes (voters, event subscribers, form types), removal of the last legacy security aliases, and stricter constructor promotion requirements on some Symfony components. The official UPGRADE-7.0.md file in the Symfony repository lists every change; reading it before starting the bump is the best investment of time on this leg.

Run the test suite immediately after the version bump and address failures in order of severity. Integration tests that exercise full request cycles catch the most realistic issues. Unit tests of individual classes often pass even when the wiring between them is broken.

Keeping Features Shipping During a Multi-Year Migration

The most common mistake in legacy Symfony upgrade projects is stopping feature development while the migration is in progress. On a codebase of any meaningful size, that is not acceptable. The migration has to run in parallel with the product.

The practical approach is the strangler fig pattern applied at the Symfony application level. New features are built in a way that is compatible with both the current and target framework version. A service defined as compatible with Symfony 4.4 can run on 6.4 if it avoids deprecated APIs. A new controller written with attributes instead of annotations will work on both if the Symfony version supports both.

Functionally, this means maintaining a strict rule during the migration period: no new code may use deprecated APIs. The migration work — fixing old deprecated usages — is a separate track from feature work. Both tracks are estimated and planned. Neither blocks the other.

For large teams, a branch-based approach works: a long-running symfony-migration branch that gets cherry-picks of non-migration features and merges the migration work back incrementally. For smaller teams, feature flags on new services — running both old and new implementations behind a toggle — give a lower-risk mechanism to test migrated code without a hard cutover.

When the Jump Is Too Wide: Parallel Framework Strategies

Some applications have layers that are simply incompatible with intermediate Symfony versions. A heavily customized bundle that uses internal Symfony APIs removed in 4.0, a custom authentication provider with deep kernel event hooks, or a form extension built against Symfony 2-era interfaces — these can make the incremental path more expensive than an alternative strategy.

In those cases, a parallel framework approach is worth considering: run the old Symfony application alongside a clean Symfony 7 skeleton and migrate endpoints one by one via a reverse proxy or shared database layer. New work happens in the modern application; legacy routes are migrated in priority order and the old application is decommissioned once the last route is confirmed.

This strategy costs more to set up but avoids the scenario where one incompatible bundle holds the entire migration hostage for months. It works best when the application has clear bounded contexts — a public API, an admin backend, a background job system — that can be migrated independently.

A Note on PHP Versions and Technical Debt

A Symfony 2 or 3 application that has never been modernized is almost certainly running a version of PHP that is beyond end-of-life. PHP 5.6 reached EOL in December 2018. PHP 7.4 ended support in November 2022. Running on an unsupported PHP version is a security liability independent of the Symfony version.

Plan the PHP upgrade as a prerequisite to the Symfony migration, not as an afterthought. The safest sequence is: PHP upgrade first (with a Symfony version that supports both the old and new PHP version), then Symfony upgrade. Running Symfony 3.4 on PHP 7.4 is a valid intermediate state that pays down the PHP technical debt before the framework work begins.

For applications where a full migration is not yet funded, the minimum defensible state is PHP 8.1 and Symfony 6.4 LTS, which receives security fixes until November 2027. That gives a meaningful runway to plan the 7.x migration without leaving the application on an unsupported platform.

What Makes a Legacy Symfony Migration Succeed

After working through many of these migrations, the patterns that reliably predict success are organizational as much as technical. Teams that succeed treat the migration as a product initiative with explicit milestones, not as background engineering work that happens when there is spare capacity. They define done for each LTS step — zero deprecation warnings, full test suite passing, deployed to production — before starting the next leg.

The Symfony codebase will not migrate itself. But the path is well-defined, the LTS releases are reliable stopping points, and the effort is entirely predictable if you start from a clear deprecation inventory at each boundary.

If you are looking at a Symfony 2 or 3 codebase and wondering where to start, the answer is always the same: run the deprecation report, quantify the work to reach 3.4 clean, and make that the first milestone. Everything else follows from there.


If your team is facing a legacy Symfony migration and needs experienced support — whether for the full multi-step path or a specific tricky boundary — reach out at hello@wolf-tech.io or visit wolf-tech.io/services/legacy-code-optimization to learn more about how we approach legacy modernization.