Skip to content

Provide Symfony HttpCache as trait #233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 24, 2015
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
137 changes: 80 additions & 57 deletions doc/symfony-cache-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,101 @@ than using Varnish or NGINX, it can still provide considerable performance
gains over an installation that is not cached at all. It can be useful for
running an application on shared hosting for instance.

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

.. warning::
.. note::

If you are using the full stack Symfony framework, have a look at the
HttpCache provided by the FOSHttpCacheBundle_ instead.
Symfony ``HttpCache`` does not currently provide support for banning.

Using the trait
~~~~~~~~~~~~~~~

.. note::

Symfony HttpCache does not currently provide support for banning.
The trait is available since version 2.0.0. Version 1.* of this library
instead provided a base ``HttpCache`` class to extend.

Your ``AppCache`` needs to implement ``CacheInvalidationInterface`` and use the
trait ``FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache``::

use FOS\HttpCache\SymfonyCache\CacheInvalidationInterface;
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;

class AppCache extends HttpCache implements CacheInvalidationInterface
{
use EventDispatchingHttpCache;

Extending the Correct HttpCache Class
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
* Made public to allow event subscribers to do refresh operations.
*
* {@inheritDoc}
*/
public function fetch(Request $request, $catch = false)
{
return parent::fetch($request, $catch);
}
}

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

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

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

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

use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use FOS\HttpCache\SymfonyCache\DebugListener();
use FOS\HttpCache\SymfonyCache\CustomTtlListener();
use FOS\HttpCache\SymfonyCache\PurgeSubscriber;
use FOS\HttpCache\SymfonyCache\RefreshSubscriber;
use FOS\HttpCache\SymfonyCache\UserContextSubscriber;
use FOS\HttpCache\SymfonyCache\CustomTtlListener();

class AppCache extends EventDispatchingHttpCache
{
/**
* Overwrite constructor to register event subscribers for FOSHttpCache.
*/
public function __construct(HttpKernelInterface $kernel, $cacheDir = null)
{
parent::__construct($kernel, $cacheDir);

$this->addSubscriber(new PurgeSubscriber());
$this->addSubscriber(new RefreshSubscriber());
$this->addSubscriber(new UserContextSubscriber());
$this->addSubscriber(new CustomTtlListener());
// ...

/**
* Overwrite constructor to register event subscribers for FOSHttpCache.
*/
public function __construct(
HttpKernelInterface $kernel,
StoreInterface $store,
SurrogateInterface $surrogate = null,
array $options = array()
) {
parent::__construct($kernel, $store, $surrogate, $options);

$this->addSubscriber(new CustomTtlListener());
$this->addSubscriber(new PurgeSubscriber());
$this->addSubscriber(new RefreshSubscriber());
$this->addSubscriber(new UserContextSubscriber());
if (isset($options['debug']) && $options['debug']) {
$this->addSubscriber(new DebugListener());
}
}

The event listeners can be tweaked by passing options to the constructor. The
Symfony configuration system does not work here because things in the cache
happen before the configuration is loaded.

Purge
~~~~~

Expand Down Expand Up @@ -204,28 +244,11 @@ Debugging
~~~~~~~~~

For the ``assertHit`` and ``assertMiss`` assertions to work, you need to add
debug information in your AppCache. Create the cache kernel with the option
``'debug' => true`` and add the following to your ``AppCache``::

public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
$response = parent::handle($request, $type, $catch);

if ($response->headers->has('X-Symfony-Cache')) {
if (false !== strpos($response->headers->get('X-Symfony-Cache'), 'miss')) {
$state = 'MISS';
} elseif (false !== strpos($response->headers->get('X-Symfony-Cache'), 'fresh')) {
$state = 'HIT';
} else {
$state = 'UNDETERMINED';
}
$response->headers->set('X-Cache', $state);
}

return $response;
}
debug information in your AppCache. When running the tests, create the cache
kernel with the option ``'debug' => true`` and add the ``DebugListener``.

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

.. _HttpCache: http://symfony.com/doc/current/book/http_cache.html#symfony-reverse-proxy
64 changes: 64 additions & 0 deletions src/SymfonyCache/DebugListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?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\SymfonyCache;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* Debug handler for the symfony built-in HttpCache.
*
* Add debug information to the response for use in cache tests.
*
* @author David Buchmann <[email protected]>
*
* {@inheritdoc}
*/
class DebugListener implements EventSubscriberInterface
{
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
Events::POST_HANDLE => 'handleDebug',
];
}

/**
* Extract the cache HIT/MISS information from the X-Symfony-Cache header.
*
* For this header to be present, the HttpCache must be created with the
* debug option set to true.
*
* @param CacheEvent $event
*/
public function handleDebug(CacheEvent $event)
{
$response = $event->getResponse();
if ($response->headers->has('X-Symfony-Cache')) {
if (false !== strpos($response->headers->get('X-Symfony-Cache'), 'miss')) {
$state = 'MISS';
} elseif (false !== strpos($response->headers->get('X-Symfony-Cache'), 'fresh')) {
$state = 'HIT';
} else {
$state = 'UNDETERMINED';
}
$response->headers->set('X-Cache', $state);
}
}
}
86 changes: 37 additions & 49 deletions src/SymfonyCache/EventDispatchingHttpCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,36 @@

