Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <yanick.witschi@terminal42.ch>
*/
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 <yanick.witschi@terminal42.ch>
*/
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 <mail@davidbu.ch>
*/
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'));
}
}