Drupal 8 API: render API

Pre-requisites

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.php creates a HTTP $request from globals then calls DrupalKernel::handle($request):
    • DrupalKernel retrieves the Symfony HttpKernel (wrapped up with middleware using StackedHttpKernel) and calls HttpKernel::handle($request).
      • HttpKernel determines the route matched by $request and the controller handling the route, using a KernelEvents::REQUEST event.
      • HttpKernel checks to see if the controller has returned a Response $response object, or something else.
      • HttpKernel passes 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 KernelEvents::VIEW event.
        • HtmlRenderer calls RendererInterface::render()
          • Renderer turns render(able) arrays recursively into markup.
          • This might be considered Drupal's "rendering engine", and we'll describe it separately below.
        • HtmlRenderer turns the markup into a HTTP 200 Response $response.
        • (event handling finishes)
      • HttpKernel permits just-in-time replacement of any placeholders (see below) using a KernelEvents::RESPONSE event.
        • ChainedPlaceholderStrategy does one last sweep of placeholders, permitting modifictations; called from HtmlResponsePlaceholderStrategySubscriber::onRespond().
        • HtmlResponseAttachmentsProcessor processes both attached placeholders and any other attached data like Javascript and CSS libraries; called from HtmlResponseSubscriber::onRespond().
          • 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)
  • /index.php sends $response and terminates the DrupalKernel.

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, \Drupal::service('renderer').

Within this 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 #theme property 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 d8api module:

<?php
 
namespace Drupal\d8api\Plugin\Block;
 
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountProxy;
 
/**
 * Block for the tutorial.
 *
 * @Block(
 *   id = "tutorial_render_block",
 *   admin_label = @Translation("Render block")
 * )
 */
class RenderBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
  /**
   * @var AccountProxy
   */
  protected $currentUser;
 
  /**
   * Standard block constructor, but with addition of the current user service.
   *
   * {@inheritDoc}
   */
  public function __construct(
    array $configuration, $plugin_id, $plugin_definition,
    AccountProxy $currentUser
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->currentUser = $currentUser;
  }
 
  /**
   * {@inheritDoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration, $plugin_id, $plugin_definition
  ) {
    return new static(
      $configuration, $plugin_id, $plugin_definition,
      $container->get('current_user')
    );
  }
 
  /**
   * {@inheritDoc}
   *
   * From BlockPluginInterface via BlockBase.
   */
  public function build() {
    return [
      '#theme' => 'item_list',
      '#items' => [
        'has_custom_tag' => [
          '#allowed_tags' => ['script'],
          '#markup' => '<script type="text/javascript">document.write("Script has run");</script>',
        ],
        'has_placeholder' => [
          '#markup' => '@replaced',
          '#attached' => [
            'placeholders' => [
              '@replaced' => ['#markup' => 'Placeholder replaced']
            ]
          ]
        ],
        'themed_no_account' => [
          '#theme' => 'username',
        ],
        'themed_with_account' => [
          '#theme' => 'username',
          '#account'=> $this->currentUser->getAccount(),
        ],
      ],
    ];
  }
 
}

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:

has_custom_tag
Includes tags not normally permitted for security reasons; requires a #allowed_tags array listing which are permitted.
has_placeholder
Includes a placeholder; placeholders are each themselves represented as render arrays, so they could be rendered at the last possible moment.
themed_no_account
Uses the username theme function, corresponding to the username.html.twig template.
themed_with_account
Uses the username theme function; this time, includes the current user's account as a #account attribute.

These four are wrapped up in a containing renderable array, which uses the theme function item_list and references these children as the #items property.

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:

Test

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:

  1. The results of the <script> tag, allowed to pass unfiltered to the browser and there executed.
  2. The placeholder text replaced during the final renderRoot() pass by HtmlResponseAttachmentsProcessor.
  3. The username theme hook, called with no account and defaulting to an unverified anonymous (and thus unlinked) username.
  4. The username theme 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.

Further reading etc.