Skip to content

Commit d501d01

Browse files
committed
Merge pull request #233 from FriendsOfSymfony/httpcache-trait
Provide Symfony HttpCache as trait
2 parents 143f15a + 9f689e9 commit d501d01

13 files changed

+767
-297
lines changed

doc/symfony-cache-configuration.rst

Lines changed: 80 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,61 +9,101 @@ than using Varnish or NGINX, it can still provide considerable performance
99
gains over an installation that is not cached at all. It can be useful for
1010
running an application on shared hosting for instance.
1111

12-
You can use features of this library with the help of the
13-
``EventDispatchingHttpCache`` provided here. The basic concept is to use event
14-
subscribers on the HttpCache class.
12+
You can use features of this library with the help of event listeners that act
13+
on events of the ``HttpCache``. The Symfony ``HttpCache`` does not have an
14+
event system, for this you need to use the trait ``EventDispatchingHttpCache``
15+
provided by this library. The event listeners handle the requests from the
16+
:doc:`proxy-clients`.
1517

16-
.. warning::
18+
.. note::
1719

18-
If you are using the full stack Symfony framework, have a look at the
19-
HttpCache provided by the FOSHttpCacheBundle_ instead.
20+
Symfony ``HttpCache`` does not currently provide support for banning.
21+
22+
Using the trait
23+
~~~~~~~~~~~~~~~
2024

2125
.. note::
2226

23-
Symfony HttpCache does not currently provide support for banning.
27+
The trait is available since version 2.0.0. Version 1.* of this library
28+
instead provided a base ``HttpCache`` class to extend.
29+
30+
Your ``AppCache`` needs to implement ``CacheInvalidationInterface`` and use the
31+
trait ``FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache``::
32+
33+
use FOS\HttpCache\SymfonyCache\CacheInvalidationInterface;
34+
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
35+
use Symfony\Component\HttpFoundation\Request;
36+
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
37+
38+
class AppCache extends HttpCache implements CacheInvalidationInterface
39+
{
40+
use EventDispatchingHttpCache;
2441

25-
Extending the Correct HttpCache Class
26-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
42+
/**
43+
* Made public to allow event subscribers to do refresh operations.
44+
*
45+
* {@inheritDoc}
46+
*/
47+
public function fetch(Request $request, $catch = false)
48+
{
49+
return parent::fetch($request, $catch);
50+
}
51+
}
2752

28-
Instead of extending ``Symfony\Component\HttpKernel\HttpCache\HttpCache``, your
29-
``AppCache`` should extend ``FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache``.
53+
The trait is adding events before and/or after kernel methods to let the
54+
listeners interfere. If you need to overwrite core ``HttpCache`` functionality
55+
in your kernel, one option is to provide your own event listeners. If you need
56+
to implement functionality directly on the methods, be careful to always call
57+
the trait methods rather than going directly to the parent, or events will not
58+
be triggered anymore. You might also need to copy a method from the trait and
59+
add your own logic between the events to not be too early or too late for the
60+
event.
3061

31-
.. tip::
62+
When starting to extend your ``AppCache``, it is recommended to use the
63+
``EventDispatchingHttpCacheTestCase`` to run tests with your kernel to be sure
64+
all events are triggered as expected.
3265

33-
If your class already needs to extend a different class, simply copy the
34-
event handling code from the EventDispatchingHttpCache into your
35-
``AppCache`` class and make it implement ``CacheInvalidationInterface``.
36-
The drawback is that you need to manually check whether you need to adjust
37-
your ``AppCache`` each time you update the FOSHttpCache library.
66+
Cache event listeners
67+
~~~~~~~~~~~~~~~~~~~~~
3868

3969
Now that you have an event dispatching kernel, you can make it register the
40-
subscribers you need. While you could do that from your bootstrap code, this is
70+
listeners you need. While you could do that from your bootstrap code, this is
4171
not the recommended way. You would need to adjust every place you instantiate
42-
the cache. Instead, overwrite the constructor of AppCache and register the
43-
subscribers there. A simple cache will look like this::
72+
the cache. Instead, overwrite the constructor of your ``AppCache`` and register
73+
the listeners you need there::
4474

