5966668af3506fcf0e54963f256930a2436166d6
[yaffs-website] / web / core / lib / Drupal / Core / Render / MainContent / HtmlRenderer.php
1 <?php
2
3 namespace Drupal\Core\Render\MainContent;
4
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;
20
21 /**
22  * Default main content renderer for HTML requests.
23  *
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
30  */
31 class HtmlRenderer implements MainContentRendererInterface {
32
33   /**
34    * The title resolver.
35    *
36    * @var \Drupal\Core\Controller\TitleResolverInterface
37    */
38   protected $titleResolver;
39
40   /**
41    * The display variant manager.
42    *
43    * @var \Drupal\Component\Plugin\PluginManagerInterface
44    */
45   protected $displayVariantManager;
46
47   /**
48    * The event dispatcher.
49    *
50    * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
51    */
52   protected $eventDispatcher;
53   /**
54    * The module handler.
55    *
56    * @var \Drupal\Core\Extension\ModuleHandlerInterface
57    */
58   protected $moduleHandler;
59
60   /**
61    * The renderer service.
62    *
63    * @var \Drupal\Core\Render\RendererInterface
64    */
65   protected $renderer;
66
67   /**
68    * The render cache service.
69    *
70    * @var \Drupal\Core\Render\RenderCacheInterface
71    */
72   protected $renderCache;
73
74   /**
75    * The renderer configuration array.
76    *
77    * @see sites/default/default.services.yml
78    *
79    * @var array
80    */
81   protected $rendererConfig;
82
83   /**
84    * Constructs a new HtmlRenderer.
85    *
86    * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
87    *   The 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
93    *   The 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.
100    */
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;
109   }
110
111   /**
112    * {@inheritdoc}
113    *
114    * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'.
115    */
116   public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
117     list($page, $title) = $this->prepare($main_content, $request, $route_match);
118
119     if (!isset($page['#type']) || $page['#type'] !== 'page') {
120       throw new \LogicException('Must be #type page');
121     }
122
123     $page['#title'] = $title;
124
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.
128     $html = [
129       '#type' => 'html',
130       'page' => $page,
131     ];
132
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);
136
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);
148     });
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);
154
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']);
159
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';
163
164     $response = new HtmlResponse($content, 200, [
165       'Content-Type' => 'text/html; charset=UTF-8',
166     ]);
167
168     return $response;
169   }
170
171   /**
172    * Prepares the HTML body: wraps the main content in #type 'page'.
173    *
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.
180    *
181    * @return array
182    *   An array with two values:
183    *   0. A #type 'page' render array.
184    *   1. The page title.
185    *
186    * @throws \LogicException
187    *   If the selected display variant does not implement PageVariantInterface.
188    */
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());
194     };
195
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);
202     }
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.
205     else {
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();
211
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
223             // render cached.
224             $main_content['#cache_properties'][] = '#title';
225           }
226           return $this->renderer->render($main_content, FALSE);
227         });
228         $main_content = $this->renderCache->getCacheableRenderArray($main_content) + [
229           '#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL
230         ];
231       }
232
233       $title = $get_title($main_content);
234
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.');
239       }
240       $page_display
241         ->setMainContent($main_content)
242         ->setTitle($title)
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());
252       }
253
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.
256       $page = [
257         '#type' => 'page',
258       ];
259       $page += $page_display->build();
260     }
261
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;
269       }
270     }
271
272     // Allow hooks to add attachments to $page['#attached'].
273     $this->invokePageAttachmentHooks($page);
274
275     return [$page, $title];
276   }
277
278   /**
279    * Invokes the page attachment hooks.
280    *
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.
284    *
285    * @throws \LogicException
286    *
287    * @internal
288    *
289    * @see hook_page_attachments()
290    * @see hook_page_attachments_alter()
291    */
292   public function invokePageAttachmentHooks(array &$page) {
293     // Modules can add attachments.
294     $attachments = [];
295     foreach ($this->moduleHandler->getImplementations('page_attachments') as $module) {
296       $function = $module . '_page_attachments';
297       $function($attachments);
298     }
299     if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
300       throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments().');
301     }
302
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().');
308     }
309
310     // Merge the attachments onto the $page render array.
311     $page = $this->renderer->mergeBubbleableMetadata($page, $attachments);
312   }
313
314   /**
315    * Invokes the page top and bottom hooks.
316    *
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).
321    *
322    * @throws \LogicException
323    *
324    * @internal
325    *
326    * @see hook_page_top()
327    * @see hook_page_bottom()
328    * @see html.html.twig
329    */
330   public function buildPageTopAndBottom(array &$html) {
331     // Modules can add render arrays to the top and bottom of the page.
332     $page_top = [];
333     $page_bottom = [];
334     foreach ($this->moduleHandler->getImplementations('page_top') as $module) {
335       $function = $module . '_page_top';
336       $function($page_top);
337     }
338     foreach ($this->moduleHandler->getImplementations('page_bottom') as $module) {
339       $function = $module . '_page_bottom';
340       $function($page_bottom);
341     }
342     if (!empty($page_top)) {
343       $html['page_top'] = $page_top;
344     }
345     if (!empty($page_bottom)) {
346       $html['page_bottom'] = $page_bottom;
347     }
348   }
349
350 }