Skip to content

Commit a2fce47

Browse files
authored
Merge pull request #440 from FriendsOfSymfony/clear-cache
Clear cache with ClearCapable
2 parents 3bec43e + e185491 commit a2fce47

File tree

12 files changed

+245
-19
lines changed

12 files changed

+245
-19
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ Changelog
33

44
See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpCache/releases).
55

6+
2.6.0 (unreleased)
7+
------------------
8+
9+
### Cache Clear
10+
11+
* Added: ClearCapable to clear the whole cache in one efficient call. Currently
12+
supported only by the Symfony HttpCache.
13+
614
2.5.4
715
-----
816

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
},
5858
"extra": {
5959
"branch-alias": {
60-
"dev-master": "2.5.x-dev"
60+
"dev-master": "2.6.x-dev"
6161
}
6262
}
6363
}

doc/invalidation-introduction.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,8 @@ all methods, please refer to proxy specific documentation for the details.
148148
invalidation, such as selecting content to be banned by regular expressions.
149149
This opens the way for powerful invalidation schemes, such as tagging cache
150150
entries.
151+
152+
Clear
153+
Clearing a cache means removing all its cache entries completely. It can be
154+
used for a more efficient cache reset rather than a ban that matches every
155+
request or purging every URL individually.

doc/proxy-clients.rst

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,25 @@ Supported invalidation methods
1919
Not all clients support all :ref:`invalidation methods <invalidation methods>`.
2020
This table provides of methods supported by each proxy client:
2121

22-
============= ======= ======= ======= =======
23-
Client Purge Refresh Ban Tagging
24-
============= ======= ======= ======= =======
22+
============= ======= ======= ======= ======= =======
23+
Client Purge Refresh Ban Tagging Clear
24+
============= ======= ======= ======= ======= =======
2525
Varnish ✓ ✓ ✓ ✓
2626
NGINX ✓ ✓
27-
Symfony Cache ✓ ✓ ✓
27+
Symfony Cache ✓ ✓ ✓ (1) ✓ (1)
2828
Noop ✓ ✓ ✓ ✓
2929
Multiplexer ✓ ✓ ✓ ✓
30-
============= ======= ======= ======= =======
30+
============= ======= ======= ======= ======= =======
3131

32-
Of course, you can also implement your own client for other needs. Have a look
32+
(1): Only when using `Toflar Psr6Store`_.
33+
34+
If needed, you can also implement your own client for other needs. Have a look
3335
at the interfaces in namespace ``FOS\HttpCache\ProxyClient\Invalidation``.
3436

37+
"Clear" can be emulated by "Ban" with a request that matches everything. If
38+
both are available, "Clear" is preferred as it can be implemented by the
39+
caching proxy more efficiently.
40+
3541
.. _client setup:
3642

3743
Setup
@@ -315,11 +321,11 @@ Implementation Notes
315321
--------------------
316322

317323
Each client is an implementation of :source:`ProxyClient <src/ProxyClient/ProxyClient.php>`.
318-
All other interfaces, ``PurgeCapable``, ``RefreshCapable``, ``BanCapable`` and
319-
``TagCapable``, extend this ``ProxyClient``. So each client implements at least
320-
one of the three :ref:`invalidation methods <invalidation methods>` depending on
324+
All other interfaces, ``PurgeCapable``, ``RefreshCapable``, ``BanCapable``, ``TagCapable``
325+
and ``ClearCapable`` extend this ``ProxyClient``. So each client implements at least
326+
one of the :ref:`invalidation methods <invalidation methods>` depending on
321327
the proxy server’s abilities. To interact with a proxy client directly, refer to
322-
the doc comments on the interfaces.
328+
the phpdoc on the interfaces.
323329

324330
The ``ProxyClient`` has one method: ``flush()``. After collecting
325331
invalidation requests, ``flush()`` needs to be called to actually send the
@@ -332,3 +338,4 @@ requests.
332338
.. _in the HTTPlug documentation: http://php-http.readthedocs.io/en/latest/clients.html
333339
.. _HTTPlug plugins: http://php-http.readthedocs.io/en/latest/plugins/index.html
334340
.. _message factory and URI factory: http://php-http.readthedocs.io/en/latest/message/message-factory.html
341+
.. _Toflar Psr6Store: https://github.com/Toflar/psr6-symfony-http-cache-store

src/CacheInvalidator.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use FOS\HttpCache\Exception\ProxyUnreachableException;
1818
use FOS\HttpCache\Exception\UnsupportedProxyOperationException;
1919
use FOS\HttpCache\ProxyClient\Invalidation\BanCapable;
20+
use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable;
2021
use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable;
2122
use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable;
2223
use FOS\HttpCache\ProxyClient\Invalidation\TagCapable;
@@ -55,6 +56,11 @@ class CacheInvalidator
5556
*/
5657
const TAGS = 'tags';
5758

59+
/**
60+
* Value to check support of clearCache operation.
61+
*/
62+
const CLEAR = 'clear';
63+
5864
/**
5965
* @var ProxyClient
6066
*/
@@ -105,6 +111,8 @@ public function supports($operation)
105111
}
106112

