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
.