- Drupal 9 Module Development
- Daniel Sipos Antonio De Marco
- 1984字
- 2021-06-11 18:36:05
Render arrays
Render arrays also existed in the previous versions of Drupal and they were important to the theme system. Since Drupal 8, however, they have become the thing—a core part of the Render API that is responsible for transforming markup representations into actual markup.
Acknowledging my limits as a writer, I will defer to the definition found in the Drupal.org documentation, which best describes what render arrays are:
Simple, but powerful.
One of the principal reasons behind having render arrays is that they allow Drupal to delay the actual rendering of something into markup to the very last moment. What do I mean by this? For example, in Drupal 7, oftentimes as module developers we were calling the actual rendering service (the theme() function) inside a preprocessor to "render" some data in order to print the resulting string (markup) in the template. However, this made it impossible to change that data later in the pipeline, for example, in another preprocessor that comes after the one that did this rendering.
For this reason, since Drupal 8, we no longer have to/should render anything manually (except in very specific circumstances). We work with render arrays at all times. Drupal will know how to turn them into markup. This way, modules and themes can intercept render arrays at various levels in the process and make alterations.
We will now talk about render arrays and the different aspects of working with them.
The structure of a render array
Render arrays are rendered by the renderer service (RendererInterface), which traverses the array and recursively renders each level. Each level of the array can have one or more elements, which can be of two types: properties or children. The properties are the ones whose keys are preceded by a # sign, whereas children are the ones whose properties are not preceded by that sign. The children can themselves be an array with properties and children. However, each level needs to have at least one property in order to be considered a level because it is responsible for telling the render system how that level needs to be rendered. As such, property names are specific to the Render API and to the actual thing they need to render, while the names of children can be flexible. In addition to these two types (yes, I lied, there can be more than two), we can also have the variables defined by a theme hook, which are also preceded by the # sign. They are not properties per se but are known by the theme system because they have been registered inside hook_theme().
There are many properties that the Render API uses to process a render array. Some of them are quite important, such as #cache and #attached. However, there are a few that are mandatory in order for a render array to make sense, in that they define its core responsibility. The following are the properties that describe what the render array should do and each render array should have one of these.
#type
The #type property specifies that the array contains data that needs to be rendered using a particular render element. Render elements are plugins (yes, plugins) that encapsulate a defined renderable component. They essentially wrap another render array, which can use a theme hook or a more complex render array to process the data they are responsible for rendering. You can think of them as essentially standardized render arrays.
There are two types of render elements: generic and form input elements. Both have their respective plugin types, annotations and interfaces. They are similar in that they both render a standardized piece of HTML; however, form input elements have the complexity of having to deal with form processing, validation, data mapping, and so on. Remember that when we defined our form in Chapter 2, Creating Your First Module, we encountered arrays with # signs. These were (form) render elements with different options (properties).
To find examples of these two types of render elements, look for plugins that implement the ElementInterface and FormElementInterface interfaces.
#theme
The #theme property ties in strongly with what we've been talking about earlier in this chapter—theme hooks. It specifies that the render array needs to render some kind of data using one of the theme hooks defined. Together with this property, you will usually encounter other properties that map to the name of the variables the theme hook has registered in hook_theme(). These are the variables the theme system uses to render the template.
This is the property you will use in your business logic to convey that your data needs to be rendered using a specific theme hook. If you thought that you can only use theme hooks you registered, you'd be incorrect. There are many theme hooks that have been already registered by Drupal core and also contributed modules that make the life of a Drupal developer much easier. Just look inside drupal_common_theme() for a bunch of common theme hooks that you can perhaps use.
#markup
Sometimes, registering a theme hook and a template for outputting some data can be overkill. Imagine that all you have is a string you need to wrap in a <span> tag or something. In this case, you can use the #markup property, which specifies that the array directly provides the HTML string that needs to be output. Note, however, that the provided HTML string is run through \Drupal\Component\Utility\Xss::filterAdmin for sanitization (mostly, XSS protection). This is perfectly fine because if the HTML you are trying to include here is stripped out, it's a good indication that you are overusing the #markup property and should instead be registering a theme hook.
Going a bit further than just simple markup is the #plain_text property via which you can specify that the text provided by this render array needs to be escaped completely. So basically if you need to output some simple text, you have the choice between these two for very fast output.
Now, if you remember in Chapter 2, Creating Your First Module, at some point our controller returned this array:
return [
'#markup' => $this->t('Hello World')
];
This is the simplest render array you'll ever see. It has only one element, a tiny string output using the #markup property. Later in this chapter we will adjust this and use a render array provided by our HelloWorldSalutation service in order to make things a bit more themeable. That will be the section where we put into practice many of the things we learn here.
However, as small as you see this array here, it is only part of a larger hierarchical render array that builds up the entire Drupal page and that contains all sorts of blocks and other components. Also responsible for building this entire big thing is the Drupal render pipeline.
The render pipeline
In Chapter 1, Developing for Drupal 9, when we outlined a high-level example of how Drupal handles a user request in order to turn it into a response, we touched on the notion of a render pipeline. So let's see what this is about, as there are essentially two render pipelines to speak of: the Symfony render pipeline and the Drupal one.
As you know, Drupal 9 uses many Symfony components, one of which being the HTTPKernel component (http://symfony.com/doc/current/components/http_kernel.html). Its main role is to turn a user request (built from PHP super globals into a Request object) into a standardized response object that gets sent back to the user. These objects are defined in the Symfony HTTP Foundation component (http://symfony.com/components/HttpFoundation). To assist in this process, it uses the Event Dispatcher component to dispatch events meant to handle the workload on multiple layers. As we saw, this is what happens in Drupal as well.
Controllers in Drupal can return one of two things—either a Response object directly or a render array. If they return the first, the job is almost done, as the Symfony render pipeline knows exactly what to do with that (assuming the response is correct). However, if they return a render array, the Drupal render pipeline kicks in at a lower level to try to turn that into a Response. We always need a Response.
The kernel.view event is triggered in order to determine who can take care of this render array. Drupal comes with the MainContentViewSubscriber, which listens to this event and checks the request format and whether the controller has returned a render array. Based on the former, it instantiates a MainContentRendererInterface object (which, by default—and most of the time—will be the HTML-based HtmlRenderer) and asks it to turn the render array into a Response. Then, it sets the Response onto the event so that the Symfony render pipeline can continue on its merry way.
In addition to the HTML renderer, Drupal comes with a few others that need to handle different types of requests:
- The AjaxRenderer handles Ajax requests and integrates with the Ajax framework. We'll see examples of Ajax-powered functionalities later in the book.
- The DialogRenderer handles requests meant to open up a dialog on the screen.
- The ModalRenderer handles requests meant to open up a modal on the screen.
Returning to the HTML renderer, let's see what it does to turn our render arrays into actual relevant HTML on a Response object. Without going into too much detail, here is a high-level description of what it does:
- Its first goal is to build a render array that has #type => 'page' as a property because this is the render element responsible for the entire page. Meaning that if the Controller returned it, it doesn't have to do much. However, usually controllers don't include that, so it dispatches an event to determine who can build this render array.
- By default, the SimplePageVariant plugin is used for building up the page array, but with the Block module enabled, the BlockPageVariant plugin is used, taking things even further down some levels in the render pipeline. The main content area gets wrapped with blocks in the sidebar, header, footer, and so on.
- Once it has the page render array, it wraps it into yet another render element, which is #type => 'html' (responsible for things such as the <head> elements).
- Once it has the main render array of the entire page, it uses the Renderer service to traverse it and do the actual rendering at each level (and there can be many). It does so by translating render elements (#type), theme hooks (#theme), simply marked-up text bits (#markup), or plain text bits (#plain_text) into their respective HTML representations.
So, as you see, the render pipeline starts at Symfony level, goes down into Drupal territory when it encounters render arrays, but continues going down to build each component found on a page around the main content returned by the Controller. Then, it comes back up those levels, all the way until a great render array is created and can be turned into HTML. Also, as it goes back up, various metadata can bubble up to the main render array.
I purposefully left out caching from this equation, which although very important, we will cover in a later chapter. However, suffice to say, cache metadata is one such example that bubbles up from the lower levels all the way to the top and is gathered to determine page-level caching. But more on that later.
Now that we know more about render arrays, how they are structured, and the pipeline they go through, we can talk a bit about asset management from a module development perspective. Because even though it is usually a theme responsibility, module developers often have to add and use CSS and JS files to their modules, and it all happens in render arrays.