Change record status: 
Project: 
Introduced in branch: 
8.x
Description: 

The cornerstone of this change is the introduction a new HtmlPage object, which is strictly a data object representing a page. The HtmlPage object represents a document as a structured object that includes the body and css classes, plus the contents of the HEAD element. The body is already string-ified, ie, the render array is already rendered.

This means that the page is no longer rendered as one massive array from HTML tag to HTML tag. Instead, it is rendered as a slightly less-massive array for the contents of the body tag, and then separately for the HTML page itself, treating the body as an opaque string.

The main wrapping _controller (HTML, Ajax response) is determined exclusively by the Accept header. What that means is that any wrapping _controller can rely on _controller being the thing that gives you a render array that is your body. That thing may be a service, or a controller string, or a closure, doesn't matter. It returns the body, and the wrapping _controller does with it whatever is appropriate. "Appropriate" in this case is to either return a Response (for the Ajax API-using controllers) or to return an HtmlPage object. It is never, ever to return a render array further down the pipeline. Just an HtmlPage object.

It is then the responsibility of a view listener to, only, render HtmlPage into an HTML string. At present that's still done using the same tools (render arrays and html.html.twig), but we've therefore separated the body from the wrapper.

This approach can be boiled down to the phrase: "return structured object (HtmlPage) from controller / mutate to Response in the view listener".

The net result is that there is a list of data structures of increasing levels of abstraction: String -> render array -> HtmlFragment -> HtmlPage -> Response. A _controller callable can return any of those it wants. A _controller can return any of the last 3, which are at the level of granularity that makes sense to be coming back from the controller layer. View listeners then convert any of those objects to the next higher object, until we get to a Response. Contrib listeners may then intercept that process at any level they want to modify the object or take over the entire process. That's a very low-complexity but high-power pipeline that gives contrib developers an enormous amount of flexibility.

Let’s look at the HtmlViewSubscriber that implements EventSubscriberInterface.

The HtmlViewSubscriber is registered as a service in core.services.yml

html_view_subscriber:
  class: Drupal\Core\EventSubscriber\HtmlViewSubscriber
  tags:
    - { name: event_subscriber }
  arguments: ['@html_page_renderer']

When an onHtmlPage event is fired from the Kernal, the HtmlViewSubscriber’s onHtmlPage listener extracts the controller-returned content, as described above, renders it, and sets the resulting string as the Response content.

/**
 * Renders an HtmlPage object to a Response.
 *
 * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
 *   The Event to process.
 */
public function onHtmlPage(GetResponseForControllerResultEvent $event) {
  $page = $event->getControllerResult();
  if ($page instanceof HtmlPage) {
    // In case renderPage() returns NULL due to an error cast it to a string
    // so as to not cause issues with Response. This also allows renderPage
    // to return an object implementing __toString(), but that is not
    // recommended.
    $response = new Response((string) $this->renderer->renderPage($page), $page->getStatusCode());
    $event->setResponse($response);
  }
}

The renderer in the code above will by default be the DefaultHtmlPageRenderer. This object is responsible for returning an HtmlPage object. Its render method processes the familiar render array content for a page that used to live in ..

public function render(HtmlFragment $fragment, $status_code = 200) {
  $page = new HtmlPage('', $fragment->getTitle());

  $page_content['main'] = array(
    '#markup' => $fragment->getContent(),
  );
  $page_content['#title'] = $page->getTitle();

  $page_array = drupal_prepare_page($page_content);

  $page = $this->preparePage($page, $page_array);

  $page->setBodyTop(drupal_render($page_array['page_top']));
  $page->setBodyBottom(drupal_render($page_array['page_bottom']));
  $page->setContent(drupal_render($page_array));

  $page->setStatusCode($status_code);

  return $page;
}

Actual API change for people writing controllers

If you want your controller to return a page, you no longer have to put _controller into your route definition instead you can use _controller.

Impacts: 
Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Not done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other: 
Other updates done