Render arrays
A Drupal controller returns not finished HTML but a render array — a description of what to show. The theme system builds the markup from it. Thanks to that, the site theme and other modules can change the output without touching your code.
The simplest array
// web/modules/custom/blog/src/Controller/BlogController.php
public function list(): array
{
return [
'#markup' => '<p>The list of articles will be here.</p>',
];
}The /blog page shows a paragraph of text. A key with a hash sign is a property: #markup means "output this markup".
Nested elements
Keys without a hash sign are nested elements. Each of them is a render array too:
// web/modules/custom/blog/src/Controller/BlogController.php
public function list(): array
{
return [
'intro' => [
'#markup' => '<p>The latest blog articles.</p>',
],
'articles' => [
'#theme' => 'item_list',
'#items' => ['First article', 'Second article', 'Third article'],
],
];
}The page shows a paragraph and a bulleted list of three items under it. Elements are output in the order they are written; the names intro and articles are your own choice.
Two kinds of elements
The #theme property names a template that receives the other properties: item_list is the list template from core. The #type property names a ready-made element with its own logic: a table, a link, a form field.
| Entry | What it outputs |
|---|---|
'#markup' => '…' | Markup. Dangerous tags are stripped |
'#plain_text' => '…' | Text with full escaping |
'#theme' => 'item_list' | A list: #items, #title |
'#type' => 'table' | A table: #header, #rows |
'#type' => 'link' | A link: #title, #url |
'#type' => 'html_tag' | A single tag: #tag, #value |
'#type' => 'details' | A collapsible block: #title |
A link
// web/modules/custom/blog/src/Controller/BlogController.php
use Drupal\Core\Url;
$build['about'] = [
'#type' => 'link',
'#title' => $this->t('About the blog'),
'#url' => Url::fromRoute('blog.about'),
];The element outputs a link to the blog.about route. The address is given as a Url object, not a string.
A table
// web/modules/custom/blog/src/Controller/BlogController.php
$build['table'] = [
'#type' => 'table',
'#header' => [$this->t('Article'), $this->t('Author')],
'#rows' => [
['First article', 'Anna'],
['Second article', 'Ivan'],
],
'#empty' => $this->t('No articles yet.'),
];Each row of #rows is an array of cells. If there are no rows, the text from #empty is shown instead of the table.
Common properties
| Property | Purpose |
|---|---|
#prefix, #suffix | Markup before and after the element |
#attributes | Tag attributes, for example ['class' => ['blog-list']] |
#weight | The order among sibling elements |
#access | FALSE — do not output the element |
#attached | Attaching styles and scripts |
#cache | Caching rules |
The last two properties have lessons of their own.
Common mistake
The #markup property lets safe tags like strong and a through, so it does not suit visitor text that must be shown as is. Use #plain_text for that: everything is escaped there. And do not build HTML by gluing strings from user data — markup belongs in Twig templates, the subject of the next lesson.