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.