107113
return $supports;
114+
case self::CLEAR:
115+
return $this->cache instanceof ClearCapable;
108116
default:
109117
throw new InvalidArgumentException('Unknown operation '.$operation);
110118
}
@@ -264,6 +272,24 @@ public function invalidateRegex($path, $contentType = null, $hosts = null)
264272
return $this;
265273
}
266274

275+
/**
276+
* Clear the cache completely.
277+
*
278+
* @throws UnsupportedProxyOperationException if HTTP cache does not support clearing the cache completely
279+
*
280+
* @return $this
281+
*/
282+
public function clearCache()
283+
{
284+
if (!$this->cache instanceof ClearCapable) {
285+
throw UnsupportedProxyOperationException::cacheDoesNotImplement('CLEAR');
286+
}
287+
288+
$this->cache->clear();
289+
290+
return $this;
291+
}
292+
267293
/**
268294
* Send all pending invalidation requests.
269295
*
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\ProxyClient;
15+
16+
/**
17+
* An HTTP cache that supports removing all of its cache entries.
18+
*
19+
* This operation allows to clear proxies that do not support banning.
20+
* Additionally, this operation is likely more efficient than a ban request
21+
* that matches everything.
22+
*/
23+
interface ClearCapable extends ProxyClient
24+
{
25+
/**
26+
* Remove all cache items from this cache.
27+
*
28+
* @return $this
29+
*/
30+
public function clear();
31+
}

src/ProxyClient/MultiplexerClient.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use FOS\HttpCache\Exception\ExceptionCollection;
1515
use FOS\HttpCache\Exception\InvalidArgumentException;
1616
use FOS\HttpCache\ProxyClient\Invalidation\BanCapable;
17+
use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable;
1718
use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable;
1819
use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable;
1920
use FOS\HttpCache\ProxyClient\Invalidation\TagCapable;
@@ -23,7 +24,7 @@
2324
*
2425
* @author Emanuele Panzeri <[email protected]>
2526
*/
26-
class MultiplexerClient implements BanCapable, PurgeCapable, RefreshCapable, TagCapable
27+
class MultiplexerClient implements BanCapable, PurgeCapable, RefreshCapable, TagCapable, ClearCapable
2728
{
2829
/**
2930
* @var ProxyClient[]
@@ -143,6 +144,18 @@ public function refresh($url, array $headers = [])
143144
return $this;
144145
}
145146

147+
/**
148+
* Forwards to all clients.
149+
*
150+
* @return $this
151+
*/
152+
public function clear()
153+
{
154+
$this->invoke(ClearCapable::class, 'clear', []);
155+
156+
return $this;
157+
}
158+
146159
/**
147160
* Invoke the given $method on all available ProxyClients implementing the
148161
* given $interface.

src/ProxyClient/Symfony.php

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

1212
namespace FOS\HttpCache\ProxyClient;
1313

14+
use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable;
1415
use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable;
1516
use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable;
1617
use FOS\HttpCache\ProxyClient\Invalidation\TagCapable;
@@ -26,7 +27,7 @@
2627
* @author David de Boer <[email protected]>
2728
* @author David Buchmann <[email protected]>
2829
*/
29-
class Symfony extends HttpProxyClient implements PurgeCapable, RefreshCapable, TagCapable
30+
class Symfony extends HttpProxyClient implements PurgeCapable, RefreshCapable, TagCapable, ClearCapable
3031
{
3132
const HTTP_METHOD_REFRESH = 'GET';
3233

@@ -56,12 +57,14 @@ protected function configureOptions()
5657
$resolver = parent::configureOptions();
5758
$resolver->setDefaults([
5859
'purge_method' => PurgeListener::DEFAULT_PURGE_METHOD,
60+
'clear_cache_header' => PurgeListener::DEFAULT_CLEAR_CACHE_HEADER,
5961
'tags_method' => PurgeTagsListener::DEFAULT_TAGS_METHOD,
6062
'tags_header' => PurgeTagsListener::DEFAULT_TAGS_HEADER,
6163
'tags_invalidate_path' => '/',
6264
'header_length' => 7500,
6365
]);
6466
$resolver->setAllowedTypes('purge_method', 'string');
67+
$resolver->setAllowedTypes('clear_cache_header', 'string');
6568
$resolver->setAllowedTypes('tags_method', 'string');
6669
$resolver->setAllowedTypes('tags_header', 'string');
6770
$resolver->setAllowedTypes('tags_invalidate_path', 'string');
@@ -71,11 +74,7 @@ protected function configureOptions()
7174
}
7275

7376
/**
74-
* Remove/Expire cache objects based on cache tags.
75-
*
76-
* @param array $tags Tags that should be removed/expired from the cache
77-
*
78-
* @return $this
77+
* {@inheritdoc}
7978
*/
8079
public function invalidateTags(array $tags)
8180
{
@@ -94,4 +93,23 @@ public function invalidateTags(array $tags)
9493

9594
return $this;
9695
}
96+
97+
/**
98+
* {@inheritdoc}
99+
*
100+
* Clearing the cache is implemented with a purge request with a special
101+
* header to indicate that the whole cache should be removed.
102+
*
103+
* @return $this
104+
*/
105+
public function clear()
106+
{
107+
$this->queueRequest(
108+
$this->options['purge_method'], '/',
109+
[$this->options['clear_cache_header'] => 'true'],
110+
false
111+
);
112+
113+
return $this;
114+
}
97115
}

