Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a8efdea

Browse files
[HttpKernel] Add #[Cache] to describe the default HTTP cache headers on controllers
1 parent 2633877 commit a8efdea

File tree

6 files changed

+638
-0
lines changed

6 files changed

+638
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver;
2525
use Symfony\Component\HttpKernel\Controller\ErrorController;
2626
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
27+
use Symfony\Component\HttpKernel\EventListener\CacheAttributeListener;
2728
use Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener;
2829
use Symfony\Component\HttpKernel\EventListener\ErrorListener;
2930
use Symfony\Component\HttpKernel\EventListener\LocaleListener;
@@ -117,5 +118,9 @@
117118
])
118119
->tag('kernel.event_subscriber')
119120
->tag('monolog.logger', ['channel' => 'request'])
121+
122+
->set('controller.cache_attribute_listener', CacheAttributeListener::class)
123+
->tag('kernel.event_subscriber')
124+
120125
;
121126
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Attribute;
13+
14+
/**
15+
* Describes the default HTTP cache headers on controllers.
16+
*
17+
* @author Fabien Potencier <[email protected]>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
20+
class Cache
21+
{
22+
public function __construct(
23+
/**
24+
* The expiration date as a valid date for the strtotime() function.
25+
*/
26+
public ?string $expires = null,
27+
28+
/**
29+
* The number of seconds that the response is considered fresh by a private
30+
* cache like a web browser.
31+
*/
32+
public int|string|null $maxage = null,
33+
34+
/**
35+
* The number of seconds that the response is considered fresh by a public
36+
* cache like a reverse proxy cache.
37+
*/
38+
public int|string|null $smaxage = null,
39+
40+
/**
41+
* Whether the response is public or not.
42+
*/
43+
public ?bool $public = null,
44+
45+
/**
46+
* Whether or not the response must be revalidated.
47+
*/
48+
public bool $mustRevalidate = false,
49+
50+
/**
51+
* Additional "Vary:"-headers.
52+
*/
53+
public array $vary = [],
54+
55+
/**
56+
* An expression to compute the Last-Modified HTTP header.
57+
*/
58+
public ?string $lastModified = null,
59+
60+
/**
61+
* An expression to compute the ETag HTTP header.
62+
*/
63+
public ?string $etag = null,
64+
65+
/**
66+
* max-stale Cache-Control header
67+
* It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...).
68+
*/
69+
public int|string|null $maxStale = null,
70+
71+
/**
72+
* stale-while-revalidate Cache-Control header
73+
* It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...).
74+
*/
75+
public int|string|null $staleWhileRevalidate = null,
76+
77+
/**
78+
* stale-if-error Cache-Control header
79+
* It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...).
80+
*/
81+
public int|string|null $staleIfError = null,
82+
) {
83+
}
84+
}

src/Symfony/Component/HttpKernel/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add constructor argument `bool $catchThrowable` to `HttpKernel`
88
* Add `ControllerEvent::getAttributes()` to handle attributes on controllers
9+
* Add `#[Cache]` to describe the default HTTP cache headers on controllers
910

