Skip to content

Commit 8b99517

Browse files
committed
Merge pull request #150 from FriendsOfSymfony/symfony-cache-purge-refresh
add support for purge and refresh with symfony HttpCache
2 parents 0f336e0 + 886fc46 commit 8b99517

15 files changed

+757
-33
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"require": {
2424
"php": ">=5.3.3",
2525
"guzzle/http": "3.*",
26-
"symfony/event-dispatcher": "~2.3"
26+
"symfony/event-dispatcher": "~2.3",
27+
"symfony/options-resolver": "~2.3"
2728
},
2829
"require-dev": {
2930
"guzzle/plugin-mock": "*",

doc/proxy-clients.rst

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,15 @@ requests.
8282
Supported invalidation methods
8383
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8484

85-
======== ======= ======= =======
86-
Client Purge Refresh Ban
87-
======== ======= ======= =======
88-
Varnish ✓ ✓ ✓
89-
Nginx ✓ ✓
90-
======== ======= ======= =======
85+
============= ======= ======= =======
86+
Client Purge Refresh Ban
87+
============= ======= ======= =======
88+
Varnish ✓ ✓ ✓
89+
Nginx ✓ ✓
90+
Symfony Cache ✓ ✓
91+
============= ======= ======= =======
92+
93+
.. _proxy-client purge:
9194

9295
Purge
9396
~~~~~
@@ -116,6 +119,8 @@ send any headers that you vary on, such as ``Accept``.
116119

117120
.. include:: includes/custom-headers.rst
118121

122+
.. _proxy-client refresh:
123+
119124
Refresh
120125
~~~~~~~
121126

doc/symfony-cache-configuration.rst

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@ concept is to use event subscribers on the HttpCache class.
1717

1818
.. warning::
1919

20-
Symfony HttpCache support is currently limited to following features:
20+
Symfony HttpCache does not currently provide support for banning.
2121

22-
* User context
23-
24-
Extending the right HttpCache
25-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22+
Extending the Correct HttpCache Class
23+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2624

2725
Instead of extending ``Symfony\Component\HttpKernel\HttpCache\HttpCache``, your
2826
``AppCache`` should extend ``FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache``.
@@ -59,6 +57,60 @@ subscribers there. A simple cache will look like this::
5957
}
6058
}
6159

60+
Purge
61+
~~~~~
62+
63+
To support :ref:`cache purging <proxy-client purge>`, register the
64+
``PurgeSubscriber``. If the default settings are right for you, you don't
65+
need to do anything more.
66+
67+
Purging is only allowed from the same machine by default. To purge data from
68+
other hosts, provide the IPs of the machines allowed to purge, or provide a
69+
RequestMatcher that checks for an Authorization header or similar. *Only set
70+
one of purge_client_ips or purge_client_matcher*.
71+
72+
* **purge_client_ips**: String with IP or array of IPs that are allowed to
73+
purge the cache.
74+
75+
**default**: ``127.0.0.1``
76+
77+
* **purge_client_matcher**: RequestMatcher that only matches requests that are
78+
allowed to purge.
79+
80+
**default**: ``null``
81+
82+
* **purge_method**: HTTP Method used with purge requests.
83+
84+
**default**: ``PURGE``
85+
86+
Refresh
87+
~~~~~~~
88+
89+
To support :ref:`cache refresh <proxy-client refresh>`, register the
90+
``RefreshSubscriber``. You can pass the constructor an option to specify
91+
what clients are allowed to refresh cache entries. Refreshing is only allowed
92+
from the same machine by default. To refresh from other hosts, provide the
93+
IPs of the machines allowed to refresh, or provide a RequestMatcher that
94+
checks for an Authorization header or similar. *Only set one of
95+
refresh_client_ips or refresh_client_matcher*.
96+
97+
The refresh subscriber needs to access the ``HttpCache::fetch`` method which
98+
is protected on the base HttpCache class. The ``EventDispatchingHttpCache``
99+
exposes the method as public, but if you implement your own kernel, you need
100+
to overwrite the method to make it public.
101+
102+
* **refresh_client_ips**: String with IP or array of IPs that are allowed to
103+
refresh the cache.
104+
105+
**default**: ``127.0.0.1``
106+
107+
* **refresh_client_matcher**: RequestMatcher that only matches requests that are
108+
allowed to refresh.
109+
110+
**default**: ``null``
111+
112+
.. _symfony-cache user context:
113+
62114
User Context
63115
~~~~~~~~~~~~
64116

