Framework

New in Symfony 8.1: Dynamic Controller Attributes

Symfony Blog

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

Nicolas Grekas
Contributed by Nicolas Grekas in #62850

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

Nicolas Grekas
Contributed by Nicolas Grekas in #63090

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

Nicolas Grekas
Contributed by Nicolas Grekas in #63032

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\Component\HttpKernel\Attribute\Cache (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.


Sponsor the Symfony project.

Articolo originale

https://symfony.com/blog/new-in-symfony-8-1-dynamic-controller-attributes?utm_source=Symfony%20Blog%20Feed&utm_medium=feed

Leggi Originale →

Ultime News

Altre news dal mondo PHP

Vedi tutte le news →