Security update for Core, with self-updated composer
[yaffs-website] / web / core / tests / Drupal / Tests / Core / Render / RendererBubblingTest.php
1 <?php
2
3 /**
4  * @file
5  * Contains \Drupal\Tests\Core\Render\RendererBubblingTest.
6  */
7
8 namespace Drupal\Tests\Core\Render;
9
10 use Drupal\Core\Cache\MemoryBackend;
11 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
12 use Drupal\Core\Lock\NullLockBackend;
13 use Drupal\Core\State\State;
14 use Drupal\Core\Cache\Cache;
15
16 /**
17  * @coversDefaultClass \Drupal\Core\Render\Renderer
18  * @group Render
19  */
20 class RendererBubblingTest extends RendererTestBase {
21
22   /**
23    * {@inheritdoc}
24    */
25   protected function setUp() {
26     // Disable the required cache contexts, so that this test can test just the
27     // bubbling behavior.
28     $this->rendererConfig['required_cache_contexts'] = [];
29
30     parent::setUp();
31   }
32
33   /**
34    * Tests bubbling of assets when NOT using #pre_render callbacks.
35    */
36   public function testBubblingWithoutPreRender() {
37     $this->setUpRequest();
38     $this->setupMemoryCache();
39
40     $this->cacheContextsManager->expects($this->any())
41       ->method('convertTokensToKeys')
42       ->willReturnArgument(0);
43
44     // Create an element with a child and subchild. Each element loads a
45     // different library using #attached.
46     $element = [
47       '#type' => 'container',
48       '#cache' => [
49         'keys' => ['simpletest', 'renderer', 'children_attached'],
50       ],
51       '#attached' => ['library' => ['test/parent']],
52       '#title' => 'Parent',
53     ];
54     $element['child'] = [
55       '#type' => 'container',
56       '#attached' => ['library' => ['test/child']],
57       '#title' => 'Child',
58     ];
59     $element['child']['subchild'] = [
60       '#attached' => ['library' => ['test/subchild']],
61       '#markup' => 'Subchild',
62     ];
63
64     // Render the element and verify the presence of #attached JavaScript.
65     $this->renderer->renderRoot($element);
66     $expected_libraries = ['test/parent', 'test/child', 'test/subchild'];
67     $this->assertEquals($element['#attached']['library'], $expected_libraries, 'The element, child and subchild #attached libraries are included.');
68
69     // Load the element from cache and verify the presence of the #attached
70     // JavaScript.
71     $element = ['#cache' => ['keys' => ['simpletest', 'renderer', 'children_attached']]];
72     $this->assertTrue(strlen($this->renderer->renderRoot($element)) > 0, 'The element was retrieved from cache.');
73     $this->assertEquals($element['#attached']['library'], $expected_libraries, 'The element, child and subchild #attached libraries are included.');
74   }
75
76   /**
77    * Tests cache context bubbling with a custom cache bin.
78    */
79   public function testContextBubblingCustomCacheBin() {
80     $bin = $this->randomMachineName();
81
82     $this->setUpRequest();
83     $this->memoryCache = new MemoryBackend();
84     $custom_cache = new MemoryBackend();
85
86     $this->cacheFactory->expects($this->atLeastOnce())
87       ->method('get')
88       ->with($bin)
89       ->willReturnCallback(function ($requested_bin) use ($bin, $custom_cache) {
90         if ($requested_bin === $bin) {
91           return $custom_cache;
92         }
93         else {
94           throw new \Exception();
95         }
96       });
97     $this->cacheContextsManager->expects($this->any())
98       ->method('convertTokensToKeys')
99       ->willReturnArgument(0);
100
101     $build = [
102       '#cache' => [
103         'keys' => ['parent'],
104         'contexts' => ['foo'],
105         'bin' => $bin,
106       ],
107       '#markup' => 'parent',
108       'child' => [
109         '#cache' => [
110           'contexts' => ['bar'],
111           'max-age' => 3600,
112         ],
113       ],
114     ];
115     $this->renderer->renderRoot($build);
116
117     $this->assertRenderCacheItem('parent:foo', [
118       '#cache_redirect' => TRUE,
119       '#cache' => [
120         'keys' => ['parent'],
121         'contexts' => ['bar', 'foo'],
122         'tags' => [],
123         'bin' => $bin,
124         'max-age' => 3600,
125       ],
126     ], $bin);
127   }
128
129   /**
130    * Tests cache context bubbling in edge cases, because it affects the CID.
131    *
132    * ::testBubblingWithPrerender() already tests the common case.
133    *
134    * @dataProvider providerTestContextBubblingEdgeCases
135    */
136   public function testContextBubblingEdgeCases(array $element, array $expected_top_level_contexts, array $expected_cache_items) {
137     $this->setUpRequest();
138     $this->setupMemoryCache();
139     $this->cacheContextsManager->expects($this->any())
140       ->method('convertTokensToKeys')
141       ->willReturnArgument(0);
142
143     $this->renderer->renderRoot($element);
144
145     $this->assertEquals($expected_top_level_contexts, $element['#cache']['contexts'], 'Expected cache contexts found.');
146     foreach ($expected_cache_items as $cid => $expected_cache_item) {
147       $this->assertRenderCacheItem($cid, $expected_cache_item);
148     }
149   }
150
151   public function providerTestContextBubblingEdgeCases() {
152     $data = [];
153
154     // Cache contexts of inaccessible children aren't bubbled (because those
155     // children are not rendered at all).
156     $test_element = [
157       '#cache' => [
158         'keys' => ['parent'],
159         'contexts' => [],
160       ],
161       '#markup' => 'parent',
162       'child' => [
163         '#access' => FALSE,
164         '#cache' => [
165           'contexts' => ['foo'],
166         ],
167       ],
168     ];
169     $expected_cache_items = [
170       'parent' => [
171         '#attached' => [],
172         '#cache' => [
173           'contexts' => [],
174           'tags' => [],
175           'max-age' => Cache::PERMANENT,
176         ],
177         '#markup' => 'parent',
178       ],
179     ];
180     $data[] = [$test_element, [], $expected_cache_items];
181
182     // Assert cache contexts are sorted when they are used to generate a CID.
183     // (Necessary to ensure that different render arrays where the same keys +
184     // set of contexts are present point to the same cache item. Regardless of
185     // the contexts' order. A sad necessity because PHP doesn't have sets.)
186     $test_element = [
187      '#cache' => [
188         'keys' => ['set_test'],
189         'contexts' => [],
190       ],
191     ];
192     $expected_cache_items = [
193       'set_test:bar:baz:foo' => [
194         '#attached' => [],
195         '#cache' => [
196           'contexts' => [],
197           'tags' => [],
198           'max-age' => Cache::PERMANENT,
199         ],
200         '#markup' => '',
201       ],
202     ];
203     $context_orders = [
204       ['foo', 'bar', 'baz'],
205       ['foo', 'baz', 'bar'],
206       ['bar', 'foo', 'baz'],
207       ['bar', 'baz', 'foo'],
208       ['baz', 'foo', 'bar'],
209       ['baz', 'bar', 'foo'],
210     ];
211     foreach ($context_orders as $context_order) {
212       $test_element['#cache']['contexts'] = $context_order;
213       sort($context_order);
214       $expected_cache_items['set_test:bar:baz:foo']['#cache']['contexts'] = $context_order;
215       $data[] = [$test_element, $context_order, $expected_cache_items];
216     }
217
218     // A parent with a certain set of cache contexts is unaffected by a child
219     // that has a subset of those contexts.
220     $test_element = [
221       '#cache' => [
222         'keys' => ['parent'],
223         'contexts' => ['foo', 'bar', 'baz'],
224       ],
225       '#markup' => 'parent',
226       'child' => [
227         '#cache' => [
228           'contexts' => ['foo', 'baz'],
229           'max-age' => 3600,
230         ],
231       ],
232     ];
233     $expected_cache_items = [
234       'parent:bar:baz:foo' => [
235         '#attached' => [],
236         '#cache' => [
237           'contexts' => ['bar', 'baz', 'foo'],
238           'tags' => [],
239           'max-age' => 3600,
240         ],
241         '#markup' => 'parent',
242       ],
243     ];
244     $data[] = [$test_element, ['bar', 'baz', 'foo'], $expected_cache_items];
245
246     // A parent with a certain set of cache contexts that is a subset of the
247     // cache contexts of a child gets a redirecting cache item for the cache ID
248     // created pre-bubbling (without the child's additional cache contexts). It
249     // points to a cache item with a post-bubbling cache ID (i.e. with the
250     // child's additional cache contexts).
251     // Furthermore, the redirecting cache item also includes the children's
252     // cache tags, since changes in the children may cause those children to get
253     // different cache contexts and therefore cause different cache contexts to
254     // be stored in the redirecting cache item.
255     $test_element = [
256       '#cache' => [
257         'keys' => ['parent'],
258         'contexts' => ['foo'],
259         'tags' => ['yar', 'har'],
260       ],
261       '#markup' => 'parent',
262       'child' => [
263         '#cache' => [
264           'contexts' => ['bar'],
265           'tags' => ['fiddle', 'dee'],
266         ],
267         '#markup' => '',
268       ],
269     ];
270     $expected_cache_items = [
271       'parent:foo' => [
272         '#cache_redirect' => TRUE,
273         '#cache' => [
274           // The keys + contexts this redirects to.
275           'keys' => ['parent'],
276           'contexts' => ['bar', 'foo'],
277           'tags' => ['dee', 'fiddle', 'har', 'yar'],
278           'bin' => 'render',
279           'max-age' => Cache::PERMANENT,
280         ],
281       ],
282       'parent:bar:foo' => [
283         '#attached' => [],
284         '#cache' => [
285           'contexts' => ['bar', 'foo'],
286           'tags' => ['dee', 'fiddle', 'har', 'yar'],
287           'max-age' => Cache::PERMANENT,
288         ],
289         '#markup' => 'parent',
290       ],
291     ];
292     $data[] = [$test_element, ['bar', 'foo'], $expected_cache_items];
293
294     // Ensure that bubbleable metadata has been collected from children and set
295     // correctly to the main level of the render array. That ensures that correct
296     // bubbleable metadata exists if render array gets rendered multiple times.
297     $test_element = [
298       '#cache' => [
299         'keys' => ['parent'],
300         'tags' => ['yar', 'har']
301       ],
302       '#markup' => 'parent',
303       'child' => [
304         '#render_children' => TRUE,
305         'subchild' => [
306           '#cache' => [
307             'contexts' => ['foo'],
308             'tags' => ['fiddle', 'dee'],
309           ],
310           '#attached' => [
311             'library' => ['foo/bar']
312           ],
313           '#markup' => '',
314         ]
315       ],
316     ];
317     $expected_cache_items = [
318       'parent:foo' => [
319         '#attached' => ['library' => ['foo/bar']],
320         '#cache' => [
321           'contexts' => ['foo'],
322           'tags' => ['dee', 'fiddle', 'har', 'yar'],
323           'max-age' => Cache::PERMANENT,
324         ],
325         '#markup' => 'parent',
326       ],
327     ];
328     $data[] = [$test_element, ['foo'], $expected_cache_items];
329
330     return $data;
331   }
332
333   /**
334    * Tests the self-healing of the redirect with conditional cache contexts.
335    */
336   public function testConditionalCacheContextBubblingSelfHealing() {
337     $current_user_role = &$this->currentUserRole;
338
339     $this->setUpRequest();
340     $this->setupMemoryCache();
341
342     $test_element = [
343       '#cache' => [
344         'keys' => ['parent'],
345         'tags' => ['a'],
346       ],
347       '#markup' => 'parent',
348       'child' => [
349         '#cache' => [
350           'contexts' => ['user.roles'],
351           'tags' => ['b'],
352         ],
353         'grandchild' => [
354           '#access_callback' => function () use (&$current_user_role) {
355             // Only role A cannot access this subtree.
356             return $current_user_role !== 'A';
357           },
358           '#cache' => [
359             'contexts' => ['foo'],
360             'tags' => ['c'],
361             // A lower max-age; the redirecting cache item should be updated.
362             'max-age' => 1800,
363           ],
364           'grandgrandchild' => [
365             '#access_callback' => function () use (&$current_user_role) {
366               // Only role C can access this subtree.
367               return $current_user_role === 'C';
368             },
369             '#cache' => [
370               'contexts' => ['bar'],
371               'tags' => ['d'],
372               // A lower max-age; the redirecting cache item should be updated.
373               'max-age' => 300,
374             ],
375           ],
376         ],
377       ],
378     ];
379
380     // Request 1: role A, the grandchild isn't accessible => bubbled cache
381     // contexts: user.roles.
382     $element = $test_element;
383     $current_user_role = 'A';
384     $this->renderer->renderRoot($element);
385     $this->assertRenderCacheItem('parent', [
386       '#cache_redirect' => TRUE,
387       '#cache' => [
388         'keys' => ['parent'],
389         'contexts' => ['user.roles'],
390         'tags' => ['a', 'b'],
391         'bin' => 'render',
392         'max-age' => Cache::PERMANENT,
393       ],
394     ]);
395     $this->assertRenderCacheItem('parent:r.A', [
396       '#attached' => [],
397       '#cache' => [
398         'contexts' => ['user.roles'],
399         'tags' => ['a', 'b'],
400         'max-age' => Cache::PERMANENT,
401       ],
402       '#markup' => 'parent',
403     ]);
404
405     // Request 2: role B, the grandchild is accessible => bubbled cache
406     // contexts: foo, user.roles + merged max-age: 1800.
407     $element = $test_element;
408     $current_user_role = 'B';
409     $this->renderer->renderRoot($element);
410     $this->assertRenderCacheItem('parent', [
411       '#cache_redirect' => TRUE,
412       '#cache' => [
413         'keys' => ['parent'],
414         'contexts' => ['foo', 'user.roles'],
415         'tags' => ['a', 'b', 'c'],
416         'bin' => 'render',
417         'max-age' => 1800,
418       ],
419     ]);
420     $this->assertRenderCacheItem('parent:foo:r.B', [
421       '#attached' => [],
422       '#cache' => [
423         'contexts' => ['foo', 'user.roles'],
424         'tags' => ['a', 'b', 'c'],
425         'max-age' => 1800,
426       ],
427       '#markup' => 'parent',
428     ]);
429
430     // Request 3: role A again, the grandchild is inaccessible again => bubbled
431     // cache contexts: user.roles; but that's a subset of the already-bubbled
432     // cache contexts, so nothing is actually changed in the redirecting cache
433     // item. However, the cache item we were looking for in request 1 is
434     // technically the same one we're looking for now (it's the exact same
435     // request), but with one additional cache context. This is necessary to
436     // avoid "cache ping-pong". (Requests 1 and 3 are identical, but without the
437     // right merging logic to handle request 2, the redirecting cache item would
438     // toggle between only the 'user.roles' cache context and both the 'foo'
439     // and 'user.roles' cache contexts, resulting in a cache miss every time.)
440     $element = $test_element;
441     $current_user_role = 'A';
442     $this->renderer->renderRoot($element);
443     $this->assertRenderCacheItem('parent', [
444       '#cache_redirect' => TRUE,
445       '#cache' => [
446         'keys' => ['parent'],
447         'contexts' => ['foo', 'user.roles'],
448         'tags' => ['a', 'b', 'c'],
449         'bin' => 'render',
450         'max-age' => 1800,
451       ],
452     ]);
453     $this->assertRenderCacheItem('parent:foo:r.A', [
454       '#attached' => [],
455       '#cache' => [
456         'contexts' => ['foo', 'user.roles'],
457         'tags' => ['a', 'b'],
458         // Note that the max-age here is unaffected. When role A, the grandchild
459         // is never rendered, so neither is its max-age of 1800 present here,
460         // despite 1800 being the max-age of the redirecting cache item.
461         'max-age' => Cache::PERMANENT,
462       ],
463       '#markup' => 'parent',
464     ]);
465
466     // Request 4: role C, both the grandchild and the grandgrandchild are
467     // accessible => bubbled cache contexts: foo, bar, user.roles + merged
468     // max-age: 300.
469     $element = $test_element;
470     $current_user_role = 'C';
471     $this->renderer->renderRoot($element);
472     $final_parent_cache_item = [
473       '#cache_redirect' => TRUE,
474       '#cache' => [
475         'keys' => ['parent'],
476         'contexts' => ['bar', 'foo', 'user.roles'],
477         'tags' => ['a', 'b', 'c', 'd'],
478         'bin' => 'render',
479         'max-age' => 300,
480       ],
481     ];
482     $this->assertRenderCacheItem('parent', $final_parent_cache_item);
483     $this->assertRenderCacheItem('parent:bar:foo:r.C', [
484       '#attached' => [],
485       '#cache' => [
486         'contexts' => ['bar', 'foo', 'user.roles'],
487         'tags' => ['a', 'b', 'c', 'd'],
488         'max-age' => 300,
489       ],
490       '#markup' => 'parent',
491     ]);
492
493     // Request 5: role A again, verifying the merging like we did for request 3.
494     $element = $test_element;
495     $current_user_role = 'A';
496     $this->renderer->renderRoot($element);
497     $this->assertRenderCacheItem('parent', $final_parent_cache_item);
498     $this->assertRenderCacheItem('parent:bar:foo:r.A', [
499       '#attached' => [],
500       '#cache' => [
501         'contexts' => ['bar', 'foo', 'user.roles'],
502         'tags' => ['a', 'b'],
503         // Note that the max-age here is unaffected. When role A, the grandchild
504         // is never rendered, so neither is its max-age of 1800 present here,
505         // nor the grandgrandchild's max-age of 300, despite 300 being the
506         // max-age of the redirecting cache item.
507         'max-age' => Cache::PERMANENT,
508       ],
509       '#markup' => 'parent',
510     ]);
511
512     // Request 6: role B again, verifying the merging like we did for request 3.
513     $element = $test_element;
514     $current_user_role = 'B';
515     $this->renderer->renderRoot($element);
516     $this->assertRenderCacheItem('parent', $final_parent_cache_item);
517     $this->assertRenderCacheItem('parent:bar:foo:r.B', [
518       '#attached' => [],
519       '#cache' => [
520         'contexts' => ['bar', 'foo', 'user.roles'],
521         'tags' => ['a', 'b', 'c'],
522         // Note that the max-age here is unaffected. When role B, the
523         // grandgrandchild is never rendered, so neither is its max-age of 300
524         // present here, despite 300 being the max-age of the redirecting cache
525         // item.
526         'max-age' => 1800,
527       ],
528       '#markup' => 'parent',
529     ]);
530   }
531
532   /**
533    * Tests bubbling of bubbleable metadata added by #pre_render callbacks.
534    *
535    * @dataProvider providerTestBubblingWithPrerender
536    */
537   public function testBubblingWithPrerender($test_element) {
538     $this->setUpRequest();
539     $this->setupMemoryCache();
540
541     // Mock the State service.
542     $memory_state = new State(new KeyValueMemoryFactory(), new MemoryBackend('test'), new NullLockBackend());
543     \Drupal::getContainer()->set('state', $memory_state);
544     $this->controllerResolver->expects($this->any())
545       ->method('getControllerFromDefinition')
546       ->willReturnArgument(0);
547
548     // Simulate the theme system/Twig: a recursive call to Renderer::render(),
549     // just like the theme system or a Twig template would have done.
550     $this->themeManager->expects($this->any())
551       ->method('render')
552       ->willReturnCallback(function ($hook, $vars) {
553         return $this->renderer->render($vars['foo']);
554       });
555
556     // ::bubblingPreRender() verifies that a #pre_render callback for a render
557     // array that is cacheable and …
558     // - … is cached does NOT get called. (Also mock a render cache item.)
559     // - … is not cached DOES get called.
560     \Drupal::state()->set('bubbling_nested_pre_render_cached', FALSE);
561     \Drupal::state()->set('bubbling_nested_pre_render_uncached', FALSE);
562     $this->memoryCache->set('cached_nested', ['#markup' => 'Cached nested!', '#attached' => [], '#cache' => ['contexts' => [], 'tags' => []]]);
563
564     // Simulate the rendering of an entire response (i.e. a root call).
565     $output = $this->renderer->renderRoot($test_element);
566
567     // First, assert the render array is of the expected form.
568     $this->assertEquals('Cache context!Cache tag!Asset!Placeholder!barquxNested!Cached nested!', trim($output), 'Expected HTML generated.');
569     $this->assertEquals(['child.cache_context'], $test_element['#cache']['contexts'], 'Expected cache contexts found.');
570     $this->assertEquals(['child:cache_tag'], $test_element['#cache']['tags'], 'Expected cache tags found.');
571     $expected_attached = [
572       'drupalSettings' => ['foo' => 'bar'],
573       'placeholders' => [],
574     ];
575     $this->assertEquals($expected_attached, $test_element['#attached'], 'Expected attachments found.');
576
577     // Second, assert that #pre_render callbacks are only executed if they don't
578     // have a render cache hit (and hence a #pre_render callback for a render
579     // cached item cannot bubble more metadata).
580     $this->assertTrue(\Drupal::state()->get('bubbling_nested_pre_render_uncached'));
581     $this->assertFalse(\Drupal::state()->get('bubbling_nested_pre_render_cached'));
582   }
583
584   /**
585    * Provides two test elements: one without, and one with the theme system.
586    *
587    * @return array
588    */
589   public function providerTestBubblingWithPrerender() {
590     $data = [];
591
592     // Test element without theme.
593     $data[] = [
594       [
595         'foo' => [
596           '#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingPreRender'],
597         ],
598       ],
599     ];
600
601     // Test element with theme.
602     $data[] = [
603       [
604         '#theme' => 'common_test_render_element',
605         'foo' => [
606           '#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingPreRender'],
607         ],
608       ],
609     ];
610
611     return $data;
612   }
613
614   /**
615    * Tests that an element's cache keys cannot be changed during its rendering.
616    */
617   public function testOverWriteCacheKeys() {
618     $this->setUpRequest();
619     $this->setupMemoryCache();
620
621     // Ensure a logic exception
622     $data = [
623       '#cache' => [
624         'keys' => ['llama', 'bar'],
625        ],
626       '#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingCacheOverwritePrerender'],
627     ];
628     $this->setExpectedException(\LogicException::class, 'Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.');
629     $this->renderer->renderRoot($data);
630   }
631
632 }
633
634
635 class BubblingTest {
636
637   /**
638    * #pre_render callback for testBubblingWithPrerender().
639    */
640   public static function bubblingPreRender($elements) {
641     $elements += [
642       'child_cache_context' => [
643         '#cache' => [
644           'contexts' => ['child.cache_context'],
645         ],
646         '#markup' => 'Cache context!',
647       ],
648       'child_cache_tag' => [
649         '#cache' => [
650           'tags' => ['child:cache_tag'],
651         ],
652         '#markup' => 'Cache tag!',
653       ],
654       'child_asset' => [
655         '#attached' => [
656           'drupalSettings' => ['foo' => 'bar'],
657         ],
658         '#markup' => 'Asset!',
659       ],
660       'child_placeholder' => [
661         '#create_placeholder' => TRUE,
662         '#lazy_builder' => [__CLASS__ . '::bubblingPlaceholder', ['bar', 'qux']],
663       ],
664       'child_nested_pre_render_uncached' => [
665         '#cache' => ['keys' => ['uncached_nested']],
666         '#pre_render' => [__CLASS__ . '::bubblingNestedPreRenderUncached'],
667       ],
668       'child_nested_pre_render_cached' => [
669         '#cache' => ['keys' => ['cached_nested']],
670         '#pre_render' => [__CLASS__ . '::bubblingNestedPreRenderCached'],
671       ],
672     ];
673     return $elements;
674   }
675
676   /**
677    * #pre_render callback for testBubblingWithPrerender().
678    */
679   public static function bubblingNestedPreRenderUncached($elements) {
680     \Drupal::state()->set('bubbling_nested_pre_render_uncached', TRUE);
681     $elements['#markup'] = 'Nested!';
682     return $elements;
683   }
684
685   /**
686    * #pre_render callback for testBubblingWithPrerender().
687    */
688   public static function bubblingNestedPreRenderCached($elements) {
689     \Drupal::state()->set('bubbling_nested_pre_render_cached', TRUE);
690     return $elements;
691   }
692
693   /**
694    * #lazy_builder callback for testBubblingWithPrerender().
695    */
696   public static function bubblingPlaceholder($foo, $baz) {
697     return [
698       '#markup' => 'Placeholder!' . $foo . $baz,
699     ];
700   }
701
702   /**
703    * #pre_render callback for testOverWriteCacheKeys().
704    */
705   public static function bubblingCacheOverwritePrerender($elements) {
706     // Overwrite the #cache entry with new data.
707     $elements['#cache'] = [
708       'keys' => ['llama', 'foo'],
709     ];
710     $elements['#markup'] = 'Setting cache keys just now!';
711     return $elements;
712   }
713
714 }