45-
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
75+
use FOS\HttpCache\SymfonyCache\DebugListener();
76+
use FOS\HttpCache\SymfonyCache\CustomTtlListener();
4677
use FOS\HttpCache\SymfonyCache\PurgeSubscriber;
4778
use FOS\HttpCache\SymfonyCache\RefreshSubscriber;
4879
use FOS\HttpCache\SymfonyCache\UserContextSubscriber;
49-
use FOS\HttpCache\SymfonyCache\CustomTtlListener();
50-
51-
class AppCache extends EventDispatchingHttpCache
52-
{
53-
/**
54-
* Overwrite constructor to register event subscribers for FOSHttpCache.
55-
*/
56-
public function __construct(HttpKernelInterface $kernel, $cacheDir = null)
57-
{
58-
parent::__construct($kernel, $cacheDir);
5980

60-
$this->addSubscriber(new PurgeSubscriber());
61-
$this->addSubscriber(new RefreshSubscriber());
62-
$this->addSubscriber(new UserContextSubscriber());
63-
$this->addSubscriber(new CustomTtlListener());
81+
// ...
82+
83+
/**
84+
* Overwrite constructor to register event subscribers for FOSHttpCache.
85+
*/
86+
public function __construct(
87+
HttpKernelInterface $kernel,
88+
StoreInterface $store,
89+
SurrogateInterface $surrogate = null,
90+
array $options = array()
91+
) {
92+
parent::__construct($kernel, $store, $surrogate, $options);
93+
94+
$this->addSubscriber(new CustomTtlListener());
95+
$this->addSubscriber(new PurgeSubscriber());
96+
$this->addSubscriber(new RefreshSubscriber());
97+
$this->addSubscriber(new UserContextSubscriber());
98+
if (isset($options['debug']) && $options['debug']) {
99+
$this->addSubscriber(new DebugListener());
64100
}
65101
}
66102

103+
The event listeners can be tweaked by passing options to the constructor. The
104+
Symfony configuration system does not work here because things in the cache
105+
happen before the configuration is loaded.
106+
67107
Purge
68108
~~~~~
69109

@@ -204,28 +244,11 @@ Debugging
204244
~~~~~~~~~
205245

206246
For the ``assertHit`` and ``assertMiss`` assertions to work, you need to add
207-
debug information in your AppCache. Create the cache kernel with the option
208-
``'debug' => true`` and add the following to your ``AppCache``::
209-
210-
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
211-
{
212-
$response = parent::handle($request, $type, $catch);
213-
214-
if ($response->headers->has('X-Symfony-Cache')) {
215-
if (false !== strpos($response->headers->get('X-Symfony-Cache'), 'miss')) {
216-
$state = 'MISS';
217-
} elseif (false !== strpos($response->headers->get('X-Symfony-Cache'), 'fresh')) {
218-
$state = 'HIT';
219-
} else {
220-
$state = 'UNDETERMINED';
221-
}
222-
$response->headers->set('X-Cache', $state);
223-
}
224-
225-
return $response;
226-
}
247+
debug information in your AppCache. When running the tests, create the cache
248+
kernel with the option ``'debug' => true`` and add the ``DebugListener``.
227249

228-
The ``UNDETERMINED`` state should never happen. If it does, it means that your
229-
HttpCache is not correctly set into debug mode.
250+
The ``UNDETERMINED`` state should never happen. If it does, it means that
251+
something went really wrong in the kernel. Have a look at ``X-Symfony-Cache``
252+
and at the HTML body of the response.
230253

231254
.. _HttpCache: http://symfony.com/doc/current/book/http_cache.html#symfony-reverse-proxy

src/SymfonyCache/DebugListener.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
* Debug handler for the symfony built-in HttpCache.
23+
*
24+
* Add debug information to the response for use in cache tests.
25+
*
26+
* @author David Buchmann <[email protected]>
27+
*
28+
* {@inheritdoc}
29+
*/
30+
class DebugListener implements EventSubscriberInterface
31+
{
32+
/**
33+
* {@inheritdoc}
34+
*/
35+
public static function getSubscribedEvents()
36+
{
37+
return [
38+
Events::POST_HANDLE => 'handleDebug',
39+
];
40+
}
41+
42+
/**
43+
* Extract the cache HIT/MISS information from the X-Symfony-Cache header.
44+
*
45+
* For this header to be present, the HttpCache must be created with the
46+
* debug option set to true.
47+
*
48+
* @param CacheEvent $event
49+
*/
50+
public function handleDebug(CacheEvent $event)
51+
{
52+
$response = $event->getResponse();
53+
if ($response->headers->has('X-Symfony-Cache')) {
54+
if (false !== strpos($response->headers->get('X-Symfony-Cache'), 'miss')) {
55+
$state = 'MISS';
56+
} elseif (false !== strpos($response->headers->get('X-Symfony-Cache'), 'fresh')) {
57+
$state = 'HIT';
58+
} else {
59+
$state = 'UNDETERMINED';
60+
}
61+
$response->headers->set('X-Cache', $state);
62+
}
63+
}
64+
}

src/SymfonyCache/EventDispatchingHttpCache.php

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,36 @@
1111

1212
namespace FOS\HttpCache\SymfonyCache;
1313

14-
use Symfony\Component\HttpFoundation\Response;
15-
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
1614
use Symfony\Component\EventDispatcher\EventDispatcher;
1715
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1816
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1917
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpFoundation\Response;
2019
use Symfony\Component\HttpKernel\HttpKernelInterface;
2120

2221
/**
23-
* Base class for enhanced Symfony reverse proxy based on the symfony component.
22+
* Trait for enhanced Symfony reverse proxy based on the symfony kernel component.
2423
*
25-
* <b>When using FOSHttpCacheBundle, look at FOS\HttpCacheBundle\HttpCache instead.</b>
24+
* Your kernel needs to implement CacheInvalidatorInterface and redeclare the
25+
* fetch method as public. (The latter is needed because the trait declaring it
26+
* public does not satisfy the interface for whatever reason. See also
27+
* http://stackoverflow.com/questions/31877844/php-trait-exposing-a-method-and-interfaces )
2628
*
27-
* This kernel supports event subscribers that can act on the events defined in
28-
* FOS\HttpCache\SymfonyCache\Events and may alter the request flow.
29+
* CacheInvalidator kernels support event subscribers that can act on the
30+
* events defined in FOS\HttpCache\SymfonyCache\Events and may alter the
31+
* request flow.
32+
*
33+
* If your kernel overwrites any of the methods defined in this trait, make
34+
* sure to also call the trait method. You might get into issues with the order
35+
* of events, in which case you will need to copy event triggering into your
36+
* kernel.
2937
*
3038
* @author Jérôme Vieilledent <[email protected]> (courtesy of eZ Systems AS)
39+
* @author David Buchmann <[email protected]>
3140
*
3241
* {@inheritdoc}
3342
*/
34-
abstract class EventDispatchingHttpCache extends HttpCache implements CacheInvalidationInterface
43+
trait EventDispatchingHttpCache
3544
{
3645
/**
3746
* @var EventDispatcherInterface
@@ -69,17 +78,13 @@ public function addSubscriber(EventSubscriberInterface $subscriber)
6978
*/
7079
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
7180
{
72-
if ($this->getEventDispatcher()->hasListeners(Events::PRE_HANDLE)) {
73-
$event = new CacheEvent($this, $request);
74-
$this->getEventDispatcher()->dispatch(Events::PRE_HANDLE, $event);
75-
if ($event->getResponse()) {
76-
return $this->dispatchPostHandle($request, $event->getResponse());
77-
}
81+
if ($response = $this->dispatch(Events::PRE_HANDLE, $request)) {
82+
return $this->dispatch(Events::POST_HANDLE, $request, $response);
7883
}
7984

8085
$response = parent::handle($request, $type, $catch);
8186

82-
return $this->dispatchPostHandle($request, $response);
87+
return $this->dispatch(Events::POST_HANDLE, $request, $response);
8388
}
8489

8590
/**
@@ -89,59 +94,42 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
8994
*/
9095
protected function store(Request $request, Response $response)
9196
{
92-
if ($this->getEventDispatcher()->hasListeners(Events::PRE_STORE)) {
93-
$event = new CacheEvent($this, $request, $response);
94-
$this->getEventDispatcher()->dispatch(Events::PRE_STORE, $event);
95-
$response = $event->getResponse();
96-
}
97+
$response = $this->dispatch(Events::PRE_STORE, $request, $response);
9798

9899
parent::store($request, $response);
99100
}
100101

101102
/**
102-
* Dispatch the POST_HANDLE event if needed.
103-
*
104-
* @param Request $request
105-
* @param Response $response
103+
* {@inheritDoc}
106104
*
107-
* @return Response The response to return which might be altered by a POST_HANDLE listener.
105+
* Adding the Events::PRE_INVALIDATE event.
108106
*/
109-
private function dispatchPostHandle(Request $request, Response $response)
107+
protected function invalidate(Request $request, $catch = false)
110108
{
111-
if ($this->getEventDispatcher()->hasListeners(Events::POST_HANDLE)) {
112-
$event = new CacheEvent($this, $request, $response);
113-
$this->getEventDispatcher()->dispatch(Events::POST_HANDLE, $event);
114-
$response = $event->getResponse();
109+
if ($response = $this->dispatch(Events::PRE_INVALIDATE, $request)) {
110+
return $response;
115111
}
116112

117-
return $response;
113+
return parent::invalidate($request, $catch);
118114
}
119115

120116
/**
121-
* Made public to allow event subscribers to do refresh operations.
117+
* Dispatch an event if needed.
122118
*
123-
* {@inheritDoc}
124-
*/
125-
public function fetch(Request $request, $catch = false)
126-
{
127-
return parent::fetch($request, $catch);
128-
}
129-
130-
/**
131-
* {@inheritDoc}
119+
* @param string $name Name of the event to trigger. One of the constants in FOS\HttpCache\SymfonyCache\Events
120+
* @param Request $request
121+
* @param Response|null $response If already available
132122
*
133-
* Adding the Events::PRE_INVALIDATE event.
123+
* @return Response The response to return, which might be provided/altered by a listener.
134124
*/
135-
protected function invalidate(Request $request, $catch = false)
125+
protected function dispatch($name, Request $request, Response $response = null)
136126
{
137-
if ($this->getEventDispatcher()->hasListeners(Events::PRE_INVALIDATE)) {
138-
$event = new CacheEvent($this, $request);
139-
$this->getEventDispatcher()->dispatch(Events::PRE_INVALIDATE, $event);
140-
if ($event->getResponse()) {
141-
return $event->getResponse();
142-
}
127+
if ($this->getEventDispatcher()->hasListeners($name)) {
128+
$event = new CacheEvent($this, $request, $response);
129+
$this->getEventDispatcher()->dispatch($name, $event);
130+
$response = $event->getResponse();
143131
}
144132

145-
return parent::invalidate($request, $catch);
133+
return $response;
146134
}
147135
}

0 commit comments

Comments
 (0)