Skip to content

Commit dba8c46

Browse files
authored
Merge pull request #578 from FriendsOfSymfony/2-to-3
2 to 3
2 parents e823564 + 79081bb commit dba8c46

8 files changed

+158
-15
lines changed

CHANGELOG.md

+38
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@ See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpC
66
3.x
77
===
88

9+
3.1.0
10+
-----
11+
12+
### Symfony HttpCache
13+
14+
* Add events `PRE_FORWARD` and `POST_FORWARD` to allow event listeners to alter
15+
the request before and after it is sent to the backend.
16+
* Changed CustomTtlListener to use the `POST_FORWARD` event instead of
17+
`PRE_STORE`. Using `PRE_STORE`, requests that are not considered cacheable by
18+
Symfony were never cached, even when they had a custom TTL header.
19+
* Add flag `fallbackToSmaxage` to `CustomTtlListener` to allow controlling
20+
fallback to `s-maxage` if custom TTL header is not defined on the response.
21+
* Fix: Do not call store if Response object is not longer cacheable after event
22+
listeners. If you use the custom TTL system, this is only a performance
23+
improvement, because the TTL of the response would still be 0. With a custom
24+
listener that changes the response explicitly to not be cached but does not
25+
change `s-maxage`, this bug might have led to caching responses that should not
26+
have been cached.
27+
928
3.0.0
1029
-----
1130

@@ -21,6 +40,25 @@ See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpC
2140
2.x
2241
===
2342

43+
2.16.0
44+
------
45+
46+
### Symfony HttpCache
47+
48+
* Add events `PRE_FORWARD` and `POST_FORWARD` to allow event listeners to alter
49+
the request before and after it is sent to the backend.
50+
* Changed CustomTtlListener to use the `POST_FORWARD` event instead of
51+
`PRE_STORE`. Using `PRE_STORE`, requests that are not considered cacheable by
52+
Symfony were never cached, even when they had a custom TTL header.
53+
* Add flag `fallbackToSmaxage` to `CustomTtlListener` to allow controlling
54+
fallback to `s-maxage` if custom TTL header is not defined on the response.
55+
* Fix: Do not call store if Response object is not longer cacheable after event
56+
listeners. If you use the custom TTL system, this is only a performance
57+
improvement, because the TTL of the response would still be 0. With a custom
58+
listener that changes the response explicitly to not be cached but does not
59+
change `s-maxage`, this bug might have led to caching responses that should not
60+
have been cached.
61+
2462
2.15.3
2563
------
2664

doc/symfony-cache-configuration.rst

+8
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,14 @@ but you can customize that in the listener constructor::
351351
new CustomTtlListener('My-TTL-Header');
352352

353353
The custom header is removed before sending the response to the client.
354+
You can enable keeping the custom header with the `keepTtlHeader` parameter::
355+
356+
new CustomTtlListener('My-TTL-Header', keepTtlHeader: true);
357+
358+
By default if the custom ttl header is not present, the listener falls back to the s-maxage cache-control directive.
359+
To disable this behavior, you can set the `fallbackToSmaxage` parameter to false::
360+
361+
new CustomTtlListener('My-TTL-Header', fallbackToSmaxage: false);
354362

355363
.. _symfony-cache x-debugging:
356364

src/SymfonyCache/CustomTtlListener.php

+17-9
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public function __construct(
3737
* Keep the custom TTL header on the response for later usage (e.g. debugging).
3838
*/
3939
private readonly bool $keepTtlHeader = false,
40+
/**
41+
* If the custom TTL header is not set, should s-maxage be used?
42+
*/
43+
private readonly bool $fallbackToSmaxage = true,
4044
) {
4145
}
4246

@@ -52,15 +56,22 @@ public function useCustomTtl(CacheEvent $e): void
5256
if (!$response) {
5357
return;
5458
}
55-
if (!$response->headers->has($this->ttlHeader)) {
59+
60+
if (!$response->headers->has($this->ttlHeader)
61+
&& true === $this->fallbackToSmaxage
62+
) {
5663
return;
5764
}
65+
5866
$backup = $response->headers->hasCacheControlDirective('s-maxage')
5967
? $response->headers->getCacheControlDirective('s-maxage')
6068
: 'false'
6169
;
6270
$response->headers->set(static::SMAXAGE_BACKUP, $backup);
63-
$response->setTtl($response->headers->get($this->ttlHeader));
71+
$response->headers->addCacheControlDirective(
72+
's-maxage',
73+
$response->headers->get($this->ttlHeader, 0)
74+
);
6475
}
6576

6677
/**
@@ -69,14 +80,10 @@ public function useCustomTtl(CacheEvent $e): void
6980
public function cleanResponse(CacheEvent $e): void
7081
{
7182
$response = $e->getResponse();
83+
7284
if (!$response) {
7385
return;
7486
}
75-
if (!$response->headers->has($this->ttlHeader)
76-
&& !$response->headers->has(static::SMAXAGE_BACKUP)
77-
) {
78-
return;
79-
}
8087

8188
if ($response->headers->has(static::SMAXAGE_BACKUP)) {
8289
$smaxage = $response->headers->get(static::SMAXAGE_BACKUP);
@@ -85,18 +92,19 @@ public function cleanResponse(CacheEvent $e): void
8592
} else {
8693
$response->headers->addCacheControlDirective('s-maxage', $smaxage);
8794
}
95+
96+
$response->headers->remove(static::SMAXAGE_BACKUP);
8897
}
8998

9099
if (!$this->keepTtlHeader) {
91100
$response->headers->remove($this->ttlHeader);
92101
}
93-
$response->headers->remove(static::SMAXAGE_BACKUP);
94102
}
95103

96104
public static function getSubscribedEvents(): array
97105
{
98106
return [
99-
Events::PRE_STORE => 'useCustomTtl',
107+
Events::POST_FORWARD => 'useCustomTtl',
100108
Events::POST_HANDLE => 'cleanResponse',
101109
];
102110
}

src/SymfonyCache/EventDispatchingHttpCache.php

+15-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ protected function store(Request $request, Response $response): void
9999
{
100100
$response = $this->dispatch(Events::PRE_STORE, $request, $response);
101101

102-
parent::store($request, $response);
102+
// CustomTtlListener or other Listener or Subscribers might have changed the response to become non-cacheable, revalidate.
103+
// Only call store for a cacheable response like Symfony core does: https://github.com/symfony/symfony/blob/v7.1.2/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php#L409
104+
if ($response->isCacheable()) {
105+
parent::store($request, $response);
106+
}
103107
}
104108

105109
/**
@@ -116,6 +120,16 @@ protected function invalidate(Request $request, bool $catch = false): Response
116120
return parent::invalidate($request, $catch);
117121
}
118122

123+
protected function forward(Request $request, bool $catch = false, ?Response $entry = null): Response
124+
{
125+
// do not abort early, if $entry is set this is a validation request
126+
$this->dispatch(Events::PRE_FORWARD, $request, $entry);
127+
128+
$response = parent::forward($request, $catch, $entry);
129+
130+
return $this->dispatch(Events::POST_FORWARD, $request, $response);
131+
}
132+
119133
/**
120134
* Dispatch an event if there are any listeners.
121135
*

src/SymfonyCache/Events.php

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ final class Events
2020

2121
public const POST_HANDLE = 'fos_http_cache.post_handle';
2222

23+
public const PRE_FORWARD = 'fos_http_cache.pre_forward';
24+
25+
public const POST_FORWARD = 'fos_http_cache.post_forward';
26+
2327
public const PRE_INVALIDATE = 'fos_http_cache.pre_invalidate';
2428

2529
public const PRE_STORE = 'fos_http_cache.pre_store';

src/Test/EventDispatchingHttpCacheTestCase.php

+33
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ public function testPreStoreCalled(): void
198198
{
199199
$request = Request::create('/foo', 'GET');
200200
$response = new Response();
201+
$response->setTtl(42);
201202

202203
$httpCache = $this->getHttpCachePartialMock();
203204
$testListener = new TestListener($this, $httpCache, $request);
@@ -220,6 +221,7 @@ public function testPreStoreResponse(): void
220221
$request = Request::create('/foo', 'GET');
221222
$regularResponse = new Response();
222223
$preStoreResponse = new Response();
224+
$preStoreResponse->setTtl(42);
223225

224226
$httpCache = $this->getHttpCachePartialMock();
225227
$testListener = new TestListener($this, $httpCache, $request);
@@ -235,6 +237,37 @@ public function testPreStoreResponse(): void
235237
$this->assertEquals(1, $testListener->preStoreCalls);
236238
}
237239

240+
/**
241+
* Assert that preStore response is used when provided.
242+
*/
243+
public function testPreStoreResponseNoStore()
244+
{
245+
$request = Request::create('/foo', 'GET');
246+
$regularResponse = new Response();
247+
$regularResponse->setTtl(42);
248+
$preStoreResponse = new Response();
249+
$preStoreResponse->setTtl(0);
250+
251+
$httpCache = $this->getHttpCachePartialMock();
252+
$testListener = new TestListener($this, $httpCache, $request);
253+
$testListener->preStoreResponse = $preStoreResponse;
254+
$httpCache->addSubscriber($testListener);
255+
256+
$store = $this->createMock(StoreInterface::class);
257+
$store->expects($this->never())->method('write');
258+
259+
$refHttpCache = new \ReflectionClass(HttpCache::class);
260+
$refStore = $refHttpCache->getProperty('store');
261+
$refStore->setAccessible(true);
262+
$refStore->setValue($httpCache, $store);
263+
264+
$refHttpCache = new \ReflectionObject($httpCache);
265+
$method = $refHttpCache->getMethod('store');
266+
$method->setAccessible(true);
267+
$method->invokeArgs($httpCache, [$request, $regularResponse]);
268+
$this->assertEquals(1, $testListener->preStoreCalls);
269+
}
270+
238271
/**
239272
* Assert that preInvalidate is called.
240273
*/

