|
| 1 | +.. index:: |
| 2 | + single: Cache; ESI |
| 3 | + single: ESI |
| 4 | + |
| 5 | +Working with Edge Side Includes |
| 6 | +=============================== |
| 7 | + |
| 8 | +Gateway caches are a great way to make your website perform better. But they |
| 9 | +have one limitation: they can only cache whole pages. If you can't cache |
| 10 | +whole pages or if parts of a page has "more" dynamic parts, you are out of |
| 11 | +luck. Fortunately, Symfony provides a solution for these cases, based on a |
| 12 | +technology called `ESI`_, or Edge Side Includes. Akamai wrote this specification |
| 13 | +almost 10 years ago and it allows specific parts of a page to have a different |
| 14 | +caching strategy than the main page. |
| 15 | + |
| 16 | +The ESI specification describes tags you can embed in your pages to communicate |
| 17 | +with the gateway cache. Only one tag is implemented in Symfony, ``include``, |
| 18 | +as this is the only useful one outside of Akamai context: |
| 19 | + |
| 20 | +.. code-block:: html |
| 21 | + |
| 22 | + <!DOCTYPE html> |
| 23 | + <html> |
| 24 | + <body> |
| 25 | + <!-- ... some content --> |
| 26 | + |
| 27 | + <!-- Embed the content of another page here --> |
| 28 | + <esi:include src="http://..." /> |
| 29 | + |
| 30 | + <!-- ... more content --> |
| 31 | + </body> |
| 32 | + </html> |
| 33 | + |
| 34 | +.. note:: |
| 35 | + |
| 36 | + Notice from the example that each ESI tag has a fully-qualified URL. |
| 37 | + An ESI tag represents a page fragment that can be fetched via the given |
| 38 | + URL. |
| 39 | + |
| 40 | +When a request is handled, the gateway cache fetches the entire page from |
| 41 | +its cache or requests it from the backend application. If the response contains |
| 42 | +one or more ESI tags, these are processed in the same way. In other words, |
| 43 | +the gateway cache either retrieves the included page fragment from its cache |
| 44 | +or requests the page fragment from the backend application again. When all |
| 45 | +the ESI tags have been resolved, the gateway cache merges each into the main |
| 46 | +page and sends the final content to the client. |
| 47 | + |
| 48 | +All of this happens transparently at the gateway cache level (i.e. outside |
| 49 | +of your application). As you'll see, if you choose to take advantage of ESI |
| 50 | +tags, Symfony makes the process of including them almost effortless. |
| 51 | + |
| 52 | +.. _using-esi-in-symfony2: |
| 53 | + |
| 54 | +Using ESI in Symfony |
| 55 | +~~~~~~~~~~~~~~~~~~~~ |
| 56 | + |
| 57 | +First, to use ESI, be sure to enable it in your application configuration: |
| 58 | + |
| 59 | +.. configuration-block:: |
| 60 | + |
| 61 | + .. code-block:: yaml |
| 62 | +
|
| 63 | + # app/config/config.yml |
| 64 | + framework: |
| 65 | + # ... |
| 66 | + esi: { enabled: true } |
| 67 | +
|
| 68 | + .. code-block:: xml |
| 69 | +
|
| 70 | + <!-- app/config/config.xml --> |
| 71 | + <?xml version="1.0" encoding="UTF-8" ?> |
| 72 | + <container xmlns="http://symfony.com/schema/dic/symfony" |
| 73 | + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| 74 | + xmlns:framework="http://symfony.com/schema/dic/symfony" |
| 75 | + xsi:schemaLocation="http://symfony.com/schema/dic/services |
| 76 | + http://symfony.com/schema/dic/services/services-1.0.xsd |
| 77 | + http://symfony.com/schema/dic/symfony |
| 78 | + http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> |
| 79 | +
|
| 80 | + <framework:config> |
| 81 | + <!-- ... --> |
| 82 | + <framework:esi enabled="true" /> |
| 83 | + </framework:config> |
| 84 | + </container> |
| 85 | +
|
| 86 | + .. code-block:: php |
| 87 | +
|
| 88 | + // app/config/config.php |
| 89 | + $container->loadFromExtension('framework', array( |
| 90 | + // ... |
| 91 | + 'esi' => array('enabled' => true), |
| 92 | + )); |
| 93 | +
|
| 94 | +Now, suppose you have a page that is relatively static, except for a news |
| 95 | +ticker at the bottom of the content. With ESI, you can cache the news ticker |
| 96 | +independent of the rest of the page. |
| 97 | + |
| 98 | +.. code-block:: php |
| 99 | +
|
| 100 | + // src/AppBundle/Controller/DefaultController.php |
| 101 | +
|
| 102 | + // ... |
| 103 | + class DefaultController extends Controller |
| 104 | + { |
| 105 | + public function aboutAction() |
| 106 | + { |
| 107 | + $response = $this->render('static/about.html.twig'); |
| 108 | + // set the shared max age - which also marks the response as public |
| 109 | + $response->setSharedMaxAge(600); |
| 110 | +
|
| 111 | + return $response; |
| 112 | + } |
| 113 | + } |
| 114 | +
|
| 115 | +In this example, the full-page cache has a lifetime of ten minutes. |
| 116 | +Next, include the news ticker in the template by embedding an action. |
| 117 | +This is done via the ``render`` helper (see :doc:`/templating/embedding_controllers` |
| 118 | +for more details). |
| 119 | + |
| 120 | +As the embedded content comes from another page (or controller for that |
| 121 | +matter), Symfony uses the standard ``render`` helper to configure ESI tags: |
| 122 | + |
| 123 | +.. configuration-block:: |
| 124 | + |
| 125 | + .. code-block:: twig |
| 126 | +
|
| 127 | + {# app/Resources/views/static/about.html.twig #} |
| 128 | +
|
| 129 | + {# you can use a controller reference #} |
| 130 | + {{ render_esi(controller('AppBundle:News:latest', { 'maxPerPage': 5 })) }} |
| 131 | +
|
| 132 | + {# ... or a URL #} |
| 133 | + {{ render_esi(url('latest_news', { 'maxPerPage': 5 })) }} |
| 134 | +
|
| 135 | + .. code-block:: html+php |
| 136 | + |
| 137 | + <!-- app/Resources/views/static/about.html.php --> |
| 138 | + |
| 139 | + // you can use a controller reference |
| 140 | + use Symfony\Component\HttpKernel\Controller\ControllerReference; |
| 141 | + <?php echo $view['actions']->render( |
| 142 | + new ControllerReference( |
| 143 | + 'AppBundle:News:latest', |
| 144 | + array('maxPerPage' => 5) |
| 145 | + ), |
| 146 | + array('strategy' => 'esi') |
| 147 | + ) ?> |
| 148 | + |
| 149 | + // ... or a URL |
| 150 | + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; |
| 151 | + <?php echo $view['actions']->render( |
| 152 | + $view['router']->generate( |
| 153 | + 'latest_news', |
| 154 | + array('maxPerPage' => 5), |
| 155 | + UrlGeneratorInterface::ABSOLUTE_URL |
| 156 | + ), |
| 157 | + array('strategy' => 'esi'), |
| 158 | + ) ?> |
| 159 | + |
| 160 | +By using the ``esi`` renderer (via the ``render_esi`` Twig function), you |
| 161 | +tell Symfony that the action should be rendered as an ESI tag. You might be |
| 162 | +wondering why you would want to use a helper instead of just writing the ESI |
| 163 | +tag yourself. That's because using a helper makes your application work even |
| 164 | +if there is no gateway cache installed. |
| 165 | + |
| 166 | +.. tip:: |
| 167 | + |
| 168 | + As you'll see below, the ``maxPerPage`` variable you pass is available |
| 169 | + as an argument to your controller (i.e. ``$maxPerPage``). The variables |
| 170 | + passed through ``render_esi`` also become part of the cache key so that |
| 171 | + you have unique caches for each combination of variables and values. |
| 172 | + |
| 173 | +When using the default ``render`` function (or setting the renderer to |
| 174 | +``inline``), Symfony merges the included page content into the main one |
| 175 | +before sending the response to the client. But if you use the ``esi`` renderer |
| 176 | +(i.e. call ``render_esi``) *and* if Symfony detects that it's talking to a |
| 177 | +gateway cache that supports ESI, it generates an ESI include tag. But if there |
| 178 | +is no gateway cache or if it does not support ESI, Symfony will just merge |
| 179 | +the included page content within the main one as it would have done if you had |
| 180 | +used ``render``. |
| 181 | + |
| 182 | +.. note:: |
| 183 | + |
| 184 | + Symfony detects if a gateway cache supports ESI via another Akamai |
| 185 | + specification that is supported out of the box by the Symfony reverse |
| 186 | + proxy. |
| 187 | + |
| 188 | +The embedded action can now specify its own caching rules, entirely independent |
| 189 | +of the master page. |
| 190 | + |
| 191 | +.. code-block:: php |
| 192 | +
|
| 193 | + // src/AppBundle/Controller/NewsController.php |
| 194 | + namespace AppBundle\Controller; |
| 195 | +
|
| 196 | + // ... |
| 197 | + class NewsController extends Controller |
| 198 | + { |
| 199 | + public function latestAction($maxPerPage) |
| 200 | + { |
| 201 | + // ... |
| 202 | + $response->setSharedMaxAge(60); |
| 203 | +
|
| 204 | + return $response; |
| 205 | + } |
| 206 | + } |
| 207 | +
|
| 208 | +With ESI, the full page cache will be valid for 600 seconds, but the news |
| 209 | +component cache will only last for 60 seconds. |
| 210 | + |
| 211 | +.. _book-http_cache-fragments: |
| 212 | + |
| 213 | +When using a controller reference, the ESI tag should reference the embedded |
| 214 | +action as an accessible URL so the gateway cache can fetch it independently of |
| 215 | +the rest of the page. Symfony takes care of generating a unique URL for any |
| 216 | +controller reference and it is able to route them properly thanks to the |
| 217 | +:class:`Symfony\\Component\\HttpKernel\\EventListener\\FragmentListener` |
| 218 | +that must be enabled in your configuration: |
| 219 | + |
| 220 | +.. configuration-block:: |
| 221 | + |
| 222 | + .. code-block:: yaml |
| 223 | +
|
| 224 | + # app/config/config.yml |
| 225 | + framework: |
| 226 | + # ... |
| 227 | + fragments: { path: /_fragment } |
| 228 | +
|
| 229 | + .. code-block:: xml |
| 230 | +
|
| 231 | + <!-- app/config/config.xml --> |
| 232 | + <?xml version="1.0" encoding="UTF-8" ?> |
| 233 | + <container xmlns="http://symfony.com/schema/dic/services" |
| 234 | + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| 235 | + xmlns:doctrine="http://symfony.com/schema/dic/framework" |
| 236 | + xsi:schemaLocation="http://symfony.com/schema/dic/services |
| 237 | + http://symfony.com/schema/dic/services/services-1.0.xsd |
| 238 | + http://symfony.com/schema/dic/symfony |
| 239 | + http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> |
| 240 | +
|
| 241 | + <!-- ... --> |
| 242 | + <framework:config> |
| 243 | + <framework:fragments path="/_fragment" /> |
| 244 | + </framework:config> |
| 245 | + </container> |
| 246 | +
|
| 247 | + .. code-block:: php |
| 248 | +
|
| 249 | + // app/config/config.php |
| 250 | + $container->loadFromExtension('framework', array( |
| 251 | + // ... |
| 252 | + 'fragments' => array('path' => '/_fragment'), |
| 253 | + )); |
| 254 | +
|
| 255 | +One great advantage of the ESI renderer is that you can make your application |
| 256 | +as dynamic as needed and at the same time, hit the application as little as |
| 257 | +possible. |
| 258 | + |
| 259 | +.. caution:: |
| 260 | + |
| 261 | + The fragment listener only responds to signed requests. Requests are only |
| 262 | + signed when using the fragment renderer and the ``render_esi`` Twig |
| 263 | + function. |
| 264 | + |
| 265 | +.. note:: |
| 266 | + |
| 267 | + Once you start using ESI, remember to always use the ``s-maxage`` |
| 268 | + directive instead of ``max-age``. As the browser only ever receives the |
| 269 | + aggregated resource, it is not aware of the sub-components, and so it will |
| 270 | + obey the ``max-age`` directive and cache the entire page. And you don't |
| 271 | + want that. |
| 272 | + |
| 273 | +The ``render_esi`` helper supports two other useful options: |
| 274 | + |
| 275 | +``alt`` |
| 276 | + Used as the ``alt`` attribute on the ESI tag, which allows you to specify an |
| 277 | + alternative URL to be used if the ``src`` cannot be found. |
| 278 | + |
| 279 | +``ignore_errors`` |
| 280 | + If set to true, an ``onerror`` attribute will be added to the ESI with a value |
| 281 | + of ``continue`` indicating that, in the event of a failure, the gateway cache |
| 282 | + will simply remove the ESI tag silently. |
| 283 | + |
| 284 | +.. _`ESI`: http://www.w3.org/TR/esi-lang |
0 commit comments