Andrew Welch

Andrew Welch · Insights · #craftcms #twig #frontend

Making the web better one site at a time, with a focus on performance, usability & SEO

· 5 min read ·

Twig Processing Order & Scope

The Twig templating language has a little-known processing order & scope that is important to understand

Twig Code

Twig is a fantastic frontend templating language from Sensio Labs that’s gaining widespread adoption. My first exposure to it was in Craft CMS, but it’s being used in a number of CMS’s & frameworks such as Symfony, Grav, Drupal 8, October CMS, Bolt, etc. It can even be used in WordPress via Timber. This widespread use makes it a valuable skill to master.

The reason Twig is gaining widespread adoption is that it combines ease of use with flexibility and power. Frontend developers who are comfortable writing HTML and maybe a little JavaScript can pick up Twig very quickly.

For the most part, Twig is pretty straightforward to learn and implement. I’ll assume that you’re familiar with Twig; if you’re not, check out the Twig Book (skip ahead to Chapter 3: Twig for Template Designers).

There are, however, two non-obvious aspects of Twig: processing order and scope.

Link Twig Processing Order

I created a handy infographic on Twig Processing Order a bit ago, and will be diving in a bit deeper here.

Twig Processing Order

Twig Processing Order infographic

Here are the code examples used in the above infographic:

{{ foo }}
{% set foo = 'ruff' %}
{% include '_include' %}

{% block content %}
{% endblock %}

{% block notoverridden %}
	{{ foo }}
{% endblock %}
{% extends '_layout' %}

{% set foo = 'bar' %}

{% block content %}
	{{ foo }}
{% endblock %}
{% set len = foo |length %}
{% set foo = 'woof' %}
{{ foo }}

This is a pretty typical template setup in Twig. We have a _layout.twig that includes _include.twig, and a page.twig that extends our _layout.twig template.

The interesting part here is the order in which these things are processed:

  1. The code in _page.twig that is not in a {% block %}
  2. The code in _layout.twig that is not in a {% block %}
  3. The code that is included in _include.twig
  4. The {% block %} code in _page.twig
  5. The {% block %} code in _layout.twig

Now, you may say “Okay, that’s great, but why does it matter?” Usually, it doesn’t. But there are situations that will leave you scratching your head trying to understand what’s going on if you don’t understand this Twig processing order.

The example code above would output the following:

bar
woof
ruff
ruff

Is that what you expected? While it may seem arbitrary, the processing order is actually done this way for a very important reason: we need to be able to override things in our templates that extend other templates.

Link Twig Scope

Hey, wait a minute… didn’t we change foo to 'woof' via {% set foo = 'woof' %} in the _include.twig template? Why was that never output? Let me introduce you to the concept of scope.

Variables in Twig exist only in the context in which they are defined. Substitute ‘template’ for ‘context’, and you’ll probably understand it a bit better. In other words, if we define a variable in one of our templates, it exists only there.

For example, if in our _include.twig template we did {% set woof = 'bark' %}, the variable woof will not be available in our page.twig or our _layout.twig templates. It’s only available for use in the _include.twig template where we defined it.

Aha, but then how are we accessing the variable foo (defined in our page.twig template) from the _include.twig template? The answer is that Twig passes down a copy of the current context to templates that we include.

Copy is the key word here. While we can access foo in _include.twig, any changes we make to it will not be propagated back up to page.twig or _layout.twig. That’s why we never saw woof get output anywhere.

And indeed, when we include templates in Twig, we can add variables to the Twig context it will inherit like this {% include '_include' with { 'someVar': 'someValue' } %}. We can even say we don’t want to include any of the parent context (only the values we pass in) via {% include '_include' with { 'someVar': 'someValue' } only %}. Check out the Twig docs for details.

Fortunately, templates that extend other templates share the same context, so variables are both accessible & changeable in this case.

Link Wrapping Up

That’s it. Twig processing order and scope may seem a bit esoteric, but understanding how your tools work under the hood will save you many hours of frustration trying to figure out why things are not working as expected.

Happy templating!

${ category } · ${ blog.postDate }

${ blog.title }

#${ tag.title }