PageRenderTime 62ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

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

http://github.com/drupal/drupal
PHP | 350 lines | 145 code | 34 blank | 171 comment | 14 complexity | 0c2e61c46d75f7f56f5cf901824f3f7f MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\Core\Render\MainContent;
  3. use Drupal\Component\Plugin\PluginManagerInterface;
  4. use Drupal\Core\Cache\Cache;
  5. use Drupal\Core\Controller\TitleResolverInterface;
  6. use Drupal\Core\Display\PageVariantInterface;
  7. use Drupal\Core\Extension\ModuleHandlerInterface;
  8. use Drupal\Core\Display\ContextAwareVariantInterface;
  9. use Drupal\Core\Render\HtmlResponse;
  10. use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
  11. use Drupal\Core\Render\RenderCacheInterface;
  12. use Drupal\Core\Render\RenderContext;
  13. use Drupal\Core\Render\RendererInterface;
  14. use Drupal\Core\Render\RenderEvents;
  15. use Drupal\Core\Routing\RouteMatchInterface;
  16. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  17. use Symfony\Component\HttpFoundation\Request;
  18. /**
  19. * Default main content renderer for HTML requests.
  20. *
  21. * For attachment handling of HTML responses:
  22. * @see template_preprocess_html()
  23. * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface
  24. * @see \Drupal\Core\Render\BareHtmlPageRenderer
  25. * @see \Drupal\Core\Render\HtmlResponse
  26. * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
  27. */
  28. class HtmlRenderer implements MainContentRendererInterface {
  29. /**
  30. * The title resolver.
  31. *
  32. * @var \Drupal\Core\Controller\TitleResolverInterface
  33. */
  34. protected $titleResolver;
  35. /**
  36. * The display variant manager.
  37. *
  38. * @var \Drupal\Component\Plugin\PluginManagerInterface
  39. */
  40. protected $displayVariantManager;
  41. /**
  42. * The event dispatcher.
  43. *
  44. * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
  45. */
  46. protected $eventDispatcher;
  47. /**
  48. * The module handler.
  49. *
  50. * @var \Drupal\Core\Extension\ModuleHandlerInterface
  51. */
  52. protected $moduleHandler;
  53. /**
  54. * The renderer service.
  55. *
  56. * @var \Drupal\Core\Render\RendererInterface
  57. */
  58. protected $renderer;
  59. /**
  60. * The render cache service.
  61. *
  62. * @var \Drupal\Core\Render\RenderCacheInterface
  63. */
  64. protected $renderCache;
  65. /**
  66. * The renderer configuration array.
  67. *
  68. * @see sites/default/default.services.yml
  69. *
  70. * @var array
  71. */
  72. protected $rendererConfig;
  73. /**
  74. * Constructs a new HtmlRenderer.
  75. *
  76. * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
  77. * The title resolver.
  78. * @param \Drupal\Component\Plugin\PluginManagerInterface $display_variant_manager
  79. * The display variant manager.
  80. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
  81. * The event dispatcher.
  82. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  83. * The module handler.
  84. * @param \Drupal\Core\Render\RendererInterface $renderer
  85. * The renderer service.
  86. * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
  87. * The render cache service.
  88. * @param array $renderer_config
  89. * The renderer configuration array.
  90. */
  91. public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, array $renderer_config) {
  92. $this->titleResolver = $title_resolver;
  93. $this->displayVariantManager = $display_variant_manager;
  94. $this->eventDispatcher = $event_dispatcher;
  95. $this->moduleHandler = $module_handler;
  96. $this->renderer = $renderer;
  97. $this->renderCache = $render_cache;
  98. $this->rendererConfig = $renderer_config;
  99. }
  100. /**
  101. * {@inheritdoc}
  102. *
  103. * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'.
  104. */
  105. public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
  106. list($page, $title) = $this->prepare($main_content, $request, $route_match);
  107. if (!isset($page['#type']) || $page['#type'] !== 'page') {
  108. throw new \LogicException('Must be #type page');
  109. }
  110. $page['#title'] = $title;
  111. // Now render the rendered page.html.twig template inside the html.html.twig
  112. // template, and use the bubbled #attached metadata from $page to ensure we
  113. // load all attached assets.
  114. $html = [
  115. '#type' => 'html',
  116. 'page' => $page,
  117. ];
  118. // The special page regions will appear directly in html.html.twig, not in
  119. // page.html.twig, hence add them here, just before rendering html.html.twig.
  120. $this->buildPageTopAndBottom($html);
  121. // Render, but don't replace placeholders yet, because that happens later in
  122. // the render pipeline. To not replace placeholders yet, we use
  123. // RendererInterface::render() instead of RendererInterface::renderRoot().
  124. // @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor.
  125. $render_context = new RenderContext();
  126. $this->renderer->executeInRenderContext($render_context, function () use (&$html) {
  127. // RendererInterface::render() renders the $html render array and updates
  128. // it in place. We don't care about the return value (which is just
  129. // $html['#markup']), but about the resulting render array.
  130. // @todo Simplify this when https://www.drupal.org/node/2495001 lands.
  131. $this->renderer->render($html);
  132. });
  133. // RendererInterface::render() always causes bubbleable metadata to be
  134. // stored in the render context, no need to check it conditionally.
  135. $bubbleable_metadata = $render_context->pop();
  136. $bubbleable_metadata->applyTo($html);
  137. $content = $this->renderCache->getCacheableRenderArray($html);
  138. // Also associate the required cache contexts.
  139. // (Because we use ::render() above and not ::renderRoot(), we manually must
  140. // ensure the HTML response varies by the required cache contexts.)
  141. $content['#cache']['contexts'] = Cache::mergeContexts($content['#cache']['contexts'], $this->rendererConfig['required_cache_contexts']);
  142. // Also associate the "rendered" cache tag. This allows us to invalidate the
  143. // entire render cache, regardless of the cache bin.
  144. $content['#cache']['tags'][] = 'rendered';
  145. $response = new HtmlResponse($content, 200, [
  146. 'Content-Type' => 'text/html; charset=UTF-8',
  147. ]);
  148. return $response;
  149. }
  150. /**
  151. * Prepares the HTML body: wraps the main content in #type 'page'.
  152. *
  153. * @param array $main_content
  154. * The render array representing the main content.
  155. * @param \Symfony\Component\HttpFoundation\Request $request
  156. * The request object, for context.
  157. * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
  158. * The route match, for context.
  159. *
  160. * @return array
  161. * An array with two values:
  162. * 0. A #type 'page' render array.
  163. * 1. The page title.
  164. *
  165. * @throws \LogicException
  166. * If the selected display variant does not implement PageVariantInterface.
  167. */
  168. protected function prepare(array $main_content, Request $request, RouteMatchInterface $route_match) {
  169. // Determine the title: use the title provided by the main content if any,
  170. // otherwise get it from the routing information.
  171. $get_title = function (array $main_content) use ($request, $route_match) {
  172. return isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
  173. };
  174. // If the _controller result already is #type => page,
  175. // we have no work to do: The "main content" already is an entire "page"
  176. // (see html.html.twig).
  177. if (isset($main_content['#type']) && $main_content['#type'] === 'page') {
  178. $page = $main_content;
  179. $title = $get_title($page);
  180. }
  181. // Otherwise, render it as the main content of a #type => page, by selecting
  182. // page display variant to do that and building that page display variant.
  183. else {
  184. // Select the page display variant to be used to render this main content,
  185. // default to the built-in "simple page".
  186. $event = new PageDisplayVariantSelectionEvent('simple_page', $route_match);
  187. $this->eventDispatcher->dispatch(RenderEvents::SELECT_PAGE_DISPLAY_VARIANT, $event);
  188. $variant_id = $event->getPluginId();
  189. // We must render the main content now already, because it might provide a
  190. // title. We set its $is_root_call parameter to FALSE, to ensure
  191. // placeholders are not yet replaced. This is essentially "pre-rendering"
  192. // the main content, the "full rendering" will happen in
  193. // ::renderResponse().
  194. // @todo Remove this once https://www.drupal.org/node/2359901 lands.
  195. if (!empty($main_content)) {
  196. $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$main_content) {
  197. if (isset($main_content['#cache']['keys'])) {
  198. // Retain #title, otherwise, dynamically generated titles would be
  199. // missing for controllers whose entire returned render array is
  200. // render cached.
  201. $main_content['#cache_properties'][] = '#title';
  202. }
  203. return $this->renderer->render($main_content, FALSE);
  204. });
  205. $main_content = $this->renderCache->getCacheableRenderArray($main_content) + [
  206. '#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL,
  207. ];
  208. }
  209. $title = $get_title($main_content);
  210. // Instantiate the page display, and give it the main content.
  211. $page_display = $this->displayVariantManager->createInstance($variant_id);
  212. if (!$page_display instanceof PageVariantInterface) {
  213. throw new \LogicException('Cannot render the main content for this page because the provided display variant does not implement PageVariantInterface.');
  214. }
  215. $page_display
  216. ->setMainContent($main_content)
  217. ->setTitle($title)
  218. ->addCacheableDependency($event)
  219. ->setConfiguration($event->getPluginConfiguration());
  220. // Some display variants need to be passed an array of contexts with
  221. // values because they can't get all their contexts globally. For example,
  222. // in Page Manager, you can create a Page which has a specific static
  223. // context (e.g. a context that refers to the Node with nid 6), if any
  224. // such contexts were added to the $event, pass them to the $page_display.
  225. if ($page_display instanceof ContextAwareVariantInterface) {
  226. $page_display->setContexts($event->getContexts());
  227. }
  228. // Generate a #type => page render array using the page display variant,
  229. // the page display will build the content for the various page regions.
  230. $page = [
  231. '#type' => 'page',
  232. ];
  233. $page += $page_display->build();
  234. }
  235. // $page is now fully built. Find all non-empty page regions, and add a
  236. // theme wrapper function that allows them to be consistently themed.
  237. $regions = \Drupal::theme()->getActiveTheme()->getRegions();
  238. foreach ($regions as $region) {
  239. if (!empty($page[$region])) {
  240. $page[$region]['#theme_wrappers'][] = 'region';
  241. $page[$region]['#region'] = $region;
  242. }
  243. }
  244. // Allow hooks to add attachments to $page['#attached'].
  245. $this->invokePageAttachmentHooks($page);
  246. return [$page, $title];
  247. }
  248. /**
  249. * Invokes the page attachment hooks.
  250. *
  251. * @param array &$page
  252. * A #type 'page' render array, for which the page attachment hooks will be
  253. * invoked and to which the results will be added.
  254. *
  255. * @throws \LogicException
  256. *
  257. * @internal
  258. *
  259. * @see hook_page_attachments()
  260. * @see hook_page_attachments_alter()
  261. */
  262. public function invokePageAttachmentHooks(array &$page) {
  263. // Modules can add attachments.
  264. $attachments = [];
  265. foreach ($this->moduleHandler->getImplementations('page_attachments') as $module) {
  266. $function = $module . '_page_attachments';
  267. $function($attachments);
  268. }
  269. if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
  270. throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments().');
  271. }
  272. // Modules and themes can alter page attachments.
  273. $this->moduleHandler->alter('page_attachments', $attachments);
  274. \Drupal::theme()->alter('page_attachments', $attachments);
  275. if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
  276. throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments_alter().');
  277. }
  278. // Merge the attachments onto the $page render array.
  279. $page = $this->renderer->mergeBubbleableMetadata($page, $attachments);
  280. }
  281. /**
  282. * Invokes the page top and bottom hooks.
  283. *
  284. * @param array &$html
  285. * A #type 'html' render array, for which the page top and bottom hooks will
  286. * be invoked, and to which the 'page_top' and 'page_bottom' children (also
  287. * render arrays) will be added (if non-empty).
  288. *
  289. * @throws \LogicException
  290. *
  291. * @internal
  292. *
  293. * @see hook_page_top()
  294. * @see hook_page_bottom()
  295. * @see html.html.twig
  296. */
  297. public function buildPageTopAndBottom(array &$html) {
  298. // Modules can add render arrays to the top and bottom of the page.
  299. $page_top = [];
  300. $page_bottom = [];
  301. foreach ($this->moduleHandler->getImplementations('page_top') as $module) {
  302. $function = $module . '_page_top';
  303. $function($page_top);
  304. }
  305. foreach ($this->moduleHandler->getImplementations('page_bottom') as $module) {
  306. $function = $module . '_page_bottom';
  307. $function($page_bottom);
  308. }
  309. if (!empty($page_top)) {
  310. $html['page_top'] = $page_top;
  311. }
  312. if (!empty($page_bottom)) {
  313. $html['page_bottom'] = $page_bottom;
  314. }
  315. }
  316. }