Symfony 8.2 Is Taking FrameworkBundle Apart, and You Will Feel It in bundles.php
Two dozen standalone bundles landed on the Symfony 8.2 branch in one week. Here is what MessengerBundle, CacheBundle and friends mean for your app.
Symfony has spent fifteen years telling everyone it is a set of decoupled components that happen to ship together. That has been true of almost everything except the one package every Symfony application installs first. FrameworkBundle has always been the exception: a single extension that owns the framework configuration tree, wires the Cache component, the Messenger component, the Serializer, the Validator, the Router, the Mailer, and roughly thirty other things, and pulls all of them into your dependency graph whether you asked for them or not.
That stopped being true in the week of September 7 to 13, 2026. If you read A Week of Symfony #1028 and skimmed past the changelog, go back and look at the 8.2 section. It is close to two dozen commits in a row, all with the same shape: [FrameworkBundle, Messenger] add MessengerBundle, [Cache, FrameworkBundle] add CacheBundle, [FrameworkBundle, Lock] add LockBundle, on down through Mailer, Notifier, Serializer, Validator, Router, Asset, AssetMapper, RateLimiter, Webhook, Scheduler, JsonStreamer, PropertyInfo, HtmlSanitizer and Workflow. The commit that explains the whole set is blunt about it: move every configuration section to a bundle shipped by its own component.
This is the biggest structural change to a Symfony application skeleton since Flex, and almost nobody is talking about it yet.
Where this came from
It did not start in September. It started in May, in Symfony 8.1, with HTTP-less applications, contributed by Nicolas Grekas in #63710 and #63880.
The problem 8.1 solved was narrow but annoying. A message consumer, a cron worker, or a CLI tool has no HTTP in it anywhere, and yet it had to install HttpKernel and HttpFoundation to get a container. Symfony 8.1 moved the kernel and bundle infrastructure down into the DependencyInjection component, added a KernelInterface that exposes only container concerns, and gave you AbstractKernel plus KernelTrait as an HTTP-free equivalent of MicroKernelTrait. It also split the core of FrameworkBundle in two: ServicesBundle in the DependencyInjection component for the foundational services (event dispatcher, filesystem, clock, env var processors), and ConsoleBundle in the Console component for command registration and the argument resolver.
The glue was a new repeatable attribute, #[RequiredBundle], which lets a bundle declare that it needs another bundle, resolves recursively, and takes ignoreOnInvalid: true for optional dependencies. That is the mechanism that makes a config/bundles.php containing one entry actually boot four bundles in the right order.
So 8.1 carved out the core. 8.2 is carving out everything else.
What your bundles.php starts to look like
Today a Symfony app registers one line and gets the world:
// config/bundles.php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
];
On the 8.2 branch, a php[architect] subscription worker that consumes Messenger, writes to a cache, takes a lock, and sends renewal notices no longer needs the router, the serializer, the asset mapper, or the HTML sanitizer in its container at all:
// config/bundles.php
return [
Symfony\Component\Console\ConsoleBundle::class => ['all' => true],
Symfony\Component\Messenger\MessengerBundle::class => ['all' => true],
Symfony\Component\Cache\CacheBundle::class => ['all' => true],
Symfony\Component\Lock\LockBundle::class => ['all' => true],
Symfony\Component\Mailer\MailerBundle::class => ['all' => true],
];
Treat those exact namespaces as provisional. 8.2 is under development until November and class locations are still moving. What is not provisional is the shape: one bundle per component, each shipped by the component that owns it, each pulling its own dependencies through #[RequiredBundle].
The kernel for that worker is the 8.1 HTTP-less one, and it is short:
namespace App;
use Symfony\Component\DependencyInjection\Kernel\AbstractKernel;
use Symfony\Component\DependencyInjection\Kernel\KernelTrait;
final class Kernel extends AbstractKernel
{
use KernelTrait;
}
If you want to see this working before 8.2 ships, Jérôme Tamarelle pointed at symfony-tui-games in the comments on the 8.1 announcement: a Symfony app with no FrameworkBundle and no HTTP, built on ConsoleBundle and a KernelTrait kernel.
The part that keeps existing apps working
A change this large would normally break every application on the planet, and Symfony’s backward compatibility promise does not allow that in a minor release. Three commits in the same batch are what make it survivable.
The first allows a configuration node to alias the configuration of another extension. The commit message does not spell out the use case, but there is only one it can be for: letting your existing framework.messenger block keep working after the Messenger config tree moves into MessengerBundle. If that holds, you should not have to touch a single YAML file to upgrade.
The second adds a container.remove_if_missing tag to the DependencyInjection component, and the name tells you most of it. Once services are defined across many bundles, you get definitions that reference services that may or may not exist depending on which bundles are installed. Rather than forcing every bundle to guard its definitions with hasDefinition() checks in a compiler pass, you tag the definition and let the container drop it when the dependency is not there. A related commit moves the compiler passes and commands that act on another package into that package, which is the same cleanup from the other direction.
The third is a small quality-of-life fix that will save you an afternoon: when an extension is missing, the error now names the package you need to install. If you have ever stared at “There is no extension able to load the configuration for framework.workflow” and had to go source-diving, that is the one.
There is also a performance angle. One commit instantiates on demand the bundles that have nothing to do at boot time. Twenty-five bundles where there used to be one would otherwise mean twenty-five object instantiations on every boot, including in FrankenPHP worker mode where you pay it once but pay it for real.
What to do about it now
Nothing urgent. Symfony 8.2 is scheduled for November 2026, requires PHP 8.4, and is supported until July 2027, so it is a standard non-LTS upgrade. If you are sitting on 7.4 LTS, none of this reaches you until you jump to the 8.x line.
Three things are worth checking in the meantime, though, because they are the places where a split will actually bite:
Hardcoded FrameworkBundle::class references. Search your codebase for it. Test kernels, custom bundles.php builders, and static analysis config are the usual suspects. A reference that assumes FrameworkBundle owns a given service is a reference that may need to move.
Custom compiler passes that reach into framework services. If you have a pass doing $container->getDefinition('messenger.bus.default') and assuming FrameworkBundle put it there, add the container.remove_if_missing pattern to your mental model now. Your pass may need to tolerate the definition simply not existing.
Bundles you maintain. If you ship a bundle for clients or for the community, #[RequiredBundle] is available today in 8.1 and worth adopting. Declaring #[RequiredBundle(DoctrineBundle::class)] is better documentation than a line in your README, and it is enforced.
The broader point is where Symfony is heading. Between HTTP-less kernels in 8.1, the TUI component that is actively getting bug fixes on the 8.1 branch, and now one bundle per component in 8.2, “Symfony application” is drifting away from meaning “web application that returns a Response.” For those of us who have been building PHP CLI tools inside a web framework and quietly apologizing for it, that is a good direction.
Sources: A Week of Symfony #1028 (September 7 to 13, 2026), Symfony Blog, New in Symfony 8.1: HTTP-Less Symfony Applications, Symfony Blog, Symfony Releases, symfony/symfony 8.2 branch commits, The Bundle System, Symfony documentation.