HtmlRenderer.php

Same filename and directory in other branches
  1. 9 core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
  2. 8.9.x core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
  3. 10 core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php

Namespace

Drupal\Core\Render\MainContent

File

core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php

View source
<?php

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\Display\ContextAwareVariantInterface;
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\Routing\RouteMatchInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Default main content renderer for HTML requests.
 *
 * For attachment handling of HTML responses:
 * @see template_preprocess_html()
 * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface
 * @see \Drupal\Core\Render\BareHtmlPageRenderer
 * @see \Drupal\Core\Render\HtmlResponse
 * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
 */
class HtmlRenderer implements MainContentRendererInterface {
    
    /**
     * The title resolver.
     *
     * @var \Drupal\Core\Controller\TitleResolverInterface
     */
    protected $titleResolver;
    
    /**
     * The display variant manager.
     *
     * @var \Drupal\Component\Plugin\PluginManagerInterface
     */
    protected $displayVariantManager;
    
    /**
     * The event dispatcher.
     *
     * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
     */
    protected $eventDispatcher;
    
    /**
     * The module handler.
     *
     * @var \Drupal\Core\Extension\ModuleHandlerInterface
     */
    protected $moduleHandler;
    
    /**
     * The renderer service.
     *
     * @var \Drupal\Core\Render\RendererInterface
     */
    protected $renderer;
    
    /**
     * The render cache service.
     *
     * @var \Drupal\Core\Render\RenderCacheInterface
     */
    protected $renderCache;
    
    /**
     * The renderer configuration array.
     *
     * @see sites/default/default.services.yml
     *
     * @var array
     */
    protected $rendererConfig;
    
    /**
     * The theme manager.
     *
     * @var \Drupal\Core\Theme\ThemeManagerInterface
     */
    protected $themeManager;
    
