Skip to content

Commit 7c4e25b

Browse files
committed
Add support for cache tagging
Fix class name Fix typo Clean up test Improve code and add docs Add another expression example Add check for characters in tag name that clash with regex Refactor BanInterface Rename ban to banPath and implement simpler ban method Make cache tags HTTP header settable Add note on dependencies Re-add cache proxy interface Make Varnish tests compatible with refactored BanInterface Make functional test case easier to use Make separate case for when tags are banned Fix banning tags and add functional test Don't flush Varnish if no requests are queued Add flush expectation Explain how to configure Varnish for cache tagging Fix link in docs
1 parent e840cf3 commit 7c4e25b

File tree

19 files changed

+715
-61
lines changed

19 files changed

+715
-61
lines changed

CacheManager.php

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
namespace FOS\HttpCacheBundle;
44

55
use FOS\HttpCacheBundle\HttpCache\HttpCacheInterface;
6+
use FOS\HttpCacheBundle\Invalidation\CacheProxyInterface;
7+
use FOS\HttpCacheBundle\Invalidation\Method\BanInterface;
8+
use Symfony\Component\HttpFoundation\Response;
69
use Symfony\Component\Routing\RouterInterface;
710

811
/**
@@ -11,6 +14,11 @@
1114
*/
1215
class CacheManager
1316
{
17+
/**
18+
* @var string
19+
*/
20+
protected $tagsHeader = 'X-Cache-Tags';
21+
1422
/**
1523
* @var HttpCacheInterface
1624
*/
@@ -31,15 +39,60 @@ class CacheManager
3139
/**
3240
* Constructor
3341
*
34-
* @param HttpCacheInterface $cache HTTP cache
35-
* @param RouterInterface $router Symfony router
42+
* @param CacheProxyInterface $cache HTTP cache
43+
* @param RouterInterface $router Symfony router
3644
*/
37-
public function __construct(HttpCacheInterface $cache, RouterInterface $router)
45+
public function __construct(CacheProxyInterface $cache, RouterInterface $router)
3846
{
3947
$this->cache = $cache;
4048
$this->router = $router;
4149
}
4250

51+
/**
52+
* Set the HTTP header name that will hold cache tags
53+
*
54+
* @param string $tagsHeader
55+
*/
56+
public function setTagsHeader($tagsHeader)
57+
{
58+
$this->tagsHeader = $tagsHeader;
59+
}
60+
61+
/**
62+
* Get the HTTP header name that will hold cache tags
63+
*
64+
* @return string
65+
*/
66+
public function getTagsHeader()
67+
{
68+
return $this->tagsHeader;
69+
}
70+
71+
/**
72+
* Assign cache tags to a response
73+
*
74+
* @param Response $response
75+
* @param array $tags
76+
* @param bool $replace Whether to replace the current tags on the
77+
* response
78+
*
79+
* @return $this
80+
*/
81+
public function tagResponse(Response $response, array $tags, $replace = false)
82+
{
83+
if (!$replace) {
84+
$tags = array_merge(
85+
$response->headers->get($this->getTagsHeader(), array()),
86+
$tags
87+
);
88+
}
89+
90+
$uniqueTags = array_unique($tags);
91+
$response->headers->set($this->getTagsHeader(), implode(',', $uniqueTags));
92+
93+
return $this;
94+
}
95+
4396
/**
4497
* Invalidate a path (URL)
4598
*
@@ -83,22 +136,32 @@ public function invalidateRegex($regex)
83136
}
84137

85138
/**
86-
* Flush all paths queued for invalidation
139+
* Invalidate cache tags
140+
*
141+
* @param array $tags Cache tags
87142
*
88-
* @return array Paths that were flushed from the queue
143+
* @return $this
144+
* @throws \RuntimeException If HTTP cache does not support BAN requests
89145
*/
90-
public function flush()
146+
public function invalidateTags(array $tags)
91147
{
92-
$queue = $this->getInvalidationQueue();
93-
94-
if (0 === count($queue)) {
95-
return $queue;
148+
if (!$this->cache instanceof BanInterface) {
149+
throw new \RuntimeException('HTTP cache does not support BAN requests');
96150
}
97151

98-
$this->cache->invalidateUrls($queue);
99-
$this->invalidationQueue = array();
152+
$headers = array($this->getTagsHeader() => '('.implode('|', $tags).')(,.+)?$');
153+
$this->cache->ban($headers);
100154

101-
return $queue;
155+
return $this;
156+
}
157+
158+
/**
159+
* Send all invalidation requests
160+
*
161+
*/
162+
public function flush()
163+
{
164+
$this->cache->flush();
102165
}
103166

104167
/**

Configuration/Tag.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace FOS\HttpCacheBundle\Configuration;
4+
5+
use FOS\HttpCacheBundle\Exception\InvalidTagException;
6+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationAnnotation;
7+
8+
/**
9+
* @Annotation
10+
*/
11+
class Tag extends ConfigurationAnnotation
12+
{
13+
protected $tags;
14+
protected $expression;
15+
16+
public function setValue($data)
17+
{
18+
$this->setTags(is_array($data) ? $data: array($data));
19+
}
20+
21+
/**
22+
* @param mixed $expression
23+
*/
24+
public function setExpression($expression)
25+
{
26+
$this->expression = $expression;
27+
}
28+
29+
/**
30+
* @return mixed
31+
*/
32+
public function getExpression()
33+
{
34+
return $this->expression;
35+
}
36+
37+
public function setTags(array $tags)
38+
{
39+
foreach ($tags as $tag) {
40+
if (false !== \strpos($tag, ',')) {
41+
throw new InvalidTagException($tag, ',');
42+
}
43+
}
44+
45+
$this->tags = $tags;
46+
}
47+
48+
public function getTags()
49+
{
50+
return $this->tags;
51+
}
52+
53+
/**
54+
* {@inheritdoc}
55+
*/
56+
public function getAliasName()
57+
{
58+
return 'tag';
59+
}
60+
61+
/**
62+
* {@inheritdoc}
63+
*/
64+
public function allowArray()
65+
{
66+
return true;
67+
}
68+
}

EventListener/TagListener.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
namespace FOS\HttpCacheBundle\EventListener;
4+
5+
use FOS\HttpCacheBundle\CacheManager;
6+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
7+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
8+
use Symfony\Component\HttpKernel\KernelEvents;
9+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
10+
11+
class TagListener implements EventSubscriberInterface
12+
{
13+
/**
14+
* @var CacheManager
15+
*/
16+
protected $cacheManager;
17+
18+
/**
19+
* Constructor
20+
*
21+
* @param CacheManager $cacheManager
22+
* @param ExpressionLanguage $expressionLanguage
23+
*/
24+
public function __construct(
25+
CacheManager $cacheManager,
26+
ExpressionLanguage $expressionLanguage = null
27+
) {
28+
$this->cacheManager = $cacheManager;
29+
$this->expressionLanguage = $expressionLanguage ?: new ExpressionLanguage();
30+
}
31+
32+
/**
33+
* Process the _tags request attribute, which is set when using the Tag
34+
* annotation
35+
*
36+
* - For a safe (GET or HEAD) request, the tags are set on the response.
37+
* - For a non-safe request, the tags will be invalidated.
38+
*
39+
* @param FilterResponseEvent $event
40+
*/
41+
public function onKernelResponse(FilterResponseEvent $event)
42+
{
43+
$request = $event->getRequest();
44+
45+
// Check for _tag request attribute that is set when using @Tag
46+
// annotation
47+
if (!$tagConfigurations = $request->attributes->get('_tag')) {
48+
return;
49+
}
50+
51+
$response = $event->getResponse();
52+
53+
// Only set cache tags or invalidate them if response is successful
54+
if (!$response->isSuccessful()) {
55+
return;
56+
}
57+
58+
$tags = array();
59+
foreach ($tagConfigurations as $tagConfiguration) {
60+
if (null !== $tagConfiguration->getExpression()) {
61+
$tags[] = $this->expressionLanguage->evaluate(
62+
$tagConfiguration->getExpression(),
63+
$request->attributes->all()
64+
);
65+
} else {
66+
$tags = array_merge($tags, $tagConfiguration->getTags());
67+
}
68+
}
69+
70+
$uniqueTags = array_unique($tags);
71+
72+
if ($request->isMethodSafe()) {
73+
// For safe requests (GET and HEAD), set cache tags on response
74+
$this->cacheManager->tagResponse($response, $uniqueTags);
75+
} else {
76+
// For non-safe methods, invalidate the tags
77+
$this->cacheManager->invalidateTags($uniqueTags);
78+
}
79+
}
80+
81+
/**
82+
* {@inheritdoc}
83+
*/
84+
public static function getSubscribedEvents()
85+
{
86+
return array(
87+
KernelEvents::RESPONSE => 'onKernelResponse'
88+
);
89+
}
90+
}

Exception/InvalidTagException.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace FOS\HttpCacheBundle\Exception;
4+
5+
class InvalidTagException extends \InvalidArgumentException
6+
{
7+
public function __construct($tag, $char)
8+
{
9+
parent:__construct(sprintf('Tag %s is invalid because it contains %s'));
10+
}
11+
}

Invalidation/CacheProxyInterface.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace FOS\HttpCacheBundle\Invalidation;
4+
5+
interface CacheProxyInterface
6+
{
7+
8+
}

Invalidation/Method/BanInterface.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,24 @@ interface BanInterface
1212
const REGEX_MATCH_ALL = '.*';
1313
const CONTENT_TYPE_ALL = self::REGEX_MATCH_ALL;
1414

15+
/**
16+
* Ban cached objects matching HTTP headers
17+
*
18+
* Please make sure to configure your HTTP caching proxy to set the headers
19+
* supplied here on the cached objects. So if you want to match objects by
20+
* host name, configure your proxy to copy the host to a custom HTTP header
21+
* such as X-Host.
22+
*
23+
* @param array $headers HTTP headers that path must match to be banned.
24+
* Each header is either a:
25+
* - regular string ('X-Host' => 'example.com')
26+
* - or a POSIX regular expression
27+
* ('X-Host' => '^(www\.)?(this|that)\.com$').
28+
*
29+
* @return $this
30+
*/
31+
public function ban(array $headers);
32+
1533
/**
1634
* Ban paths matching a regular expression
1735
*
@@ -30,5 +48,5 @@ interface BanInterface
3048
*
3149
* @return $this
3250
*/
33-
public function ban($path, $contentType = self::CONTENT_TYPE_ALL, array $hosts = null);
51+
public function banPath($path, $contentType = self::CONTENT_TYPE_ALL, array $hosts = null);
3452
}

0 commit comments

Comments
 (0)