namespace FOS\HttpCache\SymfonyCache;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;

/**
* Base class for enhanced Symfony reverse proxy based on the symfony component.
* Trait for enhanced Symfony reverse proxy based on the symfony kernel component.
*
* <b>When using FOSHttpCacheBundle, look at FOS\HttpCacheBundle\HttpCache instead.</b>
* Your kernel needs to implement CacheInvalidatorInterface and redeclare the
* fetch method as public. (The latter is needed because the trait declaring it
* public does not satisfy the interface for whatever reason. See also
* http://stackoverflow.com/questions/31877844/php-trait-exposing-a-method-and-interfaces )
*
* This kernel supports event subscribers that can act on the events defined in
* FOS\HttpCache\SymfonyCache\Events and may alter the request flow.
* CacheInvalidator kernels support event subscribers that can act on the
* events defined in FOS\HttpCache\SymfonyCache\Events and may alter the
* request flow.
*
* If your kernel overwrites any of the methods defined in this trait, make
* sure to also call the trait method. You might get into issues with the order
* of events, in which case you will need to copy event triggering into your
* kernel.
*
* @author Jérôme Vieilledent <[email protected]> (courtesy of eZ Systems AS)
* @author David Buchmann <[email protected]>
*
* {@inheritdoc}
*/
abstract class EventDispatchingHttpCache extends HttpCache implements CacheInvalidationInterface
trait EventDispatchingHttpCache
{
/**
* @var EventDispatcherInterface
Expand Down Expand Up @@ -69,17 +78,13 @@ public function addSubscriber(EventSubscriberInterface $subscriber)
*/
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
if ($this->getEventDispatcher()->hasListeners(Events::PRE_HANDLE)) {
$event = new CacheEvent($this, $request);
$this->getEventDispatcher()->dispatch(Events::PRE_HANDLE, $event);
if ($event->getResponse()) {
return $this->dispatchPostHandle($request, $event->getResponse());
}
if ($response = $this->dispatch(Events::PRE_HANDLE, $request)) {
return $this->dispatch(Events::POST_HANDLE, $request, $response);
}

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

return $this->dispatchPostHandle($request, $response);
return $this->dispatch(Events::POST_HANDLE, $request, $response);
}

/**
Expand All @@ -89,59 +94,42 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
*/
protected function store(Request $request, Response $response)
{
if ($this->getEventDispatcher()->hasListeners(Events::PRE_STORE)) {
$event = new CacheEvent($this, $request, $response);
$this->getEventDispatcher()->dispatch(Events::PRE_STORE, $event);
$response = $event->getResponse();
}
$response = $this->dispatch(Events::PRE_STORE, $request, $response);

parent::store($request, $response);
}

/**
* Dispatch the POST_HANDLE event if needed.
*
* @param Request $request
* @param Response $response
* {@inheritDoc}
*
* @return Response The response to return which might be altered by a POST_HANDLE listener.
* Adding the Events::PRE_INVALIDATE event.
*/
private function dispatchPostHandle(Request $request, Response $response)
protected function invalidate(Request $request, $catch = false)
{
if ($this->getEventDispatcher()->hasListeners(Events::POST_HANDLE)) {
$event = new CacheEvent($this, $request, $response);
$this->getEventDispatcher()->dispatch(Events::POST_HANDLE, $event);
$response = $event->getResponse();
if ($response = $this->dispatch(Events::PRE_INVALIDATE, $request)) {
return $response;
}

return $response;
return parent::invalidate($request, $catch);
}

/**
* Made public to allow event subscribers to do refresh operations.
* Dispatch an event if needed.
*
* {@inheritDoc}
*/
public function fetch(Request $request, $catch = false)
{
return parent::fetch($request, $catch);
}

/**
* {@inheritDoc}
* @param string $name Name of the event to trigger. One of the constants in FOS\HttpCache\SymfonyCache\Events
* @param Request $request
* @param Response|null $response If already available
*
* Adding the Events::PRE_INVALIDATE event.
* @return Response The response to return, which might be provided/altered by a listener.
*/
protected function invalidate(Request $request, $catch = false)
protected function dispatch($name, Request $request, Response $response = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made this protected so that kernels using the trait can also use it, if they need to expand on a method and still need the pre and post event.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing as it does not work anyways

{
if ($this->getEventDispatcher()->hasListeners(Events::PRE_INVALIDATE)) {
$event = new CacheEvent($this, $request);
$this->getEventDispatcher()->dispatch(Events::PRE_INVALIDATE, $event);
if ($event->getResponse()) {
return $event->getResponse();
}
if ($this->getEventDispatcher()->hasListeners($name)) {
$event = new CacheEvent($this, $request, $response);
$this->getEventDispatcher()->dispatch($name, $event);
$response = $event->getResponse();
}

return parent::invalidate($request, $catch);
return $response;
}
}
Loading