Commit 16146641 authored by webchick's avatar webchick

Issue #2378883 by Wim Leers, dawehner: Convert existing drupal_render()...

Issue #2378883 by Wim Leers, dawehner: Convert existing drupal_render() KernelTestBase tests to PHPUnit tests
parent d653d386
......@@ -23,6 +23,7 @@
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\Renderer;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\Response;
......@@ -33,7 +34,6 @@
use Drupal\Core\Routing\GeneratorNotInitializedException;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Renderer;
use Drupal\Core\Session\AnonymousUserSession;
/**
......@@ -1346,9 +1346,9 @@ function show(&$element) {
/**
* Generates a render cache placeholder.
*
* This is used by drupal_pre_render_render_cache_placeholder() to generate
* placeholders, but should also be called by #post_render_cache callbacks that
* want to replace the placeholder with the final markup.
* This can be used to generate placeholders, and hence should also be used by
* #post_render_cache callbacks that want to replace the placeholder with the
* final markup.
*
* @param string $callback
* The #post_render_cache callback that will replace the placeholder with its
......@@ -1363,28 +1363,11 @@ function show(&$element) {
*
* @throws \Exception
*
* @see \Drupal\Core\Render\Renderer::getFromCache()
* @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
* Use \Drupal::service('renderer')->generateCachePlaceholder().
*/
function drupal_render_cache_generate_placeholder($callback, array &$context) {
if (is_string($callback) && strpos($callback, '::') === FALSE) {
/** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
$controller_resolver = \Drupal::service('controller_resolver');
$callable = \Drupal::service('controller_resolver')->getControllerFromDefinition($callback);
}
else {
$callable = $callback;
}
if (!is_callable($callable)) {
throw new Exception(t('$callable must be a callable function or of the form service_id:method.'));
}
// Generate a unique token if one is not already provided.
$context += array(
'token' => \Drupal\Component\Utility\Crypt::randomBytesBase64(55),
);
return '<drupal-render-cache-placeholder callback="' . $callback . '" token="' . $context['token'] . '"></drupal-render-cache-placeholder>';
return \Drupal::service('renderer')->generateCachePlaceholder($callback, $context);
}
/**
......
......@@ -70,7 +70,7 @@ public function __construct(array $tags = [], array $attached = [], array $post_
public function merge(BubbleableMetadata $other) {
$result = new BubbleableMetadata();
$result->tags = Cache::mergeTags($this->tags, $other->tags);
$result->attached = drupal_merge_attached($this->attached, $other->attached);
$result->attached = Renderer::mergeAttachments($this->attached, $other->attached);
$result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache);
return $result;
}
......
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Render;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheContexts;
......@@ -343,8 +344,9 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
// We've rendered this element (and its subtree!), now update the stack.
$this->updateStack($elements);
// Cache the processed element if #cache is set.
if (isset($elements['#cache'])) {
// Cache the processed element if #cache is set, and the metadata necessary
// to generate a cache ID is present.
if (isset($elements['#cache']) && (isset($elements['#cache']['keys']) || isset($elements['#cache']['cid']))) {
$this->cacheSet($elements);
}
......@@ -491,7 +493,7 @@ protected function cacheGet(array $elements) {
}
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
if (!empty($cid) && $cache = $this->cacheFactory->get($bin)->get($cid)) {
if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) {
$cached_element = $cache->data;
// Return the cached element.
return $cached_element;
......@@ -599,4 +601,27 @@ public static function mergeAttachments(array $a, array $b) {
return NestedArray::mergeDeep($a, $b);
}
/**
* {@inheritdoc}
*/
public function generateCachePlaceholder($callback, array &$context) {
if (is_string($callback) && strpos($callback, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callback);
}
else {
$callable = $callback;
}
if (!is_callable($callable)) {
throw new \InvalidArgumentException('$callable must be a callable function or of the form service_id:method.');
}
// Generate a unique token if one is not already provided.
$context += [
'token' => Crypt::randomBytesBase64(55),
];
return '<drupal-render-cache-placeholder callback="' . $callback . '" token="' . $context['token'] . '"></drupal-render-cache-placeholder>';
}
}
......@@ -348,4 +348,27 @@ public static function mergeBubbleableMetadata(array $a, array $b);
*/
public static function mergeAttachments(array $a, array $b);
/**
* Generates a render cache placeholder.
*
* This can be used to generate placeholders, and hence should also be used by
* #post_render_cache callbacks that want to replace the placeholder with the
* final markup.
*
* @param string $callback
* The #post_render_cache callback that will replace the placeholder with its
* eventual markup.
* @param array $context
* An array providing context for the #post_render_cache callback. This array
* will be altered to provide a 'token' key/value pair, if not already
* provided, to uniquely identify the generated placeholder.
*
* @return string
* The generated placeholder HTML.
*
* @throws \InvalidArgumentException
* Thrown when no valid callable got passed in.
*/
public function generateCachePlaceholder($callback, array &$context);
}
......@@ -216,92 +216,6 @@ function common_test_cron() {
throw new Exception(t('Uncaught exception'));
}
/**
* #post_render_cache callback; modifies #markup, #attached and #context_test.
*
* @param array $element
* A render array with the following keys:
* - #markup
* - #attached
* @param array $context
* An array with the following keys:
* - foo: contains a random string.
*
* @return array $element
* The updated $element.
*/
function common_test_post_render_cache(array $element, array $context) {
// Override #markup.
$element['#markup'] = '<p>overridden</p>';
// Extend #attached.
if (!isset($element['#attached']['drupalSettings']['common_test'])) {
$element['#attached']['drupalSettings']['common_test'] = [];
}
$element['#attached']['drupalSettings']['common_test'] += $context;
// Set new property.
$element['#context_test'] = $context;
return $element;
}
/**
* #post_render_cache callback; replaces placeholder, extends #attached.
*
* @param array $element
* The renderable array that contains the to be replaced placeholder.
* @param array $context
* An array with the following keys:
* - bar: contains a random string.
*
* @return array
* A render array.
*/
function common_test_post_render_cache_placeholder(array $element, array $context) {
$placeholder = drupal_render_cache_generate_placeholder(__FUNCTION__, $context);
$replace_element = array(
'#markup' => '<bar>' . $context['bar'] . '</bar>',
'#attached' => array(
'drupalSettings' => [
'common_test' => $context,
],
),
);
$markup = drupal_render($replace_element);
$element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
return $element;
}
/**
* #post_render_cache callback; bubbles another #post_render_cache callback.
*
* @param array $element
* A render array with the following keys:
* - #markup
* - #attached
* @param array $context
* An array with the following keys:
* - foo: contains a random string.
*
* @return array $element
* The updated $element.
*/
function common_test_post_render_cache_recursion(array $element, array $context) {
// Render a child which itself also has a #post_render_cache callback that
// must be bubbled.
$child = [];
$child['#markup'] = 'foo';
$child['#post_render_cache']['common_test_post_render_cache'][] = $context;
// Render the child.
$element['#markup'] = drupal_render($child);
return $element;
}
/**
* Implements hook_page_attachments().
*
......
{#
/**
* @file
* Default theme implementation for the common test render element.
*
* Available variables:
* - foo: a render array
*
* @ingroup themeable
*/
#}
{{ foo }}
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Render\RendererBubblingTest.
*/
namespace Drupal\Tests\Core\Render;
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
use Drupal\Core\Render\Element;
use Drupal\Core\State\State;
/**
* @coversDefaultClass \Drupal\Core\Render\Renderer
* @group Render
*/
class RendererBubblingTest extends RendererTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->setUpRequest();
$this->setupMemoryCache();
}
/**
* Tests bubbling of assets when NOT using #pre_render callbacks.
*/
public function testBubblingWithoutPreRender() {
$this->elementInfo->expects($this->any())
->method('getInfo')
->willReturn([]);
// Create an element with a child and subchild. Each element loads a
// different library using #attached.
$element = [
'#type' => 'container',
'#cache' => [
'keys' => ['simpletest', 'drupal_render', 'children_attached'],
],
'#attached' => ['library' => ['test/parent']],
'#title' => 'Parent',
];
$element['child'] = [
'#type' => 'container',
'#attached' => ['library' => ['test/child']],
'#title' => 'Child',
];
$element['child']['subchild'] = [
'#attached' => ['library' => ['test/subchild']],
'#markup' => 'Subchild',
];
// Render the element and verify the presence of #attached JavaScript.
$this->renderer->render($element);
$expected_libraries = ['test/parent', 'test/child', 'test/subchild'];
$this->assertEquals($element['#attached']['library'], $expected_libraries, 'The element, child and subchild #attached libraries are included.');
// Load the element from cache and verify the presence of the #attached
// JavaScript.
$element = ['#cache' => ['keys' => ['simpletest', 'drupal_render', 'children_attached']]];
$this->assertTrue(strlen($this->renderer->render($element)) > 0, 'The element was retrieved from cache.');
$this->assertEquals($element['#attached']['library'], $expected_libraries, 'The element, child and subchild #attached libraries are included.');
}
/**
* Tests bubbling of bubbleable metadata added by #pre_render callbacks.
*
* @dataProvider providerTestBubblingWithPrerender
*/
public function testBubblingWithPrerender($test_element) {
// Mock the State service.
$memory_state = new State(new KeyValueMemoryFactory());;
\Drupal::getContainer()->set('state', $memory_state);
$this->controllerResolver->expects($this->any())
->method('getControllerFromDefinition')
->willReturnArgument(0);
// Simulate the theme system/Twig: a recursive call to Renderer::render(),
// just like the theme system or a Twig template would have done.
$this->themeManager->expects($this->any())
->method('render')
->willReturnCallback(function ($hook, $vars) {
return $this->renderer->render($vars['foo']);
});
// ::bubblingPreRender() verifies that a #pre_render callback for a render
// array that is cacheable and …
// - … is cached does NOT get called. (Also mock a render cache item.)
// - … is not cached DOES get called.
\Drupal::state()->set('bubbling_nested_pre_render_cached', FALSE);
\Drupal::state()->set('bubbling_nested_pre_render_uncached', FALSE);
$this->memoryCache->set('cached_nested', ['#markup' => 'Cached nested!', '#attached' => [], '#cache' => ['tags' => []], '#post_render_cache' => []]);
// Simulate the rendering of an entire response (i.e. a root call).
$output = $this->renderer->renderRoot($test_element);
// First, assert the render array is of the expected form.
$this->assertEquals('Cache tag!Asset!Post-render cache!barquxNested!Cached nested!', trim($output), 'Expected HTML generated.');
$this->assertEquals(['child:cache_tag'], $test_element['#cache']['tags'], 'Expected cache tags found.');
$expected_attached = [
'drupalSettings' => ['foo' => 'bar'],
];
$this->assertEquals($expected_attached, $test_element['#attached'], 'Expected assets found.');
$this->assertEquals([], $test_element['#post_render_cache'], '#post_render_cache property is empty after rendering');
// Second, assert that #pre_render callbacks are only executed if they don't
// have a render cache hit (and hence a #pre_render callback for a render
// cached item cannot bubble more metadata).
$this->assertTrue(\Drupal::state()->get('bubbling_nested_pre_render_uncached'));
$this->assertFalse(\Drupal::state()->get('bubbling_nested_pre_render_cached'));
}
/**
* Provides two test elements: one without, and one with the theme system.
*
* @return array
*/
public function providerTestBubblingWithPrerender() {
$data = [];
// Test element without theme.
$data[] = [[
'foo' => [
'#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingPreRender'],
]]];
// Test element with theme.
$data[] = [[
'#theme' => 'common_test_render_element',
'foo' => [
'#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingPreRender'],
]]];
return $data;
}
}
class BubblingTest {
/**
* #pre_render callback for testBubblingWithPrerender().
*/
public static function bubblingPreRender($elements) {
$callback = __CLASS__ . '::bubblingPostRenderCache';
$context = [
'foo' => 'bar',
'baz' => 'qux',
];
$placeholder = \Drupal::service('renderer')->generateCachePlaceholder($callback, $context);
$elements += [
'child_cache_tag' => [
'#cache' => [
'tags' => ['child:cache_tag'],
],
'#markup' => 'Cache tag!',
],
'child_asset' => [
'#attached' => [
'drupalSettings' => ['foo' => 'bar'],
],
'#markup' => 'Asset!',
],
'child_post_render_cache' => [
'#post_render_cache' => [
$callback => [
$context,
],
],
'#markup' => $placeholder,
],
'child_nested_pre_render_uncached' => [
'#cache' => ['cid' => 'uncached_nested'],
'#pre_render' => [__CLASS__ . '::bubblingNestedPreRenderUncached'],
],
'child_nested_pre_render_cached' => [
'#cache' => ['cid' => 'cached_nested'],
'#pre_render' => [__CLASS__ . '::bubblingNestedPreRenderCached'],
],
];
return $elements;
}
/**
* #pre_render callback for testBubblingWithPrerender().
*/
public static function bubblingNestedPreRenderUncached($elements) {
\Drupal::state()->set('bubbling_nested_pre_render_uncached', TRUE);
$elements['#markup'] = 'Nested!';
return $elements;
}
/**
* #pre_render callback for testBubblingWithPrerender().
*/
public static function bubblingNestedPreRenderCached($elements) {
\Drupal::state()->set('bubbling_nested_pre_render_cached', TRUE);
return $elements;
}
/**
* #post_render_cache callback for testBubblingWithPrerender().
*/
public static function bubblingPostRenderCache(array $element, array $context) {
$callback = __CLASS__ . '::bubblingPostRenderCache';
$placeholder = \Drupal::service('renderer')->generateCachePlaceholder($callback, $context);
$element['#markup'] = str_replace($placeholder, 'Post-render cache!' . $context['foo'] . $context['baz'], $element['#markup']);
return $element;
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Render\RendererRecursionTest.
*/
namespace Drupal\Tests\Core\Render;
use Drupal\Core\Render\Element;
/**
* @coversDefaultClass \Drupal\Core\Render\Renderer
* @group Render
*/
class RendererRecursionTest extends RendererTestBase {
protected function setUpRenderRecursionComplexElements() {
$complex_child_markup = '<p>Imagine this is a render array for an entity.</p>';
$parent_markup = '<p>Rendered!</p>';
$complex_child_template = [
'#markup' => $complex_child_markup,
'#attached' => [
'library' => [
'core/drupal',
],
],
'#cache' => [
'tags' => [
'test:complex_child',
],
],
'#post_render_cache' => [
'Drupal\Tests\Core\Render\PostRenderCache::callback' => [
['foo' => $this->getRandomGenerator()->string()],
],
],
];
return [$complex_child_markup, $parent_markup, $complex_child_template];
}
/**
* ::renderRoot() may not be called inside of another ::renderRoot() call.
*
* @covers ::renderRoot
* @covers ::render
* @covers ::doRender
*
* @expectedException \LogicException
*/
public function testRenderRecursionWithNestedRenderRoot() {
list($complex_child_markup, $parent_markup, $complex_child_template) = $this->setUpRenderRecursionComplexElements();
$renderer = $this->renderer;
$this->setUpRequest();
$complex_child = $complex_child_template;
$callable = function () use ($renderer, $complex_child) {
$renderer->renderRoot($complex_child);
};
$page = [
'content' => [
'#pre_render' => [
$callable
],
'#suffix' => $parent_markup,
]
];
$renderer->renderRoot($page);
}
/**
* ::render() may be called from anywhere.
*
* Including from inside of another ::renderRoot() call. Bubbling must be
* performed.
*
* @covers ::renderRoot
* @covers ::render
* @covers ::doRender
*/
public function testRenderRecursionWithNestedRender() {
list($complex_child_markup, $parent_markup, $complex_child_template) = $this->setUpRenderRecursionComplexElements();
$renderer = $this->renderer;
$this->setUpRequest();
$complex_child = $complex_child_template;
$callable = function ($elements) use ($renderer, $complex_child, $complex_child_markup, $parent_markup) {
$elements['#markup'] = $renderer->render($complex_child);
$this->assertEquals($complex_child_markup, $elements['#markup'], 'Rendered complex child output as expected, without the #post_render_cache callback executed.');
return $elements;
};
$page = [
'content' => [
'#pre_render' => [
$callable
],
'#suffix' => $parent_markup,
]
];
$output = $renderer->renderRoot($page);
$this->assertEquals('<p>overridden</p>', $output, 'Rendered output as expected, with the #post_render_cache callback executed.');
$this->assertTrue(in_array('test:complex_child', $page['#cache']['tags']), 'Cache tag bubbling performed.');
$this->assertTrue(in_array('core/drupal', $page['#attached']['library']), 'Asset bubbling performed.');
}
/**
* ::renderPlain() may be called from anywhere.
*
* Including from inside of another ::renderRoot() call.
*