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) {
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.
*
......
......@@ -24,13 +24,6 @@ class BubbleableMetadata extends CacheableMetadata {
*/
protected $attached = [];
/**
* #post_render_cache metadata.
*
* @var array[]
*/
protected $postRenderCache = [];
/**
* Merges the values of another bubbleable metadata object with this one.
*
......@@ -44,7 +37,6 @@ public function merge(CacheableMetadata $other) {
$result = parent::merge($other);
if ($other instanceof BubbleableMetadata) {
$result->attached = \Drupal::service('renderer')->mergeAttachments($this->attached, $other->attached);
$result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache);
}
return $result;
}
......@@ -58,7 +50,6 @@ public function merge(CacheableMetadata $other) {
public function applyTo(array &$build) {
parent::applyTo($build);
$build['#attached'] = $this->attached;
$build['#post_render_cache'] = $this->postRenderCache;
}
/**
......@@ -72,82 +63,82 @@ public function applyTo(array &$build) {
public static function createFromRenderArray(array $build) {
$meta = parent::createFromRenderArray($build);
$meta->attached = (isset($build['#attached'])) ? $build['#attached'] : [];
$meta->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : [];
return $meta;
}
/**
* Gets assets.
* Gets attachments.
*
* @return array
* The attachments
*/
public function getAssets() {
public function getAttachments() {
return $this->attached;
}
/**
* Adds assets.
* Adds attachments.
*
* @param array $assets
* The associated assets to be attached.
* @param array $attachments
* The attachments to add.
*
* @return $this
*/
public function addAssets(array $assets) {
$this->attached = NestedArray::mergeDeep($this->attached, $assets);
public function addAttachments(array $attachments) {
$this->attached = \Drupal::service('renderer')->mergeAttachments($this->attached, $attachments);
return $this;
}
/**
* Sets assets.
* Sets attachments.
*
* @param array $assets
* The associated assets to be attached.
* @param array $attachments
* The attachments to set.
*
* @return $this
*/
public function setAssets(array $assets) {
$this->attached = $assets;
public function setAttachments(array $attachments) {
$this->attached = $attachments;
return $this;
}
/**
* Gets #post_render_cache callbacks.
* Gets assets.
*
* @return array
*
* @deprecated Use ::getAttachments() instead. To be removed before Drupal 8.0.0.
*/
public function getPostRenderCacheCallbacks() {
return $this->postRenderCache;
public function getAssets() {
return $this->attached;
}
/**
* Adds #post_render_cache callbacks.
*
* @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.
* Adds assets.
*
* @see \Drupal\Core\Render\RendererInterface::generateCachePlaceholder()
* @param array $assets
* The associated assets to be attached.
*
* @return $this
*
* @deprecated Use ::addAttachments() instead. To be removed before Drupal 8.0.0.
*/
public function addPostRenderCacheCallback($callback, array $context) {
$this->postRenderCache[$callback][] = $context;
return $this;
public function addAssets(array $assets) {
return $this->addAttachments($assets);
}
/**
* Sets #post_render_cache callbacks.
* Sets assets.
*
* @param array $post_render_cache_callbacks
* The associated #post_render_cache callbacks to be executed.
* @param array $assets
* The associated assets to be attached.
*
* @return $this
*
* @deprecated Use ::setAttachments() instead. To be removed before Drupal 8.0.0.
*/
public function setPostRenderCacheCallbacks(array $post_render_cache_callbacks) {
$this->postRenderCache = $post_render_cache_callbacks;
public function setAssets(array $assets) {
$this->attached = $assets;
return $this;
}
......
......@@ -7,9 +7,6 @@
namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Site\Settings;
/**
* Provides a messages element.
*
......@@ -37,17 +34,6 @@ public function getInfo() {
/**
* #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
* A renderable array.
*
......@@ -55,82 +41,42 @@ public function getInfo() {
* The updated renderable array containing the placeholder.
*/
public static function generatePlaceholder(array $element) {
$plugin_id = 'status_messages';
$callback = get_class() . '::renderMessages';
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['messages_placeholder'] = [
'#lazy_builder' => [get_class() . '::renderMessages', [$element['#display']]],
'#create_placeholder' => TRUE,
];
$element['#markup'] = $placeholder;
return $element;
}
/**
* #post_render_cache 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.
* #lazy_builder callback; replaces placeholder with messages.
*
* @see ::getInfo()
*
* @param array $element
* The renderable array that contains the to be replaced placeholder.
* @param array $context
* An array with any context information.
* @param string|null $type
* Limit the messages returned by type. Defaults to NULL, meaning all types.
* Passed on to drupal_get_messages(). These values are supported:
* - NULL
* - 'status'
* - 'warning'
* - 'error'
*
* @return array
* A renderable array containing the messages.
*
* @see drupal_get_messages()
*/
public static function renderMessages(array $element, array $context) {
$renderer = static::renderer();
public static function renderMessages($type) {
// Render the messages.
$messages = [
return [
'#theme' => 'status_messages',
// @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' => t('Status message'),
'error' => t('Error 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
// 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
// #post_render_cache callbacks are executed (which may attach additional
// assets).
// placeholders are replaced (which may attach additional 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
// might add yet more assets to be attached), and therefore it must be
// rendered with drupal_render(), not drupal_render_root().
// and hence may not replace any placeholders (because they might add yet
// more assets to be attached), and therefore it must be rendered with
// drupal_render(), not drupal_render_root().
$this->renderer->render($html['page'], TRUE);
if (isset($html['page_top'])) {
$this->renderer->render($html['page_top'], TRUE);
......@@ -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
// title. We set its $is_root_call parameter to FALSE, to ensure
// #post_render_cache callbacks are not yet applied. This is essentially
// "pre-rendering" the main content, the "full rendering" will happen in
// placeholders are not yet replaced. This is essentially "pre-rendering"
// the main content, the "full rendering" will happen in
// ::renderResponse().
// @todo Remove this once https://www.drupal.org/node/2359901 lands.
if (!empty($main_content)) {
......@@ -260,15 +259,15 @@ public function invokePageAttachmentHooks(array &$page) {
$function = $module . '_page_attachments';
$function($attachments);
}
if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache', '#cache']) !== []) {
throw new \LogicException('Only #attached, #post_render_cache and #cache may be set in hook_page_attachments().');
if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments().');
}
// Modules and themes can alter page attachments.
$this->moduleHandler->alter('page_attachments', $attachments);
\Drupal::theme()->alter('page_attachments', $attachments);
if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache', '#cache']) !== []) {
throw new \LogicException('Only #attached, #post_render_cache and #cache may be set in hook_page_attachments_alter().');
if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments_alter().');
}
// Merge the attachments onto the $page render array.
......
......@@ -300,7 +300,6 @@ public function getCacheableRenderArray(array $elements) {
$data = [
'#markup' => $elements['#markup'],
'#attached' => $elements['#attached'],
'#post_render_cache' => $elements['#post_render_cache'],
'#cache' => [
'contexts' => $elements['#cache']['contexts'],
'tags' => $elements['#cache']['tags'],
......
This diff is collapsed.
......@@ -15,8 +15,7 @@ interface RendererInterface {
/**
* Renders final HTML given a structured array tree.
*
* Calls ::render() in such a way that #post_render_cache callbacks are
* applied.
* Calls ::render() in such a way that placeholders are replaced.
*
* Should therefore only be used in occasions where the final rendering is
* happening, just before sending a Response:
......@@ -36,8 +35,7 @@ public function renderRoot(&$elements);
/**
* Renders final HTML in situations where no assets are needed.
*
* Calls ::render() in such a way that #post_render_cache callbacks are
* applied.
* Calls ::render() in such a way that placeholders are replaced.
*
* 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
......@@ -88,8 +86,8 @@ public function renderPlain(&$elements);
* retrieval.
* - Cache tags, so that cached renderings are invalidated when site content
* or configuration that can affect that rendering changes.
* - #post_render_cache callbacks, for executing code to handle dynamic
* requirements that cannot be cached.
* - Placeholders, with associated self-contained placeholder render arrays,
* for executing code to handle dynamic requirements that cannot be cached.
* A stack of \Drupal\Core\Render\BubbleableMetadata objects can be used to
* perform this bubbling.
*
......@@ -148,15 +146,31 @@ public function renderPlain(&$elements);
* process render arrays and call the element info service before passing
* the array to Renderer::render(), such as form_builder() in the Form
* API.
* - If this element has an array of #pre_render functions defined, they are
* called sequentially to modify the element before rendering. After all
* the #pre_render functions have been called, #printed is checked a
* second time in case a #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.
* - If this element has #create_placeholder set to TRUE, and it has a
* #lazy_builder callback, then the element is replaced with another
* element that has only two properties: #markup and #attached. #markup
* will contain placeholder markup, and #attached contains the placeholder
* metadata, that will be used for replacing this placeholder. That
* metadata contains a very compact render array (containing only
* #lazy_builder and #cache) that will be rendered to replace the
* placeholder with its final markup. This means that when the
* #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
* from the stack, they are merged, and the result is pushed back onto the
* stack.
......@@ -253,25 +267,14 @@ public function renderPlain(&$elements);
* assumes only children's individual markup is relevant and ignores the
* parent markup. This approach is normally not needed and should be
* 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
* having been updated just before the render caching step), 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. But,
* since #post_render_cache callbacks add attach additional assets, the
* correct bubbling of those must once again be taken into account. This
* 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.
* having been updated just before the render caching step), its
* placeholder element containing a #lazy_builder function is rendered in
* isolation. The resulting markup is used to replace the placeholder, and
* any bubbleable metadata is merged.
* Placeholders must be unique, to guarantee that e.g. samples of
* placeholders are not replaced as well.
* - Just before finishing the rendering of this element, this element's
* 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
......@@ -390,27 +393,4 @@ public function addCacheableDependency(array &$elements, $dependency);
*/
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);
}
......@@ -131,7 +131,7 @@ function ckeditor_filter_format_update() {
* cause the CKEditor::getJSSettings() to be called, which will cause
* Internal::generateFormatTagsSetting() to be called, which calls
* check_markup(), which finally calls drupal_render() non-recursively, because
* a filter might apply #post_render_cache callbacks.
* a filter might add placeholders to replace.
* This would be a root call inside a root call, which breaks the stack-based
* logic for bubbling rendering metadata.
* Therefore this pre-calculates the needed values, and hence performs the
......
......@@ -15,10 +15,10 @@ services:
tags:
- { name: backend_overridable }
comment.post_render_cache:
class: Drupal\comment\CommentPostRenderCache
comment.lazy_builders:
class: Drupal\comment\CommentLazyBuilders
arguments: ['@entity.manager', '@entity.form_builder', '@current_user', '@comment.manager', '@module_handler', '@renderer']
comment.link_builder:
class: Drupal\comment\CommentLinkBuilder
arguments: ['@current_user', '@comment.manager', '@module_handler', '@string_translation']
arguments: ['@current_user', '@comment.manager', '@module_handler', '@string_translation', '@entity.manager']
......@@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\comment\CommentPostRenderCache.
* Contains \Drupal\comment\CommentLazyBuilders.
*/
namespace Drupal\comment;
......@@ -17,9 +17,9 @@
use Drupal\Core\Render\Renderer;
/**
* Defines a service for comment post render cache callbacks.
* Defines a service for comment #lazy_builder callbacks.
*/
class CommentPostRenderCache {
class CommentLazyBuilders {
/**
* The entity manager service.
......@@ -64,7 +64,7 @@ class CommentPostRenderCache {
protected $renderer;
/**
* Constructs a new CommentPostRenderCache object.
* Constructs a new CommentLazyBuilders object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
......@@ -89,87 +89,70 @@ public function __construct(EntityManagerInterface $entity_manager, EntityFormBu
}
/**
* #post_render_cache callback; replaces placeholder with comment form.
* #lazy_builder callback; builds the comment form.
*
* @param array $element
* The renderable array that contains the to be replaced placeholder.
* @param array $context
* An array with the following keys:
* - entity_type: an entity type
* - entity_id: an entity ID
* - field_name: a comment field name
* - comment_type: the comment type
* @param string $commented_entity_type_id
* The commented entity type ID.
* @param string $commented_entity_id
* The commented entity ID.
* @param string $field_name
* The comment field name.
* @param string $comment_type_id
* The comment type ID.
*
* @return array
* A renderable array containing the comment form.
*/
public function renderForm(array $element, array $context) {
public function renderForm($commented_entity_type_id, $commented_entity_id, $field_name, $comment_type_id) {
$values = array(
'entity_type' => $context['entity_type'],
'entity_id' => $context['entity_id'],
'field_name' => $context['field_name'],
'comment_type' => $context['comment_type'],
'entity_type' => $commented_entity_type_id,
'entity_id' => $commented_entity_id,
'field_name' => $field_name,
'comment_type' => $comment_type_id,
'pid' => NULL,
);
$comment = $this->entityManager->getStorage('comment')->create($values);
$form = $this->entityFormBuilder->getForm($comment);
$markup = $this->renderer->render($form);
$callback = 'comment.post_render_cache:renderForm';
$placeholder = $this->generatePlaceholder($callback, $context);
$element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
$element = $this->renderer->mergeBubbleableMetadata($element, $form);
return $element;
return $this->entityFormBuilder->getForm($comment);
}
/**
* #post_render_cache callback; replaces the placeholder with comment links.
*
* Renders the links on a comment.
* #lazy_builder callback; builds a comment's links.
*
* @param array $element
* The renderable array that contains the to be replaced placeholder.
* @param array $context
* An array with the following keys:
* - comment_entity_id: a comment entity ID
* - view_mode: the view mode in which the comment entity is being viewed
* - langcode: in which language the comment entity is being viewed
* - commented_entity_type: the entity type to which the comment is attached
* - commented_entity_id: the entity ID to which the comment is attached
* - in_preview: whether the comment is currently being previewed
* @param string $comment_entity_id
* The comment entity ID.
* @param string $view_mode
* The view mode in which the comment entity is being viewed.
* @param string $langcode
* The language in which the comment entity is being viewed.
* @param bool $is_in_preview
* Whether the comment is currently being previewed.
*
* @return array
* A renderable array representing the comment links.
*/
public function renderLinks(array $element, array $context) {
$callback = 'comment.post_render_cache:renderLinks';
$placeholder = $this->generatePlaceholder($callback, $context);
public function renderLinks($comment_entity_id, $view_mode, $langcode, $is_in_preview) {
$links = array(
'#theme' => 'links__comment',
'#pre_render' => array('drupal_pre_render_links'),
'#attributes' => array('class' => array('links', 'inline')),
);
if (!$context['in_preview']) {
if (!$is_in_preview) {
/** @var \Drupal\comment\CommentInterface $entity */
$entity = $this->entityManager->getStorage('comment')->load($context['comment_entity_id']);
$entity = $this->entityManager->getStorage('comment')->load($comment_entity_id);
$commented_entity = $entity->getCommentedEntity();
$links['comment'] = $this->buildLinks($entity, $commented_entity);
// Allow other modules to alter the comment links.
$hook_context = array(
'view_mode' => $context['view_mode'],
'langcode' => $context['langcode'],
'view_mode' => $view_mode,
'langcode' => $langcode,
'commented_entity' => $commented_entity,
);
$this->moduleHandler->alter('comment_links', $links, $entity, $hook_context);
}
$markup = $this->renderer->render($links);
$element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
return $element;
return $links;
}
/**
......@@ -239,71 +222,6 @@ protected function buildLinks(CommentInterface $entity, EntityInterface $comment
);
}
/**
* #post_render_cache callback; attaches "X new comments" link metadata.
*
* @param array $element
* A render array with the following keys:
* - #markup
* - #attached
* @param array $context
* An array with the following keys:
* - entity_type: an entity type
* - entity_id: an entity ID
* - field_name: a comment field name
*
* @return array
* The updated $element.
*/
public function attachNewCommentsLinkMetadata(array $element, array $context) {
$entity = $this->entityManager
->getStorage($context['entity_type'])
->load($context['entity_id']);
// Build "X new comments" link metadata.
$new = $this->commentManager
->getCountNewComments($entity);
// Early-return if there are zero new comments for the current user.
if ($new === 0) {
return $element;
}
$field_name = $context['field_name'];
$page_number = $this->entityManager
->getStorage('comment')
->getNewCommentPageNumber($entity->{$field_name}->comment_count, $new, $entity);
$query = $page_number ? array('page' => $page_number) : NULL;
// Attach metadata.
$element['#attached']['js'][] = array(
'type' => 'setting',
'data' => array(
'comment' => array(
'newCommentsLinks' => array(
$context['entity_type'] => array(