Twig templates and hook_theme
A template is a file with markup that data is inserted into. In Drupal templates are written in the Twig language. Your own template is needed when ready-made elements such as a list and a table are not enough and you want your own markup — for example an article card.
Three steps make a template work: declare it, create the file and use it in a render array.
Step 1. Declaring
Templates are declared by the blog_theme() function in the blog.module file. This file lives in the root of the module folder and holds hook functions — a separate lesson covers them. Create it by hand:
// web/modules/custom/blog/blog.module
/**
* Implements hook_theme().
*/
function blog_theme(): array
{
return [
'blog_card' => [
'variables' => ['title' => '', 'text' => '', 'tags' => []],
],
];
}In the real file the first line is <?php. The function declares the blog_card template with three variables and their default values.
Step 2. The template file
Module templates live in the templates folder. The file name is the template name with underscores replaced by hyphens and the .html.twig ending:
{# web/modules/custom/blog/templates/blog-card.html.twig #}
<article class="blog-card">
<h2>{{ title }}</h2>
<p>{{ text }}</p>
</article>Double curly braces output the value of a variable. It is escaped automatically: tags in the text turn into safe characters.
Step 3. Using it
// web/modules/custom/blog/src/Controller/BlogController.php
public function list(): array
{
return [
'#theme' => 'blog_card',
'#title' => 'First article',
'#text' => 'A short description of the article.',
];
}The template name goes into #theme, and the variables are properties with a hash sign. After drush cr the /blog page shows a card with a heading and a paragraph.
Conditions and loops
Twig commands are written in braces with a percent sign. Let us add a list of tags to the card:
{# web/modules/custom/blog/templates/blog-card.html.twig #}
<article class="blog-card">
<h2>{{ title }}</h2>
<p>{{ text }}</p>
{% if tags %}
<ul>
{% for tag in tags %}
<li>{{ tag }}</li>
{% endfor %}
</ul>
{% endif %}
</article>If you pass '#tags' => ['PHP', 'Drupal'], a list of two items appears under the text. Without tags the ul block is not output at all.
What Twig offers
| Entry | What it does |
|---|---|
{{ title }} | Outputs a variable |
{{ node.label }} | Reads a property or calls a method of an object |
{% if … %} … {% endif %} | A condition |
{% for item in items %} … {% endfor %} | A loop |
{{ path('blog.list') }} | An address by route name |
{{ title|upper }} | A filter: transforms the value |
{# text #} | A comment |
A render array held in a variable is output the same way: {{ content }}. Drupal turns it into markup itself.
Common mistake
A template is not picked up when the names do not agree. blog_theme() and #theme say blog_card with an underscore, while the file is called blog-card.html.twig — with a hyphen. Variables are passed as #title but declared in variables as title: an undeclared variable does not reach the template. After adding a template always run drush cr.