10000 TaggableStore and cache tag handling for Symfony HttpCache · FriendsOfSymfony/FOSHttpCache@b1ce46c · GitHub
[go: up one dir, main page]

Skip to content

Commit b1ce46c

Browse files
Toflardbu
authored andcommitted
TaggableStore and cache tag handling for Symfony HttpCache
1 parent db4b527 commit b1ce46c

File tree

13 files changed

+634
-35
lines changed

13 files changed

+634
-35
lines changed

.travis.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ cache:
77
env:
88
global:
99
- VARNISH_VERSION=5.0
10+
- DEPENDENCIES="toflar/psr6-symfony-http-cache-store:^1.0"
1011

1112
matrix:
1213
fast_finish: true
1314
include:
1415
# Minimum supported versions
1516
- php: 5.6
16-
env: VARNISH_VERSION=3.0 COMPOSER_FLAGS="--prefer-lowest"
17+
env: VARNISH_VERSION=3.0 COMPOSER_FLAGS="--prefer-lowest" DEPENDENCIES=""
1718

1819
- php: 5.6
1920
- php: 7.0
@@ -29,7 +30,7 @@ matrix:
2930
- php: 7.2
3031
env: DEPENDENCIES="symfony/lts:^2"
3132
- php: 7.2
32-
env: DEPENDENCIES="symfony/lts:^3"
33+
env: DEPENDENCIES="symfony/lts:^3 toflar/psr6-symfony-http-cache-store:^1.0"
3334

3435
# Latest commit to master
3536
- php: 7.2
@@ -47,7 +48,7 @@ branches:
4748
before_install:
4849
- if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi
4950
- if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi;
50-
- if ! [ -v "$DEPENDENCIES" ]; then composer require --no-update ${DEPENDENCIES}; fi;
51+
- if ! [ -v "$DEPENDENCIES" ]; then composer require --no-update ${DEPENDENCIES}; echo ${DEPENDENCIES}; fi;
5152

5253
install:
5354
- composer update $COMPOSER_FLAGS --prefer-dist --no-interaction

CHANGELOG.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,21 @@ See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpC
77
------------------
88

