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