3 namespace Drupal\Core\Render\MainContent;
5 use Drupal\Component\Plugin\PluginManagerInterface;
6 use Drupal\Core\Cache\Cache;
7 use Drupal\Core\Controller\TitleResolverInterface;
8 use Drupal\Core\Display\PageVariantInterface;
9 use Drupal\Core\Extension\ModuleHandlerInterface;
10 use Drupal\Core\Display\ContextAwareVariantInterface;
11 use Drupal\Core\Render\HtmlResponse;
12 use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
13 use Drupal\Core\Render\RenderCacheInterface;
14 use Drupal\Core\Render\RenderContext;
15 use Drupal\Core\Render\RendererInterface;
16 use Drupal\Core\Render\RenderEvents;
17 use Drupal\Core\Routing\RouteMatchInterface;
18 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
19 use Symfony\Component\HttpFoundation\Request;
22 * Default main content renderer for HTML requests.
24 * For attachment handling of HTML responses:
25 * @see template_preprocess_html()
26 * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface
27 * @see \Drupal\Core\Render\BareHtmlPageRenderer
28 * @see \Drupal\Core\Render\HtmlResponse
29 * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
31 class HtmlRenderer implements MainContentRendererInterface {
36 * @var \Drupal\Core\Controller\TitleResolverInterface
38 protected $titleResolver;
41 * The display variant manager.
43 * @var \Drupal\Component\Plugin\PluginManagerInterface
45 protected $displayVariantManager;
48 * The event dispatcher.
50 * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
52 protected $eventDispatcher;
56 * @var \Drupal\Core\Extension\ModuleHandlerInterface
58 protected $moduleHandler;
61 * The renderer service.
63 * @var \Drupal\Core\Render\RendererInterface
68 * The render cache service.
70 * @var \Drupal\Core\Render\RenderCacheInterface
72 protected $renderCache;
75 * The renderer configuration array.
77 * @see sites/default/default.services.yml
81 protected $rendererConfig;
84 * Constructs a new HtmlRenderer.
86 * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
88 * @param \Drupal\Component\Plugin\PluginManagerInterface $display_variant_manager
89 * The display variant manager.
90 * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
91 * The event dispatcher.
92 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
94 * @param \Drupal\Core\Render\RendererInterface $renderer
95 * The renderer service.
96 * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
97 * The render cache service.
98 * @param array $renderer_config
99 * The renderer configuration array.
101 public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, array $renderer_config) {
102 $this->titleResolver = $title_resolver;
103 $this->displayVariantManager = $display_variant_manager;
104 $this->eventDispatcher = $event_dispatcher;
105 $this->moduleHandler = $module_handler;
106 $this->renderer = $renderer;
107 $this->renderCache = $render_cache;
108 $this->rendererConfig = $renderer_config;
114 * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'.
116 public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
117 list($page, $title) = $this->prepare($main_content, $request, $route_match);
119 if (!isset($page['#type']) || $page['#type'] !== 'page') {
120 throw new \LogicException('Must be #type page');
123 $page['#title'] = $title;
125 // Now render the rendered page.html.twig template inside the html.html.twig
126 // template, and use the bubbled #attached metadata from $page to ensure we
127 // load all attached assets.
133 // The special page regions will appear directly in html.html.twig, not in
134 // page.html.twig, hence add them here, just before rendering html.html.twig.
135 $this->buildPageTopAndBottom($html);
137 // Render, but don't replace placeholders yet, because that happens later in
138 // the render pipeline. To not replace placeholders yet, we use
139 // RendererInterface::render() instead of RendererInterface::renderRoot().
140 // @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor.
141 $render_context = new RenderContext();
142 $this->renderer->executeInRenderContext($render_context, function() use (&$html) {
143 // RendererInterface::render() renders the $html render array and updates
144 // it in place. We don't care about the return value (which is just
145 // $html['#markup']), but about the resulting render array.
146 // @todo Simplify this when https://www.drupal.org/node/2495001 lands.
147 $this->renderer->render($html);
149 // RendererInterface::render() always causes bubbleable metadata to be
150 // stored in the render context, no need to check it conditionally.
151 $bubbleable_metadata = $render_context->pop();
152 $bubbleable_metadata->applyTo($html);
153 $content = $this->renderCache->getCacheableRenderArray($html);
155 // Also associate the required cache contexts.
156 // (Because we use ::render() above and not ::renderRoot(), we manually must
157 // ensure the HTML response varies by the required cache contexts.)
158 $content['#cache']['contexts'] = Cache::mergeContexts($content['#cache']['contexts'], $this->rendererConfig['required_cache_contexts']);
160 // Also associate the "rendered" cache tag. This allows us to invalidate the
161 // entire render cache, regardless of the cache bin.
162 $content['#cache']['tags'][] = 'rendered';
164 $response = new HtmlResponse($content, 200, [
165 'Content-Type' => 'text/html; charset=UTF-8',
172 * Prepares the HTML body: wraps the main content in #type 'page'.
174 * @param array $main_content
175 * The render array representing the main content.
176 * @param \Symfony\Component\HttpFoundation\Request $request
177 * The request object, for context.
178 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
179 * The route match, for context.
182 * An array with two values:
183 * 0. A #type 'page' render array.
186 * @throws \LogicException
187 * If the selected display variant does not implement PageVariantInterface.
189 protected function prepare(array $main_content, Request $request, RouteMatchInterface $route_match) {
190 // Determine the title: use the title provided by the main content if any,
191 // otherwise get it from the routing information.
192 $get_title = function (array $main_content) use ($request, $route_match) {
193 return isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
196 // If the _controller result already is #type => page,
197 // we have no work to do: The "main content" already is an entire "page"
198 // (see html.html.twig).
199 if (isset($main_content['#type']) && $main_content['#type'] === 'page') {
200 $page = $main_content;
201 $title = $get_title($page);
203 // Otherwise, render it as the main content of a #type => page, by selecting
204 // page display variant to do that and building that page display variant.
206 // Select the page display variant to be used to render this main content,
207 // default to the built-in "simple page".
208 $event = new PageDisplayVariantSelectionEvent('simple_page', $route_match);
209 $this->eventDispatcher->dispatch(RenderEvents::SELECT_PAGE_DISPLAY_VARIANT, $event);
210 $variant_id = $event->getPluginId();
212 // We must render the main content now already, because it might provide a
213 // title. We set its $is_root_call parameter to FALSE, to ensure
214 // placeholders are not yet replaced. This is essentially "pre-rendering"
215 // the main content, the "full rendering" will happen in
216 // ::renderResponse().
217 // @todo Remove this once https://www.drupal.org/node/2359901 lands.
218 if (!empty($main_content)) {
219 $this->renderer->executeInRenderContext(new RenderContext(), function() use (&$main_content) {
220 if (isset($main_content['#cache']['keys'])) {
221 // Retain #title, otherwise, dynamically generated titles would be
222 // missing for controllers whose entire returned render array is
224 $main_content['#cache_properties'][] = '#title';
226 return $this->renderer->render($main_content, FALSE);
228 $main_content = $this->renderCache->getCacheableRenderArray($main_content) + [
229 '#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL
233 $title = $get_title($main_content);
235 // Instantiate the page display, and give it the main content.
236 $page_display = $this->displayVariantManager->createInstance($variant_id);
237 if (!$page_display instanceof PageVariantInterface) {
238 throw new \LogicException('Cannot render the main content for this page because the provided display variant does not implement PageVariantInterface.');
241 ->setMainContent($main_content)
243 ->addCacheableDependency($event)
244 ->setConfiguration($event->getPluginConfiguration());
245 // Some display variants need to be passed an array of contexts with
246 // values because they can't get all their contexts globally. For example,
247 // in Page Manager, you can create a Page which has a specific static
248 // context (e.g. a context that refers to the Node with nid 6), if any
249 // such contexts were added to the $event, pass them to the $page_display.
250 if ($page_display instanceof ContextAwareVariantInterface) {
251 $page_display->setContexts($event->getContexts());
254 // Generate a #type => page render array using the page display variant,
255 // the page display will build the content for the various page regions.
259 $page += $page_display->build();
262 // $page is now fully built. Find all non-empty page regions, and add a
263 // theme wrapper function that allows them to be consistently themed.
264 $regions = \Drupal::theme()->getActiveTheme()->getRegions();
265 foreach ($regions as $region) {
266 if (!empty($page[$region])) {
267 $page[$region]['#theme_wrappers'][] = 'region';
268 $page[$region]['#region'] = $region;
272 // Allow hooks to add attachments to $page['#attached'].
273 $this->invokePageAttachmentHooks($page);
275 return [$page, $title];
279 * Invokes the page attachment hooks.
281 * @param array &$page
282 * A #type 'page' render array, for which the page attachment hooks will be
283 * invoked and to which the results will be added.
285 * @throws \LogicException
289 * @see hook_page_attachments()
290 * @see hook_page_attachments_alter()
292 public function invokePageAttachmentHooks(array &$page) {
293 // Modules can add attachments.
295 foreach ($this->moduleHandler->getImplementations('page_attachments') as $module) {
296 $function = $module . '_page_attachments';
297 $function($attachments);
299 if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
300 throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments().');
303 // Modules and themes can alter page attachments.
304 $this->moduleHandler->alter('page_attachments', $attachments);
305 \Drupal::theme()->alter('page_attachments', $attachments);
306 if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
307 throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments_alter().');
310 // Merge the attachments onto the $page render array.
311 $page = $this->renderer->mergeBubbleableMetadata($page, $attachments);
315 * Invokes the page top and bottom hooks.
317 * @param array &$html
318 * A #type 'html' render array, for which the page top and bottom hooks will
319 * be invoked, and to which the 'page_top' and 'page_bottom' children (also
320 * render arrays) will be added (if non-empty).
322 * @throws \LogicException
326 * @see hook_page_top()
327 * @see hook_page_bottom()
328 * @see html.html.twig
330 public function buildPageTopAndBottom(array &$html) {
331 // Modules can add render arrays to the top and bottom of the page.
334 foreach ($this->moduleHandler->getImplementations('page_top') as $module) {
335 $function = $module . '_page_top';
336 $function($page_top);
338 foreach ($this->moduleHandler->getImplementations('page_bottom') as $module) {
339 $function = $module . '_page_bottom';
340 $function($page_bottom);
342 if (!empty($page_top)) {
343 $html['page_top'] = $page_top;
345 if (!empty($page_bottom)) {
346 $html['page_bottom'] = $page_bottom;