Skip to content

Commit 66d2cf0

Browse files
committed
split the HTTP cache chapter
1 parent 9746b35 commit 66d2cf0

File tree

6 files changed

+635
-615
lines changed

6 files changed

+635
-615
lines changed

cache/esi.rst

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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

cache/expiration.rst

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
.. index::
2+
single: Cache; HTTP expiration
3+
4+
HTTP Cache Expiration
5+
=====================
6+
7+
The `expiration model`_ is the more efficient and straightforward of the two
8+
caching models and should be used whenever possible. When a response is cached
9+
with an expiration, the cache will store the response and return it directly
10+
without hitting the application until it expires.
11+
12+
The expiration model can be accomplished using one of two, nearly identical,
13+
HTTP headers: ``Expires`` or ``Cache-Control``.
14+
15+
.. sidebar:: Expiration and Validation
16+
17+
You can of course use both validation and expiration within the same ``Response``.
18+
As expiration wins over validation, you can easily benefit from the best of
19+
both worlds. In other words, by using both expiration and validation, you
20+
can instruct the cache to serve the cached content, while checking back
21+
at some interval (the expiration) to verify that the content is still valid.
22+
23+
.. tip::
24+
25+
You can also define HTTP caching headers for expiration and validation by using
26+
annotations. See the `FrameworkExtraBundle documentation`_.
27+
28+
.. index::
29+
single: Cache; Expires header
30+
single: HTTP headers; Expires
31+
32+
Expiration with the ``Expires`` Header
33+
--------------------------------------
34+
35+
According to the HTTP specification, "the ``Expires`` header field gives
36+
the date/time after which the response is considered stale." The ``Expires``
37+
header can be set with the ``setExpires()`` ``Response`` method. It takes a
38+
``DateTime`` instance as an argument::
39+
40+
$date = new DateTime();
41+
$date->modify('+600 seconds');
42+
43+
$response->setExpires($date);
44+
45+
The resulting HTTP header will look like this:
46+
47+
.. code-block:: text
48+
49+
Expires: Thu, 01 Mar 2011 16:00:00 GMT
50+
51+
.. note::
52+
53+
The ``setExpires()`` method automatically converts the date to the GMT
54+
timezone as required by the specification.
55+
56+
Note that in HTTP versions before 1.1 the origin server wasn't required to
57+
send the ``Date`` header. Consequently, the cache (e.g. the browser) might
58+
need to rely on the local clock to evaluate the ``Expires`` header making
59+
the lifetime calculation vulnerable to clock skew. Another limitation
60+
of the ``Expires`` header is that the specification states that "HTTP/1.1
61+
servers should not send ``Expires`` dates more than one year in the future."
62+
63+
.. index::
64+
single: Cache; Cache-Control header
65+
single: HTTP headers; Cache-Control
66+
67+
Expiration with the ``Cache-Control`` Header
68+
--------------------------------------------
69+
70+
Because of the ``Expires`` header limitations, most of the time, you should
71+
use the ``Cache-Control`` header instead. Recall that the ``Cache-Control``
72+
header is used to specify many different cache directives. For expiration,
73+
there are two directives, ``max-age`` and ``s-maxage``. The first one is
74+
used by all caches, whereas the second one is only taken into account by
75+
shared caches::
76+
77+
// Sets the number of seconds after which the response
78+
// should no longer be considered fresh
79+
$response->setMaxAge(600);
80+
81+
// Same as above but only for shared caches
82+
$response->setSharedMaxAge(600);
83+
84+
The ``Cache-Control`` header would take on the following format (it may have
85+
additional directives):
86+
87+
.. code-block:: text
88+
89+
Cache-Control: max-age=600, s-maxage=600
90+
91+
.. _`expiration model`: http://tools.ietf.org/html/rfc2616#section-13.2
92+
.. _`FrameworkExtraBundle documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/cache.html

cache/form_csrf_caching.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ How to Cache Most of the Page and still be able to Use CSRF Protection
2929
----------------------------------------------------------------------
3030

3131
To cache a page that contains a CSRF token, you can use more advanced caching
32-
techniques like :ref:`ESI fragments <edge-side-includes>`, where you cache
33-
the full page and embedding the form inside an ESI tag with no cache at all.
32+
techniques like :doc:`ESI fragments </cache/esi>`, where you cache the full
33+
page and embedding the form inside an ESI tag with no cache at all.
3434

3535
Another option would be to load the form via an uncached AJAX request, but
3636
cache the rest of the HTML response.

0 commit comments

Comments
 (0)