The #[Cache] attribute lets you define HTTP cache headers directly on controllers, including dynamic values for the Last-Modified and ETag headers. Symfony 8.1 makes this attribute easier to use and more flexible.

New request and args Variables in Expressions

HypeMC
Contributed by HypeMC in #62939

Until now, expressions used in the lastModified and etag options received all request attributes merged with all controller arguments as flat variables. This caused variable name clashes when an action argument shared a name with a request attribute, and it made the full Request object inaccessible from the expression.

Symfony 8.1 aligns this behavior with the #[IsGranted] attribute by exposing two explicit variables to expressions:

  • request: the current Request object
  • args: an array of resolved controller arguments

The previous flat variables remain available, so existing expressions keep working unchanged:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\HttpKernel\Attribute\Cache;

#[Cache(
    etag: "args['article'].computeETag()",
    lastModified: "args['article'].getUpdatedAt()",
    public: true,
)]
public function show(Article $article): Response
{
    // ...
}

You can now also access the full Request object from the expression to mix request data into the cache key:

1
2
3
4
5
#[Cache(etag: "request.headers.get('Accept-Language') ~ args['article'].getId()")]
public function show(Article $article): Response
{
    // ...
}

Using Closures Instead of Expressions

HypeMC
Contributed by HypeMC in #62940

When string expressions become more complex, some developers prefer using PHP code because it provides better static analysis, IDE support, and debugging. That's why the lastModified and etag options now accept PHP closures as an alternative to string expressions.

Closures receive the resolved controller arguments and the current Request object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\Cache;

#[Cache(
    lastModified: static function (array $args, Request $request): \DateTimeInterface {
        return $args['post']->getUpdatedAt();
    },
    etag: static function (array $args, Request $request): string {
        return (string) $args['post']->getId();
    },
)]
public function show(Post $post): Response
{
    // ...
}

Closures follow the same rules as expressions: cache headers already set on the response are not overridden.

Applying the Cache Attribute Conditionally

HypeMC
Contributed by HypeMC in #62941

The #[Cache] attribute now accepts a new if option that decides whether the attribute should be applied. The option accepts either an expression or a closure that must return a boolean:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\Cache;

#[Cache(
    public: true,
    maxage: 3600,
    if: static fn (array $args, Request $request): bool => !$request->query->has('preview'),
)]
public function show(Request $request): Response
{
    // ...
}

This is useful when caching depends on runtime conditions, feature flags, or when your controller does not return a Response object directly (for example when using a third-party view layer).

The attribute is now also repeatable, so you can stack multiple #[Cache] attributes with different conditions on the same controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[Cache(
    public: true,
    maxage: 3600,
    if: static fn (array $args, Request $request): bool => !$request->query->has('preview'),
)]
#[Cache(
    public: false,
    maxage: 0,
    if: static fn (array $args, Request $request): bool => $request->query->has('preview'),
)]
public function article(Request $request): Response
{
    // ...
}
Published in #Living on the edge