@@ -102,8 +154,8 @@ constructor:
102154
Session IDs are indeed used as keys to cache the generated use context hash.
103155

104156
Wrong session name will lead to unexpected results such as having the same
105-
user context hash for every users,
106-
or not having it cached at all (painful for performance.
157+
user context hash for every users, or not having it cached at all, which
158+
hurts performance.
107159

108160
Cleaning the Cookie Header
109161
^^^^^^^^^^^^^^^^^^^^^^^^^^

doc/user-context.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Proxy Client Configuration
4545

4646
Currently, user context caching is only supported by Varnish and by the Symfony
4747
HttpCache. See the :ref:`Varnish Configuration <varnish user context>` or
48-
:doc:`Symfony HttpCache Configuration <symfony-cache-configuration>`.
48+
:ref:`Symfony HttpCache Configuration <symfony-cache user context>`.
4949

5050
Calculating the User Context Hash
5151
---------------------------------
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCache package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\HttpCache\SymfonyCache;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\RequestMatcher;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
19+
use Symfony\Component\HttpKernel\HttpKernelInterface;
20+
21+
/**
22+
* Base class for handlers for the symfony built-in HttpCache that need access
23+
* control on requests.
24+
*
25+
* @author David Buchmann <[email protected]>
26+
*
27+
* {@inheritdoc}
28+
*/
29+
abstract class AccessControlledSubscriber implements EventSubscriberInterface
30+
{
31+
/**
32+
* @var RequestMatcher
33+
*/
34+
private $requestMatcher;
35+
36+
/**
37+
* Initializes this subscriber with either a request matcher or an IP or
38+
* list of IPs.
39+
*
40+
* Only one of request matcher or IPs may be a non-null value. If you use a
41+
* RequestMatcher, configure your IPs into it.
42+
*
43+
* If neither parameter is set, the filter is IP 127.0.0.1
44+
*
45+
* @param RequestMatcher|null $requestMatcher Request matcher configured to only match allowed requests.
46+
* @param string|string[]|null $ips IP or list of IPs that are allowed to send requests.
47+
*
48+
* @throws \InvalidArgumentException If both $requestMatcher and $ips are set.
49+
*/
50+
public function __construct(RequestMatcher $requestMatcher = null, $ips = null)
51+
{
52+
if ($requestMatcher && $ips) {
53+
throw new \InvalidArgumentException('You may not set both a request matcher and an IP.');
54+
}
55+
if (!$requestMatcher) {
56+
$requestMatcher = new RequestMatcher(null, null, null, $ips ?: '127.0.0.1');
57+
}
58+
59+
$this->requestMatcher = $requestMatcher;
60+
}
61+
62+
/**
63+
* Check whether the request is allowed.
64+
*
65+
* @param Request $request The request to check.
66+
*
67+
* @return boolean Whether access is granted.
68+
*/
69+
protected function isRequestAllowed(Request $request)
70+
{
71+
return $this->requestMatcher->matches($request);
72+
}
73+
}

src/SymfonyCache/EventDispatchingHttpCache.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,32 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
7878

7979
return parent::handle($request, $type, $catch);
8080
}
81+
82+
/**
83+
* Made public to allow event subscribers to do refresh operations.
84+
*
85+
* {@inheritDoc}
86+
*/
87+
public function fetch(Request $request, $catch = false)
88+
{
89+
return parent::fetch($request, $catch);
90+
}
91+
92+
/**
93+
* {@inheritDoc}
94+
*
95+
* Adding the Events::PRE_INVALIDATE event.
96+
*/
97+
protected function invalidate(Request $request, $catch = false)
98+
{
99+
if ($this->getEventDispatcher()->hasListeners(Events::PRE_INVALIDATE)) {
100+
$event = new CacheEvent($this, $request);
101+
$this->getEventDispatcher()->dispatch(Events::PRE_INVALIDATE, $event);
102+
if ($event->getResponse()) {
103+
return $event->getResponse();
104+
}
105+
}
106+
107+
return parent::invalidate($request, $catch);
108+
}
81109
}

