summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNathaniel Catchpole2013-11-18 14:19:10 (GMT)
committerNathaniel Catchpole2013-11-18 14:19:10 (GMT)
commit1daa6bb3bba3d00eedb40fd53a90bd1de4d1882a (patch)
treee00260542fdbf782fda68e3cf2ef91384108bf16
parent996fb5720de228797526d28822456ffdb4f22384 (diff)
Issue #2118703 by Wim Leers, amateescu: Introduce #post_render_cache callback to allow for personalization without breaking the render cache.
-rw-r--r--core/includes/common.inc268
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php365
-rw-r--r--core/modules/system/system.module7
-rw-r--r--core/modules/system/tests/modules/common_test/common_test.module60
4 files changed, 676 insertions, 24 deletions
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 8f60fe4..90641b6 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3797,7 +3797,19 @@ function drupal_render_page($page) {
* - If this element has #prefix and/or #suffix defined, they are concatenated
* to #children.
* - If this element has #cache defined, the rendered output of this element
- * is saved to drupal_render()'s internal cache.
+ * is saved to drupal_render()'s internal cache. This includes the changes
+ * made by #post_render.
+ * - If this element (or any of its children) has an array of
+ * #post_render_cache functions defined, they are called sequentially to
+ * replace placeholders in the final #markup and extend #attached.
+ * Placeholders must contain a unique token, to guarantee that e.g. samples
+ * of placeholders are not replaced also. For this, a special element named
+ * 'render_cache_placeholder' is provided.
+ * Note that these callbacks run always: when hitting the render cache, when
+ * missing, or when render caching is not used at all. This is done to allow
+ * any Drupal module to customize other render arrays without breaking the
+ * render cache if it is enabled, and to not require it to use other logic
+ * when render caching is disabled.
* - #printed is set to TRUE for this element to ensure that it is only
* rendered once.
* - The final value of #children for this element is returned as the rendered
@@ -3805,6 +3817,8 @@ function drupal_render_page($page) {
*
* @param array $elements
* The structured array describing the data to be rendered.
+ * @param bool $is_recursive_call
+ * Whether this is a recursive call or not, for internal use.
*
* @return string
* The rendered HTML.
@@ -3814,7 +3828,7 @@ function drupal_render_page($page) {
* @see drupal_process_states()
* @see drupal_process_attached()
*/
-function drupal_render(&$elements) {
+function drupal_render(&$elements, $is_recursive_call = FALSE) {
// Early-return nothing if user does not have access.
if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
return '';
@@ -3825,11 +3839,19 @@ function drupal_render(&$elements) {
return '';
}
- // Try to fetch the element's markup from cache and return.
+ // Try to fetch the prerendered element from cache, run any #post_render_cache
+ // callbacks and return the final markup.
if (isset($elements['#cache'])) {
- $cached_output = drupal_render_cache_get($elements);
- if ($cached_output !== FALSE) {
- return $cached_output;
+ $cached_element = drupal_render_cache_get($elements);
+ if ($cached_element !== FALSE) {
+ $elements = $cached_element;
+ // Only when we're not in a recursive drupal_render() call,
+ // #post_render_cache callbacks must be executed, to prevent breaking the
+ // render cache in case of nested elements with #cache set.
+ if (!$is_recursive_call) {
+ _drupal_render_process_post_render_cache($elements);
+ }
+ return $elements['#markup'];
}
}
@@ -3888,7 +3910,7 @@ function drupal_render(&$elements) {
// process as drupal_render_children() but is inlined for speed.
if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
foreach ($children as $key) {
- $elements['#children'] .= drupal_render($elements[$key]);
+ $elements['#children'] .= drupal_render($elements[$key], TRUE);
}
}
@@ -3948,17 +3970,40 @@ function drupal_render(&$elements) {
}
}
+ // We store the resulting output in $elements['#markup'], to be consistent
+ // with how render cached output gets stored. This ensures that
+ // #post_render_cache callbacks get the same data to work with, no matter if
+ // #cache is disabled, #cache is enabled, there is a cache hit or miss.
$prefix = isset($elements['#prefix']) ? $elements['#prefix'] : '';
$suffix = isset($elements['#suffix']) ? $elements['#suffix'] : '';
- $output = $prefix . $elements['#children'] . $suffix;
+ $elements['#markup'] = $prefix . $elements['#children'] . $suffix;
// Cache the processed element if #cache is set.
if (isset($elements['#cache'])) {
- drupal_render_cache_set($output, $elements);
+ // Collect all #post_render_cache callbacks associated with this element.
+ $post_render_cache = drupal_render_collect_post_render_cache($elements);
+ if ($post_render_cache) {
+ $elements['#post_render_cache'] = $post_render_cache;
+ }
+
+ drupal_render_cache_set($elements['#markup'], $elements);
+ }
+
+ // Only when we're not in a recursive drupal_render() call,
+ // #post_render_cache callbacks must be executed, to prevent breaking the
+ // render cache in case of nested elements with #cache set.
+ //
+ // By running them here, we ensure that:
+ // - they run when #cache is disabled,
+ // - they run when #cache is enabled and there is a cache miss.
+ // Only the case of a cache hit when #cache is enabled, is not handled here,
+ // that is handled earlier in drupal_render().
+ if (!$is_recursive_call) {
+ _drupal_render_process_post_render_cache($elements);
}
$elements['#printed'] = TRUE;
- return $output;
+ return $elements['#markup'];
}
/**
@@ -4074,32 +4119,33 @@ function show(&$element) {
}
/**
- * Gets the rendered output of a renderable element from the cache.
+ * Gets the cached, prerendered element of a renderable element from the cache.
*
- * @param $elements
+ * @param array $elements
* A renderable array.
*
- * @return
- * A markup string containing the rendered content of the element, or FALSE
- * if no cached copy of the element is available.
+ * @return array
+ * A renderable array, with the original element and all its children pre-
+ * rendered, or FALSE if no cached copy of the element is available.
*
* @see drupal_render()
* @see drupal_render_cache_set()
*/
-function drupal_render_cache_get($elements) {
+function drupal_render_cache_get(array $elements) {
if (!\Drupal::request()->isMethodSafe() || !$cid = drupal_render_cid_create($elements)) {
return FALSE;
}
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache';
if (!empty($cid) && $cache = cache($bin)->get($cid)) {
+ $cached_element = $cache->data;
// Add additional libraries, JavaScript, CSS and other data attached
// to this element.
- if (isset($cache->data['#attached'])) {
- drupal_process_attached($cache->data);
+ if (isset($cached_element['#attached'])) {
+ drupal_process_attached($cached_element);
}
- // Return the rendered output.
- return $cache->data['#markup'];
+ // Return the cached element.
+ return $cached_element;
}
return FALSE;
}
@@ -4112,12 +4158,12 @@ function drupal_render_cache_get($elements) {
*
* @param $markup
* The rendered output string of $elements.
- * @param $elements
+ * @param array $elements
* A renderable array.
*
* @see drupal_render_cache_get()
*/
-function drupal_render_cache_set(&$markup, $elements) {
+function drupal_render_cache_set(&$markup, array $elements) {
// Create the cache ID for the element.
if (!\Drupal::request()->isMethodSafe() || !$cid = drupal_render_cid_create($elements)) {
return FALSE;
@@ -4129,12 +4175,19 @@ function drupal_render_cache_set(&$markup, $elements) {
// $data['#real-value']) and return an include command instead. When the
// ESI command is executed by the content accelerator, the real value can
// be retrieved and used.
- $data['#markup'] = &$markup;
+ $data['#markup'] = $markup;
+
// Persist attached data associated with this element.
$attached = drupal_render_collect_attached($elements, TRUE);
if ($attached) {
$data['#attached'] = $attached;
}
+
+ // Persist #post_render_cache callbacks associated with this element.
+ if (isset($elements['#post_render_cache'])) {
+ $data['#post_render_cache'] = $elements['#post_render_cache'];
+ }
+
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache';
$expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : CacheBackendInterface::CACHE_PERMANENT;
$tags = drupal_render_collect_cache_tags($elements);
@@ -4142,6 +4195,175 @@ function drupal_render_cache_set(&$markup, $elements) {
}
/**
+ * 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.
+ *
+ * @param callable $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.
+ * @param string $token
+ * A unique token to uniquely identify the placeholder.
+ *
+ * @see drupal_render_cache_get()
+ */
+function drupal_render_cache_generate_placeholder($callback, array $context, $token) {
+ // Serialize the context into a HTML attribute; unserializing is unnecessary.
+ $context_attribute = '';
+ foreach ($context as $key => $value) {
+ $context_attribute .= $key . ':' . $value . ';';
+ }
+ return '<drupal:render-cache-placeholder callback="' . $callback . '" context="' . $context_attribute . '" token="'. $token . '" />';;
+}
+
+/**
+ * Pre-render callback: Renders a render cache placeholder into #markup.
+ *
+ * @param $elements
+ * A structured array whose keys form the arguments to l():
+ * - #callback: The #post_render_cache callback that will replace the
+ * placeholder with its eventual markup.
+ * - #context: An array providing context for the #post_render_cache callback.
+ *
+ * @return
+ * The passed-in element containing a render cache placeholder in '#markup'
+ * and a callback with context, keyed by a generated unique token in
+ * '#post_render_cache'.
+ *
+ * @see drupal_render_cache_generate_placeholder()
+ */
+function drupal_pre_render_render_cache_placeholder($element) {
+ $callback = $element['#callback'];
+ if (!is_callable($callback)) {
+ throw new Exception(t('#callback must be a callable function.'));
+ }
+ $context = $element['#context'];
+ if (!is_array($context)) {
+ throw new Exception(t('#context must be an array.'));
+ }
+ $token = \Drupal\Component\Utility\Crypt::randomStringHashed(55);
+
+ // Generate placeholder markup and store #post_render_cache callback.
+ $element['#markup'] = drupal_render_cache_generate_placeholder($callback, $context, $token);
+ $element['#post_render_cache'][$callback][$token] = $context;
+
+ return $element;
+}
+
+/**
+ * Processes #post_render_cache callbacks.
+ *
+ * #post_render_cache callbacks may modify:
+ * - #markup: to replace placeholders
+ * - #attached: to add libraries or JavaScript settings
+ *
+ * Note that in either of these cases, #post_render_cache callbacks are
+ * implicitly idempotent: a placeholder that has been replaced can't be replaced
+ * again, and duplicate attachments are ignored.
+ *
+ * @param array &$elements
+ * The structured array describing the data being rendered.
+ *
+ * @see drupal_render()
+ * @see drupal_render_collect_post_render_cache
+ */
+function _drupal_render_process_post_render_cache(array &$elements) {
+ if (isset($elements['#post_render_cache'])) {
+ // Call all #post_render_cache callbacks, while passing the provided context
+ // and if keyed by a number, no token is passed, otherwise, the token string
+ // is passed to the callback as well. This token is used to uniquely
+ // identify the placeholder in the markup.
+ $modified_elements = $elements;
+ foreach ($elements['#post_render_cache'] as $callback => $options) {
+ foreach ($elements['#post_render_cache'][$callback] as $token => $context) {
+ // The advanced option, when setting #post_render_cache directly.
+ if (is_numeric($token)) {
+ $modified_elements = call_user_func_array($callback, array($modified_elements, $context));
+ }
+ // The simple option, when using the standard placeholders, and hence
+ // also when using #type => render_cache_placeholder.
+ else {
+ // Call #post_render_cache callback to generate the element that will
+ // fill in the placeholder.
+ $generated_element = call_user_func_array($callback, array($context));
+
+ // Update #attached based on the generated element.
+ if (isset($generated_element['#attached'])) {
+ if (!isset($modified_elements['#attached'])) {
+ $modified_elements['#attached'] = array();
+ }
+ $modified_elements['#attached'] = drupal_merge_attached($modified_elements['#attached'], drupal_render_collect_attached($generated_element, TRUE));
+ }
+
+ // Replace the placeholder with the rendered markup of the generated
+ // element.
+ $placeholder = drupal_render_cache_generate_placeholder($callback, $context, $token);
+ $modified_elements['#markup'] = str_replace($placeholder, drupal_render($generated_element), $modified_elements['#markup']);
+ }
+ }
+ }
+ // Only retain changes to the #markup and #attached properties, as would be
+ // the case when the render cache was actually being used.
+ $elements['#markup'] = $modified_elements['#markup'];
+ if (isset($modified_elements['#attached'])) {
+ $elements['#attached'] = $modified_elements['#attached'];
+ }
+
+ // Make sure that any attachments added in #post_render_cache callbacks are
+ // also executed.
+ if (isset($elements['#attached'])) {
+ drupal_process_attached($elements);
+ }
+ }
+}
+
+/**
+ * Collects #post_render_cache for an element and its children into a single
+ * array.
+ *
+ * When caching elements, it is necessary to collect all #post_render_cache
+ * callbacks into a single array, from both the element itself and all child
+ * elements. This allows drupal_render() to execute all of them when the element
+ * is retrieved from the render cache.
+ *
+ * @param array $elements
+ * The element to collect #post_render_cache from.
+ * @param array $callbacks
+ * Internal use only. The #post_render_callbacks array so far.
+ * @param bool $is_root_element
+ * Internal use only. Whether the element being processed is the root or not.
+ *
+ * @return
+ * The #post_render_cache array for this element and its descendants.
+ *
+ * @see drupal_render()
+ * @see _drupal_render_process_post_render_cache()
+ */
+function drupal_render_collect_post_render_cache(array $elements, array $callbacks = array(), $is_root_element = TRUE) {
+ // Collect all #post_render_cache for this element.
+ if (isset($elements['#post_render_cache'])) {
+ $callbacks = NestedArray::mergeDeep($callbacks, $elements['#post_render_cache']);
+ }
+
+ // Child elements that have #cache set will already have collected all their
+ // children's #post_render_cache callbacks, so no need to traverse further.
+ if (!$is_root_element && isset($elements['#cache'])) {
+ return $callbacks;
+ }
+ else if ($children = element_children($elements)) {
+ foreach ($children as $child) {
+ $callbacks = drupal_render_collect_post_render_cache($elements[$child], $callbacks, FALSE);
+ }
+ }
+
+ return $callbacks;
+}
+
+/**
* Collects #attached for an element and its children into a single array.
*
* When caching elements, it is necessary to collect all libraries, JavaScript
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
index 310a46d..f3985e0 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
@@ -363,7 +363,8 @@ class RenderTest extends DrupalUnitTestBase {
// Load the element from cache and verify the presence of the #attached
// JavaScript.
drupal_static_reset('drupal_add_js');
- $this->assertTrue(drupal_render_cache_get($element), 'The element was retrieved from cache.');
+ $element = array('#cache' => array('keys' => array('simpletest', 'drupal_render', 'children_attached')));
+ $this->assertTrue(strlen(drupal_render($element)) > 0, 'The element was retrieved from cache.');
$scripts = drupal_get_js();
$this->assertTrue(strpos($scripts, $parent_js), 'The element #attached JavaScript was included when loading from cache.');
$this->assertTrue(strpos($scripts, $child_js), 'The child #attached JavaScript was included when loading from cache.');
@@ -439,4 +440,366 @@ class RenderTest extends DrupalUnitTestBase {
// Restore the previous request method.
\Drupal::request()->setMethod($request_method);
}
+
+ /**
+ * Tests post-render cache callbacks functionality.
+ */
+ function testDrupalRenderPostRenderCache() {
+ $context = array('foo' => $this->randomString());
+ $test_element = array();
+ $test_element['#markup'] = '';
+ $test_element['#attached']['js'][] = array('type' => 'setting', 'data' => array('foo' => 'bar'));
+ $test_element['#post_render_cache']['common_test_post_render_cache'] = array(
+ $context
+ );
+
+ // #cache disabled.
+ drupal_static_reset('drupal_add_js');
+ $element = $test_element;
+ $element['#markup'] = '<p>#cache disabled</p>';
+ $output = drupal_render($element);
+ $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+ $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+ $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+ $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+ // The cache system is turned off for POST requests.
+ $request_method = \Drupal::request()->getMethod();
+ \Drupal::request()->setMethod('GET');
+
+ // GET request: #cache enabled, cache miss.
+ drupal_static_reset('drupal_add_js');
+ $element = $test_element;
+ $element['#cache'] = array('cid' => 'post_render_cache_test_GET');
+ $element['#markup'] = '<p>#cache enabled, GET</p>';
+ $output = drupal_render($element);
+ $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+ $this->assertTrue(isset($element['#printed']), 'No cache hit');
+ $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+ $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+ $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+ // GET request: validate cached data.
+ $element = array('#cache' => array('cid' => 'post_render_cache_test_GET'));
+ $cached_element = cache()->get(drupal_render_cid_create($element))->data;
+ $expected_element = array(
+ '#markup' => '<p>#cache enabled, GET</p>',
+ '#attached' => $test_element['#attached'],
+ '#post_render_cache' => $test_element['#post_render_cache'],
+ );
+ $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+
+ // GET request: #cache enabled, cache hit.
+ drupal_static_reset('drupal_add_js');
+ $element['#cache'] = array('cid' => 'post_render_cache_test_GET');
+ $element['#markup'] = '<p>#cache enabled, GET</p>';
+ $output = drupal_render($element);
+ $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+ $this->assertFalse(isset($element['#printed']), 'Cache hit');
+ $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+ $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+ $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+ // Verify behavior when handling a non-GET request, e.g. a POST request:
+ // also in that case, #post_render_cache callbacks must be called.
+ \Drupal::request()->setMethod('POST');
+
+ // POST request: #cache enabled, cache miss.
+ drupal_static_reset('drupal_add_js');
+ $element = $test_element;
+ $element['#cache'] = array('cid' => 'post_render_cache_test_POST');
+ $element['#markup'] = '<p>#cache enabled, POST</p>';
+ $output = drupal_render($element);
+ $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+ $this->assertTrue(isset($element['#printed']), 'No cache hit');
+ $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+ $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+ $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+ // POST request: Ensure no data was cached.
+ $element = array('#cache' => array('cid' => 'post_render_cache_test_POST'));
+ $cached_element = cache()->get(drupal_render_cid_create($element));
+ $this->assertFalse($cached_element, 'No data is cached because this is a POST request.');
+
+ // Restore the previous request method.
+ \Drupal::request()->setMethod($request_method);
+ }
+
+ /**
+ * Tests post-render cache callbacks functionality in children elements.
+ */
+ function testDrupalRenderChildrenPostRenderCache() {
+ // The cache system is turned off for POST requests.
+ $request_method = \Drupal::request()->getMethod();
+ \Drupal::request()->setMethod('GET');
+
+ // Test case 1.
+ // Create an element with a child and subchild. Each element has the same
+ // #post_render_cache callback, but with different contexts.
+ drupal_static_reset('drupal_add_js');
+ $context_1 = array('foo' => $this->randomString());
+ $context_2 = array('bar' => $this->randomString());
+ $context_3 = array('baz' => $this->randomString());
+ $test_element = array(
+ '#type' => 'details',
+ '#cache' => array(
+ 'keys' => array('simpletest', 'drupal_render', 'children_post_render_cache'),
+ ),
+ '#post_render_cache' => array(
+ 'common_test_post_render_cache' => array($context_1)
+ ),
+ '#title' => 'Parent',
+ '#attached' => array(
+ 'js' => array(
+ array('type' => 'setting', 'data' => array('foo' => 'bar'))
+ ),
+ ),
+ );
+ $test_element['child'] = array(
+ '#type' => 'details',
+ '#post_render_cache' => array(
+ 'common_test_post_render_cache' => array($context_2)
+ ),
+ '#title' => 'Child',
+ );
+ $test_element['child']['subchild'] = array(
+ '#post_render_cache' => array(
+ 'common_test_post_render_cache' => array($context_3)
+ ),
+ '#markup' => 'Subchild',
+ );
+ $element = $test_element;
+ $output = drupal_render($element);
+ $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+ $this->assertTrue(isset($element['#printed']), 'No cache hit');
+ $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+ $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $expected_settings = $context_1 + $context_2 + $context_3;
+ $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+ $this->assertIdentical($settings['common_test'], $expected_settings, '#attached is modified; JavaScript settings for each #post_render_cache callback are added to page.');
+
+ // GET request: validate cached data.
+ $element = array('#cache' => $element['#cache']);
+ $cached_element = cache()->get(drupal_render_cid_create($element))->data;
+ $expected_element = array(
+ '#markup' => '<details class="form-wrapper" open="open"><summary role="button" aria-expanded>Parent</summary><div class="details-wrapper"><details class="form-wrapper" open="open"><summary role="button" aria-expanded>Child</summary><div class="details-wrapper">Subchild</div></details>
+</div></details>
+',
+ '#attached' => array(
+ 'js' => array(
+ array('type' => 'setting', 'data' => array('foo' => 'bar'))
+ ),
+ 'library' => array(
+ array('system', 'drupal.collapse'),
+ array('system', 'drupal.collapse'),
+ ),
+ ),
+ '#post_render_cache' => array(
+ 'common_test_post_render_cache' => array(
+ $context_1,
+ $context_2,
+ $context_3,
+ )
+ ),
+ );
+ $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+
+ // GET request: #cache enabled, cache hit.
+ drupal_static_reset('drupal_add_js');
+ $element = $test_element;
+ $output = drupal_render($element);
+ $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+ $this->assertFalse(isset($element['#printed']), 'Cache hit');
+ $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+ $this->assertIdentical($settings['common_test'], $expected_settings, '#attached is modified; JavaScript settings for each #post_render_cache callback are added to page.');
+
+ // Test case 2.
+ // Create an element with a child and subchild. Each element has the same
+ // #post_render_cache callback, but with different contexts. Both the
+ // parent and the child elements have #cache set. The cached parent element
+ // must contain the pristine child element, i.e. unaffected by its
+ // #post_render_cache callbacks. I.e. the #post_render_cache callbacks may
+ // not yet have run, or otherwise the cached parent element would contain
+ // personalized data, thereby breaking the render cache.
+ drupal_static_reset('drupal_add_js');
+ $element = $test_element;
+ $element['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_parent');
+ $element['child']['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_child');
+ $output = drupal_render($element);
+ $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+ $this->assertTrue(isset($element['#printed']), 'No cache hit');
+ $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+ $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $expected_settings = $context_1 + $context_2 + $context_3;
+ $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+ $this->assertIdentical($settings['common_test'], $expected_settings, '#attached is modified; JavaScript settings for each #post_render_cache callback are added to page.');
+
+ // GET request: validate cached data for both the parent and child.
+ $element = $test_element;
+ $element['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_parent');
+ $element['child']['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_child');
+ $cached_parent_element = cache()->get(drupal_render_cid_create($element))->data;
+ $cached_child_element = cache()->get(drupal_render_cid_create($element['child']))->data;
+ $expected_parent_element = array(
+ '#markup' => '<details class="form-wrapper" open="open"><summary role="button" aria-expanded>Parent</summary><div class="details-wrapper"><details class="form-wrapper" open="open"><summary role="button" aria-expanded>Child</summary><div class="details-wrapper">Subchild</div></details>
+</div></details>
+',
+ '#attached' => array(
+ 'js' => array(
+ array('type' => 'setting', 'data' => array('foo' => 'bar'))
+ ),
+ 'library' => array(
+ array('system', 'drupal.collapse'),
+ array('system', 'drupal.collapse'),
+ ),
+ ),
+ '#post_render_cache' => array(
+ 'common_test_post_render_cache' => array(
+ $context_1,
+ $context_2,
+ $context_3,
+ )
+ ),
+ );
+ $this->assertIdentical($cached_parent_element, $expected_parent_element, 'The correct data is cached for the parent: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+ $expected_child_element = array(
+ '#markup' => '<details class="form-wrapper" open="open"><summary role="button" aria-expanded>Child</summary><div class="details-wrapper">Subchild</div></details>
+',
+ '#attached' => array(
+ 'library' => array(
+ array('system', 'drupal.collapse'),
+ ),
+ ),
+ '#post_render_cache' => array(
+ 'common_test_post_render_cache' => array(
+ $context_2,
+ $context_3,
+ )
+ ),
+ );
+ $this->assertIdentical($cached_child_element, $expected_child_element, 'The correct data is cached for the child: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+
+ // GET request: #cache enabled, cache hit, parent element.
+ drupal_static_reset('drupal_add_js');
+ $element = $test_element;
+ $element['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_parent');
+ $output = drupal_render($element);
+ $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+ $this->assertFalse(isset($element['#printed']), 'Cache hit');
+ $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+ $this->assertIdentical($settings['common_test'], $expected_settings, '#attached is modified; JavaScript settings for each #post_render_cache callback are added to page.');
+
+ // GET request: #cache enabled, cache hit, child element.
+ drupal_static_reset('drupal_add_js');
+ $element = $test_element;
+ $element['child']['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_child');
+ $element = $element['child'];
+ $output = drupal_render($element);
+ $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+ $this->assertFalse(isset($element['#printed']), 'Cache hit');
+ $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $expected_settings = $context_2 + $context_3;
+ $this->assertTrue(!isset($settings['foo']), 'Parent JavaScript setting is not added to the page.');
+ $this->assertIdentical($settings['common_test'], $expected_settings, '#attached is modified; JavaScript settings for each #post_render_cache callback are added to page.');
+
+ // Restore the previous request method.
+ \Drupal::request()->setMethod($request_method);
+ }
+
+
+ /**
+ * Tests post-render cache-integrated 'render_cache_placeholder' element.
+ */
+ function testDrupalRenderRenderCachePlaceholder() {
+ $context = array('bar' => $this->randomString());
+ $test_element = array(
+ '#type' => 'render_cache_placeholder',
+ '#context' => $context,
+ '#callback' => 'common_test_post_render_cache_placeholder',
+ '#prefix' => '<foo>',
+ '#suffix' => '</foo>'
+ );
+ $expected_output = '<foo><bar>' . $context['bar'] . '</bar></foo>';
+
+ // #cache disabled.
+ drupal_static_reset('drupal_add_js');
+ $element = $test_element;
+ $output = drupal_render($element);
+ $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+ // The cache system is turned off for POST requests.
+ $request_method = \Drupal::request()->getMethod();
+ \Drupal::request()->setMethod('GET');
+
+ // GET request: #cache enabled, cache miss.
+ drupal_static_reset('drupal_add_js');
+ $element = $test_element;
+ $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
+ $output = drupal_render($element);
+ $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
+ $this->assertTrue(isset($element['#printed']), 'No cache hit');
+ $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+ // GET request: validate cached data.
+ $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_GET'));
+ $cached_element = cache()->get(drupal_render_cid_create($element))->data;
+ // Parse unique token out of the markup.
+ $dom = filter_dom_load($cached_element['#markup']);
+ $xpath = new \DOMXPath($dom);
+ $nodes = $xpath->query('//*[@token]');
+ $token = $nodes->item(0)->getAttribute('token');
+ $expected_element = array(
+ '#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" context="bar:' . $context['bar'] .';" token="'. $token . '" /></foo>',
+ '#post_render_cache' => array(
+ 'common_test_post_render_cache_placeholder' => array(
+ $token => $context,
+ ),
+ ),
+ );
+ $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+
+ // GET request: #cache enabled, cache hit.
+ drupal_static_reset('drupal_add_js');
+ $element = $test_element;
+ $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
+ $output = drupal_render($element);
+ $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
+ $this->assertFalse(isset($element['#printed']), 'Cache hit');
+ $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
+ $settings = $this->parseDrupalSettings(drupal_get_js());
+ $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+ // Restore the previous request method.
+ \Drupal::request()->setMethod($request_method);
+ }
+
+ protected function parseDrupalSettings($html) {
+ $startToken = 'drupalSettings = ';
+ $endToken = '}';
+ $start = strpos($html, $startToken) + strlen($startToken);
+ $end = strrpos($html, $endToken);
+ $json = drupal_substr($html, $start, $end - $start + 1);
+ $parsed_settings = drupal_json_decode($json);
+ return $parsed_settings;
+ }
+
}
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 71f2860..40c3234 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -599,6 +599,13 @@ function system_element_info() {
'#theme' => 'table',
);
+ // Other elements.
+ $types['render_cache_placeholder'] = array(
+ '#callback' => '',
+ '#context' => array(),
+ '#pre_render' => array('drupal_pre_render_render_cache_placeholder'),
+ );
+
return $types;
}
diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module
index af93332..b3b8ad8 100644
--- a/core/modules/system/tests/modules/common_test/common_test.module
+++ b/core/modules/system/tests/modules/common_test/common_test.module
@@ -197,3 +197,63 @@ function common_test_library_info() {
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.
+ $element['#attached']['js'][] = array(
+ 'type' => 'setting',
+ 'data' => array(
+ 'common_test' => $context
+ ),
+ );
+
+ // Set new property.
+ $element['#context_test'] = $context;
+
+ return $element;
+}
+
+/**
+ * #post_render_cache callback; replaces placeholder, extends #attached.
+ *
+ * @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 $context) {
+ $element = array(
+ '#markup' => '<bar>' . $context['bar'] . '</bar>',
+ '#attached' => array(
+ 'js' => array(
+ array(
+ 'type' => 'setting',
+ 'data' => array(
+ 'common_test' => $context,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ return $element;
+}