Rendering a Twig template requires several things: First, the template needs to be found by a loader, that knows where to look for the template based on the given name. Then, the template’s source code is put through a parsing and compilation stage, which translates it to PHP code. Finally, the resulting PHP document is executed with the given parameters as context.
For a more detailed description of the Twig internals, head over to the official Twig docs. In this article, we are focusing on the various Contao-specific changes and extensions, that take place in every of these steps:
ContaoFilesystemLoader
, work?The core of Twig is the Twig\Environment
class. You get an instance of it, when using the @twig
service in Symfony
and you typically use it to render a certain template, identified by a distinct name, together with an array of
parameters:
$twig->render("@Foo/bar/baz.html.twig", $params); ↑ ↑ Logical name Parameters
The environment class itself does not know anything about your template files. Instead, it delegates retrieving the
template source to a loader. Looking closer, you find, that there is a special loader, called a chain loader
in Symfony, which holds a list of multiple loaders and asks one after another until the request can be answered. Contao
makes use of this and adds its own ContaoFilesystemLoader
to the chain, that reads template files from Contao specific
locations on the filesystem.
Templates are identified by the logical name (the fully qualified template name, you could say). In order to make them
unique across different vendors, namespaces are used. Namespaces are denoted by an @
sign and form the first part of
the logical name.
$twig->render("@Foo/bar/baz.html.twig", $params); ↑ ↑ ↑ Namespace Identifier Extension
Logical name is a Symfony term. Additionally, we use the term identifier, which means everything after the namespace except for the file extension. We use this term when talking about templates from the special managed @Contao namespace.
The default filesystem loader in a typical Symfony application looks at the root templates
directory and groups all
files found there under an app-specific @__main__
namespace. Also, the contents of directories inside
templates/bundles
will each be put under a @<directory>
namespace. And finally, templates from bundles are also each
put under a @<bundle-name>
namespace.
There is a specific order and the loader will only use the first template that fits the requested logical name. That is
why you can overwrite templates of the FooBundle
by putting them in the templates/bundles/FooBundle
directory: you
made the loader return early when finding your template instead of the original one in said namespace.
For Contao extensions, you do not need the bundles
directory. We use the @Contao
namespace, that is shared across
the ecosystem. Read on for more details about this.
You might have noticed, that bundle templates also get put under a second @!<bundle-name>
namespace
(prefixed with an exclamation mark). In Symfony, this is used to allow overwriting a template while also extending
the original one. Think about how you would reference the original template: Without the second namespace, the loader
would always return the replacement, as it wins over the original one. But now, what happens if two extensions (or an
extension and the application) use this trick without knowing each other? Spoiler alert: it’s an issue and the reason
why we invented the managed namespace concept.
Symfony’s standard way has a big drawback for us: If multiple parties (e.g. extensions and/or the application) want to
adjust the same template, they must know each other and explicitly target their namespaces. Otherwise, when
overwriting, only the first party would win. Contao’s answer to that is called the “managed namespace” or @Contao
namespace, where this problem does not occur.
Our ContaoFilesystemLoader
puts every template from a Contao template directory under the following namespaces (don’t
worry, you don’t have to remember this):
Directory | Namespace | Order |
---|---|---|
Any bundle’s template directory:/vendor/foo/bar/src/Resources/contao/templates | @Contao_FooBarBundle | 1 |
Main template directory of the application:/contao/templates ( /src/Resources/contao/templates )( /app/Resources/contao/templates ) | @Contao_App | 2 |
Global template directory:/templates | @Contao_Global | 3 |
Any theme directory:/templates/<theme> /templates/foo/theme | @Contao_Theme_<theme> @Contao_Theme_foo_theme | 4 |
Optionally, you can return the path to your bundle from getPath()
method in
your bundle class. Starting from Symfony 6.1, this is done automatically if
your bundle class extends AbstractBundle
. Otherwise, you need to implement
it yourself. See Symfony docs for more details.
# /vendor/foo/bar/src/FooBarBundle.php
// Symfony "<=6.0"
class FooBarBundle extends Bundle
{
public function getPath(): string
{
return dirname(__DIR__);
}
}
// Symfony "^6.1"
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
class FooBarBundle extends AbstractBundle
{
}
This allows your template namespaces to be discovered by Contao and Symfony from directories relative to the bundle path your method returns:
Folder | Namespace |
---|---|
/vendor/foo/bar/contao/templates (instead of /vendor/foo/bar/src/Resources/contao/templates ) | @Contao_FooBarBundle |
/vendor/foo/bar/templates | @FooBar |
For theme directories (e.g. foo/bar/theme
), the path will be transformed into a snake-case slug (foo_bar_theme
),
which is used inside the theme namespace name (@Contao_Theme_foo_bar_theme
). For this reason, underscores are
forbidden characters in any directory name contributing to the slug.
But, you guessed it, there is yet another namespace called @Contao
, and that is the one primarily used in Contao.
The loader also puts every template from the above Contao namespace directories into this namespace but this is mainly
for better static analysis. The real magic happens at compile time: We replace each usage of
the @Contao
namespace inside any extends
, include
, embed
or use
tag with a more specific namespace from
the above table. This happens automatically — you don’t have to do anything for it.
{% extends "@Contao/content_element/text.html.twig" %} ↓ {% extends "@Contao_ContaoCoreBundle/content_element/text.html.twig" %}
Instead of one single unique template per logical name, you now get a hierarchy of templates. First come the app’s global and main template directory (see above), then those of all bundles in inverse loading order (if you are loaded later, you’re first). The replacement logic uses the hierarchy to always choose the next logical representation that exists (potentially skipping over levels).
By the way: Theme templates, though considered the most specific, are somewhat special in that regard. For more details, please refer to their own section further down.
You can use the debug:contao-twig
command to browse and better understand the built hierarchy. Read more on this in
the article about debugging strategies.
Let’s see the effect of the namespace replacements once again in a simplified example. We assume the core ships a
@Contao/simple_text.html.twig
template and two independent bundles want to adjust this same template.
The original template of the ContaoCoreBundle
:
<h1>Some simple text</h1>
{% block content %}
<p>{{ text }}</p>
{% endblock %}
The FooterBundle
ships a simple_text.html.twig
template that adds a paragraph to the simple text. During
compilation, the reference in line 1 will be replaced with @Contao_CoreBundle/simple_text.html.twig
.
{% extends "@Contao/simple_text.html.twig" %}
{% block text %}
{{ parent() }}
<p class="footer">Here is a footer.</p>
{% endblock %}
The BoxBundle
wraps the simple text content in a div with a border in its version of the simple_text.html.twig
file.
During compilation, the reference in line 1 will be replaced with @ContaoFooterBundle/simple_text.html.twig
.
{% extends "@Contao/simple_text.html.twig" %}
{% block text %}
<div style="border: 1px solid black">
{{ parent() }}
</div>
{% endblock %}
In an application setup like this, a render call
$this->render('@Contao/simple_text.html.twig', ['text' => 'Hello world!']);
will result in the following output:
<h1>Some simple text</h1>
<div style="border: 1px solid black">
<p>Hello world!</p>
<p>Here is a footer.</p>
</div>
Note, that neither the caller, nor the extenders need to know which bundles are installed in the application. Moreover, you could also overwrite the same template again in the application, independent of having the extensions installed or not.
In Contao, there is the option to create theme-specific representations of templates. The A
theme and B
theme could
both contain a content_element/text.html.twig
template. They can also both extend from any existing base template.
Whether the A
, B
or global version of the template gets rendered, is a runtime decision, though. And this makes them
behave a bit differently than the other templates.
If a page object is available in the current request, the theme can be derived from the (inherited) page layout setting.
In case a theme was identified and contains a more specific version of a template, the ContaoFilesystemLoader
will
use it instead. From a template hierarchy perspective, these theme templates do not exist. This is a design decision
to keep the template hierarchy static, render calls stable (and yourself sane).
Theme templates are runtime representations of otherwise existing templates. They are not part of the template hierarchy.
These are the implications that follow from this setup:
Theme templates can only be theme-specific representations of otherwise existing templates. They will, for instance, never show up in any template selection dropdown.
As a matter of fact, you cannot create a variant template in a theme directory and make it show up in the template selection. You can, however, create a theme-specific representation of an existing variant template. By creating a selectable non-theme variant template as a basis, you also make sure that, there will always be an available template when rendering.
When debugging templates via the debug:contao-twig
command, you need to
explicitly pass a theme (slug) to make the respective theme templates show up in the result.
In this section we’re talking about how templates should be named and structured and why it might be more important than in the typical Symfony ecosystem.
When creating templates, you now got the option to use the @Contao
namespace over the bundle namespace. So when to
choose what? As a rule of thumb: If external adjustments are not intended, feel free to use basic Symfony
behavior/bundle namespaces, but stick to the @Contao
namespace for anything belonging to the Contao ecosystem. Only
this way others can safely reuse/extend your output.
Won’t using the
@Contao
namespace lead to naming collisions if every vendor is using it?
This is a good point and intuitively kind of works against the idea of namespaces. But Contao has proven, it isn’t that big of a problem, either: In the old days, Contao’s templating engine did neither use any namespaces nor subdirectories*) to structure templates. We instead added vendor or type prefixes to the template names based on a naming convention. This worked surprisingly well, but lead to a lot of files in one place. With Twig templates, the path, i.e. the directories and subdirectories the template files are placed in, is now part of the template name. This means we can structure by category or vendor by creating a filesystem structure.
*) Well, there could be subdirectories on the filesystem, but they did not affect the template name.
Let’s do exactly that:
templates ← Twig root │ ├─content_element │ ├─text.html.twig │ └─image.html.twig ├─foo │ └─bar │ └─baz.json.twig …
We call the topmost directory in the above example our Twig root, because all subdirectories in there contribute to
the template name: There is a content_element/text
template and a foo/bar/baz
template. As you can tell, you can
also introduce more subdirectory layers if needed.
When users want to override/adjust templates from various sources, they need to replicate the filesystem structure. To make this a pleasant experience, please stick to the naming conventions, so that multiple structures do not mix.
Above, we referred to @Contao/content_element/text.html.twig
by just writing content_element/text
. In fact, in
the @Contao
namespace, these two things both uniquely identify the same template. When using
the identifier, we imply the namespace and find the right file extension. The latter is
guaranteed to be unique per identifier by our loader — if there would for instance also be a text.xml.twig
(note the
different file extension) in a content_element
directory, an exception would be thrown when the filesystem gets
scanned.
We treat everything after the last .
in a file name as file extension. If this last bit is twig
, we also include
the part before it. So a foo.bar.baz.html.twig
file has the extension html.twig
while it would be just baz
for
foo.bar.baz
. Even though possible, it’s considered a good practice to include the filetype and the .twig
suffix
and avoid extra dots: e.g. my_file.svg.twig
for a svg file, or foo_bar_baz.html.twig
for a HTML file.
Because we made it possible to overwrite existing legacy PHP templates with Twig templates, and because the old template system does handle directories differently, the loader cannot always safely determine if your template directory (or maybe a subdirectory) should be treated as the Twig root. In Contao 6, all the Contao template directories will implicitly be Twig roots — until then only the global template directory and the theme directories are.
For any other place, such as the main template directory (contao/templates
) and inside bundles, you need to add a
special marker file .twig-root
to denote that this directory should be used as the naming root.
Assume the FooBundle
has the following structure inside its Contao template directory:
vendor/…/FooBundle/contao/templates ├─bar │ └─baz.html.twig └─my_root ├─.twig-root └─content_element └─foobar.html.twig
Now, a @Contao/baz.html.twig
template would be available (note the ignored directory structure like with the legacy
template engine) as well as a @Contao/content_element/foobar.html.twig
, that includes the directory names under the
Twig root in the template name.
One of Contao’s features is the ability to provide variants to an existing template and let editors choose which one to use in the back end on a per-element basis. You could for instance have a bunch of specialized templates for the text content element — maybe one that can be used, when something should stand out in the design and one that wraps lengthy side notes in an expandable section.
To get what we outlined above, we need to create two new templates. In order to not repeat ourselves and to
potentially profit from adjustments made by others, we are going to extend the original text content element template
(@Contao/content_element/text.html.twig
) and only tweak some blocks.
The first variant template, content_element/text/highlight.html.twig
, adds a big red border around the text — no-one
will miss that:
{% extends "@Contao/content_element/text.html.twig" %}
{% block text %}
<div style="border: 5px red; padding: 1em;">{{ parent() }}</div>
{% endblock %}
For the second variant, content_element/text/side_note.html.twig
, we wrap the whole content in
a details disclosure HTML element, which displays
it in a closed/collapsed state until someone clicks on it.
{% extends "@Contao/simple_text.html.twig" %}
{% block content %}
<details>
<summary>Here is an interesting side note…</summary>
{{ parent() }}
</details>
{% endblock %}
We deliberately put both variant templates in a directory that follows the original template’s name. This way, it is
automatically picked up by the template Finder
and provided in the respective template options dropdown in the back
end.
If, in your own code, you need to compile a list of templates, it can get quite cumbersome to loop through and filter
the template hierarchy. For your convenience, there is a contao.twig.finder_factory
service, that makes this process
easy.
// Inject the factory service, then create a new template finder instance.
$finder = $this->finderFactory->create();
// Configure it using its fluent interface; here we only want to find templates
// named "foo/bar.json.twig" including related variants, like "foo/bar/baz.json.twig":
$finder = $finder
->identifier('foo/bar')
->extension('json.twig')
->withVariants()
;
// Then, iterate over the results…
foreach ($finder as $identifier => $extension) {
// …
}
// …or directly generate template options with labels, that can be returned in
// a DCA options listener.
$options = $finder->asTemplateOptions();
Please refer to the doc block comments on each of the fluent interface methods of the Finder
class for details on how
to use it.
For historic reasons Contao uses input encoding, but Twig embraces the more sane output encoding. You can read more about the topic (and why you should favor output encoding) in this OWASP article about preventing Cross Site Scripting (XSS) attacks. We outline further down, how we want to achieve the switch in Contao 6 and how you can already write modern template code.
The gist: You, as the template designer, have to decide how things should be output, because only you know the context in which content can be trusted or not. The exact same data can be dangerous in one context and harmless in another!
Assume you have a variable color
that should contain color names (like red
, green
, rebeccapurple
, …) and a
template that should output the name of the color inside a box with a background of this color. Maybe like so:
<style>
.box { background: <?= $this->color ?> }
</style>
[…]
<div class="box"><?= $this->color ?></div>
⚡ This is dangerous. The content of the variable has a different meaning when output in CSS or HTML! This gets particularly bad if the sanitization logic treating the input does not know about the different cases.
red; } { body: display:none;
A perfectly valid and safe value for color
in the HTML context, would produce this unwanted result in the CSS context.
This is certainly not what we want:
<style>
.box { background: red; } body { display: none; }
</style>
<script>alert(1)</script>
Likewise, even if you would strip or encode special CSS characters (like ;
, }
or {
), you would not solve the
problem. A string, such as the one above, would pass but now gets dangerous in the HTML context:
<div class="box"><script>alert(1)</script></div>
This is a dilemma. The logic storing and processing data can never know in which context the data will be used. Will this text end up in an HTML document or inside an HTML tag? Or as a property in JSON-LD? Or as a value in a CSV file? And even when there is likely only one use case, making the input side responsible for the security, is a very bad idea. You won’t be able to fix mistakes made when “input encoding”, neither will you be able to safely add another output context later on.
With Twig, we can be specific how a certain variable should be treated, depending on the context! Use the |escape
(or
short |e
) filter for this:
<style>
.box { background: {{ color|e('css') }} }
</style>
[…]
<div class="box">{{ color|e('html') }}</div>
Now, our “bad” input will be properly escaped for CSS or HTML and wouldn’t do any harm anymore:
.box { background: red\3B \20 \7D \20 \7B \20 body\3A \20 display\3A none\3B }
<div class="box"><script>alert(1)</script></div>
And because this feature is essential for secure templates, Twig will — by default — encode all parameters. It
selects the default escaper strategy depending on the template’s file extension: your .html.twig
templates will
automatically get the |e('html')
treatment, so you could omit this part in the above example.
Try it out for yourself in this TwigFiddle or read more about the escaper extension in the official Twig documentation.
Heads up: Literals and expressions which result in a literal, are never automatically escaped! For more details, please refer to the official Twig documentation.
{# The following terms will not be escaped! #}
{{ "Twig<br>" }}
{{ foo ? "Twig<br>" : "<br>Twig" }}
If you intentionally do want to output a variable without encoding, such as some raw HTML (<b>nice</b>
) in a
.html.twig
template, you need to add the |raw
filter to your variable {{ my_content|raw }}
. This tells Twig to
skip the escaper filters for this value. Otherwise, here, the encoded form <b>nice</b>
would be output and
the browser would display a text saying <b>nice</b> instead of the boldly written word nice.
You typically have this situation with text from the back end’s tinyMCE rich text editor. Here, Contao already sanitizes
the HTML with the aim to remove unwanted or potentially dangerous HTML. This depends on your contao.sanitizer
settings
including the allowed tags and attributes, though. With a too lax configuration, you are open to XSS injections if this
data is output in an unencoded form!
Only ever add |raw
to things you trust! Using |raw
on anything else may result in severe XSS vulnerabilities!
Our Twig implementation makes sure you can use Twig templates as you would with output encoding (only). The intention hereby is, that your templates can stay the same and are already safe, when we’re removing the input encoding part in Contao 6.
In case you’re wondering, how we achieve this: Under the hood, we added our own contao_html
and contao_html_attr
escaper variants for the HTML context. These work very similar to the original versions (html
and html_attr
), except
they prevent double encoding using htmlspecialchars(double_encode: false)
. At template compile time, we then exchange
escaper calls to point to ours instead.
In order to not cause unwanted output for any other Symfony bundle or your app’s custom templates, we only do this for
the @Contao
and @Contao_*
namespaces. If you want this behavior for other templates as well, you can add your own
escaper rule. A rule is a regular expression — if it matches the template’s logical name, the Contao escapers will be
enabled.
// Do this in your kernel or in a compiler pass
$contaoExtension = $twig->getExtension(ContaoExtension::class);
$contaoExtension->addContaoEscaperRule('%^@MyNamespace/%');
To make the transition to Twig as easy as possible, we build the ability to overwrite and extend existing PHP templates with Twig counterparts. There are a lot of things happening in the background to make this work, and we are going to drop this complexity together with the whole legacy template engine in the future. You should not use this feature as an excuse to create new PHP templates if there is a modern alternative.
To overwrite a PHP template with a Twig template, name it exactly like the PHP template, but use .html.twig
as the
file extension. To adjust the fe_page.html5
template, you would create a fe_page.html.twig
template in a Contao
template directory. If there are both versions, the legacy template will be ignored.
The fe_page
template is still an old one where there is no modern replacement, yet. We can still adjust it by writing
a Twig template. Assume we want to add a style block to the head. We would create a file fe_page.html.twig
in our
template directory and add the following contents:
{% extends "@Contao/fe_page.html5" %}
{% block head %}
{{ parent() }}
<style>
.thing { color: orange; }
</style>
{% endblock %}
Here we target existing blocks and use the parent()
function as we would in a regular Twig template.
You can only use Twig templates to extend from the legacy PHP templates, not the other way round. This also means, that an extension doing this for a template, would force everyone to change their versions to Twig as well. In this case, the behavior is likely not what you want, and you should use the legacy template, still.
The data passed to the legacy templates gets transformed and passed to the environment’s render()
function as an
array. In most cases you would not note a difference, but there is a small caveat if callables like anonymous functions
or closures were used.
Every element in Twig’s context needs to be a literal or an object, so when transforming, we’re wrapping functions in
anonymous objects, that implement a __toString()
function. In case you need to supply arguments, or you need anything
else than string output, you’ll need to utilize the object’s invoke()
function.
Assume you got a legacy template, that includes callables in its template data…
$this->Template->setData([
'normalValue' => 'foo',
'lazyValue' => static function(): string {
return 'foo';
},
'fooFunction' => static function(string $value): string {
return "foo-$value";
},
'lazyArray' => static function(): array {
return [1, 2, 3, 4, 5];
},
]);
… you would then access this data/evaluate the functions like so:
<?= $this->normalValue ?>
<?= $this->lazyValue ?>
<?= $this->fooFunction('bar') ?>
<?= implode(', ', $this->lazyArray) ?>
{{ normalValue }}
{{ lazyValue }}
{{ fooFunction.invoke('bar') }}
{{ lazyArray.invoke()|join(', ') }}
The context transformation is done by the @contao.twig.interop.context_factory
service. Although, you could use it to
make callables work with your own templates, we do not advice in doing so. Most times it is better to create a real
object for this use case — in doing so, you can also profit from getting autocompletion by type hinting the variable in
your template (if your IDE supports this). We likely want to drop this service in the future after removing support for
legacy templates together with everything else in the Contao\CoreBundle\Twig\Interop
namespace.
🟢 As an extension developer you might ask yourself which Contao versions your extension can be compatible with. As a rule of thumb, we suggest to stick to Contao 5 only for new extensions, if you can.
🟡 Alternatively, you might want to support Contao 5 and Contao 4.13 LTS at the same time. In general, this should be doable without maintaining a completely different branch but there are some pitfalls. Read more about that in the following section.
🔴 From a Twig support perspective, supporting Contao 4.9 LTS is not feasible, as native support for it was only added to the core in Contao 4.12. Also note, that Contao 4.9 is in the security-only phase as of mid february 2023 and will not be maintained anymore a year after that.
In Contao 5.0, a new structure for content elements was introduced, that features directories (content_element
),
instead of prefixes (ce_
). Since then, several changes have been back ported to Contao 4.13. In the latest release,
you can now do the following things (Twig only):
foo.json.twig
).Finder
via the contao.twig.finder_factory
service.For new major releases, we strongly suggest to follow the new template directory structure, even in Contao 4.13. Otherwise, you likely need to release another major version when doing the change in the future (due to everyone having to migrate their templates.) For people creating new applications with Contao 5, the added benefit is a clean homogeneous template structure from the get-go.
Depending on where you make your templates available, you might need to curate the template options yourself. Using the
Finder
, this should be straight-forward, though.
When using fragment controllers, please note, that the template name, that is auto-generated from the type and class
name, will be different in Contao 4.13 and Contao 5! A FooController
content element would use a ce_foo
template in
Contao 4.13 and a content_element/foo
template in Contao 5. Make sure to explicitly define the template identifier
in the controller attribute, annotation or service tag, in case you want to use the new version with Contao 4.13:
#[AsContentElement(category: 'bar', template: 'content_element/foo')]
class FooController extends AbstractContentElement
{
…
}
You might need to get a bit creative when you want to use templates, that were only added in Contao 5. One way of working around that issue, is, to provide the missing template(s) yourself and make the usage forward compatible.
Put the template in the compat
directory. Templates inside this directory are never meant to be extended by
others, so the structure is not important. You might want to copy the structure you are missing, though. For example,
create a compat/content_element/code.html.twig
template as a replacement for the content_element/code.html.twig
template, that is available in Contao 5.
Use dynamic inheritance to tell Twig, that it should use your compat template if the original one isn’t available. When referencing the compat template, use the extension-specific namespace, so that your template is targeted, even if another extension used the same name:
{# Twig will use the first available template, when providing an array of options #}
{% extends [
"@Contao/content_element/_base.html.twig",
"@Contao_FooBarBundle/compat/content_element/_base.html.twig"
] %}