Skip to content

Commit

Permalink
Merge pull request #541 from FriendsOfSymfony/header-parser
Browse files Browse the repository at this point in the history
add tag header parser to cleanly respect custom glue
  • Loading branch information
dbu authored Dec 1, 2022
2 parents ec878d6 + f307e8e commit 9150062
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 9 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
------

Expand Down
7 changes: 5 additions & 2 deletions doc/symfony-cache-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``

Expand All @@ -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``::

Expand Down
19 changes: 19 additions & 0 deletions src/ResponseTagger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down
16 changes: 11 additions & 5 deletions src/SymfonyCache/PurgeTagsListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand All @@ -65,6 +72,7 @@ public function __construct(array $options = [])

$this->tagsMethod = $options['tags_method'];
$this->tagsHeader = $options['tags_header'];
$this->tagsParser = $options['tags_parser'];
}

/**
Expand Down Expand Up @@ -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');
Expand All @@ -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;
}
Expand Down
13 changes: 12 additions & 1 deletion src/TagHeaderFormatter/CommaSeparatedTagHeaderFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*
* @author Yanick Witschi <[email protected]>
*/
class CommaSeparatedTagHeaderFormatter implements TagHeaderFormatter
class CommaSeparatedTagHeaderFormatter implements TagHeaderFormatter, TagHeaderParser
{
/**
* @var string
Expand Down Expand Up @@ -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));
}
}
11 changes: 10 additions & 1 deletion src/TagHeaderFormatter/MaxHeaderValueLengthFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*
* @author Yanick Witschi <[email protected]>
*/
class MaxHeaderValueLengthFormatter implements TagHeaderFormatter
class MaxHeaderValueLengthFormatter implements TagHeaderFormatter, TagHeaderParser
{
/**
* @var TagHeaderFormatter
Expand Down Expand Up @@ -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
*
Expand Down
29 changes: 29 additions & 0 deletions src/TagHeaderFormatter/TagHeaderParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the FOSHttpCache package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* 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 <[email protected]>
*/
interface TagHeaderParser
{
/**
* Split the tag header into a list of tags.
*
* @param string|string[] $tags
*
* @return string[]
*/
public function parseTagsHeaderValue($tags): array;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
}

0 comments on commit 9150062

Please sign in to comment.