Symfony HttpCache Configuration
Symfony’s HttpKernel component provides a reverse proxy implemented completely in PHP, called HttpCache. While it is certainly less efficient 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 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
cache invalidator.
Note
Symfony HttpCache
does not currently provide support for banning.
Using the trait
Note
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 CacheInvalidation
and use the
trait FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache
:
// app/AppCache.php
use FOS\HttpCache\SymfonyCache\CacheInvalidation;
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
class AppCache extends HttpCache implements CacheInvalidation
{
use EventDispatchingHttpCache;
/**
* Made public to allow event listeners to do refresh operations.
*
* {@inheritDoc}
*/
public function fetch(Request $request, $catch = false)
{
return parent::fetch($request, $catch);
}
}
The trait adds the addSubscriber
and addListener
methods as defined in
the EventDispatcherInterface
to your cache kernel. In addition, it triggers
events before and/or after kernel methods to let the listeners interact. If you
need to overwrite core HttpCache
functionality in your kernel, you can
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.
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.
Note
If you use HttpKernel::loadClassCache
from the console, you will need
to add class_exists('FOS\\HttpCache\\SymfonyCache\\CacheEvent');
right
after the inclusion of bootstrap.php.cache
in app/console
. For web
requests, this is done automatically by the trait. If you miss to do so,
you will get the following error:
Fatal error: Cannot redeclare class Symfony\Component\EventDispatcher\Event in app/cache/dev/classes.php on line ...
Cache event listeners
Now that you have an event dispatching kernel, you can make it register the
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 your AppCache
and register
the listeners you need there:
use FOS\HttpCache\SymfonyCache\DebugListener();
use FOS\HttpCache\SymfonyCache\CustomTtlListener();
use FOS\HttpCache\SymfonyCache\PurgeListener;
use FOS\HttpCache\SymfonyCache\RefreshListener;
use FOS\HttpCache\SymfonyCache\UserContextListener;
// ...
/**
* Overwrite constructor to register event listeners for FOSHttpCache.
*/
public function __construct(
HttpKernelInterface $kernel,
StoreInterface $store,
SurrogateInterface $surrogate = null,
array $options = []
) {
parent::__construct($kernel, $store, $surrogate, $options);
$this->addSubscriber(new CustomTtlListener());
$this->addSubscriber(new PurgeListener());
$this->addSubscriber(new RefreshListener());
$this->addSubscriber(new UserContextListener());
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
To support cache invalidation, register the
PurgeListener
. If the default settings are right for you, you don’t
need to do anything more.
Purging is only allowed from the same machine by default. To purge data from other hosts, provide the IPs of the machines allowed to purge, or provide a RequestMatcher that checks for an Authorization header or similar. Only set one of ``client_ips`` or ``client_matcher``.
client_ips: String with IP or array of IPs that are allowed to purge the cache.
default:
127.0.0.1
client_matcher: RequestMatcherInterface that only matches requests that are allowed to purge.
default:
null
purge_method: HTTP Method used with purge requests.
default:
PURGE
Refresh
To support cache refresh, register the
RefreshListener
. You can pass the constructor an option to specify
what clients are allowed to refresh cache entries.
The refresh listener needs to access the HttpCache::fetch
method which
is protected on the base HttpCache class. The EventDispatchingHttpCache
exposes the method as public, but if you implement your own kernel, you need
to overwrite the method to make it public.
Refreshing is only allowed from the same machine by default. To refresh from other hosts, provide the IPs of the machines allowed to refresh, or provide a RequestMatcher that checks for an Authorization header or similar. Only set one of ``client_ips`` or ``client_matcher``.
client_ips: String with IP or array of IPs that are allowed to refresh the cache.
default:
127.0.0.1
client_matcher: RequestMatcher that only matches requests that are allowed to refresh.
default:
null
Tagging
To support cache tags, require the additional package
toflar/psr6-symfony-http-cache-store:^1.0
with composer and register the
PurgeTagsListener
in your cache kernel. The purge listener needs your cache
to use the special Toflar\Psr6HttpCacheStore\Psr6Store
store, as the default
store does not have tagging support.
Note
Symfony’s HttpCache
store implementation does not support tags.
Therefore, you need the Toflar Psr6Store which implements the Symfony
Store interface but supports cache tagging. See the project README for more
information on the store.
To install the store, run
composer require toflar/psr6-symfony-http-cache-store
.
You should also add the CleanupCacheTagsListener
to make sure the final
response when sent to the client does not contain any cache tags in the
headers anymore.
Purging tags is only allowed from the same machine by default. To change this,
you have the same configuration options as with the PurgeListener
. Only
set one of ``client_ips`` or ``client_matcher``. Additionally, you can
configure the HTTP method and header used for tag purging:
client_ips: String with IP or array of IPs that are allowed to purge the cache.
default:
127.0.0.1
client_matcher: RequestMatcherInterface that only matches requests that are allowed to purge.
default:
null
tags_method: HTTP Method used with purge tags requests.
default:
PURGETAGS
tags_header: HTTP Header that contains the comma-separated tags to purge.
default:
X-Cache-Tags
tags_invalidate_path: Path on the caching proxy to which the purge tags request should be sent.
default:
/
tags_parser: Overwrite if you use a non-default glue to combine the tags in the header. This option expects a FOSHttpCacheTagHeaderFormatterTagHeaderParser instance, configured with the glue you want to use.
To get cache tagging support, register the PurgeTagsListener
and use the
Psr6Store
in your AppCache
:
// app/AppCache.php
use Toflar\Psr6HttpCacheStore\Psr6Store;
use FOS\HttpCache\SymfonyCache\PurgeTagsListener;
use FOS\HttpCache\SymfonyCache\CleanupCacheTagsListener;
const TAGS_HEADER = 'Custom-Cache-Tags-Header';
// ...
/**
* Overwrite constructor to register the Psr6Store and PurgeTagsListener.
*/
public function __construct(
HttpKernelInterface $kernel,
SurrogateInterface $surrogate = null,
array $options = []
) {
$store = new Psr6Store([
'cache_directory' => $kernel->getCacheDir(),
'cache_tags_header' => self::TAGS_HEADER,
]);
parent::__construct($kernel, $store, $surrogate, $options);
$this->addSubscriber(new PurgeTagsListener());
$this->addSubscriber(new CleanupCacheTagsListener(self::TAGS_HEADER));
}
User Context
To support user context hashing you need to register the
UserContextListener
. The user context is then automatically recognized
based on session cookies or authorization headers. If the default settings are
right for you, you don’t need to do anything more. You can customize a number of
options through the constructor:
anonymous_hash: Hard-coded hash to use for anonymous users. This is a performance optimization to not do a backend request for users that are not logged in. If you specify a non-empty value for this field, that is used as context hash header instead of doing a hash lookup for anonymous users.
user_hash_accept_header: Accept header value to be used to request the user hash to the backend application. Must match the setup of the backend application.
default:
application/vnd.fos.user-context-hash
user_hash_header: Name of the header the user context hash will be stored into. Must match the setup for the Vary header in the backend application.
default:
X-User-Context-Hash
user_hash_uri: Target URI used in the request for user context hash generation.
default:
/_fos_user_context_hash
user_hash_method: HTTP Method used with the hash lookup request for user context hash generation.
default:
GET
user_identifier_headers: List of request headers that authenticate a non-anonymous request.
default:
['Authorization', 'HTTP_AUTHORIZATION', 'PHP_AUTH_USER']
session_name_prefix: Prefix for session cookies. Must match your PHP session configuration. If cookies are not relevant in your application, you can set this to
false
to ignore any cookies. (Only set this to ``false`` if you do not use sessions at all.)default:
PHPSESSID
Warning
If you have a customized session name, it is very important that this constant matches it. Session IDs are indeed used as keys to cache the generated use context hash.
Wrong session name will lead to unexpected results such as having the same user context hash for every users, or not having it cached at all, which hurts performance.
Note
To use authorization headers for user context, you might have to add some server configuration to make these headers available to PHP.
With Apache, you can do this for example in a .htaccess
file:
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
Custom TTL
By default, the proxy server looks at the s-maxage
instruction in the
Cache-Control
header to know for how long it should cache a page. But the
Cache-Control
header is also sent to the client. Any caches on the Internet,
for example the Internet provider or from a cooperate network might look at
s-maxage
and cache the page. This can be a problem, notably when you do
explicit cache invalidation. In that
scenario, you want your proxy server to keep a page in cache for a long time,
but caches outside your control must not keep the page for a long duration.
One option could be to set a high s-maxage
for the proxy and simply rewrite
the response to remove or reduce the s-maxage
. This is not a good solution
however, as you start to duplicate your caching rule definitions.
The solution to this issue provided here is to use a separate, different header
called X-Reverse-Proxy-TTL
that controls the TTL of the proxy server to
keep s-maxage
for other proxies. Because this is not a standard feature,
you need to add configuration to your proxy server.
The CustomTtlListener
looks at a specific header to determine the TTL,
preferring that over s-maxage
. The default header is X-Reverse-Proxy-TTL
but you can customize that in the listener constructor:
new CustomTtlListener('My-TTL-Header');
The custom header is removed before sending the response to the client. You can enable keeping the custom header with the keepTtlHeader parameter:
new CustomTtlListener('My-TTL-Header', keepTtlHeader: true);
By default if the custom ttl header is not present, the listener falls back to the s-maxage cache-control directive. To disable this behavior, you can set the fallbackToSmaxage parameter to false:
new CustomTtlListener('My-TTL-Header', fallbackToSmaxage: false);
Debugging
For the assertHit
and assertMiss
assertions to work, you need to add
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
something went really wrong in the kernel. Have a look at X-Symfony-Cache
and at the HTML body of the response.