99
* Support Symfony 4.
10+
11+
### Testing
12+
13+
* Upgraded phpunit to 5.7 / 6. If you use anything from the
14+
`FOS\HttpCache\Test` namespace you need to update your project to use
15+
PHPUnit 6 (or 5.7, if you are using PHP 5.6).
16+
17+
### Symfony HttpCache
18+
19+
* Added a `PurgeTagsListener` for tag based invalidation with the Symfony
20+
`HttpCache` reverse caching proxy. This requires the newly created
21+
[Toflar Psr6Store](https://github.com/Toflar/psr6-symfony-http-cache-store)
22+
built on PSR-6 cache and supporting pruning expired cache entries.
1023
* Using Request::isMethodCacheable rather than Request::isMethodSafe to
1124
correctly handle OPTIONS and TRACE requests.
12-
* Upgraded phpunit to 5.7 / 6. If you use anything from the FOS\HttpCache\Test
13-
namespace you need to update your project to use phpunit 6 (or 5.7, if you
14-
are using PHP 5.6).
1525

1626
2.0.2
1727
-----

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@
3434
"monolog/monolog": "^1.0",
3535
"php-http/guzzle6-adapter": "^1.0.0",
3636
"php-http/mock-client": "^0.3.2",
37+
"phpunit/phpunit": "^5.7 || ^6.0",
3738
"symfony/process": "^2.3 || ^3.0 || ^4.0",
38-
"symfony/http-kernel": "^2.3 || ^3.0 || ^4.0",
39-
"phpunit/phpunit": "^5.7 || ^6.0"
39+
"symfony/http-kernel": "^2.3 || ^3.0 || ^4.0"
4040
},
4141
"suggest": {
4242
"friendsofsymfony/http-cache-bundle": "For integration with the Symfony framework",

doc/proxy-clients.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Client Purge Refresh Ban Tagging
2424
============= ======= ======= ======= =======
2525
Varnish ✓ ✓ ✓ ✓
2626
NGINX ✓ ✓
27-
Symfony Cache ✓ ✓
27+
Symfony Cache ✓ ✓
2828
Noop ✓ ✓ ✓ ✓
2929
Multiplexer ✓ ✓ ✓ ✓
3030
============= ======= ======= ======= =======

doc/response-tagging.rst

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,28 @@ The response tagger uses an instance of ``TagHeaderFormatter`` to know the
1919
header name used to mark tags on the content and to format the tags into the
2020
correct header value. This library ships with a
2121
``CommaSeparatedTagHeaderFormatter`` that formats an array of tags into a
22-
comma-separated list. This format is expected for invalidation with the
23-
Varnish reverse proxy. When using the default settings, everything is created
24-
automatically and the ``X-Cache-Tags`` header will be used::
22+
comma-separated list. The format for specifying the tags depends on the caching
23+
proxy you use and its configuration. The default settings are made to match and
24+
work out of the box. If you need to change anything, be aware that the caching
25+
proxy is configured separately from your PHP application and the
26+
``ResponseTagger`` - it is up to you to make sure the configurations match.
27+
28+
For example, the :doc:`default configuration of Varnish <varnish-configuration>`
29+
provided in this library uses the header ``X-Cache-Tags`` with a
30+
comma-separated list of tags. If you don't change the ``TagHeaderFormatter`` nor
31+
the header name, just instantiate the response tagger with its default settings::
2532

2633
use FOS\HttpCache\ResponseTagger;
2734

2835
$responseTagger = new ResponseTagger();
2936

3037
.. _response_tagger_optional_parameters:
3138

32-
If you need a different behavior, you can provide your own implementation of
33-
the ``TagHeaderFormatter`` interface. But be aware that your
34-
:ref:`Varnish configuration <varnish_tagging>` has to match with the tag on the response.
35-
For example, to use a different header name::
39+
If you need a different behavior, you can provide your own
40+
``TagHeaderFormatter`` instance. Don't forget to also adjust your
41+
:doc:`proxy configuration <proxy-configuration>` to match the response. To use
42+
a different header name, instantiate the ``CommaSeparatedTagHeaderFormatter``
43+
yourself and pass it to the ``ResponseTagger``::
3644

3745
use FOS\HttpCache\ResponseTagger;
3846
use FOS\HttpCache\TagHeaderFormatter;

doc/symfony-cache-configuration.rst

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Using the trait
3030
Your ``AppCache`` needs to implement ``CacheInvalidation`` and use the
3131
trait ``FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache``::
3232

33+
// app/AppCache.php
34+
3335
use FOS\HttpCache\SymfonyCache\CacheInvalidation;
3436
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
3537
use Symfony\Component\HttpFoundation\Request;
@@ -148,17 +150,19 @@ Refresh
148150

149151
To support :ref:`cache refresh <cache refresh>`, register the
150152
``RefreshListener``. You can pass the constructor an option to specify
151-
what clients are allowed to refresh cache entries. Refreshing is only allowed
152-
from the same machine by default. To refresh from other hosts, provide the
153-
IPs of the machines allowed to refresh, or provide a RequestMatcher that
154-
checks for an Authorization header or similar. *Only set one of
155-
``client_ips`` or ``client_matcher``*.
153+
what clients are allowed to refresh cache entries.
156154

157155
The refresh listener needs to access the ``HttpCache::fetch`` method which
158156
is protected on the base HttpCache class. The ``EventDispatchingHttpCache``
159157
exposes the method as public, but if you implement your own kernel, you need
160158
to overwrite the method to make it public.
161159

160+
Refreshing is only allowed from the same machine by default. To refresh from
161+
other hosts, provide the IPs of the machines allowed to refresh, or provide a
162+
RequestMatcher that checks for an Authorization header or similar. *Only set
163+
one of ``client_ips`` or ``client_matcher``*.
164+
165+
162166
* **client_ips**: String with IP or array of IPs that are allowed to
163167
refresh the cache.
164168

@@ -169,6 +173,81 @@ to overwrite the method to make it public.
169173

170174
**default**: ``null``
171175

176+
Tagging
177+
~~~~~~~
178+
179+
.. versionadded:: 2.1
180+
181+
Support for tag invalidation with Symfony HttpCache has been added in
182+
version 2.1.
183+
184+
To support :doc:`cache tags <response-tagging>`, require the additional package
185+
``toflar/psr6-symfony-http-cache-store:^1.0`` with composer and register the
186+
``PurgeTagsListener`` in your cache kernel. The purge listener needs your cache
187+
to use the special ``Toflar\Psr6HttpCacheStore\Psr6Store`` store, as the default
188+
store does not have tagging support.
189+
190+
.. note::
191+
192+
Symfony's ``HttpCache`` store implementation does not support tags.
193+
Therefore, you need the `Toflar Psr6Store`_ which implements the Symfony
194+
Store interface but supports cache tagging. See the project README for more
195+
information on the store.
196+
197+
To install the store, run
198+
``composer require toflar/psr6-symfony-http-cache-store``.
199+
200+
Purging tags is only allowed from the same machine by default. To change this,
201+
you have the same configuration options as with the ``PurgeListener``. *Only
202+
set one of ``client_ips`` or ``client_matcher``*. Additionally, you can
203+
configure the HTTP method and header used for tag purging:
204+
205+
* **client_ips**: String with IP or array of IPs that are allowed to
206+
purge the cache.
207+
208+
**default**: ``127.0.0.1``
209+
210+
* **client_matcher**: RequestMatcherInterface that only matches requests that are
211+
allowed to purge.
212+
213+
**default**: ``null``
214+
215+
* **tags_method**: HTTP Method used with purge tags requests.
216+
217+
**default**: ``PURGETAGS``
218+
219+
* **tags_header**: HTTP Header that contains the comma-separated tags to purge.
220+
221+
**default**: ``X-Cache-Tags``
222+
223+
To get cache tagging support, register the ``PurgeTagsListener`` and use the
224+
``Psr6Store`` in your ``AppCache``::
225+
226+
// app/AppCache.php
227+
228+
use Toflar\Psr6HttpCacheStore\Psr6Store;
229+
use FOS\HttpCache\SymfonyCache\PurgeTagsListener;
230+
231+
// ...
232+
233+
/**
234+
* Overwrite constructor to register the Psr6Store and PurgeTagsListener.
235+
*/
236+
public function __construct(
237+
HttpKernelInterface $kernel,
238+
SurrogateInterface $surrogate = null,
239+
array $options = []
240+
) {
241+
$store = new Psr6Store([
242+
'cache_directory' => $kernel->getCacheDir(),
243+
'cache_tags_header' => 'X-Cache-Tags',
244+
]);
245+
246+
parent::__construct($kernel, $store, $surrogate, $options);
247+
248+
$this->addSubscriber(new PurgeTagsListener());
249+
}
250+
172251
.. _symfony-cache user context:
173252

174253
User Context
@@ -268,3 +347,4 @@ and at the HTML body of the response.
268347

269348
.. _HttpCache: http://symfony.com/doc/current/book/http_cache.html#symfony-reverse-proxy
270349
.. _HttpKernel: http://symfony.com/doc/current/components/http_kernel.html
350+
.. _Toflar Psr6Store: https://github.com/Toflar/psr6-symfony-http-cache-store

src/ProxyClient/HttpProxyClient.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,50 @@ protected function queueRequest($method, $url, array $headers, $validateHost = t
102102
* Reusable function for proxy clients.
103103
* Escapes `,` and `\n` (newline) characters.
104104
*
105+
* Note: This is not a safe escaping function, it can lead to collisions,
106+
* e.g. between "foo,bar" and "foo_bar". But from the nature of the data,
107+
* such collisions are unlikely, and from the function of cache tagging,
108+
* collisions would in the worst case lead to unintended invalidations,
109+
* which is not a bug.
110+
*
105111
* @param array $tags The tags to escape
106112
*
107113
* @return array Sane tags
108114
*/
109115
protected function escapeTags(array $tags)
110116
{
111117
array_walk($tags, function (&$tag) {
112-
$tag = str_replace([',', "\n"], ['_', '_'], $tag);
118+
// WARNING: changing the list of characters that are escaped is a BC break for existing installations,
119+
// as existing tags on the cache would not be invalidated anymore if they contain a character that is
120+
// newly escaped
121+
$tag = str_replace([',', "\n"], '_', $tag);
113122
});
114123

115124
return $tags;
116125
}
126+
127+
/**
128+
* Calculate how many tags fit into the header.
129+
*
130+
* This assumes that the tags are separated by one character.
131+
*
132+
* @param string[] $escapedTags
133+
* @param string $glue The concatenation string to use
134+
*
135+
* @return int Number of tags per tag invalidation request
136+
*/
137+
protected function determineTagsPerHeader($escapedTags, $glue)
138+
{
139+
if (mb_strlen(implode($glue, $escapedTags)) < $this->options['header_length']) {
140+
return count($escapedTags);
141+
}
142+
143+
/*
144+
* estimate the amount of tags to invalidate by dividing the max
145+
* header length by the largest tag (minus the glue length)
146+
*/
147+
$tagsize = max(array_map('mb_strlen', $escapedTags));
148+
149+
return floor($this->options['header_length'] / ($tagsize + strlen($glue))) ?: 1;
150+
}
117151
}

src/ProxyClient/Symfony.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable;
1515
use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable;
16+
use FOS\HttpCache\ProxyClient\Invalidation\TagCapable;
1617
use FOS\HttpCache\SymfonyCache\PurgeListener;
18+
use FOS\HttpCache\SymfonyCache\PurgeTagsListener;
1719

1820
/**
1921
* Symfony HttpCache invalidator.
@@ -24,7 +26,7 @@
2426
* @author David de Boer <david@driebit.nl>
2527
* @author David Buchmann <mail@davidbu.ch>
2628
*/
27-
class Symfony extends HttpProxyClient implements PurgeCapable, RefreshCapable
29+
class Symfony extends HttpProxyClient implements PurgeCapable, RefreshCapable, TagCapable
2830
{
2931
const HTTP_METHOD_REFRESH = 'GET';
3032

@@ -54,8 +56,38 @@ protected function configureOptions()
5456
$resolver = parent::configureOptions();
5557
$resolver->setDefaults([
5658
'purge_method' => PurgeListener::DEFAULT_PURGE_METHOD,
59+
'tags_method' => PurgeTagsListener::DEFAULT_TAGS_METHOD,
60+
'tags_header' => PurgeTagsListener::DEFAULT_TAGS_HEADER,
61+
'header_length' => 7500,
5762
]);
63+
$resolver->setAllowedTypes('purge_method', 'string');
64+
$resolver->setAllowedTypes('tags_method', 'string');
65+
$resolver->setAllowedTypes('tags_header', 'string');
66+
$resolver->setAllowedTypes('header_length', 'int');
5867

5968
return $resolver;
6069
}
70+
71+
/**
72+
* Remove/Expire cache objects based on cache tags.
73+
*
74+
* @param array $tags Tags that should be removed/expired from the cache
75+
*
76+
* @return $this
77+
*/
78+
public function invalidateTags(array $tags)
79+
{
80+
$escapedTags = $this->escapeTags($tags);
81+
82+
$chunkSize = $this->determineTagsPerHeader($escapedTags, ',');
83+
84+
foreach (array_chunk($escapedTags, $chunkSize) as $tagchunk) {
85+
$this->queueRequest(
86+
$this->options['tags_method'],
87+
'/',
88+
[$this->options['tags_header'] => implode(',', $tagchunk)]);
89+
}
90+
91+
return $this;
92+
}
6193
}

