20e4b0c142bc6e6b02d73beb7ff3ed0330e049e6
[yaffs-website] / web / core / lib / Drupal / Core / Render / RenderCache.php
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 }