Rendering pages in Drupal with a layered pipeline
Rendering is the act of turning data structures into content, usually HTML. Before we go into the details of what data structures Drupal's rendering engine demands, let's consider an overview of how requests, responses and rendering all work together in Drupal.
Drupal and Symfony communicate between themselves to determine how an incoming HTTP request should be dealt with, to generate the response, and to deliver the response back to the browser. This is often referred to as the Drupal 8 render pipeline; in practice, it's a set of embedded pipeline layers.
Very broadly, the following details a typical request/response cycle in Drupal, with each level of indentation representing another layer in the pipeline:
/index.phpcreates a HTTP
$requestfrom globals then calls
DrupalKernelretrieves the Symfony
HttpKernel(wrapped up with middleware using
StackedHttpKernel) and calls
HttpKerneldetermines the route matched by
$requestand the controller handling the route, using a
HttpKernelchecks to see if the controller has returned a
Response $responseobject, or something else.
HttpKernelpasses any something-else that looks like a render(able) data structure on to the
MainContentRendererInterface::renderResponse()method that matches the desired response content type e.g.
HtmlRenderer::renderResponse(), using a
Rendererturns render(able) arrays recursively into markup.
- This might be considered Drupal's "rendering engine", and we'll describe it separately below.
HtmlRendererturns the markup into a HTTP 200
- (event handling finishes)
HttpKernelpermits just-in-time replacement of any placeholders (see below) using a
ChainedPlaceholderStrategydoes one last sweep of placeholders, permitting modifictations; called from
Renderer::renderRoot()make one more render pass over the content; unlike
::render()above, it injects placeholders.
- (event handling finishes)
- (Symfony kernel hands back to Drupal)
- (Drupal kernel hands back to the executable)
$responseand terminates the
Symfony's key contribution to the above is the
HttpKernel and the concept of event subscription, which we've discussed in a previous tutorial.
There are obviously other potential logical routes, to handle such circumstances as an uninstalled site, or a HTTP error condition (40x errors, 30x redirects etc.) but this captures the broad idea of a set of pipeline layers, each one handing off code execution to the layer below it (often using event subscription) and then dealing with the returned values.
A closer look at the rendering engine: the lowest pipeline layer
The deepest layer of pipeline above—the Renderer class—is available as the
renderer service for the purposes of dependency injection (at the command line,
Renderer layer, we can identify three or four separate sub-layers:
public function renderRoot()
- The "safest" method to use, from outside the rendering process; indeed, this is how you might render markup at the command line (see below). This performs a full rendering sweep, and then replaces any placeholders: these are markers in the HTML, like "@link_to_current_user", that might sometimes be the only markup that changes from page to page: hence, Drupal can hold off replacing them until the very end, to take advantage of caching earlier on. Internally, this method calls:
public function render()
- This can be invoked from outside the rendering process, but the calling object must do some other heavy lifting: providing the method with a persisting "render context" for storing information that might be needed between render calls; dealing with placeholder replacement somehow (
render()does not by default do it, although
renderRoot()can prompt it to do so.) The method is actually a "safer" wrapper for:
protected function doRender()
- An internal method, which calls itself recursively, to travel first down into the lowermost "children" of the renderable array, and then gradually travel back up, processing each layer of children in turn. It's a long and complex method, which performs lots of different checks before (usually) handing off the job of creating each fragment of markup to:
- the theme API
- which we don't discuss here; handing-off points are usually identified by the presence of a
#themeproperty in a renderable array. This layer handles the autodiscovery, parsing and caching of Twig templates, and the turning of provided further properties (node objects, user accounts etc.) into human-readable markup.
Normally, though, your own code won't need to call any of these layers directly, as rendering is controlled entirely by renderable arrays: as we'll see next.
Demonstrating different renderable arrays with a new block
Let's create another block in our custom module, and use that to demonstrate a number of possible options in renderable arrays.
Save the following class as
src/Plugin/Block/RenderBlock.php within the
We've discussed blocks previously, including dependency injection, so we won't reiterate on that; except to note that we're injecting the
current_user service into our block, so we have access to the currently logged-in user's account.
The block contains four renderable sub-arrays, demonstrating four different permutations of attributes:
- Includes tags not normally permitted for security reasons; requires a
#allowed_tagsarray listing which are permitted.
- Includes a placeholder; placeholders are each themselves represented as render arrays, so they could be rendered at the last possible moment.
- Uses the
usernametheme function, corresponding to the
- Uses the
usernametheme function; this time, includes the current user's account as a
These four are wrapped up in a containing renderable array, which uses the theme function
item_list and references these children as the
If you're tweaking this code to see how it works, be warned that, for logged-in users, blocks are strongly cached, and you'll need to clear the cache to see any changes. You can play with the
#cache settings on any given renderable array, although that's out of the scope of this tutorial. Nicely, though, for the sake of this example: blocks are cached on a per-user basis; this means that the
current_user won't accidentally show a link to user A in the block for user B.
What you should see
Firstly, as is usually the case, you should install Drush, then rebuild caches so that Drupal finds the new block. As this has been discussed in previous tutorials, we don't discuss it here.
Before we look at the block, we'll just call the renderer service directly at the command line:
drush php-eval '$renderable = ["#markup" => "Test"]; print \Drupal::service("renderer")->renderRoot($renderable);'
This should output the following, with no terminating newline:
For reference, then, you can use this service directly in future to trial all sorts of renderable arrays, and see how they turn out in isolation from the rest of the page's rendering.
To enable the block created above, (as previously) you should log in and navigate to Structure > Block layout and click "Place block" by the "Sidebar first" region. In the modal popup you should see "Render block": click this and go through the process of adding it to the region as before.
When you navigate back to the front page of the website, you should see this block in place:
From top to bottom, you can see in the block:
- The results of the
<script>tag, allowed to pass unfiltered to the browser and there executed.
- The placeholder text replaced during the final
usernametheme hook, called with no account and defaulting to an unverified anonymous (and thus unlinked) username.
usernametheme hook, called with the current user's account and hence generating a link to that user's profile.
If you can see all this, then congratulations! you have successfully returned renderable arrays from a block and demonstrated some of the potential of renderable arrays.