forked from davidpv/symfony2cheatsheet
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcache.html.twig
249 lines (183 loc) · 11.7 KB
/
cache.html.twig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
<p>For the purposes of learning how to cache with Symfony2, we'll cover the subject in four steps:</p>
<ul>
<li><strong>Step 1:</strong> A gateway cache, or reverse proxy, is an independent layer that sits in front of your application. The reverse proxy caches responses as they're returned from your application and answers requests with cached responses before they hit your application. Symfony2 provides its own reverse proxy, but any reverse proxy can be used.</li>
<li><strong>Step 2:</strong> HTTP cache headers are used to communicate with the gateway cache and any other caches between your application and the client. Symfony2 provides sensible defaults and a powerful interface for interacting with the cache headers.</li>
<li><strong>Step 3:</strong> HTTP expiration and validation are the two models used for determining whether cached content is fresh (can be reused from the cache) or stale (should be regenerated by the application).</li>
<li><strong>Step 4:</strong> Edge Side Includes (ESI) allow HTTP cache to be used to cache page fragments (even nested fragments) independently. With ESI, you can even cache an entire page for 60 minutes, but an embedded sidebar for only 5 minutes.</li>
</ul>
<h3>Caching with a Gateway Cache</h3>
<p>To enable caching, modify the code of a front controller to use the caching kernel:</p>
{% raw %}<pre><code>// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
require_once __DIR__.'/../app/AppCache.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
// wrap the default AppKernel with the AppCache one
$kernel = new AppCache($kernel);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
</code></pre>{% endraw %}
<p>The cache kernel has a special getLog() method that returns a string representation of what happened in the cache layer. In the development environment, use it to debug and validate your cache strategy:</p>
{% raw %}<pre><code>error_log($kernel->getLog());
</code></pre>{% endraw %}
<h3>Introduction to HTTP Caching</h3>
<p>HTTP specifies four response cache headers that we're concerned with:</p>
<ul>
<li>Cache-Control</li>
<li>Expires</li>
<li>ETag</li>
<li>Last-Modified</li>
</ul>
<p><strong>The Cache-Control Header</strong></p>
<p>The Cache-Control header is unique in that it contains not one, but various pieces of information about the cacheability of a response. Each piece of information is separated by a comma:</p>
{% raw %}<pre><code>Cache-Control: private, max-age=0, must-revalidate
Cache-Control: max-age=3600, must-revalidate
</code></pre>{% endraw %}
<p>Symfony provides abstraction layer:</p>
{% raw %}<pre><code>$response = new Response();
// mark the response as either public or private
$response->setPublic();
$response->setPrivate();
// set the private or shared max age
$response->setMaxAge(600);
$response->setSharedMaxAge(600);
// set a custom Cache-Control directive
$response->headers->addCacheControlDirective('must-revalidate', true);
</code></pre>{% endraw %}
<p><strong>Public vs Private Responses</strong></p>
<p>Both gateway and proxy caches are considered "shared" caches as the cached content is shared by more than one user. If a user-specific response were ever mistakenly stored by a shared cache, it might be returned later to any number of different users. Imagine if your account information were cached and then returned to every subsequent user who asked for their account page!</p>
{% raw %}<pre><code>public: Indicates that the response may be cached by both private and shared caches;
private: Indicates that all or part of the response message is intended for a single user and must not be cached by a shared cache.
</code></pre>{% endraw %}
<p>HTTP caching only works for "safe" HTTP methods (like GET and HEAD). Being safe means that you never change the application's state on the server when serving the request (you can of course log information, cache data, etc).</p>
<p><strong>Caching Rules and Defaults</strong></p>
<p>Symfony2 automatically sets a sensible and conservative Cache-Control header when none is set by the developer by following these rules:</p>
<ul>
<li>If no cache header is defined (Cache-Control, Expires, ETag or Last-Modified), Cache-Control is set to no-cache, meaning that the response will not be cached;</li>
<li>If Cache-Control is empty (but one of the other cache headers is present), its value is set to private, must-revalidate;</li>
<li>But if at least one Cache-Control directive is set, and no 'public' or private directives have been explicitly added, Symfony2 adds the private directive automatically (except when s-maxage is set).</li>
</ul>
<p><strong>Expiration with Expires Header</strong></p>
{% raw %}<pre><code>$date = new DateTime();
$date->modify('+600 seconds');
$response->setExpires($date);
</code></pre>{% endraw %}
<p><strong>Expiration with the Cache-Control Header</strong></p>
{% raw %}<pre><code>/ Sets the number of seconds after which the response
// should no longer be considered fresh
$response->setMaxAge(600);
// Same as above but only for shared caches
$response->setSharedMaxAge(600);
</code></pre>{% endraw %}
<p><strong>Validation the the ETag Header</strong></p>
<p>The ETag header is a string header (called the "entity-tag") that uniquely identifies one representation of the target resource. It's entirely generated and set by your application so that you can tell, for example, if the /about resource that's stored by the cache is up-to-date with what your application would return. An ETag is like a fingerprint and is used to quickly compare if two different versions of a resource are equivalent. Like fingerprints, each ETag must be unique across all representations of the same resource.</p>
{% raw %}<pre><code>public function indexAction()
{
$response = $this->render('MyBundle:Main:index.html.twig');
$response->setETag(md5($response->getContent()));
$response->setPublic(); // make sure the response is public/cacheable
$response->isNotModified($this->getRequest());
return $response;
}
</code></pre>{% endraw %}
<p><strong>Validation with the Last-Modified Header</strong></p>
<p>The Last-Modified header is the second form of validation. According to the HTTP specification, "The Last-Modified header field indicates the date and time at which the origin server believes the representation was last modified."</p>
{% raw %}<pre><code>public function showAction($articleSlug)
{
// ...
$articleDate = new \DateTime($article->getUpdatedAt());
$authorDate = new \DateTime($author->getUpdatedAt());
$date = $authorDate > $articleDate ? $authorDate : $articleDate;
$response->setLastModified($date);
// Set response as public. Otherwise it will be private by default.
$response->setPublic();
if ($response->isNotModified($this->getRequest())) {
return $response;
}
// do more work to populate the response will the full content
return $response;
}
</code></pre>{% endraw %}
<p>The main goal of any caching strategy is to lighten the load on the application. Put another way, the less you do in your application to return a 304 response, the better. The Response::isNotModified() method does exactly that by exposing a simple and efficient pattern:</p>
{% raw %}<pre><code>public function showAction($articleSlug)
{
// Get the minimum information to compute
// the ETag or the Last-Modified value
// (based on the Request, data is retrieved from
// a database or a key-value store for instance)
$article = ...;
// create a Response with a ETag and/or a Last-Modified header
$response = new Response();
$response->setETag($article->computeETag());
$response->setLastModified($article->getPublishedAt());
// Set response as public. Otherwise it will be private by default.
$response->setPublic();
// Check that the Response is not modified for the given Request
if ($response->isNotModified($this->getRequest())) {
// return the 304 Response immediately
return $response;
} else {
// do more work here - like retrieving more data
$comments = ...;
// or render a template with the $response you've already started
return $this->render(
'MyBundle:MyController:article.html.twig',
array('article' => $article, 'comments' => $comments),
$response
);
}
}
</code></pre>{% endraw %}
<p><strong>Varying the Response</strong></p>
<p>So far, we've assumed that each URI has exactly one representation of the target resource. By default, HTTP caching is done by using the URI of the resource as the cache key. If two people request the same URI of a cacheable resource, the second person will receive the cached version.
Sometimes this isn't enough and different versions of the same URI need to be cached based on one or more request header values. For instance, if you compress pages when the client supports it, any given URI has two representations: one when the client supports compression, and one when it does not. This determination is done by the value of the Accept-Encoding request header.</p>
{% raw %}<pre><code>// set one vary header
$response->setVary('Accept-Encoding');
// set multiple vary headers
$response->setVary(array('Accept-Encoding', 'User-Agent'));
// Marks the Response stale
$response->expire();
// Force the response to return a proper 304 response with no content
$response->setNotModified();
</code></pre>{% endraw %}
<h3>Using Edge Side Includes</h3>
<p>Gateway caches are a great way to make your website perform better. But they have one limitation: they can only cache whole pages. If you can't cache whole pages or if parts of a page has "more" dynamic parts, you are out of luck. Fortunately, Symfony2 provides a solution for these cases, based on a technology called ESI, or Edge Side Includes. Akamaï wrote this specification almost 10 years ago, and it allows specific parts of a page to have a different caching strategy than the main page.</p>
{% raw %}<pre><code># app/config/config.yml
framework:
# ...
esi: { enabled: true }
</code></pre>{% endraw %}
<p>Let's suppose that we hace an static page with a dynamic tickets section:</p>
{% raw %}<pre><code>public function indexAction()
{
$response = $this->render('MyBundle:MyController:index.html.twig');
// set the shared max age - which also marks the response as public
$response->setSharedMaxAge(600);
return $response;
}
</code></pre>{% endraw %}
<p>Now, let's embedd the ticket content using twig render's tag.</p>
{% raw %}<pre><code>{% render '...:news' with {}, {'standalone': true} %}
</code></pre>{% endraw %}
<p>Using the
<strong>standalone</strong> true tells symfony to use ESI tags. The embedded action can now specify its own caching rules, entirely independent of the master page.
</p>
{% raw %}<pre><code>public function newsAction()
{
// ...
$response->setSharedMaxAge(60);
}
</code></pre>{% endraw %}
<p>For the ESI include tag to work properly, you must define the _internal route:</p>
{% raw %}<pre><code># app/config/routing.yml
_internal:
resource: "@FrameworkBundle/Resources/config/routing/internal.xml"
prefix: /_internal
</code></pre>{% endraw %}
<p>Learn more
<a href="http://symfony.com/doc/current/cookbook/cache/varnish.html">How to use Varnish to speed up my Website</a>
</p>