tests/Functional/Symfony/EventDispatchingHttpCacheTest.php

+4-3
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ public function testEventListeners(): void
4242

4343
$httpKernel = \Mockery::mock(HttpKernelInterface::class)
4444
->shouldReceive('handle')
45-
->withArgs([$request, HttpKernelInterface::MAIN_REQUEST, true])
4645
->andReturn($expectedResponse)
4746
->getMock();
4847
$store = \Mockery::mock(StoreInterface::class)
48+
->shouldReceive('lookup')->andReturn(null)->times(1)
49+
->shouldReceive('write')->times(1)
50+
->shouldReceive('unlock')->times(1)
4951
// need to declare the cleanup function explicitly to avoid issue between register_shutdown_function and mockery
50-
->shouldReceive('cleanup')
51-
->atMost(1)
52+
->shouldReceive('cleanup')->atMost(1)
5253
->getMock();
5354
$kernel = new AppCache($httpKernel, $store);
5455
$kernel->addSubscriber(new CustomTtlListener());

tests/Unit/SymfonyCache/CustomTtlListenerTest.php

+39-2
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,24 @@ public function testNoCustomTtl(): void
8484
$this->assertFalse($response->headers->has(CustomTtlListener::SMAXAGE_BACKUP));
8585
}
8686

87-
public function testCleanup(): void
87+
public function testNoCustomTtlNoFallback()
88+
{
89+
$ttlListener = new CustomTtlListener('X-Reverse-Proxy-TTL', false, false);
90+
$request = Request::create('http://example.com/foo', 'GET');
91+
$response = new Response('', 200, [
92+
'Cache-Control' => 'max-age=30, s-maxage=33',
93+
]);
94+
$event = new CacheEvent($this->kernel, $request, $response);
95+
96+
$ttlListener->useCustomTtl($event);
97+
$response = $event->getResponse();
98+
99+
$this->assertInstanceOf(Response::class, $response);
100+
$this->assertSame('0', $response->headers->getCacheControlDirective('s-maxage'));
101+
$this->assertTrue($response->headers->has(CustomTtlListener::SMAXAGE_BACKUP));
102+
}
103+
104+
public function testCleanup()
88105
{
89106
$ttlListener = new CustomTtlListener();
90107
$request = Request::create('http://example.com/foo', 'GET');
@@ -105,7 +122,27 @@ public function testCleanup(): void
105122
$this->assertFalse($response->headers->has(CustomTtlListener::SMAXAGE_BACKUP));
106123
}
107124

108-
public function testCleanupNoSmaxage(): void
125+
public function testCleanupNoReverseProxyTtl()
126+
{
127+
$ttlListener = new CustomTtlListener();
128+
$request = Request::create('http://example.com/foo', 'GET');
129+
$response = new Response('', 200, [
130+
'Cache-Control' => 's-maxage=0, max-age=30',
131+
CustomTtlListener::SMAXAGE_BACKUP => '60',
132+
]);
133+
$event = new CacheEvent($this->kernel, $request, $response);
134+
135+
$ttlListener->cleanResponse($event);
136+
$response = $event->getResponse();
137+
138+
$this->assertInstanceOf(Response::class, $response);
139+
$this->assertTrue($response->headers->hasCacheControlDirective('s-maxage'));
140+
$this->assertSame('60', $response->headers->getCacheControlDirective('s-maxage'));
141+
$this->assertFalse($response->headers->has('X-Reverse-Proxy-TTL'));
142+
$this->assertFalse($response->headers->has(CustomTtlListener::SMAXAGE_BACKUP));
143+
}
144+
145+
public function testCleanupNoSmaxage()
109146
{
110147
$ttlListener = new CustomTtlListener();
111148
$request = Request::create('http://example.com/foo', 'GET');

0 commit comments

Comments
 (0)