From f307e8e536d4f8e33f9ca93d4d0ff98e29a9ceaf Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Thu, 1 Dec 2022 11:16:18 +0100 Subject: [PATCH] add tag header parser to cleanly respect custom glue --- CHANGELOG.md | 6 ++++ doc/symfony-cache-configuration.rst | 7 +++-- src/ResponseTagger.php | 19 ++++++++++++ src/SymfonyCache/PurgeTagsListener.php | 16 ++++++---- .../CommaSeparatedTagHeaderFormatter.php | 13 ++++++++- .../MaxHeaderValueLengthFormatter.php | 11 ++++++- src/TagHeaderFormatter/TagHeaderParser.php | 29 +++++++++++++++++++ .../CommaSeparatedTagHeaderFormatterTest.php | 15 ++++++++++ 8 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 src/TagHeaderFormatter/TagHeaderParser.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab63e366..14c57c720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ Changelog See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpCache/releases). +2.15.0 +------ + +* Provide a `TagHeaderParser` that can split up a tag header into the list of tags. + This allows to correctly handle non-default tag separators in all places. + 2.14.2 ------ diff --git a/doc/symfony-cache-configuration.rst b/doc/symfony-cache-configuration.rst index 87ef3d20b..f08bc98aa 100644 --- a/doc/symfony-cache-configuration.rst +++ b/doc/symfony-cache-configuration.rst @@ -208,8 +208,7 @@ you have the same configuration options as with the ``PurgeListener``. *Only set one of ``client_ips`` or ``client_matcher``*. Additionally, you can configure the HTTP method and header used for tag purging: -* **client_ips**: String with IP or array of IPs that are allowed to - purge the cache. +* **client_ips**: String with IP or array of IPs that are allowed to purge the cache. **default**: ``127.0.0.1`` @@ -230,6 +229,10 @@ configure the HTTP method and header used for tag purging: **default**: ``/`` +* **tags_parser**: Overwrite if you use a non-default glue to combine the tags in the header. + This option expects a `FOS\HttpCache\TagHeaderFormatter\TagHeaderParser` instance, configured + with the glue you want to use. + To get cache tagging support, register the ``PurgeTagsListener`` and use the ``Psr6Store`` in your ``AppCache``:: diff --git a/src/ResponseTagger.php b/src/ResponseTagger.php index e4839032d..7bc15fc05 100644 --- a/src/ResponseTagger.php +++ b/src/ResponseTagger.php @@ -14,6 +14,7 @@ use FOS\HttpCache\Exception\InvalidTagException; use FOS\HttpCache\TagHeaderFormatter\CommaSeparatedTagHeaderFormatter; use FOS\HttpCache\TagHeaderFormatter\TagHeaderFormatter; +use FOS\HttpCache\TagHeaderFormatter\TagHeaderParser; use Psr\Http\Message\ResponseInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -96,6 +97,24 @@ public function getTagsHeaderValue() return $this->headerFormatter->getTagsHeaderValue($this->tags); } + /** + * Split the tag header into a list of tags. + * + * @param string|string[] $headers + * + * @return string[] + */ + protected function parseTagsHeaderValue($headers): array + { + if ($this->headerFormatter instanceof TagHeaderParser) { + return $this->headerFormatter->parseTagsHeaderValue($headers); + } + + return array_merge(...array_map(function ($header) { + return explode(',', $header); + }, $headers)); + } + /** * Check whether the tag handler has any tags to set on the response. * diff --git a/src/SymfonyCache/PurgeTagsListener.php b/src/SymfonyCache/PurgeTagsListener.php index ae3ad27e4..d9fa961d0 100644 --- a/src/SymfonyCache/PurgeTagsListener.php +++ b/src/SymfonyCache/PurgeTagsListener.php @@ -11,6 +11,8 @@ namespace FOS\HttpCache\SymfonyCache; +use FOS\HttpCache\TagHeaderFormatter\CommaSeparatedTagHeaderFormatter; +use FOS\HttpCache\TagHeaderFormatter\TagHeaderParser; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\OptionsResolver\OptionsResolver; use Toflar\Psr6HttpCacheStore\Psr6StoreInterface; @@ -42,6 +44,11 @@ class PurgeTagsListener extends AccessControlledListener */ private $tagsHeader; + /** + * @var TagHeaderParser + */ + private $tagsParser; + /** * When creating the purge listener, you can configure an additional option. * @@ -65,6 +72,7 @@ public function __construct(array $options = []) $this->tagsMethod = $options['tags_method']; $this->tagsHeader = $options['tags_header']; + $this->tagsParser = $options['tags_parser']; } /** @@ -125,11 +133,7 @@ public function handlePurgeTags(CacheEvent $event) $headers = $request->headers->get($this->tagsHeader, '', false); } - foreach ($headers as $header) { - foreach (explode(',', $header) as $tag) { - $tags[] = $tag; - } - } + $tags = $this->tagsParser->parseTagsHeaderValue($headers); if ($store->invalidateTags($tags)) { $response->setStatusCode(200, 'Purged'); @@ -151,9 +155,11 @@ protected function getOptionsResolver() $resolver->setDefaults([ 'tags_method' => static::DEFAULT_TAGS_METHOD, 'tags_header' => static::DEFAULT_TAGS_HEADER, + 'tags_parser' => new CommaSeparatedTagHeaderFormatter(), ]); $resolver->setAllowedTypes('tags_method', 'string'); $resolver->setAllowedTypes('tags_header', 'string'); + $resolver->setAllowedTypes('tags_parser', TagHeaderParser::class); return $resolver; } diff --git a/src/TagHeaderFormatter/CommaSeparatedTagHeaderFormatter.php b/src/TagHeaderFormatter/CommaSeparatedTagHeaderFormatter.php index 625da9a4b..67be51bb0 100644 --- a/src/TagHeaderFormatter/CommaSeparatedTagHeaderFormatter.php +++ b/src/TagHeaderFormatter/CommaSeparatedTagHeaderFormatter.php @@ -16,7 +16,7 @@ * * @author Yanick Witschi */ -class CommaSeparatedTagHeaderFormatter implements TagHeaderFormatter +class CommaSeparatedTagHeaderFormatter implements TagHeaderFormatter, TagHeaderParser { /** * @var string @@ -53,4 +53,15 @@ public function getTagsHeaderValue(array $tags) { return implode($this->glue, $tags); } + + public function parseTagsHeaderValue($tags): array + { + if (is_string($tags)) { + $tags = [$tags]; + } + + return array_merge(...array_map(function ($tagsFragment) { + return explode($this->glue, $tagsFragment); + }, $tags)); + } } diff --git a/src/TagHeaderFormatter/MaxHeaderValueLengthFormatter.php b/src/TagHeaderFormatter/MaxHeaderValueLengthFormatter.php index 91049374c..e00c936fe 100644 --- a/src/TagHeaderFormatter/MaxHeaderValueLengthFormatter.php +++ b/src/TagHeaderFormatter/MaxHeaderValueLengthFormatter.php @@ -21,7 +21,7 @@ * * @author Yanick Witschi */ -class MaxHeaderValueLengthFormatter implements TagHeaderFormatter +class MaxHeaderValueLengthFormatter implements TagHeaderFormatter, TagHeaderParser { /** * @var TagHeaderFormatter @@ -83,6 +83,15 @@ public function getTagsHeaderValue(array $tags) return $newValues; } + public function parseTagsHeaderValue($tags): array + { + if ($this->inner instanceof TagHeaderParser) { + return $this->inner->parseTagsHeaderValue($tags); + } + + throw new \BadMethodCallException('The inner formatter does not implement '.TagHeaderParser::class); + } + /** * @param string $value * diff --git a/src/TagHeaderFormatter/TagHeaderParser.php b/src/TagHeaderFormatter/TagHeaderParser.php new file mode 100644 index 000000000..c176cf981 --- /dev/null +++ b/src/TagHeaderFormatter/TagHeaderParser.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\HttpCache\TagHeaderFormatter; + +/** + * The TagHeaderParser can convert the tag header into an array of tags. + * + * @author David Buchmann + */ +interface TagHeaderParser +{ + /** + * Split the tag header into a list of tags. + * + * @param string|string[] $tags + * + * @return string[] + */ + public function parseTagsHeaderValue($tags): array; +} diff --git a/tests/Unit/TagHeaderFormatter/CommaSeparatedTagHeaderFormatterTest.php b/tests/Unit/TagHeaderFormatter/CommaSeparatedTagHeaderFormatterTest.php index 78ad9a783..5e4eab1de 100644 --- a/tests/Unit/TagHeaderFormatter/CommaSeparatedTagHeaderFormatterTest.php +++ b/tests/Unit/TagHeaderFormatter/CommaSeparatedTagHeaderFormatterTest.php @@ -46,4 +46,19 @@ public function testGetCustomGlueTagsHeaderValue() $this->assertSame('tag1', $formatter->getTagsHeaderValue(['tag1'])); $this->assertSame('tag1 tag2 tag3', $formatter->getTagsHeaderValue(['tag1', 'tag2', 'tag3'])); } + + public function testParseTagsHeaderValue() + { + $parser = new CommaSeparatedTagHeaderFormatter(); + + $this->assertSame(['a', 'b', 'c'], $parser->parseTagsHeaderValue('a,b,c')); + $this->assertSame(['a', 'b', 'c'], $parser->parseTagsHeaderValue(['a', 'b,c'])); + } + + public function testParseCustomGlueTagsHeaderValue() + { + $parser = new CommaSeparatedTagHeaderFormatter(TagHeaderFormatter::DEFAULT_HEADER_NAME, ' '); + + $this->assertSame(['a', 'b,c'], $parser->parseTagsHeaderValue('a b,c')); + } }