Controller attributes such as #[Cache], #[IsGranted], #[Template]
and #[MapRequestPayload] are a core part of modern Symfony applications.
Technically, they are static reflection metadata resolved from source code.
Symfony 8.1 introduces several improvements to make these attributes dynamic and easier to override, easier to use from event listeners, and easier to extend with custom attribute-based features.
Decoupled Controller Attributes
Until now, controller attributes were always read directly from the controller's source code via reflection. This meant that listeners had no way to modify or replace those attributes at runtime: the attributes effectively belonged to the PHP code, not to the request being processed.
Symfony 8.1 stores controller attributes in a new request attribute named
_controller_attributes. The first call to ControllerEvent::getAttributes()
populates it from reflection, and subsequent reads (including in later kernel
events) reuse the stored value. Listeners can now override attributes for a
specific request by calling setController() with a custom set of attributes:
use Symfony\Component\HttpKernel\Attribute\Cache;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
public function onKernelController(ControllerEvent $event): void
{
// replace the controller's #[Cache] attribute for this request only,
// without touching the controller's source code
$attributes = $event->getAttributes();
$attributes[Cache::class] = [new Cache(maxage: 60, public: true)];
$event->setController(
$event->getController(),
array_merge(...array_values($attributes)),
);
}
This restores the declarative nature of PHP attributes: the source code defines the defaults, and the rest of the application can adapt them when needed.
Flat List of Controller Attributes
Symfony 8.1 also simplifies how controller attributes are represented. Instead
of always returning them grouped by class name, getAttributes() accepts a
new '*' argument to return a flat list of attribute instances in their
declaration order:
// get all attributes grouped by class name (default behavior)
$event->getAttributes();
// returns: [Cache::class => [new Cache(...)], IsGranted::class => [new IsGranted(...)], ...]
// get all attributes as a flat list, in declaration order
$event->getAttributes('*');
// returns: [new Cache(...), new IsGranted(...), ...]
// get all attributes of a specific class
$event->getAttributes(Cache::class);
// returns: [new Cache(...), ...]
In addition, ResponseEvent now exposes a public readonly
$controllerArgumentsEvent property so response listeners can read the
controller attributes that applied to the request without re-reflecting the
controller:
use Symfony\Component\HttpKernel\Attribute\Cache;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
public function onKernelResponse(ResponseEvent $event): void
{
$cacheAttributes = $event->controllerArgumentsEvent?->getAttributes(Cache::class) ?? [];
// ... act on the response based on the controller's attributes
}
Events Named After Controller Attributes
Before Symfony 8.1, attribute-based listeners had to subscribe to generic kernel events and manually inspect controller attributes. Symfony 8.1 introduces dedicated events for each controller attribute instead.
The event name follows the convention {kernelEvent}.{AttributeFQCN}, so a
#[Cache] attribute triggers the event
kernel.controller_arguments.Symfony
(or KernelEvents::CONTROLLER_ARGUMENTS.'.'.Cache::class).
Each event receives a ControllerAttributeEvent object that combines the
attribute instance with the underlying kernel event. For example, if you define
this custom attribute in your application:
namespace App\Attribute;
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
final class RateLimit
{
public function __construct(
public int $maxRequests = 100,
public int $periodInSeconds = 60,
) {
}
}
You can apply the attribute to any controller and write the following listener to handle it:
namespace App\EventListener;
use App\Attribute\RateLimit;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent;
use Symfony\Component\HttpKernel\KernelEvents;
#[AsEventListener(event: KernelEvents::CONTROLLER_ARGUMENTS.'.'.RateLimit::class)]
final class RateLimitListener
{
public function __invoke(ControllerAttributeEvent $event): void
{
$rateLimit = $event->attribute; // RateLimit instance
$request = $event->kernelEvent->getRequest();
// ... enforce $rateLimit->maxRequests within $rateLimit->periodInSeconds
}
}
Attribute events are available for all controller-related kernel events. Symfony also preserves attribute declaration order automatically, so listeners compose naturally around controllers.
Symfony optimizes these events internally, dispatching them only when listeners
exist and supporting attribute inheritance automatically. Built-in listeners
such as CacheAttributeListener, IsGrantedAttributeListener and
TemplateAttributeListener have been migrated to this new system.