Mercurial > hg > isophonics-drupal-site
comparison core/lib/Drupal/Core/Render/RenderCache.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 129ea1e6d783 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\Core\Render; | |
4 | |
5 use Drupal\Core\Cache\Cache; | |
6 use Drupal\Core\Cache\CacheableMetadata; | |
7 use Drupal\Core\Cache\Context\CacheContextsManager; | |
8 use Drupal\Core\Cache\CacheFactoryInterface; | |
9 use Symfony\Component\HttpFoundation\RequestStack; | |
10 | |
11 /** | |
12 * Wraps the caching logic for the render caching system. | |
13 * | |
14 * @internal | |
15 * | |
16 * @todo Refactor this out into a generic service capable of cache redirects, | |
17 * and let RenderCache use that. https://www.drupal.org/node/2551419 | |
18 */ | |
19 class RenderCache implements RenderCacheInterface { | |
20 | |
21 /** | |
22 * The request stack. | |
23 * | |
24 * @var \Symfony\Component\HttpFoundation\RequestStack | |
25 */ | |
26 protected $requestStack; | |
27 | |
28 /** | |
29 * The cache factory. | |
30 * | |
31 * @var \Drupal\Core\Cache\CacheFactoryInterface | |
32 */ | |
33 protected $cacheFactory; | |
34 | |
35 /** | |
36 * The cache contexts manager. | |
37 * | |
38 * @var \Drupal\Core\Cache\Context\CacheContextsManager | |
39 */ | |
40 protected $cacheContextsManager; | |
41 | |
42 /** | |
43 * Constructs a new RenderCache object. | |
44 * | |
45 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack | |
46 * The request stack. | |
47 * @param \Drupal\Core\Cache\CacheFactoryInterface $cache_factory | |
48 * The cache factory. | |
49 * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager | |
50 * The cache contexts manager. | |
51 */ | |
52 public function __construct(RequestStack $request_stack, CacheFactoryInterface $cache_factory, CacheContextsManager $cache_contexts_manager) { | |
53 $this->requestStack = $request_stack; | |
54 $this->cacheFactory = $cache_factory; | |
55 $this->cacheContextsManager = $cache_contexts_manager; | |
56 } | |
57 | |
58 /** | |
59 * {@inheritdoc} | |
60 */ | |
61 public function get(array $elements) { | |
62 // Form submissions rely on the form being built during the POST request, | |
63 // and render caching of forms prevents this from happening. | |
64 // @todo remove the isMethodCacheable() check when | |
65 // https://www.drupal.org/node/2367555 lands. | |
66 if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$cid = $this->createCacheID($elements)) { | |
67 return FALSE; | |
68 } | |
69 $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; | |
70 | |
71 if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) { | |
72 $cached_element = $cache->data; | |
73 // Two-tier caching: redirect to actual (post-bubbling) cache item. | |
74 // @see \Drupal\Core\Render\RendererInterface::render() | |
75 // @see ::set() | |
76 if (isset($cached_element['#cache_redirect'])) { | |
77 return $this->get($cached_element); | |
78 } | |
79 // Return the cached element. | |
80 return $cached_element; | |
81 } | |
82 return FALSE; | |
83 } | |
84 | |
85 /** | |
86 * {@inheritdoc} | |
87 */ | |
88 public function set(array &$elements, array $pre_bubbling_elements) { | |
89 // Form submissions rely on the form being built during the POST request, | |
90 // and render caching of forms prevents this from happening. | |
91 // @todo remove the isMethodCacheable() check when | |
92 // https://www.drupal.org/node/2367555 lands. | |
93 if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$cid = $this->createCacheID($elements)) { | |
94 return FALSE; | |
95 } | |
96 | |
97 $data = $this->getCacheableRenderArray($elements); | |
98 | |
99 $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; | |
100 $cache = $this->cacheFactory->get($bin); | |
101 | |
102 // Calculate the pre-bubbling CID. | |
103 $pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements); | |
104 | |
105 // Two-tier caching: detect different CID post-bubbling, create redirect, | |
106 // update redirect if different set of cache contexts. | |
107 // @see \Drupal\Core\Render\RendererInterface::render() | |
108 // @see ::get() | |
109 if ($pre_bubbling_cid && $pre_bubbling_cid !== $cid) { | |
110 // The cache redirection strategy we're implementing here is pretty | |
111 // simple in concept. Suppose we have the following render structure: | |
112 // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) | |
113 // -- B (specifies #cache['contexts'] = ['b']) | |
114 // | |
115 // At the time that we're evaluating whether A's rendering can be | |
116 // retrieved from cache, we won't know the contexts required by its | |
117 // children (the children might not even be built yet), so cacheGet() | |
118 // will only be able to get what is cached for a $cid of 'foo'. But at | |
119 // the time we're writing to that cache, we do know all the contexts that | |
120 // were specified by all children, so what we need is a way to | |
121 // persist that information between the cache write and the next cache | |
122 // read. So, what we can do is store the following into 'foo': | |
123 // [ | |
124 // '#cache_redirect' => TRUE, | |
125 // '#cache' => [ | |
126 // ... | |
127 // 'contexts' => ['b'], | |
128 // ], | |
129 // ] | |
130 // | |
131 // This efficiently lets cacheGet() redirect to a $cid that includes all | |
132 // of the required contexts. The strategy is on-demand: in the case where | |
133 // there aren't any additional contexts required by children that aren't | |
134 // already included in the parent's pre-bubbled #cache information, no | |
135 // cache redirection is needed. | |
136 // | |
137 // When implementing this redirection strategy, special care is needed to | |
138 // resolve potential cache ping-pong problems. For example, consider the | |
139 // following render structure: | |
140 // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) | |
141 // -- B (pre-bubbling, specifies #cache['contexts'] = ['b']) | |
142 // --- C (pre-bubbling, specifies #cache['contexts'] = ['c']) | |
143 // --- D (pre-bubbling, specifies #cache['contexts'] = ['d']) | |
144 // | |
145 // Additionally, suppose that: | |
146 // - C only exists for a 'b' context value of 'b1' | |
147 // - D only exists for a 'b' context value of 'b2' | |
148 // This is an acceptable variation, since B specifies that its contents | |
149 // vary on context 'b'. | |
150 // | |
151 // A naive implementation of cache redirection would result in the | |
152 // following: | |
153 // - When a request is processed where context 'b' = 'b1', what would be | |
154 // cached for a $pre_bubbling_cid of 'foo' is: | |
155 // [ | |
156 // '#cache_redirect' => TRUE, | |
157 // '#cache' => [ | |
158 // ... | |
159 // 'contexts' => ['b', 'c'], | |
160 // ], | |
161 // ] | |
162 // - When a request is processed where context 'b' = 'b2', we would | |
163 // retrieve the above from cache, but when following that redirection, | |
164 // get a cache miss, since we're processing a 'b' context value that | |
165 // has not yet been cached. Given the cache miss, we would continue | |
166 // with rendering the structure, perform the required context bubbling | |
167 // and then overwrite the above item with: | |
168 // [ | |
169 // '#cache_redirect' => TRUE, | |
170 // '#cache' => [ | |
171 // ... | |
172 // 'contexts' => ['b', 'd'], | |
173 // ], | |
174 // ] | |
175 // - Now, if a request comes in where context 'b' = 'b1' again, the above | |
176 // would redirect to a cache key that doesn't exist, since we have not | |
177 // yet cached an item that includes 'b'='b1' and something for 'd'. So | |
178 // we would process this request as a cache miss, at the end of which, | |
179 // we would overwrite the above item back to: | |
180 // [ | |
181 // '#cache_redirect' => TRUE, | |
182 // '#cache' => [ | |
183 // ... | |
184 // 'contexts' => ['b', 'c'], | |
185 // ], | |
186 // ] | |
187 // - The above would always result in accurate renderings, but would | |
188 // result in poor performance as we keep processing requests as cache | |
189 // misses even though the target of the redirection is cached, and | |
190 // it's only the redirection element itself that is creating the | |
191 // ping-pong problem. | |
192 // | |
193 // A way to resolve the ping-pong problem is to eventually reach a cache | |
194 // state where the redirection element includes all of the contexts used | |
195 // throughout all requests: | |
196 // [ | |
197 // '#cache_redirect' => TRUE, | |
198 // '#cache' => [ | |
199 // ... | |
200 // 'contexts' => ['b', 'c', 'd'], | |
201 // ], | |
202 // ] | |
203 // | |
204 // We can't reach that state right away, since we don't know what the | |
205 // result of future requests will be, but we can incrementally move | |
206 // towards that state by progressively merging the 'contexts' value | |
207 // across requests. That's the strategy employed below and tested in | |
208 // \Drupal\Tests\Core\Render\RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing(). | |
209 | |
210 // Get the cacheability of this element according to the current (stored) | |
211 // redirecting cache item, if any. | |
212 $redirect_cacheability = new CacheableMetadata(); | |
213 if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) { | |
214 $redirect_cacheability = CacheableMetadata::createFromRenderArray($stored_cache_redirect->data); | |
215 } | |
216 | |
217 // Calculate the union of the cacheability for this request and the | |
218 // current (stored) redirecting cache item. We need: | |
219 // - the union of cache contexts, because that is how we know which cache | |
220 // item to redirect to; | |
221 // - the union of cache tags, because that is how we know when the cache | |
222 // redirect cache item itself is invalidated; | |
223 // - the union of max ages, because that is how we know when the cache | |
224 // redirect cache item itself becomes stale. (Without this, we might end | |
225 // up toggling between a permanently and a briefly cacheable cache | |
226 // redirect, because the last update's max-age would always "win".) | |
227 $redirect_cacheability_updated = CacheableMetadata::createFromRenderArray($data)->merge($redirect_cacheability); | |
228 | |
229 // Stored cache contexts incomplete: this request causes cache contexts to | |
230 // be added to the redirecting cache item. | |
231 if (array_diff($redirect_cacheability_updated->getCacheContexts(), $redirect_cacheability->getCacheContexts())) { | |
232 $redirect_data = [ | |
233 '#cache_redirect' => TRUE, | |
234 '#cache' => [ | |
235 // The cache keys of the current element; this remains the same | |
236 // across requests. | |
237 'keys' => $elements['#cache']['keys'], | |
238 // The union of the current element's and stored cache contexts. | |
239 'contexts' => $redirect_cacheability_updated->getCacheContexts(), | |
240 // The union of the current element's and stored cache tags. | |
241 'tags' => $redirect_cacheability_updated->getCacheTags(), | |
242 // The union of the current element's and stored cache max-ages. | |
243 'max-age' => $redirect_cacheability_updated->getCacheMaxAge(), | |
244 // The same cache bin as the one for the actual render cache items. | |
245 'bin' => $bin, | |
246 ], | |
247 ]; | |
248 $cache->set($pre_bubbling_cid, $redirect_data, $this->maxAgeToExpire($redirect_cacheability_updated->getCacheMaxAge()), Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered'])); | |
249 } | |
250 | |
251 // Current cache contexts incomplete: this request only uses a subset of | |
252 // the cache contexts stored in the redirecting cache item. Vary by these | |
253 // additional (conditional) cache contexts as well, otherwise the | |
254 // redirecting cache item would be pointing to a cache item that can never | |
255 // exist. | |
256 if (array_diff($redirect_cacheability_updated->getCacheContexts(), $data['#cache']['contexts'])) { | |
257 // Recalculate the cache ID. | |
258 $recalculated_cid_pseudo_element = [ | |
259 '#cache' => [ | |
260 'keys' => $elements['#cache']['keys'], | |
261 'contexts' => $redirect_cacheability_updated->getCacheContexts(), | |
262 ] | |
263 ]; | |
264 $cid = $this->createCacheID($recalculated_cid_pseudo_element); | |
265 // Ensure the about-to-be-cached data uses the merged cache contexts. | |
266 $data['#cache']['contexts'] = $redirect_cacheability_updated->getCacheContexts(); | |
267 } | |
268 } | |
269 $cache->set($cid, $data, $this->maxAgeToExpire($elements['#cache']['max-age']), Cache::mergeTags($data['#cache']['tags'], ['rendered'])); | |
270 } | |
271 | |
272 /** | |
273 * Maps a #cache[max-age] value to an "expire" value for the Cache API. | |
274 * | |
275 * @param int $max_age | |
276 * A #cache[max-age] value. | |
277 * | |
278 * @return int | |
279 * A corresponding "expire" value. | |
280 * | |
281 * @see \Drupal\Core\Cache\CacheBackendInterface::set() | |
282 */ | |
283 protected function maxAgeToExpire($max_age) { | |
284 return ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $max_age; | |
285 } | |
286 | |
287 /** | |
288 * Creates the cache ID for a renderable element. | |
289 * | |
290 * Creates the cache ID string based on #cache['keys'] + #cache['contexts']. | |
291 * | |
292 * @param array &$elements | |
293 * A renderable array. | |
294 * | |
295 * @return string | |
296 * The cache ID string, or FALSE if the element may not be cached. | |
297 */ | |
298 protected function createCacheID(array &$elements) { | |
299 // If the maximum age is zero, then caching is effectively prohibited. | |
300 if (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] === 0) { | |
301 return FALSE; | |
302 } | |
303 | |
304 if (isset($elements['#cache']['keys'])) { | |
305 $cid_parts = $elements['#cache']['keys']; | |
306 if (!empty($elements['#cache']['contexts'])) { | |
307 $context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']); | |
308 $cid_parts = array_merge($cid_parts, $context_cache_keys->getKeys()); | |
309 CacheableMetadata::createFromRenderArray($elements) | |
310 ->merge($context_cache_keys) | |
311 ->applyTo($elements); | |
312 } | |
313 return implode(':', $cid_parts); | |
314 } | |
315 return FALSE; | |
316 } | |
317 | |
318 /** | |
319 * {@inheritdoc} | |
320 */ | |
321 public function getCacheableRenderArray(array $elements) { | |
322 $data = [ | |
323 '#markup' => $elements['#markup'], | |
324 '#attached' => $elements['#attached'], | |
325 '#cache' => [ | |
326 'contexts' => $elements['#cache']['contexts'], | |
327 'tags' => $elements['#cache']['tags'], | |
328 'max-age' => $elements['#cache']['max-age'], | |
329 ], | |
330 ]; | |
331 | |
332 // Preserve cacheable items if specified. If we are preserving any cacheable | |
333 // children of the element, we assume we are only interested in their | |
334 // individual markup and not the parent's one, thus we empty it to minimize | |
335 // the cache entry size. | |
336 if (!empty($elements['#cache_properties']) && is_array($elements['#cache_properties'])) { | |
337 $data['#cache_properties'] = $elements['#cache_properties']; | |
338 | |
339 // Extract all the cacheable items from the element using cache | |
340 // properties. | |
341 $cacheable_items = array_intersect_key($elements, array_flip($elements['#cache_properties'])); | |
342 $cacheable_children = Element::children($cacheable_items); | |
343 if ($cacheable_children) { | |
344 $data['#markup'] = ''; | |
345 // Cache only cacheable children's markup. | |
346 foreach ($cacheable_children as $key) { | |
347 // We can assume that #markup is safe at this point. | |
348 $cacheable_items[$key] = ['#markup' => Markup::create($cacheable_items[$key]['#markup'])]; | |
349 } | |
350 } | |
351 $data += $cacheable_items; | |
352 } | |
353 | |
354 $data['#markup'] = Markup::create($data['#markup']); | |
355 return $data; | |
356 } | |
357 | |
358 } |