123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- <?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 Symfony\Component\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\Component\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;
- /**
- * 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\Component\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.
- */
- 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;
- }
- /**
- * {@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) {
- list($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 isset($main_content['#title']) ? $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);
- }
- // Otherwise, render it as the main content of a #type => page, by selecting
- // page display variant to do that and building that page display variant.
- 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(RenderEvents::SELECT_PAGE_DISPLAY_VARIANT, $event);
- $variant_id = $event->getPluginId();
- // 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' => isset($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);
- 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)
- ->setConfiguration($event->getPluginConfiguration());
- // 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 = \Drupal::theme()->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->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 = [];
- foreach ($this->moduleHandler->getImplementations('page_attachments') as $module) {
- $function = $module . '_page_attachments';
- $function($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', '#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 = [];
- foreach ($this->moduleHandler->getImplementations('page_top') as $module) {
- $function = $module . '_page_top';
- $function($page_top);
- }
- foreach ($this->moduleHandler->getImplementations('page_bottom') as $module) {
- $function = $module . '_page_bottom';
- $function($page_bottom);
- }
- if (!empty($page_top)) {
- $html['page_top'] = $page_top;
- }
- if (!empty($page_bottom)) {
- $html['page_bottom'] = $page_bottom;
- }
- }
- }
|