    /**
     * Constructs a new HtmlRenderer.
     *
     * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
     *   The title resolver.
     * @param \Drupal\Component\Plugin\PluginManagerInterface $display_variant_manager
     *   The display variant manager.
     * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
     *   The event dispatcher.
     * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
     *   The module handler.
     * @param \Drupal\Core\Render\RendererInterface $renderer
     *   The renderer service.
     * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
     *   The render cache service.
     * @param array $renderer_config
     *   The renderer configuration array.
     * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
     *   The theme manager.
     */
    public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, array $renderer_config, ThemeManagerInterface $theme_manager) {
        $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;
        $this->themeManager = $theme_manager;
    }
    
    /**
     * {@inheritdoc}
     *
     * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'.
     */
    public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
        [
            $page,
            $title,
        ] = $this->prepare($main_content, $request, $route_match);
        if (!isset($page['#type']) || $page['#type'] !== 'page') {
            throw new \LogicException('Must be #type page');
        }
        $page['#title'] = $title;
        // Now render the rendered page.html.twig template inside the html.html.twig
        // template, and use the bubbled #attached metadata from $page to ensure we
        // load all attached assets.
        $html = [
            '#type' => 'html',
            'page' => $page,
        ];
        // The special page regions will appear directly in html.html.twig, not in
        // page.html.twig, hence add them here, just before rendering html.html.twig.
        $this->buildPageTopAndBottom($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';
        $response = new HtmlResponse($content, 200, [
            'Content-Type' => 'text/html; charset=UTF-8',
        ]);
        return $response;
    }
    
    /**
     * Prepares the HTML body: wraps the main content in #type 'page'.
     *
     * @param array $main_content
     *   The render array representing the main content.
     * @param \Symfony\Component\HttpFoundation\Request $request
     *   The request object, for context.
     * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
     *   The route match, for context.
     *
     * @return array
     *   An array with two values:
     *   0. A #type 'page' render array.
     *   1. The page title.
     *
     * @throws \LogicException
     *   If the selected display variant does not implement PageVariantInterface.
     */
    protected function prepare(array $main_content, Request $request, RouteMatchInterface $route_match) {
        // Determine the title: use the title provided by the main content if any,
        // otherwise get it from the routing information.
        $get_title = function (array $main_content) use ($request, $route_match) {
            return $main_content['#title'] ?? $this->titleResolver
                ->getTitle($request, $route_match->getRouteObject());
        };
        // If the _controller result already is #type => page,
        // we have no work to do: The "main content" already is an entire "page"
        // (see html.html.twig).
        if (isset($main_content['#type']) && $main_content['#type'] === 'page') {
            $page = $main_content;
            $title = $get_title($page);
        }
        else {
            // Select the page display variant to be used to render this main content,
            // default to the built-in "simple page".
            $event = new PageDisplayVariantSelectionEvent('simple_page', $route_match);
            $this->eventDispatcher
                ->dispatch($event, RenderEvents::SELECT_PAGE_DISPLAY_VARIANT);
            $variant_id = $event->getPluginId();
            $variant_configuration = $event->getPluginConfiguration();
            // 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
            // 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)) {
                $this->renderer
                    ->executeInRenderContext(new RenderContext(), function () use (&$main_content) {
                    if (isset($main_content['#cache']['keys'])) {
                        // Retain #title, otherwise, dynamically generated titles would be
                        // missing for controllers whose entire returned render array is
                        // render cached.
                        $main_content['#cache_properties'][] = '#title';
                    }
                    return $this->renderer
                        ->render($main_content, FALSE);
                });
                $main_content = $this->renderCache
                    ->getCacheableRenderArray($main_content) + [
                    '#title' => $main_content['#title'] ?? NULL,
                ];
            }
            $title = $get_title($main_content);
            // Instantiate the page display, and give it the main content.
            $page_display = $this->displayVariantManager
                ->createInstance($variant_id, $variant_configuration);
            if (!$page_display instanceof PageVariantInterface) {
                throw new \LogicException('Cannot render the main content for this page because the provided display variant does not implement PageVariantInterface.');
            }
            $page_display->setMainContent($main_content)
                ->setTitle($title)
                ->addCacheableDependency($event);
            // Some display variants need to be passed an array of contexts with
            // values because they can't get all their contexts globally. For example,
            // in Page Manager, you can create a Page which has a specific static
            // context (e.g. a context that refers to the Node with nid 6), if any
            // such contexts were added to the $event, pass them to the $page_display.
            if ($page_display instanceof ContextAwareVariantInterface) {
                $page_display->setContexts($event->getContexts());
            }
            // Generate a #type => page render array using the page display variant,
            // the page display will build the content for the various page regions.
            $page = [
                '#type' => 'page',
            ];
            $page += $page_display->build();
        }
        // $page is now fully built. Find all non-empty page regions, and add a
        // theme wrapper function that allows them to be consistently themed.
        $regions = $this->themeManager
            ->getActiveTheme()
            ->getRegions();
        foreach ($regions as $region) {
            if (!empty($page[$region])) {
                $page[$region]['#theme_wrappers'][] = 'region';
                $page[$region]['#region'] = $region;
            }
        }
        // Allow hooks to add attachments to $page['#attached'].
        $this->renderer
            ->executeInRenderContext(new RenderContext(), function () use (&$page) {
            $this->invokePageAttachmentHooks($page);
        });
        return [
            $page,
            $title,
        ];
    }
    
    /**
     * Invokes the page attachment hooks.
     *
     * @param array &$page
     *   A #type 'page' render array, for which the page attachment hooks will be
     *   invoked and to which the results will be added.
     *
     * @throws \LogicException
     *
     * @internal
     *
     * @see hook_page_attachments()
     * @see hook_page_attachments_alter()
     */
    public function invokePageAttachmentHooks(array &$page) {
        // Modules can add attachments.
        $attachments = [];
        $this->moduleHandler
            ->invokeAllWith('page_attachments', function (callable $hook, string $module) use (&$attachments) {
            $hook($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);
        $this->themeManager
            ->alter('page_attachments', $attachments);
        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.
        $page = $this->renderer
            ->mergeBubbleableMetadata($page, $attachments);
    }
    
    /**
     * Invokes the page top and bottom hooks.
     *
     * @param array &$html
     *   A #type 'html' render array, for which the page top and bottom hooks will
     *   be invoked, and to which the 'page_top' and 'page_bottom' children (also
     *   render arrays) will be added (if non-empty).
     *
     * @throws \LogicException
     *
     * @internal
     *
     * @see hook_page_top()
     * @see hook_page_bottom()
     * @see html.html.twig
     */
    public function buildPageTopAndBottom(array &$html) {
        // Modules can add render arrays to the top and bottom of the page.
        $page_top = [];
        $page_bottom = [];
        $this->moduleHandler
            ->invokeAllWith('page_top', function (callable $hook, string $module) use (&$page_top) {
            $hook($page_top);
        });
        $this->moduleHandler
            ->invokeAllWith('page_bottom', function (callable $hook, string $module) use (&$page_bottom) {
            $hook($page_bottom);
        });
        if (!empty($page_top)) {
            $html['page_top'] = $page_top;
        }
        if (!empty($page_bottom)) {
            $html['page_bottom'] = $page_bottom;
        }
    }

}

Classes

Title Deprecated Summary
HtmlRenderer Default main content renderer for HTML requests.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.