Skip to content

Commit 5783c99

Browse files
committed
Add abstracted tag handling
Aims to allow support for tagging also on Symfomy HTTP Cache and potentially other caches by limiting the feature to focus on only tags and not any random header. Closes #234
1 parent 33db556 commit 5783c99

File tree

8 files changed

+149
-20
lines changed

8 files changed

+149
-20
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ This library integrates your PHP applications with HTTP caching proxies such as
1212
Use this library to send invalidation requests from your application to the caching proxy
1313
and to test your caching and invalidation code against a Varnish setup.
1414

15+
It does this by abstracting some caching concepts and attempting to make sure these
16+
can be supported across Varnish, Nginx and Symfony HTTPCache.
17+
1518
If you use Symfony2, have a look at the
1619
[FOSHttpCacheBundle](https://github.com/FriendsOfSymfony/FOSHttpCacheBundle).
1720
The bundle provides the invalidator as a service, along with a number of
@@ -22,6 +25,7 @@ Features
2225

2326
* Send [cache invalidation requests](http://foshttpcache.readthedocs.org/en/stable/cache-invalidator.html)
2427
with minimal impact on performance.
28+
* Cache tagging abstraction use of BAN with Varnish and allowing support for other caching proxies in the future.
2529
* Use the built-in support for [Varnish](http://foshttpcache.readthedocs.org/en/stable/varnish-configuration.html)
2630
3 and 4, [NGINX](http://foshttpcache.readthedocs.org/en/stable/nginx-configuration.html), the
2731
[Symfony reverse proxy from the http-kernel component](http://foshttpcache.readthedocs.org/en/stable/symfony-cache-configuration.html)

src/CacheInvalidator.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@
1717
use FOS\HttpCache\Exception\ProxyUnreachableException;
1818
use FOS\HttpCache\Exception\UnsupportedProxyOperationException;
1919
use FOS\HttpCache\ProxyClient\ProxyClientInterface;
20+
use FOS\HttpCache\ProxyClient\Invalidation\TagsInterface;
2021
use FOS\HttpCache\ProxyClient\Invalidation\BanInterface;
2122
use FOS\HttpCache\ProxyClient\Invalidation\PurgeInterface;
2223
use FOS\HttpCache\ProxyClient\Invalidation\RefreshInterface;
2324
use Symfony\Component\EventDispatcher\EventDispatcher;
2425
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
25-
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
2626

2727
/**
2828
* Manages HTTP cache invalidation.
2929
*
3030
* @author David de Boer <[email protected]>
3131
* @author David Buchmann <[email protected]>
32+
* @author André Rømcke <[email protected]>
3233
*/
3334
class CacheInvalidator
3435
{
@@ -47,6 +48,11 @@ class CacheInvalidator
4748
*/
4849
const INVALIDATE = 'invalidate';
4950

51+
/**
52+
* Value to check support of invalidateTags operation.
53+
*/
54+
const TAGS = 'tags';
55+
5056
/**
5157
* @var ProxyClientInterface
5258
*/
@@ -90,6 +96,8 @@ public function supports($operation)
9096
return $this->cache instanceof RefreshInterface;
9197
case self::INVALIDATE:
9298
return $this->cache instanceof BanInterface;
99+
case self::TAGS:
100+
return $this->cache instanceof TagsInterface;
93101
default:
94102
throw new InvalidArgumentException('Unknown operation ' . $operation);
95103
}
@@ -197,6 +205,27 @@ public function invalidate(array $headers)
197205
return $this;
198206
}
199207

208+
/**
209+
* Remove/Expire cache objects based on cache tags
210+
*
211+
* @see TagsInterface::tags()
212+
*
213+
* @param array $tags Tags that should be removed/expired from the cache
214+
* @param string $tagsHeader Name of the HTTP header used to provide the tags
215+
*
216+
* @throws UnsupportedProxyOperationException If HTTP cache does not support Tags invalidation
217+
*
218+
* @return $this
219+
*/
220+
public function invalidateTags(array $tags, $tagsHeader = 'X-Cache-Tags')
221+
{
222+
if (!$this->cache instanceof TagsInterface) {
223+
throw UnsupportedProxyOperationException::cacheDoesNotImplement('Tags');
224+
}
225+
$this->cache->invalidateTags($tags, $tagsHeader);
226+
return $this;
227+
}
228+
200229
/**
201230
* Invalidate URLs based on a regular expression for the URI, an optional
202231
* content type and optional limit to certain hosts.

src/Handler/TagHandler.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
namespace FOS\HttpCache\Handler;
1313

1414
use FOS\HttpCache\CacheInvalidator;
15+
use FOS\HttpCache\Exception\InvalidArgumentException;
1516
use FOS\HttpCache\Exception\UnsupportedProxyOperationException;
1617

1718
/**
1819
* Handler for cache tagging.
1920
*
2021
* @author David de Boer <[email protected]>
2122
* @author David Buchmann <[email protected]>
23+
* @author André Rømcke <[email protected]>
2224
*/
2325
class TagHandler
2426
{
@@ -47,8 +49,8 @@ class TagHandler
4749
*/
4850
public function __construct(CacheInvalidator $invalidator, $tagsHeader = 'X-Cache-Tags')
4951
{
50-
if (!$invalidator->supports(CacheInvalidator::INVALIDATE)) {
51-
throw UnsupportedProxyOperationException::cacheDoesNotImplement('BAN');
52+
if (!$invalidator->supports(CacheInvalidator::TAGS)) {
53+
throw UnsupportedProxyOperationException::cacheDoesNotImplement('Tags');
5254
}
5355
$this->invalidator = $invalidator;
5456
$this->tagsHeader = $tagsHeader;
@@ -107,7 +109,7 @@ public function addTags(array $tags)
107109
* tag header.
108110
*
109111
* The cache manager is told to invalidate all content with the specified
110-
* tags. The invalidaton requests are sent to the caching proxy on kernel
112+
* tags. The invalidation requests are sent to the caching proxy on kernel
111113
* termination (both for web requests and command runs). To immediately
112114
* send the invalidation request, call the CacheManager::flush() method.
113115
*
@@ -117,9 +119,14 @@ public function addTags(array $tags)
117119
*/
118120
public function invalidateTags(array $tags)
119121
{
120-
$tagExpression = sprintf('(%s)(,.+)?$', implode('|', array_map('preg_quote', $this->escapeTags($tags))));
121-
$headers = [$this->tagsHeader => $tagExpression];
122-
$this->invalidator->invalidate($headers);
122+
if (empty($tags)) {
123+
return $this;
124+
}
125+
126+
$this->invalidator->invalidateTags(
127+
$this->escapeTags($tags),
128+
$this->tagsHeader
129+
);
123130

124131
return $this;
125132
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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\ProxyClient\Invalidation;
13+
14+
use FOS\HttpCache\ProxyClient\ProxyClientInterface;
15+
16+
/**
17+
* An HTTP cache that supports invalidation by a cache tag, that is, removing, or expiring
18+
* objects from the cache tagged with a given tag or set of tags.
19+
*/
20+
interface TagsInterface extends ProxyClientInterface
21+
{
22+
/**
23+
* Remove/Expire cache objects based on cache tags
24+
*
25+
* @param array $tags Tags that should be removed/expired from the cache
26+
* @param string $tagsHeader Name of the HTTP header used to provide the tags.
27+
*
28+
* @return $this
29+
*/
30+
public function invalidateTags(array $tags, $tagsHeader = 'X-Cache-Tags');
31+
}

src/ProxyClient/Varnish.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use FOS\HttpCache\ProxyClient\Invalidation\BanInterface;
1717
use FOS\HttpCache\ProxyClient\Invalidation\PurgeInterface;
1818
use FOS\HttpCache\ProxyClient\Invalidation\RefreshInterface;
19+
use FOS\HttpCache\ProxyClient\Invalidation\TagsInterface;
1920
use FOS\HttpCache\ProxyClient\Request\InvalidationRequest;
2021
use FOS\HttpCache\ProxyClient\Request\RequestQueue;
2122
use Http\Adapter\HttpAdapter;
@@ -25,7 +26,7 @@
2526
*
2627
* @author David de Boer <[email protected]>
2728
*/
28-
class Varnish extends AbstractProxyClient implements BanInterface, PurgeInterface, RefreshInterface
29+
class Varnish extends AbstractProxyClient implements BanInterface, PurgeInterface, RefreshInterface, TagsInterface
2930
{
3031
const HTTP_METHOD_BAN = 'BAN';
3132
const HTTP_METHOD_PURGE = 'PURGE';
@@ -86,6 +87,16 @@ public function setDefaultBanHeader($name, $value)
8687
$this->defaultBanHeaders[$name] = $value;
8788
}
8889

90+
/**
91+
* {@inheritdoc}
92+
*/
93+
public function invalidateTags(array $tags, $tagsHeader = 'X-Cache-Tags')
94+
{
95+
$tagExpression = sprintf('(%s)(,.+)?$', implode('|', array_map('preg_quote', $tags)));
96+
97+
return $this->ban([$tagsHeader => $tagExpression]);
98+
}
99+
89100
/**
90101
* {@inheritdoc}
91102
*/

tests/Unit/CacheInvalidatorTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public function testSupportsTrue()
3434
$this->assertTrue($cacheInvalidator->supports(CacheInvalidator::PATH));
3535
$this->assertTrue($cacheInvalidator->supports(CacheInvalidator::REFRESH));
3636
$this->assertTrue($cacheInvalidator->supports(CacheInvalidator::INVALIDATE));
37+
$this->assertTrue($cacheInvalidator->supports(CacheInvalidator::TAGS));
3738
}
3839

3940
public function testSupportsFalse()
@@ -45,6 +46,7 @@ public function testSupportsFalse()
4546
$this->assertFalse($cacheInvalidator->supports(CacheInvalidator::PATH));
4647
$this->assertFalse($cacheInvalidator->supports(CacheInvalidator::REFRESH));
4748
$this->assertFalse($cacheInvalidator->supports(CacheInvalidator::INVALIDATE));
49+
$this->assertFalse($cacheInvalidator->supports(CacheInvalidator::TAGS));
4850
}
4951

5052
/**
@@ -108,6 +110,23 @@ public function testInvalidate()
108110
$cacheInvalidator->invalidate($headers);
109111
}
110112

113+
public function testInvalidateTags()
114+
{
115+
$tags = [
116+
'post-8',
117+
'post-type-2'
118+
];
119+
120+
$tagHandler = \Mockery::mock('\FOS\HttpCache\ProxyClient\Invalidation\TagsInterface')
121+
->shouldReceive('invalidateTags')
122+
->with($tags, $tagsHeader = 'X-Cache-TRex')
123+
->once()
124+
->getMock();
125+
126+
$cacheInvalidator = new CacheInvalidator($tagHandler);
127+
$cacheInvalidator->invalidateTags($tags, $tagsHeader);
128+
}
129+
111130
public function testInvalidateRegex()
112131
{
113132
$ban = \Mockery::mock('\FOS\HttpCache\ProxyClient\Invalidation\BanInterface')
@@ -148,6 +167,12 @@ public function testMethodException()
148167
} catch (UnsupportedProxyOperationException $e) {
149168
// success
150169
}
170+
try {
171+
$cacheInvalidator->invalidateTags([]);
172+
$this->fail('Expected exception');
173+
} catch (UnsupportedProxyOperationException $e) {
174+
// success
175+
}
151176
}
152177

153178
/**

tests/Unit/Handler/TagHandlerTest.php

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ class TagHandlerTest extends \PHPUnit_Framework_TestCase
1919
public function testInvalidateTags()
2020
{
2121
$cacheInvalidator = \Mockery::mock('FOS\HttpCache\CacheInvalidator')
22-
->shouldReceive('invalidate')
23-
->with(['X-Cache-Tags' => '(post\-1|posts)(,.+)?$'])
22+
->shouldReceive('invalidateTags')
23+
->with(['post-1', 'posts'], 'X-Cache-Tags')
2424
->once()
2525
->shouldReceive('supports')
26-
->with(CacheInvalidator::INVALIDATE)
26+
->with(CacheInvalidator::TAGS)
2727
->once()
2828
->andReturn(true)
2929
->getMock();
@@ -35,11 +35,11 @@ public function testInvalidateTags()
3535
public function testInvalidateTagsCustomHeader()
3636
{
3737
$cacheInvalidator = \Mockery::mock('FOS\HttpCache\CacheInvalidator')
38-
->shouldReceive('invalidate')
39-
->with(['Custom-Tags' => '(post\-1)(,.+)?$'])
38+
->shouldReceive('invalidateTags')
39+
->with(['post-1'], 'Custom-Tags')
4040
->once()
4141
->shouldReceive('supports')
42-
->with(CacheInvalidator::INVALIDATE)
42+
->with(CacheInvalidator::TAGS)
4343
->once()
4444
->andReturn(true)
4545
->getMock();
@@ -52,11 +52,11 @@ public function testInvalidateTagsCustomHeader()
5252
public function testEscapingTags()
5353
{
5454
$cacheInvalidator = \Mockery::mock('FOS\HttpCache\CacheInvalidator')
55-
->shouldReceive('invalidate')
56-
->with(['X-Cache-Tags' => '(post_test)(,.+)?$'])
55+
->shouldReceive('invalidateTags')
56+
->with(['post_test'], 'X-Cache-Tags')
5757
->once()
5858
->shouldReceive('supports')
59-
->with(CacheInvalidator::INVALIDATE)
59+
->with(CacheInvalidator::TAGS)
6060
->once()
6161
->andReturn(true)
6262
->getMock();
@@ -72,7 +72,7 @@ public function testInvalidateUnsupported()
7272
{
7373
$cacheInvalidator = \Mockery::mock('FOS\HttpCache\CacheInvalidator')
7474
->shouldReceive('supports')
75-
->with(CacheInvalidator::INVALIDATE)
75+
->with(CacheInvalidator::TAGS)
7676
->once()
7777
->andReturn(false)
7878
->getMock();
@@ -84,7 +84,7 @@ public function testTagResponse()
8484
{
8585
$cacheInvalidator = \Mockery::mock('FOS\HttpCache\CacheInvalidator')
8686
->shouldReceive('supports')
87-
->with(CacheInvalidator::INVALIDATE)
87+
->with(CacheInvalidator::TAGS)
8888
->once()
8989
->andReturn(true)
9090
->getMock();

tests/Unit/ProxyClient/VarnishTest.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@ public function testBanPathEmptyHost()
9999
$varnish->banPath('/articles/.*', 'text/html', $hosts);
100100
}
101101

102+
public function testTagsHeaders()
103+
{
104+
$varnish = new Varnish(['127.0.0.1:123'], 'fos.lo', $this->client);
105+
$varnish->setDefaultBanHeaders(
106+
['A' => 'B']
107+
);
108+
$varnish->setDefaultBanHeader('Test', '.*');
109+
$varnish->invalidateTags(['post-1', 'post-type-3'], 'X-Cache-Tags')->flush();
110+
111+
$requests = $this->getRequests();
112+
113+
$this->assertCount(1, $requests);
114+
$this->assertEquals('BAN', $requests[0]->getMethod());
115+
116+
$this->assertEquals('(post\-1|post\-type\-3)(,.+)?$', $requests[0]->getHeaderLine('X-Cache-Tags'));
117+
$this->assertEquals('fos.lo', $requests[0]->getHeaderLine('Host'));
118+
119+
// This being taken into account, might not be expected from user POV?
120+
$this->assertEquals('.*', $requests[0]->getHeaderLine('Test'));
121+
$this->assertEquals('B', $requests[0]->getHeaderLine('A'));
122+
}
123+
102124
public function testPurge()
103125
{
104126
$ips = ['127.0.0.1:8080', '123.123.123.2'];
@@ -142,7 +164,7 @@ protected function setUp()
142164
}
143165

144166
/**
145-
* @return array|RequestInterface[]
167+
* @return array|\Psr\Http\Message\RequestInterface[]
146168
*/
147169
protected function getRequests()
148170
{

0 commit comments

Comments
 (0)