Commit 45092042 authored by catch's avatar catch

Issue #2478483 by Wim Leers, Fabianx: Introduce placeholders (#lazy_builder)...

Issue #2478483 by Wim Leers, Fabianx: Introduce placeholders (#lazy_builder) to replace #post_render_cache
parent c219ec8f
...@@ -1191,33 +1191,6 @@ function show(&$element) { ...@@ -1191,33 +1191,6 @@ function show(&$element) {
return $element; return $element;
} }
/**
* 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 \Exception
*
* @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) {
return \Drupal::service('renderer')->generateCachePlaceholder($callback, $context);
}
/** /**
* Retrieves the default properties for the defined element type. * Retrieves the default properties for the defined element type.
* *
......
...@@ -24,13 +24,6 @@ class BubbleableMetadata extends CacheableMetadata { ...@@ -24,13 +24,6 @@ class BubbleableMetadata extends CacheableMetadata {
*/ */
protected $attached = []; protected $attached = [];
/**
* #post_render_cache metadata.
*
* @var array[]
*/
protected $postRenderCache = [];
/** /**
* Merges the values of another bubbleable metadata object with this one. * Merges the values of another bubbleable metadata object with this one.
* *
...@@ -44,7 +37,6 @@ public function merge(CacheableMetadata $other) { ...@@ -44,7 +37,6 @@ public function merge(CacheableMetadata $other) {
$result = parent::merge($other); $result = parent::merge($other);
if ($other instanceof BubbleableMetadata) { if ($other instanceof BubbleableMetadata) {
$result->attached = \Drupal::service('renderer')->mergeAttachments($this->attached, $other->attached); $result->attached = \Drupal::service('renderer')->mergeAttachments($this->attached, $other->attached);
$result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache);
} }
return $result; return $result;
} }
...@@ -58,7 +50,6 @@ public function merge(CacheableMetadata $other) { ...@@ -58,7 +50,6 @@ public function merge(CacheableMetadata $other) {
public function applyTo(array &$build) { public function applyTo(array &$build) {
parent::applyTo($build); parent::applyTo($build);
$build['#attached'] = $this->attached; $build['#attached'] = $this->attached;
$build['#post_render_cache'] = $this->postRenderCache;
} }
/** /**
...@@ -72,82 +63,82 @@ public function applyTo(array &$build) { ...@@ -72,82 +63,82 @@ public function applyTo(array &$build) {
public static function createFromRenderArray(array $build) { public static function createFromRenderArray(array $build) {
$meta = parent::createFromRenderArray($build); $meta = parent::createFromRenderArray($build);
$meta->attached = (isset($build['#attached'])) ? $build['#attached'] : []; $meta->attached = (isset($build['#attached'])) ? $build['#attached'] : [];
$meta->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : [];
return $meta; return $meta;
} }
/** /**
* Gets assets. * Gets attachments.
* *
* @return array * @return array
* The attachments
*/ */
public function getAssets() { public function getAttachments() {
return $this->attached; return $this->attached;
} }
/** /**
* Adds assets. * Adds attachments.
* *
* @param array $assets * @param array $attachments
* The associated assets to be attached. * The attachments to add.
* *
* @return $this * @return $this
*/ */
public function addAssets(array $assets) { public function addAttachments(array $attachments) {
$this->attached = NestedArray::mergeDeep($this->attached, $assets); $this->attached = \Drupal::service('renderer')->mergeAttachments($this->attached, $attachments);
return $this; return $this;
} }
/** /**
* Sets assets. * Sets attachments.
* *
* @param array $assets * @param array $attachments
* The associated assets to be attached. * The attachments to set.
* *
* @return $this * @return $this
*/ */
public function setAssets(array $assets) { public function setAttachments(array $attachments) {
$this->attached = $assets; $this->attached = $attachments;
return $this; return $this;
} }
/** /**
* Gets #post_render_cache callbacks. * Gets assets.
* *
* @return array * @return array
*
* @deprecated Use ::getAttachments() instead. To be removed before Drupal 8.0.0.
*/ */
public function getPostRenderCacheCallbacks() { public function getAssets() {
return $this->postRenderCache; return $this->attached;
} }
/** /**
* Adds #post_render_cache callbacks. * Adds assets.
*
* @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.
* *
* @see \Drupal\Core\Render\RendererInterface::generateCachePlaceholder() * @param array $assets
* The associated assets to be attached.
* *
* @return $this * @return $this
*
* @deprecated Use ::addAttachments() instead. To be removed before Drupal 8.0.0.
*/ */
public function addPostRenderCacheCallback($callback, array $context) { public function addAssets(array $assets) {
$this->postRenderCache[$callback][] = $context; return $this->addAttachments($assets);
return $this;
} }
/** /**
* Sets #post_render_cache callbacks. * Sets assets.
* *
* @param array $post_render_cache_callbacks * @param array $assets
* The associated #post_render_cache callbacks to be executed. * The associated assets to be attached.
* *
* @return $this * @return $this
*
* @deprecated Use ::setAttachments() instead. To be removed before Drupal 8.0.0.
*/ */
public function setPostRenderCacheCallbacks(array $post_render_cache_callbacks) { public function setAssets(array $assets) {
$this->postRenderCache = $post_render_cache_callbacks; $this->attached = $assets;
return $this; return $this;
} }
......
...@@ -7,9 +7,6 @@ ...@@ -7,9 +7,6 @@
namespace Drupal\Core\Render\Element; namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Site\Settings;
/** /**
* Provides a messages element. * Provides a messages element.
* *
...@@ -37,17 +34,6 @@ public function getInfo() { ...@@ -37,17 +34,6 @@ public function getInfo() {
/** /**
* #pre_render callback to generate a placeholder. * #pre_render callback to generate a placeholder.
* *
* Ensures the same token is used for all instances, hence resulting in the
* same placeholder for all places rendering the status messages for this
* request (e.g. in multiple blocks). This ensures we can put the rendered
* messages in all placeholders in one go.
* Also ensures the same context key is used for the #post_render_cache
* property, this ensures that if status messages are rendered multiple times,
* their individual (but identical!) #post_render_cache properties are merged,
* ensuring the callback is only invoked once.
*
* @see ::renderMessages()
*
* @param array $element * @param array $element
* A renderable array. * A renderable array.
* *
...@@ -55,82 +41,42 @@ public function getInfo() { ...@@ -55,82 +41,42 @@ public function getInfo() {
* The updated renderable array containing the placeholder. * The updated renderable array containing the placeholder.
*/ */
public static function generatePlaceholder(array $element) { public static function generatePlaceholder(array $element) {
$plugin_id = 'status_messages'; $element['messages_placeholder'] = [
'#lazy_builder' => [get_class() . '::renderMessages', [$element['#display']]],
$callback = get_class() . '::renderMessages'; '#create_placeholder' => TRUE,
try {
$hash_salt = Settings::getHashSalt();
}
catch (\RuntimeException $e) {
// Status messages are also shown during the installer, at which time no
// hash salt is defined yet.
$hash_salt = Crypt::randomBytes(8);
}
$key = $plugin_id . $element['#display'];
$context = [
'display' => $element['#display'],
'token' => Crypt::hmacBase64($key, $hash_salt),
];
$placeholder = static::renderer()->generateCachePlaceholder($callback, $context);
$element['#post_render_cache'] = [
$callback => [
$key => $context,
],
]; ];
$element['#markup'] = $placeholder;
return $element; return $element;
} }
/** /**
* #post_render_cache callback; replaces placeholder with messages. * #lazy_builder callback; replaces placeholder with messages.
*
* Note: this is designed to replace all #post_render_cache placeholders for
* messages in a single #post_render_cache callback; hence all placeholders
* must be identical.
* *
* @see ::getInfo() * @param string|null $type
* * Limit the messages returned by type. Defaults to NULL, meaning all types.
* @param array $element * Passed on to drupal_get_messages(). These values are supported:
* The renderable array that contains the to be replaced placeholder. * - NULL
* @param array $context * - 'status'
* An array with any context information. * - 'warning'
* - 'error'
* *
* @return array * @return array
* A renderable array containing the messages. * A renderable array containing the messages.
*
* @see drupal_get_messages()
*/ */
public static function renderMessages(array $element, array $context) { public static function renderMessages($type) {
$renderer = static::renderer();
// Render the messages. // Render the messages.
$messages = [ return [
'#theme' => 'status_messages', '#theme' => 'status_messages',
// @todo Improve when https://www.drupal.org/node/2278383 lands. // @todo Improve when https://www.drupal.org/node/2278383 lands.
'#message_list' => drupal_get_messages($context['display']), '#message_list' => drupal_get_messages($type),
'#status_headings' => [ '#status_headings' => [
'status' => t('Status message'), 'status' => t('Status message'),
'error' => t('Error message'), 'error' => t('Error message'),
'warning' => t('Warning message'), 'warning' => t('Warning message'),
], ],
]; ];
$markup = $renderer->render($messages);
// Replace the placeholder.
$callback = get_class() . '::renderMessages';
$placeholder = $renderer->generateCachePlaceholder($callback, $context);
$element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
$element = $renderer->mergeBubbleableMetadata($element, $messages);
return $element;
}
/**
* Wraps the renderer.
*
* @return \Drupal\Core\Render\RendererInterface
*/
protected static function renderer() {
return \Drupal::service('renderer');
} }
} }
...@@ -121,12 +121,11 @@ public function renderResponse(array $main_content, Request $request, RouteMatch ...@@ -121,12 +121,11 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
// The three parts of rendered markup in html.html.twig (page_top, page and // The three parts of rendered markup in html.html.twig (page_top, page and
// page_bottom) must be rendered with drupal_render_root(), so that their // page_bottom) must be rendered with drupal_render_root(), so that their
// #post_render_cache callbacks are executed (which may attach additional // placeholders are replaced (which may attach additional assets).
// assets).
// html.html.twig must be able to render the final list of attached assets, // html.html.twig must be able to render the final list of attached assets,
// and hence may not execute any #post_render_cache_callbacks (because they // and hence may not replace any placeholders (because they might add yet
// might add yet more assets to be attached), and therefore it must be // more assets to be attached), and therefore it must be rendered with
// rendered with drupal_render(), not drupal_render_root(). // drupal_render(), not drupal_render_root().
$this->renderer->render($html['page'], TRUE); $this->renderer->render($html['page'], TRUE);
if (isset($html['page_top'])) { if (isset($html['page_top'])) {
$this->renderer->render($html['page_top'], TRUE); $this->renderer->render($html['page_top'], TRUE);
...@@ -193,8 +192,8 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte ...@@ -193,8 +192,8 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
// We must render the main content now already, because it might provide a // We must render the main content now already, because it might provide a
// title. We set its $is_root_call parameter to FALSE, to ensure // title. We set its $is_root_call parameter to FALSE, to ensure
// #post_render_cache callbacks are not yet applied. This is essentially // placeholders are not yet replaced. This is essentially "pre-rendering"
// "pre-rendering" the main content, the "full rendering" will happen in // the main content, the "full rendering" will happen in
// ::renderResponse(). // ::renderResponse().
// @todo Remove this once https://www.drupal.org/node/2359901 lands. // @todo Remove this once https://www.drupal.org/node/2359901 lands.
if (!empty($main_content)) { if (!empty($main_content)) {
...@@ -260,15 +259,15 @@ public function invokePageAttachmentHooks(array &$page) { ...@@ -260,15 +259,15 @@ public function invokePageAttachmentHooks(array &$page) {
$function = $module . '_page_attachments'; $function = $module . '_page_attachments';
$function($attachments); $function($attachments);
} }
if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache', '#cache']) !== []) { if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
throw new \LogicException('Only #attached, #post_render_cache and #cache may be set in hook_page_attachments().'); throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments().');
} }
// Modules and themes can alter page attachments. // Modules and themes can alter page attachments.
$this->moduleHandler->alter('page_attachments', $attachments); $this->moduleHandler->alter('page_attachments', $attachments);
\Drupal::theme()->alter('page_attachments', $attachments); \Drupal::theme()->alter('page_attachments', $attachments);
if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache', '#cache']) !== []) { if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
throw new \LogicException('Only #attached, #post_render_cache and #cache may be set in hook_page_attachments_alter().'); throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments_alter().');
} }
// Merge the attachments onto the $page render array. // Merge the attachments onto the $page render array.
......
...@@ -300,7 +300,6 @@ public function getCacheableRenderArray(array $elements) { ...@@ -300,7 +300,6 @@ public function getCacheableRenderArray(array $elements) {
$data = [ $data = [
'#markup' => $elements['#markup'], '#markup' => $elements['#markup'],
'#attached' => $elements['#attached'], '#attached' => $elements['#attached'],
'#post_render_cache' => $elements['#post_render_cache'],
'#cache' => [ '#cache' => [
'contexts' => $elements['#cache']['contexts'], 'contexts' => $elements['#cache']['contexts'],
'tags' => $elements['#cache']['tags'], 'tags' => $elements['#cache']['tags'],
......
This diff is collapsed.
...@@ -15,8 +15,7 @@ interface RendererInterface { ...@@ -15,8 +15,7 @@ interface RendererInterface {
/** /**
* Renders final HTML given a structured array tree. * Renders final HTML given a structured array tree.
* *
* Calls ::render() in such a way that #post_render_cache callbacks are * Calls ::render() in such a way that placeholders are replaced.
* applied.
* *
* Should therefore only be used in occasions where the final rendering is * Should therefore only be used in occasions where the final rendering is
* happening, just before sending a Response: * happening, just before sending a Response:
...@@ -36,8 +35,7 @@ public function renderRoot(&$elements); ...@@ -36,8 +35,7 @@ public function renderRoot(&$elements);
/** /**
* Renders final HTML in situations where no assets are needed. * Renders final HTML in situations where no assets are needed.
* *
* Calls ::render() in such a way that #post_render_cache callbacks are * Calls ::render() in such a way that placeholders are replaced.
* applied.
* *
* Useful for e.g. rendering the values of tokens or e-mails, which need a * Useful for e.g. rendering the values of tokens or e-mails, which need a
* render array being turned into a string, but don't need any of the * render array being turned into a string, but don't need any of the
...@@ -88,8 +86,8 @@ public function renderPlain(&$elements); ...@@ -88,8 +86,8 @@ public function renderPlain(&$elements);
* retrieval. * retrieval.
* - Cache tags, so that cached renderings are invalidated when site content * - Cache tags, so that cached renderings are invalidated when site content
* or configuration that can affect that rendering changes. * or configuration that can affect that rendering changes.
* - #post_render_cache callbacks, for executing code to handle dynamic * - Placeholders, with associated self-contained placeholder render arrays,
* requirements that cannot be cached. * for executing code to handle dynamic requirements that cannot be cached.
* A stack of \Drupal\Core\Render\BubbleableMetadata objects can be used to * A stack of \Drupal\Core\Render\BubbleableMetadata objects can be used to
* perform this bubbling. * perform this bubbling.
* *
...@@ -148,15 +146,31 @@ public function renderPlain(&$elements); ...@@ -148,15 +146,31 @@ public function renderPlain(&$elements);
* process render arrays and call the element info service before passing * process render arrays and call the element info service before passing
* the array to Renderer::render(), such as form_builder() in the Form * the array to Renderer::render(), such as form_builder() in the Form
* API. * API.
* - If this element has an array of #pre_render functions defined, they are * - If this element has #create_placeholder set to TRUE, and it has a
* called sequentially to modify the element before rendering. After all * #lazy_builder callback, then the element is replaced with another
* the #pre_render functions have been called, #printed is checked a * element that has only two properties: #markup and #attached. #markup
* second time in case a #pre_render function flags the element as * will contain placeholder markup, and #attached contains the placeholder
* printed. If #printed is set, we return early and hence no rendering * metadata, that will be used for replacing this placeholder. That
* work is left to be done, similarly to a render cache hit. Once again, * metadata contains a very compact render array (containing only
* the empty (and topmost) frame that was just pushed onto the stack is * #lazy_builder and #cache) that will be rendered to replace the
* updated with all bubbleable rendering metadata from the element whose * placeholder with its final markup. This means that when the
* #printed = TRUE. * #lazy_builder callback is called, it received a render array to add to
* that only contains #cache.
* - If this element has a #lazy_builder or an array of #pre_render
* functions defined, they are called sequentially to modify the element
* before rendering. #lazy_builder is preferred, since it allows for
* placeholdering (see previous step), but #pre_render is still supported.
* Both have their use case: #lazy_builder is for building a render array,
* #pre_render is for decorating an existing render array.
* After the #lazy_builder function is called, #lazy_builder is removed,
* and #built is set to TRUE.
* After the #lazy_builder and all #pre_render functions have been called,
* #printed is checked a second time in case a #lazy_builder or
* #pre_render function flags the element as printed. If #printed is set,
* we return early and hence no rendering work is left to be done,
* similarly to a render cache hit. Once again, the empty (and topmost)
* frame that was just pushed onto the stack is updated with all
* bubbleable rendering metadata from the element whose #printed = TRUE.
* Then, this stack frame is bubbled: the two topmost frames are popped * Then, this stack frame is bubbled: the two topmost frames are popped
* from the stack, they are merged, and the result is pushed back onto the * from the stack, they are merged, and the result is pushed back onto the
* stack. * stack.
...@@ -253,25 +267,14 @@ public function renderPlain(&$elements); ...@@ -253,25 +267,14 @@ public function renderPlain(&$elements);
* assumes only children's individual markup is relevant and ignores the * assumes only children's individual markup is relevant and ignores the
* parent markup. This approach is normally not needed and should be * parent markup. This approach is normally not needed and should be
* adopted only when dealing with very advanced use cases. * adopted only when dealing with very advanced use cases.
* - If this element has an array of #post_render_cache functions defined, * - If this element has attached placeholders ([#attached][placeholders]),
* or any of its children has (which we would know thanks to the stack * or any of its children has (which we would know thanks to the stack
* having been updated just before the render caching step), they are * having been updated just before the render caching step), its
* called sequentially to replace placeholders in the final #markup and * placeholder element containing a #lazy_builder function is rendered in
* extend #attached. Placeholders must contain a unique token, to * isolation. The resulting markup is used to replace the placeholder, and
* guarantee that e.g. samples of placeholders are not replaced also. But, * any bubbleable metadata is merged.
* since #post_render_cache callbacks add attach additional assets, the * Placeholders must be unique, to guarantee that e.g. samples of
* correct bubbling of those must once again be taken into account. This * placeholders are not replaced as well.
* final stage of rendering should be considered as if it were the parent
* of the current element, because it takes that as its input, and then
* alters its #markup. Hence, just before calling the #post_render_cache
* callbacks, a new empty frame is pushed onto the stack, where all assets
* #attached during the execution of those callbacks will end up in. Then,
* after the execution of those callbacks, we merge that back into the
* element. 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.
* - Just before finishing the rendering of this element, this element's * - Just before finishing the rendering of this element, this element's
* stack frame (the topmost one) is bubbled: the two topmost frames are * stack frame (the topmost one) is bubbled: the two topmost frames are
* popped from the stack, they are merged and the result is pushed back * popped from the stack, they are merged and the result is pushed back
...@@ -390,27 +393,4 @@ public function addCacheableDependency(array &$elements, $dependency); ...@@ -390,27 +393,4 @@ public function addCacheableDependency(array &$elements, $dependency);
*/ */
public function mergeAttachments(array $a, array $b); public 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);
} }