Using fragment cache with ESI

First of all, what’s ESI? If you are familiar with cache approaches, you may have heard about it. The ESI or Edge Side Includes is a small markup language for edge level dynamic web content assembly with the purpose to solve the following problem, e.g.:

A web page has some dynamic components that always change, on the other hand, others static parts of the same page barely are modified like the footer or header, what kind of cache strategy we can use? When you are using reverse cache also known as gateway cache in your application or web servers like Apache or Nginx at this context, this sort of cache will be useless, because in every request will be necessary to refresh the cache system to show the dynamic content because the cache works for the whole page. On this sample, the cache layer will be only an extra step to increase the latency. So, how to solve that problem?

When the ESI was proposed to the W3C by a great player in CDN, the Akamai, the aim was to solve this exact problem, in summary, we can use the ESI to define independent caching rules for any section of a page.

The ESI specification describes tags you can embed in your pages to communicate with the reverse cache.

How to implement using Symfony:

First, you have to enable the ESI in the config.yml

imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: services.yml }

parameters:
    locale: en

framework:
    esi: { enabled: true }
    fragments: { path: /_fragment }
    #translator: { fallbacks: ['%locale%'] }
    secret: '%secret%'
    router:
        resource: '%kernel.project_dir%/app/config/routing.yml'
        strict_requirements: ~
    form: ~
    csrf_protection: ~
    validation: { enable_annotations: true }<br><br><br><br>

Now inside the controller, we have to treat each part of the page as a single request and let our gateway cache deal with it by itself.

<?php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class CacheTestController extends Controller
{
	public function indexAction()
	{
		$response = $this->render('cache/index.html.twig');
		return $response;
	}

	public function headerAction()
	{
		$response = $this->render('cache/header.html.twig');
		$response->setSharedMaxAge(600);
		return $response;
	}
}

Enable the Reverse Proxy
Symfony comes with a reverse proxy written in PHP. It’s not a fully-featured reverse proxy cache like Varnish but allows us to verify what’is going on with our ESI tests. For enabling the reverse proxy to follow the code below.

require __DIR__.'/../vendor/autoload.php';
Debug::enable();

$kernel = new AppKernel('dev', true);
if (PHP_VERSION_ID < 70000) {
    $kernel->loadClassCache();
}
$kernel = new AppCache($kernel);

$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

Rendering
As the last step, in our view instead of using the regular “render” method use the “render_esi” method, in my case I’m using the twig and two partials page, one for the main page and the another for the header which is being rendered using ESI.

{{ render_esi(url('header', { 'maxPerPage': 5 })) }}
This is the body

In the image below you can now see two entries in the X-Symfony-Cache header – one for the main page request and another for the header but the page is still rendered as a single result, we could now apply a particular cache rule only for our header which will be less modified than the rest of page.

X-Symfony-Cache: GET /cache_test: stale, invalid, store; GET /header?maxPerPage=5: miss

References:

https://knpuniversity.com/screencast/new-symfony-2.2/fragments-esi-caching
https://www.packtpub.com/web-development/mastering-symfony
http://symfony.com/doc/current/http_cache.html#symfony2-reverse-proxy

Handling 10k requests per second with Symfony and Varnish – SymfonyCon Berlin 2016 from Alexander Lisachenko