HtmlRenderer.php 11.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

/**
 * @file
 * Contains \Drupal\Core\Render\MainContent\HtmlRenderer.
 */

namespace Drupal\Core\Render\MainContent;

use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Display\PageVariantInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
17
use Drupal\Core\Render\Renderer;
18
use Drupal\Core\Render\RendererInterface;
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
use Drupal\Core\Render\RenderEvents;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Default main content renderer for HTML requests.
 */
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;

59
60
61
62
63
64
65
  /**
   * The renderer service.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

66
67
68
69
70
71
72
73
74
75
76
  /**
   * 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.
77
78
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer service.
79
   */
80
  public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer) {
81
82
83
84
    $this->titleResolver = $title_resolver;
    $this->displayVariantManager = $display_variant_manager;
    $this->eventDispatcher = $event_dispatcher;
    $this->moduleHandler = $module_handler;
85
    $this->renderer = $renderer;
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
  }

  /**
   * {@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,
    ];
    $html += element_info('html');

    // 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);

    // The three parts of rendered markup in html.html.twig (page_top, page and
    // page_bottom) must be rendered with drupal_render_root(), so that their
    // #post_render_cache callbacks are executed (which may attach additional
    // assets).
    // html.html.twig must be able to render the final list of attached assets,
    // and hence may not execute any #post_render_cache_callbacks (because they
    // might add yet more assets to be attached), and therefore it must be
    // rendered with drupal_render(), not drupal_render_root().
123
    $this->renderer->render($html['page'], TRUE);
124
    if (isset($html['page_top'])) {
125
      $this->renderer->render($html['page_top'], TRUE);
126
127
    }
    if (isset($html['page_bottom'])) {
128
      $this->renderer->render($html['page_bottom'], TRUE);
129
    }
130
    $content = $this->renderer->render($html);
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169

    // Store the cache tags associated with this page in a X-Drupal-Cache-Tags
    // header. Also associate the "rendered" cache tag. This allows us to
    // invalidate the entire render cache, regardless of the cache bin.
    $cache_tags = Cache::mergeTags(
      isset($html['page_top']) ? $html['page_top']['#cache']['tags'] : [],
      $html['page']['#cache']['tags'],
      isset($html['page_bottom']) ? $html['page_bottom']['#cache']['tags'] : [],
      ['rendered']
    );

    // Set the generator in the HTTP header.
    list($version) = explode('.', \Drupal::VERSION, 2);

    return new Response($content, 200,[
      'X-Drupal-Cache-Tags' => implode(' ', $cache_tags),
      'X-Generator' => 'Drupal ' . $version . ' (http://drupal.org)'
    ]);
  }

  /**
   * 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) {
170
171
172
    // 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).
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
    if (isset($main_content['#type']) && $main_content['#type'] === 'page') {
      $page = $main_content;
    }
    // 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
      // #post_render_cache callbacks are not yet applied. This is essentially
      // "pre-rendering" the main content, the "full rendering" will happen in
189
      // ::renderResponse().
190
191
      // @todo Remove this once https://www.drupal.org/node/2359901 lands.
      if (!empty($main_content)) {
192
        $this->renderer->render($main_content, FALSE);
193
194
        $main_content = $this->renderer->getCacheableRenderArray($main_content) + [
          '#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
        ];
      }

      // 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);

      // Generate a #type => page render array using the page display variant,
      // the page display will build the content for the various page regions.
      $page = array(
        '#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 = system_region_list(\Drupal::theme()->getActiveTheme()->getName());
    foreach (array_keys($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);

    // Determine the title: use the title provided by the main content if any,
    // otherwise get it from the routing information.
    $title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());

    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', '#post_render_cache']) !== []) {
      throw new \LogicException('Only #attached and #post_render_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', '#post_render_cache']) !== []) {
      throw new \LogicException('Only #attached and #post_render_cache may be set in hook_page_attachments_alter().');
    }

    // Merge the attachments onto the $page render array.
266
    $page = Renderer::mergeBubbleableMetadata($page, $attachments);
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
  }

  /**
   * 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;
    }
  }

}