Same name and namespace in other branches
  1. 8.9.x core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php \Drupal\Core\Render\MainContent\HtmlRenderer
  2. 9 core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php \Drupal\Core\Render\MainContent\HtmlRenderer

Default main content renderer for HTML requests.

For attachment handling of HTML responses:

Hierarchy

Expanded class hierarchy of HtmlRenderer

See also

template_preprocess_html()

\Drupal\Core\Render\AttachmentsResponseProcessorInterface

\Drupal\Core\Render\BareHtmlPageRenderer

\Drupal\Core\Render\HtmlResponse

\Drupal\Core\Render\HtmlResponseAttachmentsProcessor

1 string reference to 'HtmlRenderer'
core.services.yml in core/core.services.yml
core/core.services.yml
1 service uses HtmlRenderer
main_content_renderer.html in core/core.services.yml
Drupal\Core\Render\MainContent\HtmlRenderer

File

core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php, line 32

Namespace

Drupal\Core\Render\MainContent
View source
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;
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
HtmlRenderer::$displayVariantManager protected property The display variant manager.
HtmlRenderer::$eventDispatcher protected property The event dispatcher.
HtmlRenderer::$moduleHandler protected property The module handler.
HtmlRenderer::$renderCache protected property The render cache service.
HtmlRenderer::$renderer protected property The renderer service.
HtmlRenderer::$rendererConfig protected property The renderer configuration array.
HtmlRenderer::$themeManager protected property The theme manager.
HtmlRenderer::$titleResolver protected property The title resolver.
HtmlRenderer::buildPageTopAndBottom public function Invokes the page top and bottom hooks.
HtmlRenderer::invokePageAttachmentHooks public function Invokes the page attachment hooks.
HtmlRenderer::prepare protected function Prepares the HTML body: wraps the main content in #type 'page'.
HtmlRenderer::renderResponse public function The entire HTML: takes a #type 'page' and wraps it in a #type 'html'. Overrides MainContentRendererInterface::renderResponse
HtmlRenderer::__construct public function Constructs a new HtmlRenderer.