What Breaks When You Upgrade from Symfony 7.4 to 8.1
Table of Contents
If you’re on Symfony 8.0, you’ve been running unsupported code since July 30, 2026 — 8.0 got exactly eight months of support and it’s over. If you’re on 7.4, you’re fine for years, but you still have to decide whether to stay on the LTS or move to 8.1, released May 29, 2026. Neither path is a rewrite. Both have changes that don’t throw an error — they just quietly change what your application does.
Symfony 7.4 and 8.0 shipped the same day, November 27, 2025, with identical features. The only difference is that 7.4 kept the deprecation layers accumulated since 7.0, and 8.0 deleted them. That’s the whole story of “Symfony 8”: it’s 7.4 with the safety net removed. 8.1, released half a year later, adds real new features on top and is the version you land on if you take the regular (non-LTS) track.
Where are you coming from?
| You’re on | Status today (Sep 2026) | What to do |
|---|---|---|
| Symfony 8.0 | Unmaintained since Jul 30, 2026 — no bug fixes, no security patches | Upgrade to 8.1 now. Same deprecation-free codebase, no code changes required by the BC promise |
| Symfony 7.4 (LTS) | Bug fixes until Nov 2028, security until Nov 2029 | Stay, or move to 8.1 for new features. No urgency either way |
| Symfony 6.4 or older | Already past security support | Go through 7.4 first. Fix every deprecation there with phpunit --display-deprecations, then decide 7.4-LTS vs 8.x |
The reference table, straight from symfony.com/releases:
| Version | Type | Released | PHP min | Bug fixes until | Security until |
|---|---|---|---|---|---|
| 7.4 | LTS | Nov 27, 2025 | 8.2.0 | Nov 2028 | Nov 2029 |
| 8.0 | Regular | Nov 27, 2025 | 8.4.0 | Jul 2026 | Jul 2026 |
| 8.1 | Regular | May 29, 2026 | 8.4.0 | Jan 2027 | Jan 2027 |
Two things worth reading twice: 8.0’s entire support window was eight months, both bug fixes and security together — that’s the standard policy for a non-LTS minor, not a shortened one. And 8.1 requires PHP 8.4, a full two versions ahead of what 7.4 needs. If you’re still on PHP 8.2 or 8.3, raising the PHP version is real infrastructure work, separate from anything in this list — do it first, verify it, then touch composer.json.
High impact
1. XML configuration is gone — and it broke in silence first
This is the one that catches people off guard, because it doesn’t fail the day it should. Symfony 7.4 deprecated XML configuration; Symfony 8.0 removed it outright. If your bundles, routes, or services are configured in XML and nobody was watching the deprecation log during the 7.4 window, the upgrade to 8.0 or 8.1 is a hard stop: the loader is gone, not just discouraged.
// Removed without alternative in Symfony 8.0
ExtensionInterface::getXsdValidationBasePath()
ExtensionInterface::getNamespace()
Routing, FrameworkBundle, and WebProfilerBundle no longer load XML routes at all. YAML remains the default and stays fully supported. PHP is the recommended alternative for code-based configuration, and it changed shape too (see #4). There’s an automated converter for bundle maintainers — symfony-config-xml-to-php — but application-level XML config you’ll be rewriting by hand.
The reason this qualifies as “breaks in silence”: the deprecation notice in 7.4 is just a log line. Nobody greps deprecation logs on a schedule. The failure shows up months later, on the 8.x upgrade, as a fatal error that looks unrelated to anything you changed that week.
2. Security: erased credentials, firewall listeners, error exposure
Three changes land in the same component and compound:
UserInterface::eraseCredentials()andTokenInterface::eraseCredentials()are removed. Use__serialize()to control what survives on the token instead.- Firewall listeners must extend
AbstractListeneror implementFirewallListenerInterface. Registering a bare callable as a listener no longer works. hide_user_not_foundis gone; useexpose_security_errors.
# Symfony <= 7.x
security:
hide_user_not_found: false
# Symfony >= 8.0
security:
expose_security_errors: account_status # none | account_status | all
expose_security_errors is more precise than the boolean it replaces — account_status shows exceptions only for users who supplied the correct password, all hides nothing, none is the safe default. If your security.yaml still has hide_user_not_found, the container fails to compile on 8.x. That one is loud. What isn’t loud: remember-me cookies. RememberMeDetails no longer embeds the user’s fully-qualified class name in the cookie payload. Old cookies issued before the upgrade fail to authenticate silently — the user isn’t shown an error, they’re just logged out and have to sign in again. If you run a high-traffic app, expect a support-ticket spike the week you deploy, not an incident alert.
3. Console commands: static metadata methods removed
// Symfony <= 7.x
class SyncCommand extends Command
{
protected static function getDefaultName(): string
{
return 'app:sync';
}
}
// Symfony >= 8.0
#[AsCommand(name: 'app:sync')]
class SyncCommand extends Command
{
}
Command::getDefaultName() and getDefaultDescription() are removed — use the #[AsCommand] attribute. Application::add() is replaced by Application::addCommand(). If you register commands dynamically by instantiating Command subclasses and pushing them into the application, this is a mechanical but mandatory rename.
Medium impact
4. Fluent PHP config is gone; there’s a new array-shape format
Symfony 5.3 introduced a fluent, builder-style PHP configuration format (SecurityConfig, FrameworkConfig, and friends). It’s removed in 8.0. What replaces it isn’t a return to arrays-as-usual — it’s a new array-shape PHP format with typed metadata that IDEs and static analyzers can actually understand.
// Removed: fluent PHP config
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security) {
$security->firewall('main')->pattern('^/*')->lazy(true);
};
// New: array-shape PHP config
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'security' => [
'firewalls' => [
'main' => ['pattern' => '^/*', 'lazy' => true],
],
],
]);
The stated reason is that the fluent builders couldn’t represent every semantic config tree shape, which made keeping Symfony recipes automatically up to date needlessly hard. YAML is still the recommended default; this format is for teams that specifically want PHP.
5. Doctrine: no more auto-mapping entities in controller arguments
The ParamConverter-style auto-mapping of route parameters straight into Doctrine entity arguments is removed. If a controller action typed a Doctrine entity and relied on the bundle resolving it from the route implicitly, that resolution is gone — you now wire it explicitly with #[MapEntity] or fetch it yourself in the action body. DoctrineExtractor::getTypes() is also gone; use getType().
6. HttpFoundation: Request::get() removed, method override narrowed
// Removed
$request->get('id');
// Use the specific bag
$request->attributes->get('id');
$request->query->get('id');
$request->request->get('id');
Request::get() searched attributes, then query, then the request body, in that order — convenient and a frequent source of bugs when two bags had the same key. It’s gone; be explicit about which bag you mean. Separately, HTTP method override (X-HTTP-METHOD-OVERRIDE / _method) no longer applies to GET, HEAD, CONNECT, or TRACE — those are meant to be safe and idempotent, and Symfony now enforces it at the request layer instead of trusting the override header.
New in 8.1 that changes behavior without an error
This is the section worth reading closely if you’re already past 8.0 and evaluating 8.1, because none of these throw — they just do something different.
7. Messenger no longer throws on decode failure
Serializers now return an Envelope wrapping a MessageDecodingFailedException instead of throwing it. If your transport or middleware caught MessageDecodingFailedException around the decode call, that catch block stops firing. The failure isn’t surfaced as an exception anymore — it’s data on the envelope that you have to check for. Code that isn’t updated for this will silently process (or silently drop) a decode failure that used to be impossible to miss.
8. UrlType stops guessing a protocol
// Symfony <= 8.0: default_protocol defaults to 'http'
// typing "example.com" into the field saves "http://example.com"
// Symfony >= 8.1: default_protocol defaults to null
// typing "example.com" saves "example.com" — no protocol prepended
If your forms use UrlType and you display or link that value later assuming it always has a scheme, it now doesn’t unless the user typed one. Nothing errors; you get a relative-looking string that behaves like a broken link the first time someone clicks it.
9. ParameterBag::getInt() / getBoolean() start throwing
The opposite direction from the previous two: these used to silently coerce an unconvertible value to 0 or false. In 8.1 they throw UnexpectedValueException instead. This is a case where 8.1 turns a previous silent failure into a loud one — good for catching bad input, but it means code that leaned on the old fallback needs a try/catch or upstream validation now.
10. Required collapsed ChoiceType renders its placeholder as hidden
A required, non-expanded ChoiceType field now marks its placeholder <option> with the hidden attribute instead of just being an empty-value option. Purely visual, but if you have JavaScript or CSS that selects on the placeholder option, or automated UI tests asserting on its markup, this is a quiet DOM change. Restore the old behavior with placeholder_attr: [] if you need it.
What does NOT break
- Symfony 8.0 and 8.1 are not “the next 7.x.” They removed deprecations; they didn’t add a wave of new breaking behavior beyond that removal. Most of what breaks on the 7.4→8.0 jump is stuff that was already deprecated and logged in 7.x — if you cleared deprecations on 7.4, the 8.0 upgrade is close to a non-event.
- YAML configuration is not going anywhere. It’s explicitly the format Symfony recipes keep targeting.
- The 8.1 upgrade from 8.0 needs no code changes under the backward-compatibility promise — 8.1 is a minor version on top of 8.0, not a major.
The actual migration flow
- Land on 7.4 first if you’re not there. Even if your target is 8.x, upgrading onto the LTS gives you the deprecation layer to work against.
- Run the deprecation report and fix what’s yours:
php bin/phpunit --display-deprecations
# or, project-wide with the bridge:
SYMFONY_DEPRECATIONS_HELPER=weak_vendors php bin/phpunit
weak_vendors fails the suite only on deprecations triggered by your own code, not by third-party packages you don’t control yet — useful for triaging what’s actually actionable this sprint.
- Automate the mechanical renames with Rector.
rector/rector-symfonyships upgrade sets that handle a chunk of this list —getDefaultName()→ attribute, deprecated method calls, and similar — without hand-editing every file. - Update dependencies and let Flex apply new recipes:
composer require symfony/symfony:^8.1 --with-all-dependencies
- Grep for what’s specific to your app before you assume the automation caught it:
grep -rln '<?xml' config/
grep -rn "hide_user_not_found\|->get(" config/ src/
grep -rn "getDefaultName\|getDefaultDescription" src/
- Diff your
config/tree against a freshsymfony/skeletonon the target branch. Most of what’s on this list lives in configuration files, not framework code, andcomposer updatewon’t touch config for you.
When should you upgrade?
- On 8.0 today: move to 8.1 immediately. There’s no code-change cost under the BC promise, and every day past July 30, 2026 is a day without security patches.
- On 7.4, stable, no pressure for new features: stay. You have until November 2028 for bug fixes and November 2029 for security. There’s no clock forcing a move.
- On 7.4, want the newest features or plan to track the regular release cadence: go to 8.1 now, then follow the twice-yearly minors (8.2 is due November 2026). Fix deprecations from XML config, fluent PHP config, and
hide_user_not_foundbefore you jump, not during. - On 6.4 or older: you’re already unsupported. Route through 7.4, clear every deprecation with the PHPUnit bridge, then pick LTS-vs-regular based on how much you want the newest features versus a long, quiet support window.
Further reading: