Skip to content

Commit cdaf00a

Browse files
committed
Symfony HttpCache listener for custom ttl header
1 parent cf67da5 commit cdaf00a

File tree

5 files changed

+264
-6
lines changed

5 files changed

+264
-6
lines changed

src/SymfonyCache/CacheEvent.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ class CacheEvent extends Event
4141
/**
4242
* Make sure your $kernel implements CacheInvalidationInterface
4343
*
44-
* @param CacheInvalidationInterface$kernel The kernel raising with this event.
45-
* @param Request $request The request being processed.
44+
* @param CacheInvalidationInterface $kernel The kernel raising with this event.
45+
* @param Request $request The request being processed.
46+
* @param Response $response The response, if available
4647
*/
47-
public function __construct(CacheInvalidationInterface $kernel, Request $request)
48+
public function __construct(CacheInvalidationInterface $kernel, Request $request, Response $response = null)
4849
{
4950
$this->kernel = $kernel;
5051
$this->request = $request;
52+
$this->response = $response;
5153
}
5254

5355
/**
@@ -71,6 +73,9 @@ public function getRequest()
7173
}
7274

7375
/**
76+
* Events that occur after the response is created provide the default response.
77+
* Event listeners can also set the response to make it available here.
78+
*
7479
* @return Response|null The response if one was set.
7580
*/
7681
public function getResponse()

src/SymfonyCache/EventDispatchingHttpCache.php

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace FOS\HttpCache\SymfonyCache;
1313

14+
use Symfony\Component\HttpFoundation\Response;
1415
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
1516
use Symfony\Component\EventDispatcher\EventDispatcher;
1617
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -64,19 +65,56 @@ public function addSubscriber(EventSubscriberInterface $subscriber)
6465
/**
6566
* {@inheritDoc}
6667
*
67-
* Adding the Events::PRE_HANDLE event.
68+
* Adding the Events::PRE_HANDLE and Events::POST_HANDLE events.
6869
*/
6970
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
7071
{
7172
if ($this->getEventDispatcher()->hasListeners(Events::PRE_HANDLE)) {
7273
$event = new CacheEvent($this, $request);
7374
$this->getEventDispatcher()->dispatch(Events::PRE_HANDLE, $event);
7475
if ($event->getResponse()) {
75-
return $event->getResponse();
76+
return $this->dispatchPostHandle($request, $event->getResponse());
7677
}
7778
}
7879

79-
return parent::handle($request, $type, $catch);
80+
$response = parent::handle($request, $type, $catch);
81+
82+
return $this->dispatchPostHandle($request, $response);
83+
}
84+
85+
/**
86+
* {@inheritDoc}
87+
*
88+
* Trigger event to alter response before storing it in the cache.
89+
*/
90+
protected function store(Request $request, Response $response)
91+
{
92+
if ($this->getEventDispatcher()->hasListeners(Events::PRE_STORE)) {
93+
$event = new CacheEvent($this, $request, $response);
94+
$this->getEventDispatcher()->dispatch(Events::PRE_STORE, $event);
95+
$response = $event->getResponse();
96+
}
97+
98+
parent::store($request, $response);
99+
}
100+
101+
/**
102+
* Dispatch the POST_HANDLE event if needed.
103+
*
104+
* @param Request $request
105+
* @param Response $response
106+
*
107+
* @return Response The response to return which might be altered by a POST_HANDLE listener.
108+
*/
109+
private function dispatchPostHandle(Request $request, Response $response)
110+
{
111+
if ($this->getEventDispatcher()->hasListeners(Events::POST_HANDLE)) {
112+
$event = new CacheEvent($this, $request, $response);
113+
$this->getEventDispatcher()->dispatch(Events::POST_HANDLE, $event);
114+
$response = $event->getResponse();
115+
}
116+
117+
return $response;
80118
}
81119

82120
/**

src/SymfonyCache/Events.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@
1717
final class Events
1818
{
1919
const PRE_HANDLE = 'fos_http_cache.pre_handle';
20+
const POST_HANDLE = 'fos_http_cache.post_handle';
2021
const PRE_INVALIDATE = 'fos_http_cache.pre_invalidate';
22+
const PRE_STORE = 'fos_http_cache.pre_store';
2123
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
namespace FOS\HttpCache\SymfonyCache;
4+
5+
use FOS\HttpCache\SymfonyCache\CacheEvent;
6+
use FOS\HttpCache\SymfonyCache\Events;
7+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
8+
9+
/**
10+
* Custom TTL handler for the symfony built-in HttpCache.
11+
*
12+
* This allows to use a custom header to control time to live in HttpCache and
13+
* keep s-maxage for 3rd party proxies.
14+
*
15+
* @author David Buchmann <[email protected]>
16+
*
17+
* {@inheritdoc}
18+
*/
19+
class ReverseProxyTtlListener implements EventSubscriberInterface
20+
{
21+
/**
22+
* @var string
23+
*/
24+
private $ttlHeader;
25+
26+
/**
27+
* Header used for backing up the s-maxage
28+
*
29+
* @var string
30+
*/
31+
const SMAXAGE_BACKUP = 'FOS-Smaxage-Backup';
32+
33+
/**
34+
* @param string $ttlHeader The header that is used to specify the TTL header
35+
*/
36+
public function __construct($ttlHeader = 'X-Reverse-Proxy-TTL')
37+
{
38+
$this->ttlHeader = $ttlHeader;
39+
}
40+
41+
/**
42+
* Use the TTL from the custom header rather than the default one.
43+
*
44+
* If there is such a header, the original s_maxage is backed up to the
45+
* static::SMAXAGE_BACKUP header.
46+
*
47+
* @param CacheEvent $e
48+
*/
49+
public function useCustomTtl(CacheEvent $e)
50+
{
51+
$response = $e->getResponse();
52+
if (!$response->headers->has($this->ttlHeader)) {
53+
return;
54+
}
55+
$backup = $response->headers->hasCacheControlDirective('s-maxage')
56+
? $response->headers->getCacheControlDirective('s-maxage')
57+
: 'false'
58+
;
59+
$response->headers->set(static::SMAXAGE_BACKUP, $backup);
60+
$response->setTtl($response->headers->get($this->ttlHeader));
61+
}
62+
63+
/**
64+
* Remove the custom TTL header and restore s_maxage from the backup.
65+
*
66+
* @param CacheEvent $e
67+
*/
68+
public function cleanResponse(CacheEvent $e)
69+
{
70+
$response = $e->getResponse();
71+
if (!$response->headers->has($this->ttlHeader)
72+
&& !$response->headers->has(static::SMAXAGE_BACKUP)
73+
) {
74+
return;
75+
}
76+
77+
if ($response->headers->has(static::SMAXAGE_BACKUP)) {
78+
$smaxage = $response->headers->get(static::SMAXAGE_BACKUP);
79+
if ('false' === $smaxage) {
80+
$response->headers->removeCacheControlDirective('s-maxage');
81+
} else {
82+
$response->headers->addCacheControlDirective('s-maxage', $smaxage);
83+
}
84+
}
85+
$response->headers->remove($this->ttlHeader);
86+
$response->headers->remove(static::SMAXAGE_BACKUP);
87+
}
88+
89+
/**
90+
* {@inheritdoc}
91+
*/
92+
public static function getSubscribedEvents()
93+
{
94+
return array(
95+
Events::PRE_STORE => 'useCustomTtl',
96+
Events::POST_HANDLE => 'cleanResponse',
97+
);
98+
}
99+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCache package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
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 FOS\HttpCache\Tests\Unit\SymfonyCache;
13+
14+
use FOS\HttpCache\SymfonyCache\CacheEvent;
15+
use FOS\HttpCache\SymfonyCache\PurgeSubscriber;
16+
use FOS\HttpCache\SymfonyCache\ReverseProxyTtlListener;
17+
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpFoundation\RequestMatcher;
19+
use Symfony\Component\HttpFoundation\Response;
20+
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
21+
22+
class ReverseProxyTtlListenerTest extends \PHPUnit_Framework_TestCase
23+
{
24+
/**
25+
* @var HttpCache|\PHPUnit_Framework_MockObject_MockObject
26+
*/
27+
private $kernel;
28+
29+
public function setUp()
30+
{
31+
$this->kernel = $this
32+
->getMockBuilder('FOS\HttpCache\SymfonyCache\CacheInvalidationInterface')
33+
->disableOriginalConstructor()
34+
->getMock()
35+
;
36+
}
37+
38+
public function testCustomTtl()
39+
{
40+
$ttlListener = new ReverseProxyTtlListener();
41+
$request = Request::create('http://example.com/foo', 'GET');
42+
$response = new Response('', 200, array(
43+
'X-Reverse-Proxy-TTL' => '120',
44+
'Cache-Control' => 's-maxage=60, max-age=30',
45+
));
46+
$event = new CacheEvent($this->kernel, $request, $response);
47+
48+
$ttlListener->useCustomTtl($event);
49+
$response = $event->getResponse();
50+
51+
$this->assertInstanceOf('Symfony\\Component\\HttpFoundation\\Response', $response);
52+
$this->assertSame('120', $response->headers->getCacheControlDirective('s-maxage'));
53+
$this->assertSame('60', $response->headers->get(ReverseProxyTtlListener::SMAXAGE_BACKUP));
54+
}
55+
56+
public function testCustomTtlNoSmaxage()
57+
{
58+
$ttlListener = new ReverseProxyTtlListener();
59+
$request = Request::create('http://example.com/foo', 'GET');
60+
$response = new Response('', 200, array(
61+
'X-Reverse-Proxy-TTL' => '120',
62+
'Cache-Control' => 'max-age=30',
63+
));
64+
$event = new CacheEvent($this->kernel, $request, $response);
65+
66+
$ttlListener->useCustomTtl($event);
67+
$response = $event->getResponse();
68+
69+
$this->assertInstanceOf('Symfony\\Component\\HttpFoundation\\Response', $response);
70+
$this->assertSame('120', $response->headers->getCacheControlDirective('s-maxage'));
71+
$this->assertSame('false', $response->headers->get(ReverseProxyTtlListener::SMAXAGE_BACKUP));
72+
}
73+
74+
public function testCleanup()
75+
{
76+
$ttlListener = new ReverseProxyTtlListener();
77+
$request = Request::create('http://example.com/foo', 'GET');
78+
$response = new Response('', 200, array(
79+
'X-Reverse-Proxy-TTL' => '120',
80+
'Cache-Control' => 's-maxage=120, max-age=30',
81+
ReverseProxyTtlListener::SMAXAGE_BACKUP => '60',
82+
));
83+
$event = new CacheEvent($this->kernel, $request, $response);
84+
85+
$ttlListener->cleanResponse($event);
86+
$response = $event->getResponse();
87+
88+
$this->assertInstanceOf('Symfony\\Component\\HttpFoundation\\Response', $response);
89+
$this->assertTrue($response->headers->hasCacheControlDirective('s-maxage'));
90+
$this->assertSame('60', $response->headers->getCacheControlDirective('s-maxage'));
91+
$this->assertFalse($response->headers->has('X-Reverse-Proxy-TTL'));
92+
$this->assertFalse($response->headers->has(ReverseProxyTtlListener::SMAXAGE_BACKUP));
93+
}
94+
95+
public function testCleanupNoSmaxage()
96+
{
97+
$ttlListener = new ReverseProxyTtlListener();
98+
$request = Request::create('http://example.com/foo', 'GET');
99+
$response = new Response('', 200, array(
100+
'X-Reverse-Proxy-TTL' => '120',
101+
'Cache-Control' => 's-maxage: 120, max-age: 30',
102+
ReverseProxyTtlListener::SMAXAGE_BACKUP => 'false',
103+
));
104+
$event = new CacheEvent($this->kernel, $request, $response);
105+
106+
$ttlListener->cleanResponse($event);
107+
$response = $event->getResponse();
108+
109+
$this->assertInstanceOf('Symfony\\Component\\HttpFoundation\\Response', $response);
110+
$this->assertFalse($response->headers->hasCacheControlDirective('s_maxage'));
111+
$this->assertFalse($response->headers->has('X-Reverse-Proxy-TTL'));
112+
$this->assertFalse($response->headers->has(ReverseProxyTtlListener::SMAXAGE_BACKUP));
113+
}
114+
}

0 commit comments

Comments
 (0)