diff --git a/core/core.services.yml b/core/core.services.yml index 65c0bc7..afd35a0 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -925,7 +925,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', '@render_strategy_manager'] + arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache'] tags: - { name: render.main_content_renderer, format: html } main_content_renderer.ajax: @@ -1480,6 +1480,11 @@ services: tags: - { name: event_subscriber } # Render strategies for rendering placeholders. + render_strategy_html.subscriber: + class: Drupal\Core\EventSubscriber\HtmlRenderStrategySubscriber + tags: + - { name: event_subscriber } + arguments: ['@render_strategy_manager'] render_strategy_manager: class: Drupal\Core\Render\Strategy\RenderStrategyManager arguments: ['@renderer'] diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlRenderStrategySubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlRenderStrategySubscriber.php new file mode 100644 index 0000000..d40f451 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlRenderStrategySubscriber.php @@ -0,0 +1,74 @@ +renderStrategyManager = $render_strategy_manager; + } + + /** + * Processes attachments for HtmlResponse responses. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onRespond(FilterResponseEvent $event) { + if (!$event->isMasterRequest()) { + return; + } + + $response = $event->getResponse(); + if (!$response instanceof HtmlResponse) { + return; + } + + $attachments = $response->getAttachments(); + if (empty($attachments['placeholders'])) { + return; + } + + $attachments['placeholders'] = $this->renderStrategyManager->processPlaceholders($attachments['placeholders']); + + $response->setAttachments($attachments); + $event->setResponse($response); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Run shortly before HtmlResponseSubscriber. + $events[KernelEvents::RESPONSE][] = ['onRespond', 5]; + return $events; + } + +} diff --git a/core/lib/Drupal/Core/Render/HtmlResponse.php b/core/lib/Drupal/Core/Render/HtmlResponse.php index c5339d6..7f9ab4d 100644 --- a/core/lib/Drupal/Core/Render/HtmlResponse.php +++ b/core/lib/Drupal/Core/Render/HtmlResponse.php @@ -42,6 +42,6 @@ public function setContent($content) { $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 50ff584..67f3507 100644 --- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php @@ -99,6 +99,12 @@ public function processAttachments(AttachmentsInterface $response) { 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. + $this->renderPlaceholders($response); + $attached = $response->getAttachments(); // Get the placeholders from attached and then remove them. @@ -118,7 +124,7 @@ public function processAttachments(AttachmentsInterface $response) { } // Now replace the placeholders in the response content with the real data. - $this->renderPlaceholders($response, $placeholders, $variables); + $this->renderHtmlResponseAttachmentPlaceholders($response, $placeholders, $variables); // Finally set the headers on the response. $headers = drupal_get_http_header(); @@ -127,6 +133,31 @@ public function processAttachments(AttachmentsInterface $response) { return $response; } + /** + * Renders placeholders (#attached[placeholders]). + * + * @param \Drupal\Core\Render\HtmlResponse $response + * The HTML response whose placeholders to replace. + * + * @see \Drupal\Core\Render\Renderer::replacePlaceholders() + * @see \Drupal\Core\Render\Renderer::renderPlaceholder() + */ + protected function renderPlaceholders(HtmlResponse $response) { + // Render the placeholders in the HTML Response object. + $build = [ + '#markup' => SafeString::create($response->getContent()), + '#attached' => $response->getAttachments(), + ]; + $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()); + } + /** * Processes asset libraries into render arrays. * @@ -186,7 +217,7 @@ protected function processAssetLibraries(array $attached, array $placeholders) { * 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 6da01e1..b701344 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -11,13 +11,13 @@ 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; use Drupal\Core\Render\RenderContext; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\RenderEvents; -use Drupal\Core\Render\Strategy\RenderStrategyManagerInterface; use Drupal\Core\Routing\RouteMatchInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; @@ -76,13 +76,6 @@ class HtmlRenderer implements MainContentRendererInterface { protected $renderCache; /** - * The render strategy manager service. - * - * @var \Drupal\Core\Render\Strategy\RenderStrategyManager - */ - protected $renderStrategyManager; - - /** * Constructs a new HtmlRenderer. * * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver @@ -98,14 +91,13 @@ class HtmlRenderer implements MainContentRendererInterface { * @param \Drupal\Core\Render\RenderCacheInterface $render_cache * The render cache service. */ - public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, RenderStrategyManagerInterface $render_strategy_manager) { + public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache) { $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->renderStrategyManager = $render_strategy_manager; } /** @@ -134,13 +126,25 @@ public function renderResponse(array $main_content, Request $request, RouteMatch // page.html.twig, hence add them here, just before rendering html.html.twig. $this->buildPageTopAndBottom($html); - // This uses render() instead of renderRoot() to ensure that placeholders - // are not replaced. - $this->renderer->render($html); + // Render, but don't replace placeholders yet, because that happens later in + // the render pipeline. + // @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor. + $render_context = new RenderContext(); + $this->renderer->executeInRenderContext($render_context, function() use (&$html) { + // RendererInterface::render() renders the $html render array, it updates + // $html by reference. 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); + }); + if (!$render_context->isEmpty()) { + $bubbleable_metadata = $render_context->pop(); + BubbleableMetadata::createFromRenderArray($html) + ->merge($bubbleable_metadata) + ->applyTo($html); + } $content = $this->renderCache->getCacheableRenderArray($html); - $this->renderStrategyManager->renderPlaceholders($content); - // Also associate the "rendered" cache tag. This allows us to invalidate the // entire render cache, regardless of the cache bin. $content['#cache']['tags'][] = 'rendered'; diff --git a/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php index 4bc05e0..2a18a72 100644 --- a/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php @@ -51,13 +51,12 @@ public function addRenderStrategy(RenderStrategyInterface $strategy) { /** * {@inheritdoc} */ - public function renderPlaceholders(&$elements) { - if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) { - return FALSE; + public function processPlaceholders(array $placeholders) { + if (empty($placeholders)) { + return []; } $new_placeholders = []; - $placeholders = $elements['#attached']['placeholders']; // Give each render strategy a chance to replace all not-yet replaced // placeholders. @@ -71,14 +70,7 @@ public function renderPlaceholders(&$elements) { } } - // Now render the new placeholders. - $elements['#attached']['placeholders'] = $new_placeholders; - foreach (array_keys($elements['#attached']['placeholders']) as $placeholder) { - $elements = $this->renderer->renderPlaceholder($placeholder, $elements); - } - unset($elements['#attached']['placeholders']); - - return TRUE; + return $new_placeholders; } } diff --git a/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManagerInterface.php b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManagerInterface.php index e7c0385..0120f50 100644 --- a/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManagerInterface.php +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManagerInterface.php @@ -8,19 +8,19 @@ namespace Drupal\Core\Render\Strategy; /** - * Provides an interface which allows to render placeholders. + * Provides an interface which allows to process placeholders. */ interface RenderStrategyManagerInterface { /** - * Renders placeholders with different strategies. + * Processes placeholders with different strategies. * - * @param array $elements - * The render cache array containing #markup and #attached placeholders. + * @param array $placeholders + * The placeholders to process. * - * @return bool - * TRUE when placeholders have been replaced, FALSE otherwise. + * @return array + * The processed placeholders. */ - public function renderPlaceholders(&$elements); + public function processPlaceholders(array $placeholders); }