Skip to content

Commit 6d4c770

Browse files
committed
Merge pull request #2 from ddeboer/tagging
Support cache tagging
2 parents e840cf3 + 7c4e25b commit 6d4c770

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)