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
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 currentRequestobjectargs: 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
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
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
{
// ...
}
Banger!
It's incredible how you've kept making Symfony better and better over the years. It's really great and very useful.