Twig is the template engine used by Drupal 8 and other modern PHP applications. Twig's expressiveness, consistency and secure-by-default policy are still unparalleled among PHP's template engines.
In this session you'll learn advanced techniques, tips and tricks useful for real-world applications and uncommonly used features that will allow you to master Twig.
6. We won't talk about
Twig basics.
We won't provide all
the low-level details.
How can
I create a
theme? Which is
the syntax of
Twig?
Read the excellent Twig
documentation to get
those details.
10. My favorite feature
Twig defines a very
small set of features…
… which are enough to
create any template
Consistent
same syntax and
behavior since day one!
easy to learn!
20. This is how Twig resolves complex variables
<nav>{{ content.links }}</nav>
!
$content['links']
$content->links
$content->links()
$content->getLinks()
$content->isLinks()
null
Twig tries all these alternatives
and uses the first one that exists.
22. Resolving variables is quite expensive
<nav>{{ content.links }}</nav>
!
$content['links']
$content->links
$content->links()
$content->getLinks()
$content->isLinks()
null
Resolving a variable is the most
expensive Twig task, specially
for very complex templates.
23. Improving Twig performance
• Twig provides a PHP extension.
• This extension only implements
the variable resolving logic.
• See twig.sensiolabs.org/doc/
installation.html#installing-the-
c-extension
EXPECTED
PERFORMANCE
INCREASE
15%
24. Some Drupal variables names are special
!
$variables['site_slogan']['#markup'] = ...
!
{{ site_slogan.#markup }}
core/themes/bartik/bartik.theme
This doesn't work because
of the # character
25. Some Drupal variables names are special
!
$variables['site_slogan']['#markup'] = ...
!
{{ site_slogan.#markup }}
core/themes/bartik/bartik.theme
This doesn't work because
of the # character
{{ site_slogan['#markup'] }}
{{ attribute(site_slogan, '#markup') }}
29. The two recommended safeguards
{% if variable %}
...
{% endif %}
!
!
Hi {{ variable|default('user') }}
30. The two recommended safeguards
{% if variable %}
...
{% endif %}
!
!
Hi {{ variable|default('user') }}
It checks that variable is not null
or empty or zero
!
ONLY works if variable is defined
31. The two recommended safeguards
{% if variable %}
...
{% endif %}
!
!
Hi {{ variable|default('user') }}
It checks that variable is not null
or empty or zero
!
ONLY works if variable is defined
It checks that variable is not null,
empty or undefined
!
It ALWAYS works as expected
32. Combining both safeguards
{% if variable|default('user') %}
...
{% endif %} It doesn't matter if the variable is not
defined, because the expression will
always have a default value.
33. Checking that the variable is defined
{% if variable is defined %}
...
{% endif %} A good practice when the rendered
template cannot be sure about the
variables passed from the code.
!
In Drupal 8 this problem should not
happen (the variable list is strict).
34. Other safeguards available
{% if variable is null %} ... {% endif %}
!
!
{% if variable is empty %} ... {% endif %}
{% if variable is not empty %} ... {% endif %}
35. Be ready when iterating empty collections
{% for item in collection %}
...
{% else %}
There are no items.
{% endfor %}
36. Filter values before using them in the loop
{% for item in collection if item.published %}
...
{% else %}
There are no items.
{% endfor %}
37. Avoid missing templates
{{ include('menu.twig') }}
This will always work because our
theme will provide this template.
38. Avoid missing templates
{{ include('menu.twig') }}
This will always work because our
theme will provide this template.
Templates with dynamic
paths are very prone to error
{{ include('users/' ~ user.name ~ '/bio.twig') }}
39. Define fallback templates
{{ include([
'users/' ~ user.name ~ '/bio.twig',
'users/' ~ user.name ~ '/default.twig',
'common/user_bio.twig'
]) }}
Twig includes the first
template that exists
40. Avoid missing templates
• Sometimes it's not possible to provide fallback
templates.
• Moreover, in some cases, it's better to ignore the
missing template instead of displaying an error to
the user.
43. Twig filters defined by Drupal 8
{{ value|t }}
{{ value|trans }}
{{ value|passthrough }}
{{ value|placeholder }}
{{ value|drupal_escape }}
{{ value|safe_join }}
{{ value|without }}
{{ value|clean_class }}
{{ value|clean_id }}
{{ value|render }}
It's common for a long-
standing and complex project
to add and remove filters.
!
If Drupal removes a filter
used by your templates, your
site/app will break.
44. Declare filters as deprecated
new Twig_SimpleFilter('old_filter', ..., array(
'deprecated' => true,
'alternative' => 'new_filter'
));
45. Declare filters as deprecated
new Twig_SimpleFilter('old_filter', ..., array(
'deprecated' => true,
'alternative' => 'new_filter'
));
These deprecations notices are not displayed or logged
anywhere on Drupal yet.
NOTE
46. Avoid missing blocks
{% if 'title' is block %}
<title>{{ block('title') }}<title>
{% endif %}
47. This feature is not available yet. It will be included in the
upcoming 1.23 version of Twig.
NOTE
Avoid missing blocks
{% if 'title' is block %}
<title>{{ block('title') }}<title>
{% endif %}
51. <ul>
{% for i in 1..3 %}
<li>{{ i }}</li>
{% endfor %}
</ul>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
The "problem" of white spaces
Twig template HTML page
52. <ul>
{% for i in 1..3 %}
<li>{{ i }}</li>
{% endfor %}
</ul>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
The "problem" of white spaces
Twig template HTML page
53. <ul>
{% for i in 1..3 %}
<li>{{ i }}</li>
{% endfor %}
</ul>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
The "problem" of white spaces
Twig template HTML page
54. <ul>
{% for i in 1..3 %}
<li>{{ i }}</li>
{% endfor %}
</ul>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
The "problem" of white spaces
Twig template HTML page
55. <ul>
{% for i in 1..3 %}
<li>{{ i }}</li>
{% endfor %}
</ul>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
The "problem" of white spaces
Twig template HTML page
56. Removing white spaces
<ul>
{%- for i in 1..3 -%}
<li>{{ i }}</li>
{%- endfor -%}
</ul>
<ul>
{% spaceless %}
{% for i in 1..3 %}
<li>{{ i }}</li>
{% endfor %}
{% endspaceless %}
</ul>
61. White spaces around HTML attributes
<h2{{ title_attributes }}>{{ label }}</h2>
core/modules/block/templates/block.html.twig
62. White spaces around HTML attributes
<h2{{ title_attributes }}>{{ label }}</h2>
core/modules/block/templates/block.html.twig
63. White spaces around HTML attributes
<h2{{ title_attributes }}>{{ label }}</h2>
core/modules/block/templates/block.html.twig
no white space when
the attributes are empty
<h2 class="..."> ... </h2>
<h2> ... </h2>
64. Add white spaces to separate Twig & HTML
<h2 {{ title_attributes }}>{{ label }}</h2>
!
!
<h2 class="..."> ... </h2>
<h2 > ... </h2>
65. Add white spaces to separate Twig & HTML
<h2 {{ title_attributes }}>{{ label }}</h2>
!
!
<h2 class="..."> ... </h2>
<h2 > ... </h2>
white space when the
attributes are empty
66. Add white spaces to separate Twig & HTML
<h2 {{ title_attributes }}>{{ label }}</h2>
!
!
<h2 class="..."> ... </h2>
<h2 > ... </h2>
white space when the
attributes are empty
IT DOES NOT MATTER
Twig template is
more readable
HTML code with white
spaces is still valid
69. Hiding HTML code inside Twig strings
<div id="site-name"{{ hide_name ? ' class="hidden"' }}>
!
!
!
!
!
<div id="site-name">
<div id="site-name" class="hidden">
WARNING
HTML attributes
defined in Twig strings
are easy to overlook
core/themes/bartik/templates/maintenance-page.html.twig
70. Hiding HTML code inside Twig strings
<div id="site-name"{{ hide_name ? ' class="hidden"' }}>
!
!
!
!
!
<div id="site-name">
<div id="site-name" class="hidden">
WARNING
HTML attributes
defined in Twig strings
are easy to overlook
core/themes/bartik/templates/maintenance-page.html.twig
DANGER
If you miss this single
white space, the page
design breaks
71. Don't hide HTML code inside Twig strings
<div id="site-name" class="{{ hide_name ? 'hidden' }}">
!
!
!
!
!
<div id="site-name" class="">
<div id="site-name" class="hidden">
HTML & Twig
are decoupled
A single white space
won't break the page
72. Don't hide HTML code inside Twig strings
<div id="site-name" class="{{ hide_name ? 'hidden' }}">
!
!
!
!
!
<div id="site-name" class="">
<div id="site-name" class="hidden">
Valid HTML code
(tested with the W3C validator)
HTML & Twig
are decoupled
A single white space
won't break the page
76. Configure Twig behavior
# sites/default/services.yml
parameters:
twig.config:
debug: true
auto_reload: null
cache: true
Include debug information in
the rendered HTML contents
In production server,
always set it to false
78. HTML content when Twig debug is enabled
<!-- THEME DEBUG -->
<!-- THEME HOOK: 'block' -->
<!-- FILE NAME SUGGESTIONS:
* block--bartik-login.html.twig
* block--user-login-block.html.twig
* block--user.html.twig
x block.html.twig
-->
<!-- BEGIN OUTPUT from 'core/themes/bartik/templates/block.html.twig' -->
<div id="block-bartik-login" class="contextual-region block block-user block-user-login-block"
role="form">
<h2>User login</h2>
<div data-contextual-id="block:block=bartik_login:langcode=en"></div>
<div class="content">
!
<!-- THEME DEBUG -->
<!-- THEME HOOK: 'form' -->
<!-- BEGIN OUTPUT from 'core/themes/classy/templates/form/form.html.twig' -->
<form class="user-login-form" data-drupal-selector="user-login-form" action="/node?destination=/
79. How to override the current template
<!-- THEME DEBUG -->
<!-- THEME HOOK: 'block' -->
<!-- FILE NAME SUGGESTIONS:
* block--bartik-login.html.twig
* block--user-login-block.html.twig
* block--user.html.twig
x block.html.twig
-->
<!-- BEGIN OUTPUT from 'core/themes/bartik/templates/block.html.twig' -->
80. How to override the current template
<!-- THEME DEBUG -->
<!-- THEME HOOK: 'block' -->
<!-- FILE NAME SUGGESTIONS:
* block--bartik-login.html.twig
* block--user-login-block.html.twig
* block--user.html.twig
x block.html.twig
-->
<!-- BEGIN OUTPUT from 'core/themes/bartik/templates/block.html.twig' -->
Drupal tried to use all these
templates…
81. How to override the current template
<!-- THEME DEBUG -->
<!-- THEME HOOK: 'block' -->
<!-- FILE NAME SUGGESTIONS:
* block--bartik-login.html.twig
* block--user-login-block.html.twig
* block--user.html.twig
x block.html.twig
-->
<!-- BEGIN OUTPUT from 'core/themes/bartik/templates/block.html.twig' -->
…before deciding to use this
template.
Drupal tried to use all these
templates…
82. Which variables are passed to the template?
Built-in templates include
comments with the full
list of variables passed to
the Twig template.
83. Easier way to introspect all variables
<pre>
{{ dump() }}
</pre>
84. Easier way to introspect all variables
<pre>
{{ dump() }}
</pre>
It dumps the contents
of all the variables
defined in the template.
85. It's better to dump just the variables you need
<pre>
{{ dump(label, title_attributes) }}
</pre>
86. It's better to dump just the variables you need
<pre>
{{ dump(label, title_attributes) }}
</pre>
It dumps only the
given variables
93. By default, contents are escaped for HTML
Hi {{ content }}!
$content = '<strong>John</strong>';
94. By default, contents are escaped for HTML
Hi {{ content }}!
$content = '<strong>John</strong>';
What you expect…
Hi John!
What you get…
Hi <strong>John
</strong>!
95. The "raw" filter prevents the escaping
Hi {{ content|raw }}!
$content = '<strong>John</strong>';
96. The "raw" filter prevents the escaping
Hi {{ content|raw }}!
$content = '<strong>John</strong>';
What you expect…
Hi John!
What you get…
Hi John!
97. What if contents are used in URLs or JS?
<a href="...?param={{ value }}"></a>
!
!
!
<script>
var variable = "{{ content }}";
</script>
98. What if contents are used in URLs or JS?
<a href="...?param={{ value }}"></a>
!
!
!
<script>
var variable = "{{ content }}";
</script>
WRONG HTML
ESCAPING
99. Applying different escaping strategies
<a href="...?param={{ value|e('url') }}"></a>
!
!
!
<script>
var variable = "{{ content|e('js') }}";
</script>
111. Other templates can reuse this layout
{% extends 'layout.twig' %}
!
{% block title %}Community{% endblock %}
{% block content %}
<div> ... </div>
{% endblock %}
112. When should you use {% extends %}
• To create the layout of your theme.
• If your site/app is very complex, create two
inheritance levels (base layout and section layouts).
layout.twig schedule.twig training.twig
extends layout.twig extends schedule.twig
113. Lots of different ways to reuse templates
{% embed %} {% extends %}include()
{% set %} {% use %}macro()
115. When should you use include( )
• To reuse large fragments of code, such as sidebars,
navigation menus, etc.
116. Lots of different ways to reuse templates
{% embed %} {% extends %}include()
{% set %} {% use %}macro()
117. Repetitive HTML fragments
<div class="form-group">
<label for="{{ id }}">{{ label }}</label>
<input type="{{ type }}" class="form-control"
id="{{ id }}">
</div> Repeating the same HTML
code for all the form fields
is cumbersome
118. Reusing fragments with macro( )
{% macro form_field(id, label, type="text") %}
<div class="form-group">
<label for="{{ id }}">{{ label }}</label>
<input type="{{ type }}" class="form-control"
id="{{ id }}">
</div>
{% endmacro %}
120. Using "macros" inside templates
{% import _self as macro %}
!
<form>
{{ macro.form_field('first_name', 'First Name') }}
{{ macro.form_field('last_name', 'Last Name') }}
{{ macro.form_field('email', 'Email', 'email') }}
...
</form>
Before using a macro, you must
"import" them (they can be
defined in a different template)
121. When should you use macro( )
• To reuse short fragments of code, usually in the
same template (e.g. listings, grids, forms, etc.)
• If your site/app is very complex, store all the macros
in a single file and reuse it from any other template.
122. Lots of different ways to reuse templates
{% embed %} {% extends %}include()
{% set %} {% use %}macro()
125. You can't use "include" to solve this problem
{{ include('common/grid_3.twig') }}
126. You can't use "include" to solve this problem
{{ include('common/grid_3.twig') }}
you can't change the included
contents (you include both the
structure and the content)
127. You can't use "extends" to solve this problem
{% extends 'common/grid_3.twig' %}
!
{% block column1 %} ... {% endblock %}
{% block column2 %} ... {% endblock %}
{% block column3 %} ... {% endblock %}
128. You can't use "extends" to solve this problem
{% extends 'common/grid_3.twig' %}
!
{% block column1 %} ... {% endblock %}
{% block column2 %} ... {% endblock %}
{% block column3 %} ... {% endblock %}
you can't make the whole structure
of the page (grid 2, grid 3, etc.)
because you can't extend from
multiple templates at the same time
131. When should you use {% embed %}
• To reuse page structures across different templates
(e.g. grids)
132. Lots of different ways to reuse templates
{% embed %} {% extends %}include()
{% set %} {% use %}macro()
133. Reusing fragments with {% set %}
{% set navigation %}
<a href="...">Previous</a>
...
...
<a href="...">Next</a>
{% endset %}
134. Reusing fragments with {% set %}
{% set navigation %}
<a href="...">Previous</a>
...
...
<a href="...">Next</a>
{% endset %} {{ navigation }}
{{ navigation }}
135. When should you use {% set %}
• To reuse short fragments of code inside a template
(if those fragments are configurable, use a macro).
• It's like an internal include() made from inside the
template itself.
136. Lots of different ways to reuse templates
{% embed %} {% extends %}include()
{% set %} {% use %}macro()
137. When should you use {% use %}
• This is too advanced and for very specific use
cases.
• You should probably never use it when creating
themes.
139. WHY IS THIS IMPORTANT??
Because Drupal allows to
create sites with very
advanced needs.
140. Templates created on-the-fly
{% set code = 'Hi {{ name }}' %}
{% set template = template_from_string(code) %}
!
{{ include(template) }}
141. Templates created on-the-fly
{% set code = 'Hi {{ name }}' %}
{% set template = template_from_string(code) %}
!
{{ include(template) }}
{% extends template %}
{% embed template %} It works here too
143. Templates created and modified on-the-fly
{% set code = 'Hi {{ name }}' %}
{% set code = code|replace({ 'Hi': 'Bye' }) %}
!
{% set template = template_from_string(code) %}
!
{{ include(template) }}
144. Getting the source of any template
{{ source('core/modules/block/templates/
block.html.twig') }}
It gets the source of the
given template without
actually rendering it.
145. Imagine a site which allows this customization
Section 1
Section 2
Default design
146. Customizable site
• Users can provide their own Twig snippets to
customize the design and content of some sections.
• Problem: even if customization is restricted to a
group of controlled users (e.g. "editors") you can't
trust those templates.
147. Twig Sandbox
• It's used to render "untrusted templates".
• It restricts the Twig features that can be used by the
template.
• Useful for letting users create their own templates
and maintain the application safe.
149. Twig Sandbox in practice
$policy = new Twig_Sandbox_SecurityPolicy(
$tags,
$filters,
$methods,
$properties,
$functions
);
!
$sandbox = new Twig_Extension_Sandbox($policy);
$twig->addExtension($sandbox);
Policy is defined as a
white-list of allowed
tags, filters, etc.
155. Comparing dates
{% if event.startsAt > date('now') %}
Buy tickets
{% endif %}
NOTE This is the date( )
function, not the date filter
156. Modifying dates semantically
Early Bird ends at
{{ event.startsAt|date_modify('-15 days')|date }}
!
Confirm your sign up before
{{ user.createdAt|date_modify('+48 hours')|date }}
167. Named parameters
{{ content|convert_encoding('UTF-8', 'iso-2022-jp') }}
which is the original charset and
which one the target charset?
{{ content|convert_encoding(
from = 'UTF-8', to = 'iso-2022-jp'
) }}
{{ content|convert_encoding(
to = 'UTF-8', from = 'iso-2022-jp'
) }}
168. Named parameters
{{ content|convert_encoding('UTF-8', 'iso-2022-jp') }}
PROPOSED FOR
PHP
which is the original charset and
which one the target charset?
{{ content|convert_encoding(
from = 'UTF-8', to = 'iso-2022-jp'
) }}
{{ content|convert_encoding(
to = 'UTF-8', from = 'iso-2022-jp'
) }}
170. It's common to do things in batches
1
Image gallery
2 3
4 5 6
171. The HTML of the image gallery
<div class="row">
<div class="image"> ... </div>
<div class="image"> ... </div>
<div class="image"> ... </div>
</div>
!
<div class="row">
<div class="image"> ... </div>
<div class="image"> ... </div>
<div class="image"> ... </div>
</div>
172. The template without the "batch" filter
{% for i, image in images %}
{% if i is divisible by(3) %} <div class="row"> {% endif %}
!
<div class="image">
<img src="" alt="" >
<p>...</p>
</div>
!
{% if i is divisible by(3) %} </div> {% endif %}
{% endfor %}
173. The template without the "batch" filter
{% for i, image in images %}
{% if i is divisible by(3) %} <div class="row"> {% endif %}
!
<div class="image">
<img src="" alt="" >
<p>...</p>
</div>
!
{% if i is divisible by(3) %} </div> {% endif %}
{% endfor %}
UGLY CODE
174. The template with the "batch" filter
{% for row in images|batch(3) %}
<div class="row">
!
{% for image in row %}
<div class="image">
<img src="" alt="" >
<p>...</p>
</div>
{% endfor %}
!
</div>
{% endfor %}
175. The template with the "batch" filter
{% for row in images|batch(3) %}
<div class="row">
!
{% for image in row %}
<div class="image">
<img src="" alt="" >
<p>...</p>
</div>
{% endfor %}
!
</div>
{% endfor %}
178. Short ternary operator
$result = $condition ? 'is true';
ERROR Parse error: syntax error, unexpected ';' on line 1
{{ condition ? 'is true' }}
179. Short ternary operator
$result = $condition ? 'is true';
ERROR Parse error: syntax error, unexpected ';' on line 1
OK This works perfectly on Twig
{{ condition ? 'is true' }}
181. Short ternary operator
<li class="{{ condition ? 'selected' }}">
...
</li>
!
<li class="{{ condition ? 'selected' : '' }}">
...
</li>
always use this
182. Short slice syntax
!
!
!
{{ user.friends[0:3] }}
{{ user.friends[:-3] }}
It combines array_slice, mb_substr
and substr PHP functions.
get first three friends
get last three friends
189. {{ product.photo|image(400, 150, 0.9) }}
{{ product.photo|image(
width = 400, height = 150, opacity = 0.9
) }}
The problem with filter arguments
What if I need to define more arguments?
190. {{ product.photo|image(400, 150, 0.9) }}
{{ product.photo|image(
width = 400, height = 150, opacity = 0.9
) }}
The problem with filter arguments
What if I need to define more arguments?
this is a valid solution for Twig, but the
underlying PHP code is still very complex
191. Defining a filter with lots or arguments
$filter = new Twig_SimpleFilter('image', function (
$path, $width, $height, $opacity
) {
$path = ...
$width = ...
$height = ...
$opacity = ...
});