1011
6.1
1112
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\HttpKernel\Attribute\Cache;
18+
use Symfony\Component\HttpKernel\Event\ControllerEvent;
19+
use Symfony\Component\HttpKernel\Event\ResponseEvent;
20+
use Symfony\Component\HttpKernel\KernelEvents;
21+
22+
/**
23+
* Handles HTTP cache headers configured via the Cache attribute.
24+
*
25+
* @author Fabien Potencier <[email protected]>
26+
*/
27+
class CacheAttributeListener implements EventSubscriberInterface
28+
{
29+
/**
30+
* @var \SplObjectStorage<Request, string>
31+
*/
32+
private \SplObjectStorage $lastModified;
33+
34+
/**
35+
* @var \SplObjectStorage<Request, string>
36+
*/
37+
private \SplObjectStorage $etags;
38+
39+
public function __construct(
40+
private ExpressionLanguage $expressionLanguage = null,
41+
) {
42+
$this->lastModified = new \SplObjectStorage();
43+
$this->etags = new \SplObjectStorage();
44+
}
45+
46+
/**
47+
* Handles HTTP validation headers.
48+
*/
49+
public function onKernelController(ControllerEvent $event)
50+
{
51+
$request = $event->getRequest();
52+
53+
if (!\is_array($attributes = $request->attributes->get('_cache') ?? $event->getAttributes()[Cache::class] ?? null)) {
54+
return;
55+
}
56+
57+
$request->attributes->set('_cache', $attributes);
58+
$response = null;
59+
$lastModified = null;
60+
$etag = null;
61+
62+
/** @var Cache[] $attributes */
63+
foreach ($attributes as $cache) {
64+
if (null !== $cache->lastModified) {
65+
$lastModified = $this->getExpressionLanguage()->evaluate($cache->lastModified, $request->attributes->all());
66+
($response ??= new Response())->setLastModified($lastModified);
67+
}
68+
69+
if (null !== $cache->etag) {
70+
$etag = hash('sha256', $this->getExpressionLanguage()->evaluate($cache->etag, $request->attributes->all()));
71+
($response ??= new Response())->setEtag($etag);
72+
}
73+
}
74+
75+
if ($response?->isNotModified($request)) {
76+
$event->setController(static fn () => $response);
77+
$event->stopPropagation();
78+
79+
return;
80+
}
81+
82+
if (null !== $etag) {
83+
$this->etags[$request] = $etag;
84+
}
85+
if (null !== $lastModified) {
86+
$this->lastModified[$request] = $lastModified;
87+
}
88+
}
89+
90+
/**
91+
* Modifies the response to apply HTTP cache headers when needed.
92+
*/
93+
public function onKernelResponse(ResponseEvent $event)
94+
{
95+
$request = $event->getRequest();
96+
97+
/** @var Cache[] $attributes */
98+
if (!\is_array($attributes = $request->attributes->get('_cache'))) {
99+
return;
100+
}
101+
$response = $event->getResponse();
102+
103+
// http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-12#section-3.1
104+
if (!\in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 304, 404, 410])) {
105+
unset($this->lastModified[$request]);
106+
unset($this->etags[$request]);
107+
108+
return;
109+
}
110+
111+
if (isset($this->lastModified[$request]) && !$response->headers->has('Last-Modified')) {
112+
$response->setLastModified($this->lastModified[$request]);
113+
}
114+
115+
if (isset($this->etags[$request]) && !$response->headers->has('Etag')) {
116+
$response->setEtag($this->etags[$request]);
117+
}
118+
119+
unset($this->lastModified[$request]);
120+
unset($this->etags[$request]);
121+
$hasVary = $response->headers->has('Vary');
122+
123+
foreach (array_reverse($attributes) as $cache) {
124+
if (null !== $cache->smaxage && !$response->headers->hasCacheControlDirective('s-maxage')) {
125+
$response->setSharedMaxAge($this->toSeconds($cache->smaxage));
126+
}
127+
128+
if ($cache->mustRevalidate) {
129+
$response->headers->addCacheControlDirective('must-revalidate');
130+
}
131+
132+
if (null !== $cache->maxage && !$response->headers->hasCacheControlDirective('max-age')) {
133+
$response->setMaxAge($this->toSeconds($cache->maxage));
134+
}
135+
136+
if (null !== $cache->maxStale && !$response->headers->hasCacheControlDirective('max-stale')) {
137+
$response->headers->addCacheControlDirective('max-stale', $this->toSeconds($cache->maxStale));
138+
}
139+
140+
if (null !== $cache->staleWhileRevalidate && !$response->headers->hasCacheControlDirective('stale-while-revalidate')) {
141+
$response->headers->addCacheControlDirective('stale-while-revalidate', $this->toSeconds($cache->staleWhileRevalidate));
142+
}
143+
144+
if (null !== $cache->staleIfError && !$response->headers->hasCacheControlDirective('stale-if-error')) {
145+
$response->headers->addCacheControlDirective('stale-if-error', $this->toSeconds($cache->staleIfError));
146+
}
147+
148+
if (null !== $cache->expires && !$response->headers->has('Expires')) {
149+
$response->setExpires(new \DateTimeImmutable('@'.strtotime($cache->expires, time())));
150+
}
151+
152+
if (!$hasVary && $cache->vary) {
153+
$response->setVary($cache->vary, false);
154+
}
155+
}
156+
157+
foreach ($attributes as $cache) {
158+
if (true === $cache->public) {
159+
$response->setPublic();
160+
}
161+
162+
if (false === $cache->public) {
163+
$response->setPrivate();
164+
}
165+
}
166+
}
167+
168+
public static function getSubscribedEvents(): array
169+
{
170+
return [
171+
KernelEvents::CONTROLLER => ['onKernelController', 10],
172+
KernelEvents::RESPONSE => ['onKernelResponse', -10],
173+
];
174+
}
175+
176+
private function getExpressionLanguage(): ExpressionLanguage
177+
{
178+
if (!$this->expressionLanguage && !class_exists(ExpressionLanguage::class)) {
179+
throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
180+
}
181+
182+
return $this->expressionLanguage ??= new ExpressionLanguage();
183+
}
184+
185+
private function toSeconds(int|string $time): int
186+
{
187+
if (!is_numeric($time)) {
188+
$now = time();
189+
$time = strtotime($time, $now) - $now;
190+
}
191+
192+
return $time;
193+
}
194+
}

0 commit comments

Comments
 (0)