src/SymfonyCache/PurgeListener.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,36 @@
1313

1414
use Symfony\Component\HttpFoundation\Response;
1515
use Symfony\Component\OptionsResolver\OptionsResolver;
16+
use Toflar\Psr6HttpCacheStore\Psr6StoreInterface;
1617

1718
/**
1819
* Purge handler for the symfony built-in HttpCache.
1920
*
2021
* @author David Buchmann <[email protected]>
22+
* @author Yanick Witschi <[email protected]>
2123
*
2224
* {@inheritdoc}
2325
*/
2426
class PurgeListener extends AccessControlledListener
2527
{
2628
const DEFAULT_PURGE_METHOD = 'PURGE';
2729

30+
const DEFAULT_CLEAR_CACHE_HEADER = 'Clear-Cache';
31+
2832
/**
2933
* The purge method to use.
3034
*
3135
* @var string
3236
*/
3337
private $purgeMethod;
3438

39+
/**
40+
* The clear cache header to use.
41+
*
42+
* @var string
43+
*/
44+
private $clearCacheHeader;
45+
3546
/**
3647
* When creating the purge listener, you can configure an additional option.
3748
*
@@ -47,7 +58,9 @@ public function __construct(array $options = [])
4758
{
4859
parent::__construct($options);
4960

50-
$this->purgeMethod = $this->getOptionsResolver()->resolve($options)['purge_method'];
61+
$options = $this->getOptionsResolver()->resolve($options);
62+
$this->purgeMethod = $options['purge_method'];
63+
$this->clearCacheHeader = $options['clear_cache_header'];
5164
}
5265

5366
/**
@@ -83,6 +96,24 @@ public function handlePurge(CacheEvent $event)
8396
$response = new Response();
8497
$store = $event->getKernel()->getStore();
8598

99+
// Purge whole cache
100+
if ($request->headers->has($this->clearCacheHeader)) {
101+
if (!$store instanceof Psr6StoreInterface) {
102+
$response->setStatusCode(400);
103+
$response->setContent('Store must be an instance of '.Psr6StoreInterface::class.'. Please check your proxy configuration.');
104+
$event->setResponse($response);
105+
106+
return;
107+
}
108+
109+
$store->prune();
110+
111+
$response->setStatusCode(200, 'Pruned');
112+
$event->setResponse($response);
113+
114+
return;
115+
}
116+
86117
if ($store->purge($request->getUri())) {
87118
$response->setStatusCode(200, 'Purged');
88119
} else {
@@ -100,7 +131,9 @@ protected function getOptionsResolver()
100131
{
101132
$resolver = parent::getOptionsResolver();
102133
$resolver->setDefault('purge_method', static::DEFAULT_PURGE_METHOD);
134+
$resolver->setDefault('clear_cache_header', static::DEFAULT_CLEAR_CACHE_HEADER);
103135
$resolver->setAllowedTypes('purge_method', 'string');
136+
$resolver->setAllowedTypes('clear_cache_header', 'string');
104137

105138
return $resolver;
106139
}

tests/Unit/ProxyClient/MultiplexerClientTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace FOS\HttpCache\Tests\Unit\ProxyClient;
1313

1414
use FOS\HttpCache\ProxyClient\Invalidation\BanCapable;
15+
use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable;
1516
use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable;
1617
use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable;
1718
use FOS\HttpCache\ProxyClient\Invalidation\TagCapable;
@@ -134,6 +135,7 @@ public function testPurge()
134135
->getMock();
135136
$mockClient2 = \Mockery::mock(PurgeCapable::class)
136137
->shouldReceive('purge')
138+
->once()
137139
->with($url, $headers)
138140
->getMock();
139141
$mockClient3 = \Mockery::mock(ProxyClient::class);
@@ -143,6 +145,23 @@ public function testPurge()
143145
$this->assertSame($multiplexer, $multiplexer->purge($url, $headers));
144146
}
145147

148+
public function testClear()
149+
{
150+
$mockClient1 = \Mockery::mock(ClearCapable::class)
151+
->shouldReceive('clear')
152+
->once()
153+
->getMock();
154+
$mockClient2 = \Mockery::mock(ClearCapable::class)
155+
->shouldReceive('clear')
156+
->once()
157+
->getMock();
158+
$mockClient3 = \Mockery::mock(ProxyClient::class);
159+
160+
$multiplexer = new MultiplexerClient([$mockClient1, $mockClient2, $mockClient3]);
161+
162+
$this->assertSame($multiplexer, $multiplexer->clear());
163+
}
164+
146165
public function provideInvalidClient()
147166
{
148167
return [

0 commit comments

Comments
 (0)