src/SymfonyCache/Events.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@
1717
final class Events
1818
{
1919
const PRE_HANDLE = 'fos_http_cache.pre_handle';
20+
const PRE_INVALIDATE = 'fos_http_cache.pre_invalidate';
2021
}

src/SymfonyCache/PurgeSubscriber.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCache package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\HttpCache\SymfonyCache;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\RequestMatcher;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\HttpKernel\HttpKernelInterface;
19+
use Symfony\Component\OptionsResolver\OptionsResolver;
20+
21+
/**
22+
* Purge handler for the symfony built-in HttpCache.
23+
*
24+
* @author David Buchmann <[email protected]>
25+
*
26+
* {@inheritdoc}
27+
*/
28+
class PurgeSubscriber extends AccessControlledSubscriber
29+
{
30+
/**
31+
* The options configured in the constructor argument or default values.
32+
*
33+
* @var array
34+
*/
35+
private $options = array();
36+
37+
/**
38+
* When creating this subscriber, you can configure a number of options.
39+
*
40+
* - purge_method: HTTP method that identifies purge requests.
41+
* - purge_client_matcher: RequestMatcher to identify valid purge clients.
42+
* - purge_client_ips: IP or array of IPs that are allowed to purge.
43+
*
44+
* Only set one of purge_client_ips and purge_client_matcher.
45+
*
46+
* @param array $options Options to overwrite the default options
47+
*
48+
* @throws \InvalidArgumentException if unknown keys are found in $options
49+
*/
50+
public function __construct(array $options = array())
51+
{
52+
$resolver = new OptionsResolver();
53+
$resolver->setDefined(array('purge_client_matcher', 'purge_client_ips', 'purge_method'));
54+
$resolver->setDefaults(array(
55+
'purge_client_matcher' => null,
56+
'purge_client_ips' => null,
57+
'purge_method' => 'PURGE',
58+
));
59+
60+
$this->options = $resolver->resolve($options);
61+
62+
parent::__construct($this->options['purge_client_matcher'], $this->options['purge_client_ips']);
63+
}
64+
65+
/**
66+
* {@inheritdoc}
67+
*/
68+
public static function getSubscribedEvents()
69+
{
70+
return array(
71+
Events::PRE_INVALIDATE => 'handlePurge',
72+
);
73+
}
74+
75+
/**
76+
* Look at unsafe requests and handle purge requests.
77+
*
78+
* Prevents access when the request comes from a non-authorized client.
79+
*
80+
* @param CacheEvent $event
81+
*/
82+
public function handlePurge(CacheEvent $event)
83+
{
84+
$request = $event->getRequest();
85+
if ($this->options['purge_method'] !== $request->getMethod()) {
86+
return;
87+
}
88+
89+
if (!$this->isRequestAllowed($request)) {
90+
$event->setResponse(new Response('', 400));
91+
92+
return;
93+
}
94+
95+
$response = new Response();
96+
if ($event->getKernel()->getStore()->purge($request->getUri())) {
97+
$response->setStatusCode(200, 'Purged');
98+
} else {
99+
$response->setStatusCode(200, 'Not found');
100+
}
101+
$event->setResponse($response);
102+
}
103+
}

0 commit comments

Comments
 (0)