Symfony 8.1 includes many small features and improvements across different components. This is the second article in the series that highlights some of the most useful DX (developer experience) improvements.

Map Request Headers to Controller Arguments

Steven RENAUX
Contributed by Steven RENAUX in #51379

Symfony provides several attributes, such as #[MapQueryParameter] and #[MapRequestPayload], to map parts of the HTTP request to controller arguments. However, there was no equivalent for HTTP headers, so you had to inject the entire Request object just to read one of them.

In Symfony 8.1 we're introducing a new #[MapRequestHeader] attribute to map request headers directly to controller arguments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use Symfony\Component\HttpFoundation\AcceptHeader;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestHeader;

class DashboardController extends AbstractController
{
    public function index(
        // argument names are converted from camelCase to the
        // kebab-case header name (in this case: accept-language)
        #[MapRequestHeader] string $acceptLanguage,

        // use the 'name' option when the header name cannot be
        // derived from the argument name
        #[MapRequestHeader(name: 'x-request-id')] ?string $requestId,

        // type-hint the argument as array to get all the header values
        #[MapRequestHeader] array $accept,

        // type-hint it as AcceptHeader to get a parsed object that
        // allows sorting and filtering values by their quality
        #[MapRequestHeader] AcceptHeader $acceptEncoding,
    ): Response {
        // ...
    }
}

If a required header is missing, Symfony returns a 400 Bad Request response. You can customize this status code with the validationFailedStatusCode option of the attribute.

Use the Controller Instance in Security Expressions

Valtteri R
Contributed by Valtteri R in #63201

The #[IsGranted] attribute accepts an expression as its subject option. Inside that expression, you could only use the request and args variables. This was an issue for invokable classes that store their state in properties (e.g. live components), because there was no way to pass those properties as the subject.

In Symfony 8.1, subject expressions can also use a new this variable, which refers to the controller instance itself:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;

#[AsLiveComponent]
class PostComponent
{
    #[LiveProp]
    public Post $post;

    // 'this' refers to this PostComponent instance, so you can use
    // any of its properties and methods as the subject
    #[IsGranted('POST_EDIT', subject: new Expression('this.post'))]
    public function __invoke(): void
    {
        // ...
    }
}

CSP-Compliant dump() Output

Pascal CESCON
Contributed by Pascal CESCON in #63762 and #64087

The dump() function renders its HTML output using inline <script> and <style> tags. If your application enforces a strict Content Security Policy, browsers block those tags unless they include a valid nonce. That's why, until now, the web debug toolbar disabled CSP entirely for any page that displayed a dump.

In Symfony 8.1, the HtmlDumper class provides a new setNonce() method to add a CSP nonce to all the tags it generates:

1
2
3
4
5
6
7
8
use Symfony\Component\VarDumper\Dumper\HtmlDumper;

$dumper = new HtmlDumper();
$dumper->setNonce($request->attributes->get('_csp_nonce'));

// in long-running workers, where each request has its own nonce,
// pass a closure to resolve the nonce lazily
$dumper->setNonce(static fn (): string => $nonceProvider->current());

In addition, when using the web profiler in the dev environment, Symfony now forwards its own CSP nonces to the dumped contents automatically. Your Content Security Policy remains fully enabled while dumps display correctly, and you don't need to change anything in your application.

Custom Marshallers per Cache Pool

Mark van Duijker
Contributed by Mark van Duijker in #63356

Cache marshallers transform cache values before storing them (e.g. to serialize, compress, or encrypt them). Previously, you could only replace the default marshaller globally, affecting all cache pools at once.

In Symfony 8.1, cache pools support a new marshaller option, so you can mix different strategies in the same application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# config/packages/cache.yaml
framework:
    cache:
        pools:
            # encrypt the contents of this pool
            cache.tokens:
                adapter: cache.adapter.redis
                marshaller: 'app.sodium_marshaller'
            # compress the contents of this pool
            cache.large_data:
                adapter: cache.adapter.filesystem
                marshaller: 'app.deflate_marshaller'

services:
    app.sodium_marshaller:
        class: Symfony\Component\Cache\Marshaller\SodiumMarshaller
        arguments:
            - ['%env(base64:CACHE_DECRYPTION_KEY)%']
            - '@cache.default_marshaller'

    app.deflate_marshaller:
        class: Symfony\Component\Cache\Marshaller\DeflateMarshaller
        arguments: ['@cache.default_marshaller']

Configurable Webhook Headers and Signing Algorithm

Louis-Arnaud
Contributed by Louis-Arnaud in #63520

When sending webhooks with the Webhook component, Symfony transmits the event metadata in the Webhook-Event, Webhook-Id and Webhook-Signature headers, and signs the payload with HMAC-SHA256. These values were hardcoded, which was a problem when the receiving endpoints expected different header names or signing algorithms.

In Symfony 8.1 you can configure all of them:

1
2
3
4
5
6
7
# config/packages/webhook.yaml
framework:
    webhook:
        event_header_name: 'X-Acme-Event'
        id_header_name: 'X-Acme-Id'
        signature_header_name: 'X-Acme-Signature'
        signing_algorithm: 'sha512'

Safer Lock Stores on Shared Servers

Nicolas Grekas
Contributed by Nicolas Grekas in #63263

By default, the Lock component uses a semaphore store when available, or a flock store otherwise. Both rely on resources shared across the entire machine (system semaphores and files in the temporary directory), so two applications running on the same server could collide if they used the same lock names.

Starting in Symfony 8.1, both stores are scoped by the kernel.project_id parameter (an identifier derived from the project directory). Locks from different applications no longer collide, and you don't have to change anything in your applications to benefit from this.

When using the Lock component as a standalone library, pass the project identifier yourself:

1
2
3
use Symfony\Component\Lock\Store\SemaphoreStore;

$store = new SemaphoreStore($projectId);

Bundles as Compiler Passes

Yonel Ceruto
Contributed by Yonel Ceruto in #62800

Bundles often need to register compiler passes to modify the service container during its compilation. Even if the bundle class itself implemented CompilerPassInterface, you still had to override the build() method and call $container->addCompilerPass($this) explicitly.

In Symfony 8.1, that boilerplate is gone: any bundle class that implements CompilerPassInterface is registered automatically as a compiler pass:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class AcmeBundle extends AbstractBundle implements CompilerPassInterface
{
    // there's no need to override the build() method to register this;
    // Symfony calls it automatically when compiling the container
    public function process(ContainerBuilder $container): void
    {
        // ... manipulate the container services
    }
}
Published in #Living on the edge