diff --git a/core/core.services.yml b/core/core.services.yml index 6508178272f..bc94ced6371 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -943,7 +943,7 @@ services: - { name: event_subscriber } main_content_renderer.html: class: Drupal\Core\Render\MainContent\HtmlRenderer - arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache'] + arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache', '%renderer.config%'] tags: - { name: render.main_content_renderer, format: html } main_content_renderer.ajax: diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 009bb33e1d9..fd06a1ddead 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1294,7 +1294,7 @@ function template_preprocess_html(&$variables) { '@token' => $token, ]); $variables[$type]['#markup'] = $placeholder; - $variables[$type]['#attached']['html_response_placeholders'][$type] = $placeholder; + $variables[$type]['#attached']['html_response_attachment_placeholders'][$type] = $placeholder; } } diff --git a/core/lib/Drupal/Core/Render/HtmlResponse.php b/core/lib/Drupal/Core/Render/HtmlResponse.php index c5339d613fa..4447267268c 100644 --- a/core/lib/Drupal/Core/Render/HtmlResponse.php +++ b/core/lib/Drupal/Core/Render/HtmlResponse.php @@ -36,12 +36,16 @@ class HtmlResponse extends Response implements CacheableResponseInterface, Attac // A render array can automatically be converted to a string and set the // necessary metadata. if (is_array($content) && (isset($content['#markup']))) { - $content += ['#attached' => ['html_response_placeholders' => []]]; + $content += ['#attached' => [ + 'html_response_attachment_placeholders' => [], + 'placeholders' => []], + ]; $this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content)); $this->setAttachments($content['#attached']); $content = $content['#markup']; } - parent::setContent($content); + return parent::setContent($content); } + } diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php index 0cd74174e4f..cc20063cb1e 100644 --- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php @@ -99,25 +99,31 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn throw new \InvalidArgumentException('\Drupal\Core\Render\HtmlResponse instance expected.'); } + // First, render the actual placeholders; this may cause additional + // attachments to be added to the response, which the attachment + // placeholders rendered by renderHtmlResponseAttachmentPlaceholders() will + // need to include. + $response = $this->renderPlaceholders($response); + $attached = $response->getAttachments(); // Get the placeholders from attached and then remove them. - $placeholders = $attached['html_response_placeholders']; - unset($attached['html_response_placeholders']); + $attachment_placeholders = $attached['html_response_attachment_placeholders']; + unset($attached['html_response_attachment_placeholders']); - $variables = $this->processAssetLibraries($attached, $placeholders); + $variables = $this->processAssetLibraries($attached, $attachment_placeholders); // Handle all non-asset attachments. This populates drupal_get_html_head(). $all_attached = ['#attached' => $attached]; drupal_process_attached($all_attached); // Get HTML head elements - if present. - if (isset($placeholders['head'])) { + if (isset($attachment_placeholders['head'])) { $variables['head'] = drupal_get_html_head(FALSE); } - // Now replace the placeholders in the response content with the real data. - $this->renderPlaceholders($response, $placeholders, $variables); + // Now replace the attachment placeholders. + $this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables); // Finally set the headers on the response if any bubbled. if (!empty($attached['http_header'])) { @@ -127,6 +133,55 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn return $response; } + /** + * Renders placeholders (#attached['placeholders']). + * + * First, the HTML response object is converted to an equivalent render array, + * with #markup being set to the response's content and #attached being set to + * the response's attachments. Among these attachments, there may be + * placeholders that need to be rendered (replaced). + * + * Next, RendererInterface::renderRoot() is called, which renders the + * placeholders into their final markup. + * + * The markup that results from RendererInterface::renderRoot() is now the + * original HTML response's content, but with the placeholders rendered. We + * overwrite the existing content in the original HTML response object with + * this markup. The markup that was rendered for the placeholders may also + * have attachments (e.g. for CSS/JS assets) itself, and cacheability metadata + * that indicates what that markup depends on. That metadata is also added to + * the HTML response object. + * + * @param \Drupal\Core\Render\HtmlResponse $response + * The HTML response whose placeholders are being replaced. + * + * @return \Drupal\Core\Render\HtmlResponse + * The updated HTML response, with replaced placeholders. + * + * @see \Drupal\Core\Render\Renderer::replacePlaceholders() + * @see \Drupal\Core\Render\Renderer::renderPlaceholder() + */ + protected function renderPlaceholders(HtmlResponse $response) { + $build = [ + '#markup' => SafeString::create($response->getContent()), + '#attached' => $response->getAttachments(), + ]; + // RendererInterface::renderRoot() renders the $build render array and + // updates it in place. We don't care about the return value (which is just + // $build['#markup']), but about the resulting render array. + // @todo Simplify this when https://www.drupal.org/node/2495001 lands. + $this->renderer->renderRoot($build); + + // Update the Response object now that the placeholders have been rendered. + $placeholders_bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build); + $response + ->setContent($build['#markup']) + ->addCacheableDependency($placeholders_bubbleable_metadata) + ->setAttachments($placeholders_bubbleable_metadata->getAttachments()); + + return $response; + } + /** * Processes asset libraries into render arrays. * @@ -174,8 +229,7 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn } /** - * Renders variables into HTML markup and replaces placeholders in the - * response content. + * Renders HTML response attachment placeholders. * * @param \Drupal\Core\Render\HtmlResponse $response * The HTML response to update. @@ -186,7 +240,7 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn * The variables to render and replace, keyed by type with renderable * arrays as values. */ - protected function renderPlaceholders(HtmlResponse $response, array $placeholders, array $variables) { + protected function renderHtmlResponseAttachmentPlaceholders(HtmlResponse $response, array $placeholders, array $variables) { $content = $response->getContent(); foreach ($placeholders as $type => $placeholder) { if (isset($variables[$type])) { diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index 7e06aabd455..5cfa3121eaa 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -8,9 +8,11 @@ namespace Drupal\Core\Render\MainContent; use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\Core\Cache\Cache; use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Display\PageVariantInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\HtmlResponse; use Drupal\Core\Render\PageDisplayVariantSelectionEvent; use Drupal\Core\Render\RenderCacheInterface; @@ -74,6 +76,15 @@ class HtmlRenderer implements MainContentRendererInterface { */ protected $renderCache; + /** + * The renderer configuration array. + * + * @see sites/default/default.services.yml + * + * @var array + */ + protected $rendererConfig; + /** * Constructs a new HtmlRenderer. * @@ -89,14 +100,17 @@ class HtmlRenderer implements MainContentRendererInterface { * The renderer service. * @param \Drupal\Core\Render\RenderCacheInterface $render_cache * The render cache service. + * @param array $renderer_config + * The renderer configuration array. */ - public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache) { + public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, array $renderer_config) { $this->titleResolver = $title_resolver; $this->displayVariantManager = $display_variant_manager; $this->eventDispatcher = $event_dispatcher; $this->moduleHandler = $module_handler; $this->renderer = $renderer; $this->renderCache = $render_cache; + $this->rendererConfig = $renderer_config; } /** @@ -125,11 +139,29 @@ class HtmlRenderer implements MainContentRendererInterface { // page.html.twig, hence add them here, just before rendering html.html.twig. $this->buildPageTopAndBottom($html); - // @todo https://www.drupal.org/node/2495001 Make renderRoot return a - // cacheable render array directly. - $this->renderer->renderRoot($html); + // Render, but don't replace placeholders yet, because that happens later in + // the render pipeline. To not replace placeholders yet, we use + // RendererInterface::render() instead of RendererInterface::renderRoot(). + // @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor. + $render_context = new RenderContext(); + $this->renderer->executeInRenderContext($render_context, function() use (&$html) { + // RendererInterface::render() renders the $html render array and updates + // it in place. We don't care about the return value (which is just + // $html['#markup']), but about the resulting render array. + // @todo Simplify this when https://www.drupal.org/node/2495001 lands. + $this->renderer->render($html); + }); + // RendererInterface::render() always causes bubbleable metadata to be + // stored in the render context, no need to check it conditionally. + $bubbleable_metadata = $render_context->pop(); + $bubbleable_metadata->applyTo($html); $content = $this->renderCache->getCacheableRenderArray($html); + // Also associate the required cache contexts. + // (Because we use ::render() above and not ::renderRoot(), we manually must + // ensure the HTML response varies by the required cache contexts.) + $content['#cache']['contexts'] = Cache::mergeContexts($content['#cache']['contexts'], $this->rendererConfig['required_cache_contexts']); + // Also associate the "rendered" cache tag. This allows us to invalidate the // entire render cache, regardless of the cache bin. $content['#cache']['tags'][] = 'rendered';