1<?php 2 3/** 4 * @file 5 * Contains \Drupal\Tests\Core\Render\RendererTest. 6 */ 7 8namespace Drupal\Tests\Core\Render; 9 10use Drupal\Component\Render\MarkupInterface; 11use Drupal\Core\Access\AccessResult; 12use Drupal\Core\Access\AccessResultInterface; 13use Drupal\Core\Cache\Cache; 14use Drupal\Core\Security\TrustedCallbackInterface; 15use Drupal\Core\Render\Element; 16use Drupal\Core\Render\Markup; 17use Drupal\Core\Template\Attribute; 18 19/** 20 * @coversDefaultClass \Drupal\Core\Render\Renderer 21 * @group Render 22 */ 23class RendererTest extends RendererTestBase { 24 25 protected $defaultThemeVars = [ 26 '#cache' => [ 27 'contexts' => [ 28 'languages:language_interface', 29 'theme', 30 ], 31 'tags' => [], 32 'max-age' => Cache::PERMANENT, 33 ], 34 '#attached' => [], 35 '#children' => '', 36 ]; 37 38 /** 39 * @covers ::render 40 * @covers ::doRender 41 * 42 * @dataProvider providerTestRenderBasic 43 */ 44 public function testRenderBasic($build, $expected, callable $setup_code = NULL) { 45 if (isset($setup_code)) { 46 $setup_code = $setup_code->bindTo($this); 47 $setup_code(); 48 } 49 50 if (isset($build['#markup'])) { 51 $this->assertNotInstanceOf(MarkupInterface::class, $build['#markup']); 52 } 53 $render_output = $this->renderer->renderRoot($build); 54 $this->assertSame($expected, (string) $render_output); 55 if ($render_output !== '') { 56 $this->assertInstanceOf(MarkupInterface::class, $render_output); 57 $this->assertInstanceOf(MarkupInterface::class, $build['#markup']); 58 } 59 } 60 61 /** 62 * Provides a list of render arrays to test basic rendering. 63 * 64 * @return array 65 */ 66 public function providerTestRenderBasic() { 67 $data = []; 68 69 // Part 1: the most simplistic render arrays possible, none using #theme. 70 71 // Pass a NULL. 72 $data[] = [NULL, '']; 73 // Pass an empty string. 74 $data[] = ['', '']; 75 // Previously printed, see ::renderTwice for a more integration-like test. 76 $data[] = [ 77 ['#markup' => 'foo', '#printed' => TRUE], 78 '', 79 ]; 80 // Printed in pre_render. 81 $data[] = [ 82 [ 83 '#markup' => 'foo', 84 '#pre_render' => [[new TestCallables(), 'preRenderPrinted']], 85 ], 86 '', 87 ]; 88 // Basic #markup based renderable array. 89 $data[] = [ 90 ['#markup' => 'foo'], 91 'foo', 92 ]; 93 // Basic #markup based renderable array with value '0'. 94 $data[] = [ 95 ['#markup' => '0'], 96 '0', 97 ]; 98 // Basic #markup based renderable array with value 0. 99 $data[] = [ 100 ['#markup' => 0], 101 '0', 102 ]; 103 // Basic #markup based renderable array with value ''. 104 $data[] = [ 105 ['#markup' => ''], 106 '', 107 ]; 108 // Basic #markup based renderable array with value NULL. 109 $data[] = [ 110 ['#markup' => NULL], 111 '', 112 ]; 113 // Basic #plain_text based renderable array. 114 $data[] = [ 115 ['#plain_text' => 'foo'], 116 'foo', 117 ]; 118 // Mixing #plain_text and #markup based renderable array. 119 $data[] = [ 120 ['#plain_text' => '<em>foo</em>', '#markup' => 'bar'], 121 '<em>foo</em>', 122 ]; 123 // Safe strings in #plain_text are still escaped. 124 $data[] = [ 125 ['#plain_text' => Markup::create('<em>foo</em>')], 126 '<em>foo</em>', 127 ]; 128 // #plain_text based renderable array with value '0'. 129 $data[] = [ 130 ['#plain_text' => '0'], 131 '0', 132 ]; 133 // #plain_text based renderable array with value 0. 134 $data[] = [ 135 ['#plain_text' => 0], 136 '0', 137 ]; 138 // #plain_text based renderable array with value ''. 139 $data[] = [ 140 ['#plain_text' => ''], 141 '', 142 ]; 143 // #plain_text based renderable array with value NULL. 144 $data[] = [ 145 ['#plain_text' => NULL], 146 '', 147 ]; 148 // Renderable child element. 149 $data[] = [ 150 ['child' => ['#markup' => 'bar']], 151 'bar', 152 ]; 153 // XSS filtering test. 154 $data[] = [ 155 ['child' => ['#markup' => "This is <script>alert('XSS')</script> test"]], 156 "This is alert('XSS') test", 157 ]; 158 // XSS filtering test. 159 $data[] = [ 160 [ 161 'child' => [ 162 '#markup' => "This is <script>alert('XSS')</script> test", 163 '#allowed_tags' => ['script'], 164 ], 165 ], 166 "This is <script>alert('XSS')</script> test", 167 ]; 168 // XSS filtering test. 169 $data[] = [ 170 [ 171 'child' => [ 172 '#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>", 173 '#allowed_tags' => ['em', 'strong'], 174 ], 175 ], 176 "This is <em>alert('XSS')</em> <strong>test</strong>", 177 ]; 178 // Html escaping test. 179 $data[] = [ 180 [ 181 'child' => [ 182 '#plain_text' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>", 183 ], 184 ], 185 "This is <script><em>alert('XSS')</em></script> <strong>test</strong>", 186 ]; 187 // XSS filtering by default test. 188 $data[] = [ 189 [ 190 'child' => [ 191 '#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>", 192 ], 193 ], 194 "This is <em>alert('XSS')</em> <strong>test</strong>", 195 ]; 196 // Ensure non-XSS tags are not filtered out. 197 $data[] = [ 198 [ 199 'child' => [ 200 '#markup' => "This is <strong><script>alert('not a giraffe')</script></strong> test", 201 ], 202 ], 203 "This is <strong>alert('not a giraffe')</strong> test", 204 ]; 205 // #children set but empty, and renderable children. 206 $data[] = [ 207 ['#children' => '', 'child' => ['#markup' => 'bar']], 208 'bar', 209 ]; 210 // #children set, not empty, and renderable children. #children will be 211 // assumed oto be the rendered child elements, even though the #markup for 212 // 'child' differs. 213 $data[] = [ 214 ['#children' => 'foo', 'child' => ['#markup' => 'bar']], 215 'foo', 216 ]; 217 // Ensure that content added to #markup via a #pre_render callback is safe. 218 $data[] = [ 219 [ 220 '#markup' => 'foo', 221 '#pre_render' => [function ($elements) { 222 $elements['#markup'] .= '<script>alert("bar");</script>'; 223 return $elements; 224 }, 225 ], 226 ], 227 'fooalert("bar");', 228 ]; 229 // Test #allowed_tags in combination with #markup and #pre_render. 230 $data[] = [ 231 [ 232 '#markup' => 'foo', 233 '#allowed_tags' => ['script'], 234 '#pre_render' => [function ($elements) { 235 $elements['#markup'] .= '<script>alert("bar");</script>'; 236 return $elements; 237 }, 238 ], 239 ], 240 'foo<script>alert("bar");</script>', 241 ]; 242 // Ensure output is escaped when adding content to #check_plain through 243 // a #pre_render callback. 244 $data[] = [ 245 [ 246 '#plain_text' => 'foo', 247 '#pre_render' => [function ($elements) { 248 $elements['#plain_text'] .= '<script>alert("bar");</script>'; 249 return $elements; 250 }, 251 ], 252 ], 253 'foo<script>alert("bar");</script>', 254 ]; 255 256 // Part 2: render arrays using #theme and #theme_wrappers. 257 258 // Tests that #theme and #theme_wrappers can co-exist on an element. 259 $build = [ 260 '#theme' => 'common_test_foo', 261 '#foo' => 'foo', 262 '#bar' => 'bar', 263 '#theme_wrappers' => ['container'], 264 '#attributes' => ['class' => ['baz']], 265 ]; 266 $setup_code_type_link = function () { 267 $this->themeManager->expects($this->exactly(2)) 268 ->method('render') 269 ->with($this->logicalOr('common_test_foo', 'container')) 270 ->willReturnCallback(function ($theme, $vars) { 271 if ($theme == 'container') { 272 return '<div' . (string) (new Attribute($vars['#attributes'])) . '>' . $vars['#children'] . "</div>\n"; 273 } 274 return $vars['#foo'] . $vars['#bar']; 275 }); 276 }; 277 $data[] = [$build, '<div class="baz">foobar</div>' . "\n", $setup_code_type_link]; 278 279 // Tests that #theme_wrappers can disambiguate element attributes shared 280 // with rendering methods that build #children by using the alternate 281 // #theme_wrappers attribute override syntax. 282 $build = [ 283 '#type' => 'link', 284 '#theme_wrappers' => [ 285 'container' => [ 286 '#attributes' => ['class' => ['baz']], 287 ], 288 ], 289 '#attributes' => ['id' => 'foo'], 290 '#url' => 'https://www.drupal.org', 291 '#title' => 'bar', 292 ]; 293 $setup_code_type_link = function () { 294 $this->themeManager->expects($this->exactly(2)) 295 ->method('render') 296 ->with($this->logicalOr('link', 'container')) 297 ->willReturnCallback(function ($theme, $vars) { 298 if ($theme == 'container') { 299 return '<div' . (string) (new Attribute($vars['#attributes'])) . '>' . $vars['#children'] . "</div>\n"; 300 } 301 $attributes = new Attribute(['href' => $vars['#url']] + (isset($vars['#attributes']) ? $vars['#attributes'] : [])); 302 return '<a' . (string) $attributes . '>' . $vars['#title'] . '</a>'; 303 }); 304 }; 305 $data[] = [$build, '<div class="baz"><a href="https://www.drupal.org" id="foo">bar</a></div>' . "\n", $setup_code_type_link]; 306 307 // Tests that #theme_wrappers can disambiguate element attributes when the 308 // "base" attribute is not set for #theme. 309 $build = [ 310 '#type' => 'link', 311 '#url' => 'https://www.drupal.org', 312 '#title' => 'foo', 313 '#theme_wrappers' => [ 314 'container' => [ 315 '#attributes' => ['class' => ['baz']], 316 ], 317 ], 318 ]; 319 $data[] = [$build, '<div class="baz"><a href="https://www.drupal.org">foo</a></div>' . "\n", $setup_code_type_link]; 320 321 // Tests two 'container' #theme_wrappers, one using the "base" attributes 322 // and one using an override. 323 $build = [ 324 '#attributes' => ['class' => ['foo']], 325 '#theme_wrappers' => [ 326 'container' => [ 327 '#attributes' => ['class' => ['bar']], 328 ], 329 'container', 330 ], 331 ]; 332 $setup_code = function () { 333 $this->themeManager->expects($this->exactly(2)) 334 ->method('render') 335 ->with('container') 336 ->willReturnCallback(function ($theme, $vars) { 337 return '<div' . (string) (new Attribute($vars['#attributes'])) . '>' . $vars['#children'] . "</div>\n"; 338 }); 339 }; 340 $data[] = [$build, '<div class="foo"><div class="bar"></div>' . "\n" . '</div>' . "\n", $setup_code]; 341 342 // Tests array syntax theme hook suggestion in #theme_wrappers. 343 $build = [ 344 '#theme_wrappers' => [['container']], 345 '#attributes' => ['class' => ['foo']], 346 ]; 347 $setup_code = function () { 348 $this->themeManager->expects($this->once()) 349 ->method('render') 350 ->with(['container']) 351 ->willReturnCallback(function ($theme, $vars) { 352 return '<div' . (string) (new Attribute($vars['#attributes'])) . '>' . $vars['#children'] . "</div>\n"; 353 }); 354 }; 355 $data[] = [$build, '<div class="foo"></div>' . "\n", $setup_code]; 356 357 // Part 3: render arrays using #markup as a fallback for #theme hooks. 358 359 // Theme suggestion is not implemented, #markup should be rendered. 360 $build = [ 361 '#theme' => ['suggestionnotimplemented'], 362 '#markup' => 'foo', 363 ]; 364 $setup_code = function () { 365 $this->themeManager->expects($this->once()) 366 ->method('render') 367 ->with(['suggestionnotimplemented'], $this->anything()) 368 ->willReturn(FALSE); 369 }; 370 $data[] = [$build, 'foo', $setup_code]; 371 372 // Tests unimplemented theme suggestion, child #markup should be rendered. 373 $build = [ 374 '#theme' => ['suggestionnotimplemented'], 375 'child' => [ 376 '#markup' => 'foo', 377 ], 378 ]; 379 $setup_code = function () { 380 $this->themeManager->expects($this->once()) 381 ->method('render') 382 ->with(['suggestionnotimplemented'], $this->anything()) 383 ->willReturn(FALSE); 384 }; 385 $data[] = [$build, 'foo', $setup_code]; 386 387 // Tests implemented theme suggestion: #markup should not be rendered. 388 $build = [ 389 '#theme' => ['common_test_empty'], 390 '#markup' => 'foo', 391 ]; 392 $theme_function_output = $this->randomContextValue(); 393 $setup_code = function () use ($theme_function_output) { 394 $this->themeManager->expects($this->once()) 395 ->method('render') 396 ->with(['common_test_empty'], $this->anything()) 397 ->willReturn($theme_function_output); 398 }; 399 $data[] = [$build, $theme_function_output, $setup_code]; 400 401 // Tests implemented theme suggestion: children should not be rendered. 402 $build = [ 403 '#theme' => ['common_test_empty'], 404 'child' => [ 405 '#markup' => 'foo', 406 ], 407 ]; 408 $data[] = [$build, $theme_function_output, $setup_code]; 409 410 // Part 4: handling of #children and child renderable elements. 411 412 // #theme is implemented so the values of both #children and 'child' will 413 // be ignored - it is the responsibility of the theme hook to render these 414 // if appropriate. 415 $build = [ 416 '#theme' => 'common_test_foo', 417 '#children' => 'baz', 418 'child' => ['#markup' => 'boo'], 419 ]; 420 $setup_code = function () { 421 $this->themeManager->expects($this->once()) 422 ->method('render') 423 ->with('common_test_foo', $this->anything()) 424 ->willReturn('foobar'); 425 }; 426 $data[] = [$build, 'foobar', $setup_code]; 427 428 // #theme is implemented but #render_children is TRUE. As in the case where 429 // #theme is not set, empty #children means child elements are rendered 430 // recursively. 431 $build = [ 432 '#theme' => 'common_test_foo', 433 '#children' => '', 434 '#render_children' => TRUE, 435 'child' => [ 436 '#markup' => 'boo', 437 ], 438 ]; 439 $setup_code = function () { 440 $this->themeManager->expects($this->never()) 441 ->method('render'); 442 }; 443 $data[] = [$build, 'boo', $setup_code]; 444 445 // #theme is implemented but #render_children is TRUE. As in the case where 446 // #theme is not set, #children will take precedence over 'child'. 447 $build = [ 448 '#theme' => 'common_test_foo', 449 '#children' => 'baz', 450 '#render_children' => TRUE, 451 'child' => [ 452 '#markup' => 'boo', 453 ], 454 ]; 455 $setup_code = function () { 456 $this->themeManager->expects($this->never()) 457 ->method('render'); 458 }; 459 $data[] = [$build, 'baz', $setup_code]; 460 461 // #theme is implemented but #render_children is TRUE. In this case the 462 // calling code is expecting only the children to be rendered. #prefix and 463 // #suffix should not be inherited for the children. 464 $build = [ 465 '#theme' => 'common_test_foo', 466 '#children' => '', 467 '#prefix' => 'kangaroo', 468 '#suffix' => 'unicorn', 469 '#render_children' => TRUE, 470 'child' => [ 471 '#markup' => 'kitten', 472 ], 473 ]; 474 $setup_code = function () { 475 $this->themeManager->expects($this->never()) 476 ->method('render'); 477 }; 478 $data[] = [$build, 'kitten', $setup_code]; 479 480 return $data; 481 } 482 483 /** 484 * @covers ::render 485 * @covers ::doRender 486 */ 487 public function testRenderSorting() { 488 $first = $this->randomMachineName(); 489 $second = $this->randomMachineName(); 490 // Build an array with '#weight' set for each element. 491 $elements = [ 492 'second' => [ 493 '#weight' => 10, 494 '#markup' => $second, 495 ], 496 'first' => [ 497 '#weight' => 0, 498 '#markup' => $first, 499 ], 500 ]; 501 $output = $this->renderer->renderRoot($elements); 502 503 // The lowest weight element should appear last in $output. 504 $this->assertGreaterThan(strpos($output, $first), strpos($output, $second)); 505 506 // Confirm that the $elements array has '#sorted' set to TRUE. 507 $this->assertTrue($elements['#sorted'], "'#sorted' => TRUE was added to the array"); 508 509 // Pass $elements through \Drupal\Core\Render\Element::children() and 510 // ensure it remains sorted in the correct order. 511 // \Drupal::service('renderer')->render() will return an empty string if 512 // used on the same array in the same request. 513 $children = Element::children($elements); 514 $this->assertSame('first', array_shift($children), 'Child found in the correct order.'); 515 $this->assertSame('second', array_shift($children), 'Child found in the correct order.'); 516 } 517 518 /** 519 * @covers ::render 520 * @covers ::doRender 521 */ 522 public function testRenderSortingWithSetHashSorted() { 523 $first = $this->randomMachineName(); 524 $second = $this->randomMachineName(); 525 // The same array structure again, but with #sorted set to TRUE. 526 $elements = [ 527 'second' => [ 528 '#weight' => 10, 529 '#markup' => $second, 530 ], 531 'first' => [ 532 '#weight' => 0, 533 '#markup' => $first, 534 ], 535 '#sorted' => TRUE, 536 ]; 537 $output = $this->renderer->renderRoot($elements); 538 539 // The elements should appear in output in the same order as the array. 540 $this->assertLessThan(strpos($output, $first), strpos($output, $second)); 541 } 542 543 /** 544 * @covers ::render 545 * @covers ::doRender 546 * 547 * @dataProvider providerAccessValues 548 */ 549 public function testRenderWithPresetAccess($access) { 550 $build = [ 551 '#access' => $access, 552 ]; 553 554 $this->assertAccess($build, $access); 555 } 556 557 /** 558 * @covers ::render 559 * @covers ::doRender 560 * 561 * @dataProvider providerAccessValues 562 */ 563 public function testRenderWithAccessCallbackCallable($access) { 564 $build = [ 565 '#access_callback' => function () use ($access) { 566 return $access; 567 }, 568 ]; 569 570 $this->assertAccess($build, $access); 571 } 572 573 /** 574 * Ensures that the #access property wins over the callable. 575 * 576 * @covers ::render 577 * @covers ::doRender 578 * 579 * @dataProvider providerAccessValues 580 */ 581 public function testRenderWithAccessPropertyAndCallback($access) { 582 $build = [ 583 '#access' => $access, 584 '#access_callback' => function () { 585 return TRUE; 586 }, 587 ]; 588 589 $this->assertAccess($build, $access); 590 } 591 592 /** 593 * @covers ::render 594 * @covers ::doRender 595 * 596 * @dataProvider providerAccessValues 597 */ 598 public function testRenderWithAccessControllerResolved($access) { 599 600 switch ($access) { 601 case AccessResult::allowed(): 602 $method = 'accessResultAllowed'; 603 break; 604 605 case AccessResult::forbidden(): 606 $method = 'accessResultForbidden'; 607 break; 608 609 case FALSE: 610 $method = 'accessFalse'; 611 break; 612 613 case TRUE: 614 $method = 'accessTrue'; 615 break; 616 } 617 618 $build = [ 619 '#access_callback' => 'Drupal\Tests\Core\Render\TestAccessClass::' . $method, 620 ]; 621 622 $this->assertAccess($build, $access); 623 } 624 625 /** 626 * @covers ::render 627 * @covers ::doRender 628 */ 629 public function testRenderAccessCacheabilityDependencyInheritance() { 630 $build = [ 631 '#access' => AccessResult::allowed()->addCacheContexts(['user']), 632 ]; 633 634 $this->renderer->renderPlain($build); 635 636 $this->assertEquals(['languages:language_interface', 'theme', 'user'], $build['#cache']['contexts']); 637 } 638 639 /** 640 * Tests rendering same render array twice. 641 * 642 * Tests that a first render returns the rendered output and a second doesn't 643 * because of the #printed property. Also tests that correct metadata has been 644 * set for re-rendering. 645 * 646 * @covers ::render 647 * @covers ::doRender 648 * 649 * @dataProvider providerRenderTwice 650 */ 651 public function testRenderTwice($build) { 652 $this->assertEquals('kittens', $this->renderer->renderRoot($build)); 653 $this->assertEquals('kittens', $build['#markup']); 654 $this->assertEquals(['kittens-147'], $build['#cache']['tags']); 655 $this->assertTrue($build['#printed']); 656 657 // We don't want to reprint already printed render arrays. 658 $this->assertEquals('', $this->renderer->renderRoot($build)); 659 } 660 661 /** 662 * Provides a list of render array iterations. 663 * 664 * @return array 665 */ 666 public function providerRenderTwice() { 667 return [ 668 [ 669 [ 670 '#markup' => 'kittens', 671 '#cache' => [ 672 'tags' => ['kittens-147'], 673 ], 674 ], 675 ], 676 [ 677 [ 678 'child' => [ 679 '#markup' => 'kittens', 680 '#cache' => [ 681 'tags' => ['kittens-147'], 682 ], 683 ], 684 ], 685 ], 686 [ 687 [ 688 '#render_children' => TRUE, 689 'child' => [ 690 '#markup' => 'kittens', 691 '#cache' => [ 692 'tags' => ['kittens-147'], 693 ], 694 ], 695 ], 696 ], 697 ]; 698 } 699 700 /** 701 * Ensures that #access is taken in account when rendering #render_children. 702 */ 703 public function testRenderChildrenAccess() { 704 $build = [ 705 '#access' => FALSE, 706 '#render_children' => TRUE, 707 'child' => [ 708 '#markup' => 'kittens', 709 ], 710 ]; 711 712 $this->assertEquals('', $this->renderer->renderRoot($build)); 713 } 714 715 /** 716 * Provides a list of both booleans. 717 * 718 * @return array 719 */ 720 public function providerAccessValues() { 721 return [ 722 [FALSE], 723 [TRUE], 724 [AccessResult::forbidden()], 725 [AccessResult::allowed()], 726 ]; 727 } 728 729 /** 730 * Asserts that a render array with access checking renders correctly. 731 * 732 * @param array $build 733 * A render array with either #access or #access_callback. 734 * @param bool $access 735 * Whether the render array is accessible or not. 736 */ 737 protected function assertAccess($build, $access) { 738 $sensitive_content = $this->randomContextValue(); 739 $build['#markup'] = $sensitive_content; 740 if (($access instanceof AccessResultInterface && $access->isAllowed()) || $access === TRUE) { 741 $this->assertSame($sensitive_content, (string) $this->renderer->renderRoot($build)); 742 } 743 else { 744 $this->assertSame('', (string) $this->renderer->renderRoot($build)); 745 } 746 } 747 748 /** 749 * @covers ::render 750 * @covers ::doRender 751 */ 752 public function testRenderWithoutThemeArguments() { 753 $element = [ 754 '#theme' => 'common_test_foo', 755 ]; 756 757 $this->themeManager->expects($this->once()) 758 ->method('render') 759 ->with('common_test_foo', $this->defaultThemeVars + $element) 760 ->willReturn('foobar'); 761 762 // Test that defaults work. 763 $this->assertEquals('foobar', $this->renderer->renderRoot($element), 'Defaults work'); 764 } 765 766 /** 767 * @covers ::render 768 * @covers ::doRender 769 */ 770 public function testRenderWithThemeArguments() { 771 $element = [ 772 '#theme' => 'common_test_foo', 773 '#foo' => $this->randomMachineName(), 774 '#bar' => $this->randomMachineName(), 775 ]; 776 777 $this->themeManager->expects($this->once()) 778 ->method('render') 779 ->with('common_test_foo', $this->defaultThemeVars + $element) 780 ->willReturnCallback(function ($hook, $vars) { 781 return $vars['#foo'] . $vars['#bar']; 782 }); 783 784 // Tests that passing arguments to the theme function works. 785 $this->assertEquals($this->renderer->renderRoot($element), $element['#foo'] . $element['#bar'], 'Passing arguments to theme functions works'); 786 } 787 788 /** 789 * @covers ::render 790 * @covers ::doRender 791 * @covers \Drupal\Core\Render\RenderCache::get 792 * @covers \Drupal\Core\Render\RenderCache::set 793 * @covers \Drupal\Core\Render\RenderCache::createCacheID 794 */ 795 public function testRenderCache() { 796 $this->setUpRequest(); 797 $this->setupMemoryCache(); 798 799 // Create an empty element. 800 $test_element = [ 801 '#cache' => [ 802 'keys' => ['render_cache_test'], 803 'tags' => ['render_cache_tag'], 804 ], 805 '#markup' => '', 806 'child' => [ 807 '#cache' => [ 808 'keys' => ['render_cache_test_child'], 809 'tags' => ['render_cache_tag_child:1', 'render_cache_tag_child:2'], 810 ], 811 '#markup' => '', 812 ], 813 ]; 814 815 // Render the element and confirm that it goes through the rendering 816 // process (which will set $element['#printed']). 817 $element = $test_element; 818 $this->renderer->renderRoot($element); 819 $this->assertTrue(isset($element['#printed']), 'No cache hit'); 820 821 // Render the element again and confirm that it is retrieved from the cache 822 // instead (so $element['#printed'] will not be set). 823 $element = $test_element; 824 $this->renderer->renderRoot($element); 825 $this->assertFalse(isset($element['#printed']), 'Cache hit'); 826 827 // Test that cache tags are correctly collected from the render element, 828 // including the ones from its subchild. 829 $expected_tags = [ 830 'render_cache_tag', 831 'render_cache_tag_child:1', 832 'render_cache_tag_child:2', 833 ]; 834 $this->assertEquals($expected_tags, $element['#cache']['tags'], 'Cache tags were collected from the element and its subchild.'); 835 836 // The cache item also has a 'rendered' cache tag. 837 $cache_item = $this->cacheFactory->get('render')->get('render_cache_test:en:stark'); 838 $this->assertSame(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags); 839 } 840 841 /** 842 * @covers ::render 843 * @covers ::doRender 844 * @covers \Drupal\Core\Render\RenderCache::get 845 * @covers \Drupal\Core\Render\RenderCache::set 846 * @covers \Drupal\Core\Render\RenderCache::createCacheID 847 * 848 * @dataProvider providerTestRenderCacheMaxAge 849 */ 850 public function testRenderCacheMaxAge($max_age, $is_render_cached, $render_cache_item_expire) { 851 $this->setUpRequest(); 852 $this->setupMemoryCache(); 853 854 $element = [ 855 '#cache' => [ 856 'keys' => ['render_cache_test'], 857 'max-age' => $max_age, 858 ], 859 '#markup' => '', 860 ]; 861 $this->renderer->renderRoot($element); 862 863 $cache_item = $this->cacheFactory->get('render')->get('render_cache_test:en:stark'); 864 if (!$is_render_cached) { 865 $this->assertFalse($cache_item); 866 } 867 else { 868 $this->assertNotFalse($cache_item); 869 $this->assertSame($render_cache_item_expire, $cache_item->expire); 870 } 871 } 872 873 public function providerTestRenderCacheMaxAge() { 874 return [ 875 [0, FALSE, NULL], 876 [60, TRUE, (int) $_SERVER['REQUEST_TIME'] + 60], 877 [Cache::PERMANENT, TRUE, -1], 878 ]; 879 } 880 881 /** 882 * Tests that #cache_properties are properly handled. 883 * 884 * @param array $expected_results 885 * An associative array of expected results keyed by property name. 886 * 887 * @covers ::render 888 * @covers ::doRender 889 * @covers \Drupal\Core\Render\RenderCache::get 890 * @covers \Drupal\Core\Render\RenderCache::set 891 * @covers \Drupal\Core\Render\RenderCache::createCacheID 892 * @covers \Drupal\Core\Render\RenderCache::getCacheableRenderArray 893 * 894 * @dataProvider providerTestRenderCacheProperties 895 */ 896 public function testRenderCacheProperties(array $expected_results) { 897 $this->setUpRequest(); 898 $this->setupMemoryCache(); 899 900 $element = $original = [ 901 '#cache' => [ 902 'keys' => ['render_cache_test'], 903 ], 904 // Collect expected property names. 905 '#cache_properties' => array_keys(array_filter($expected_results)), 906 'child1' => ['#markup' => Markup::create('1')], 907 'child2' => ['#markup' => Markup::create('2')], 908 // Mark the value as safe. 909 '#custom_property' => Markup::create('custom_value'), 910 '#custom_property_array' => ['custom value'], 911 ]; 912 913 $this->renderer->renderRoot($element); 914 915 $cache = $this->cacheFactory->get('render'); 916 $data = $cache->get('render_cache_test:en:stark')->data; 917 918 // Check that parent markup is ignored when caching children's markup. 919 $this->assertEquals($data['#markup'] === '', (bool) Element::children($data)); 920 921 // Check that the element properties are cached as specified. 922 foreach ($expected_results as $property => $expected) { 923 $cached = !empty($data[$property]); 924 $this->assertEquals($cached, (bool) $expected); 925 // Check that only the #markup key is preserved for children. 926 if ($cached) { 927 $this->assertEquals($data[$property], $original[$property]); 928 } 929 } 930 // #custom_property_array can not be a safe_cache_property. 931 $safe_cache_properties = array_diff(Element::properties(array_filter($expected_results)), ['#custom_property_array']); 932 foreach ($safe_cache_properties as $cache_property) { 933 $this->assertInstanceOf(MarkupInterface::class, $data[$cache_property]); 934 } 935 } 936 937 /** 938 * Data provider for ::testRenderCacheProperties(). 939 * 940 * @return array 941 * An array of associative arrays of expected results keyed by property 942 * name. 943 */ 944 public function providerTestRenderCacheProperties() { 945 return [ 946 [[]], 947 [['child1' => 0, 'child2' => 0, '#custom_property' => 0, '#custom_property_array' => 0]], 948 [['child1' => 0, 'child2' => 0, '#custom_property' => 1, '#custom_property_array' => 0]], 949 [['child1' => 0, 'child2' => 1, '#custom_property' => 0, '#custom_property_array' => 0]], 950 [['child1' => 0, 'child2' => 1, '#custom_property' => 1, '#custom_property_array' => 0]], 951 [['child1' => 1, 'child2' => 0, '#custom_property' => 0, '#custom_property_array' => 0]], 952 [['child1' => 1, 'child2' => 0, '#custom_property' => 1, '#custom_property_array' => 0]], 953 [['child1' => 1, 'child2' => 1, '#custom_property' => 0, '#custom_property_array' => 0]], 954 [['child1' => 1, 'child2' => 1, '#custom_property' => 1, '#custom_property_array' => 0]], 955 [['child1' => 1, 'child2' => 1, '#custom_property' => 1, '#custom_property_array' => 1]], 956 ]; 957 } 958 959 /** 960 * @covers ::addCacheableDependency 961 * 962 * @dataProvider providerTestAddCacheableDependency 963 */ 964 public function testAddCacheableDependency(array $build, $object, array $expected) { 965 $this->renderer->addCacheableDependency($build, $object); 966 $this->assertEquals($build, $expected); 967 } 968 969 public function providerTestAddCacheableDependency() { 970 return [ 971 // Empty render array, typical default cacheability. 972 [ 973 [], 974 new TestCacheableDependency([], [], Cache::PERMANENT), 975 [ 976 '#cache' => [ 977 'contexts' => [], 978 'tags' => [], 979 'max-age' => Cache::PERMANENT, 980 ], 981 ], 982 ], 983 // Empty render array, some cacheability. 984 [ 985 [], 986 new TestCacheableDependency(['user.roles'], ['foo'], Cache::PERMANENT), 987 [ 988 '#cache' => [ 989 'contexts' => ['user.roles'], 990 'tags' => ['foo'], 991 'max-age' => Cache::PERMANENT, 992 ], 993 ], 994 ], 995 // Cacheable render array, some cacheability. 996 [ 997 [ 998 '#cache' => [ 999 'contexts' => ['theme'], 1000 'tags' => ['bar'], 1001 'max-age' => 600, 1002 ], 1003 ], 1004 new TestCacheableDependency(['user.roles'], ['foo'], Cache::PERMANENT), 1005 [ 1006 '#cache' => [ 1007 'contexts' => ['theme', 'user.roles'], 1008 'tags' => ['bar', 'foo'], 1009 'max-age' => 600, 1010 ], 1011 ], 1012 ], 1013 // Cacheable render array, no cacheability. 1014 [ 1015 [ 1016 '#cache' => [ 1017 'contexts' => ['theme'], 1018 'tags' => ['bar'], 1019 'max-age' => 600, 1020 ], 1021 ], 1022 new \stdClass(), 1023 [ 1024 '#cache' => [ 1025 'contexts' => ['theme'], 1026 'tags' => ['bar'], 1027 'max-age' => 0, 1028 ], 1029 ], 1030 ], 1031 ]; 1032 } 1033 1034} 1035 1036class TestAccessClass implements TrustedCallbackInterface { 1037 1038 public static function accessTrue() { 1039 return TRUE; 1040 } 1041 1042 public static function accessFalse() { 1043 return FALSE; 1044 } 1045 1046 public static function accessResultAllowed() { 1047 return AccessResult::allowed(); 1048 } 1049 1050 public static function accessResultForbidden() { 1051 return AccessResult::forbidden(); 1052 } 1053 1054 /** 1055 * {@inheritdoc} 1056 */ 1057 public static function trustedCallbacks() { 1058 return ['accessTrue', 'accessFalse', 'accessResultAllowed', 'accessResultForbidden']; 1059 } 1060 1061} 1062 1063class TestCallables implements TrustedCallbackInterface { 1064 1065 public function preRenderPrinted($elements) { 1066 $elements['#printed'] = TRUE; 1067 return $elements; 1068 } 1069 1070 /** 1071 * {@inheritdoc} 1072 */ 1073 public static function trustedCallbacks() { 1074 return ['preRenderPrinted']; 1075 } 1076 1077} 1078