src/ProxyClient/Varnish.php

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,9 @@ public function invalidateTags(array $tags)
6363
{
6464
$escapedTags = array_map('preg_quote', $this->escapeTags($tags));
6565

66-
if (mb_strlen(implode('|', $escapedTags)) >= $this->options['header_length']) {
67-
/*
68-
* estimate the amount of tags to invalidate by dividing the max
69-
* header length by the largest tag (minus 1 for the implode character)
70-
*/
71-
$tagsize = max(array_map('mb_strlen', $escapedTags));
72-
$elems = floor($this->options['header_length'] / ($tagsize - 1)) ?: 1;
73-
} else {
74-
$elems = count($escapedTags);
75-
}
66+
$chunkSize = $this->determineTagsPerHeader($escapedTags, '|');
7667

77-
foreach (array_chunk($escapedTags, $elems) as $tagchunk) {
68+
foreach (array_chunk($escapedTags, $chunkSize) as $tagchunk) {
7869
$tagExpression = sprintf('(%s)(,|$)', implode('|', $tagchunk));
7970
$this->ban([$this->options['tags_header'] => $tagExpression]);
8071
}

src/SymfonyCache/PurgeListener.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ public function handlePurge(CacheEvent $event)
8181
}
8282

8383
$response = new Response();
84-
if ($event->getKernel()->getStore()->purge($request->getUri())) {
84+
$store = $event->getKernel()->getStore();
85+
86+
if ($store->purge($request->getUri())) {
8587
$response->setStatusCode(200, 'Purged');
8688
} else {
8789
$response->setStatusCode(200, 'Not found');

0 commit comments

Comments
 (0)
0