Andrew Welch · Insights · #craftcms #templating #twig

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Going Global with Twig Globals in Craft CMS

Learn how to sim­pli­fy some Twig tem­plat­ing chal­lenges by lever­ag­ing the glob­al vari­ables & the _​globals Col­lec­tion added in Craft CMS 4.5

Craft cms getting global with globals

The Twig tem­plat­ing lan­guage used by Craft CMS is pow­er­ful, but it also has some inten­tion­al lim­i­ta­tions and, like all pro­gram­ming lan­guages, some quirks as well.

One exam­ple of this quirk­i­ness is the scope of vari­ables in Twig.

Vari­able scope refers to the blocks of code where a vari­able can be accessed and/​or modified.

Global variables can be accessed anywhere

A glob­al vari­able, then, is a vari­able that can be accessed from any code any­where in your code base.

It’s not always as sim­ple as you might expect to cre­ate a vari­able that is glob­al­ly avail­able in all of your tem­plates in Twig.

To under­stand why, read the Twig Pro­cess­ing Order & Scope arti­cle for an in-depth dis­cus­sion on this rather nuanced topic.

Link Globals are bad

Ask any vet­er­an pro­gram­mer about glob­al vari­ables, and you’ll like­ly get involved in a long sto­ried dis­cus­sion of their evil ways.

That’s because they’ve spent many hours attempt­ing to fix prob­lems that have arisen from glob­al­ly scoped vari­ables that can be accessed or changed from anywhere.

Glob­als aren’t evil, of course, but they allow for cod­ing pat­terns that can be dif­fi­cult to debug.

Any­where your code access­es a glob­al vari­able is a depen­den­cy on that vari­able, and any­where your code changes a glob­al vari­able could poten­tial­ly be a break­ing change for all of those dependencies.

Dependency hell

That’s why things like depen­den­cy injec­tion are used to build more com­plex sys­tems, and many mod­ern fron­tend lan­guages use par­a­digms like props (think para­me­ters or vari­ables) that can only be passed down to com­po­nents in a one-way data flow.

This depen­den­cy also makes code less mod­u­lar, because if the code you write is inher­ent­ly depen­dent on a glob­al vari­able, it makes it dif­fi­cult to re-use that code elsewhere. 

All of that said, there are valid times when you need to use glob­al vari­ables, and many web sites are not com­pli­cat­ed enough that you can get your­self into too much trouble.

Just be aware of the trade-offs, and try to avoid glob­al vari­ables as a design pat­tern if you can.

Link A Bad Example

Here’s a sim­plis­tic exam­ple of using glob­al vari­ables in a way that can get you in trou­ble. We have an index.twig template:

{% set width = 200 %}
{% set height = 100 %}
{% set color = 'red' %}
{% set title = 'My Cool Button' %}

{% include 'globals-example/button.twig' %}

So, we set some glob­al vari­ables, and then we include our button.twig tem­plate, which is our com­po­nent that ren­ders the button:

<button style="height:{{ height }}px; width:{{ width }}px; background-color:{{ color }};">
    {{ title }}
</button>

There are some glar­ing prob­lems with the way we’ve writ­ten this:

  • The button.twig com­po­nent is tight­ly cou­pled to the index.twig tem­plate, because it relies on the glob­al vari­ables height, width, color, and title being set there. This kills the reusabil­i­ty of our com­po­nent; we can’t use it in anoth­er project by just copy­ing the file over.
  • We’re pol­lut­ing the glob­al vari­able name­space by using a bunch of fair­ly gener­ic vari­able names that oth­er com­po­nents might want to use, too. For instance, what if we wrote anoth­er checkbox.twig com­po­nent that also need­ed a height, width, color, and title?
  • Because the vari­ables the button.twig com­po­nent needs are glob­al vari­ables, oth­er code could inad­ver­tent­ly change them, caus­ing side effects in how our but­ton renders.

Let’s do a quick refac­tor to clean it up a bit to elim­i­nate these issues:

{% include 'globals-example/button.twig' with {
    width: 200,
    height: 100,
    color: 'red',
    title: 'My Cool Button'
} only %}

Instead of set­ting the vari­ables our com­po­nent needs in the glob­al name­space, we pass them into our component.

This elim­i­nates all of the issues we called out in our bul­let points above:

  • The index.twig and button.twig are no longer tight­ly cou­pled. We can just copy our button.twig com­po­nent to re-use it in anoth­er project.
  • We are no longer pol­lut­ing the glob­al vari­able name­space. The vari­ables only exist in the con­text of our button.twig component.
  • We avoid side effects because oth­er code can no longer inad­ver­tent­ly mod­i­fy the vari­ables our button.twig com­po­nent needs.

While our exam­ple is sim­ple, the more com­pli­cat­ed your code­base becomes the more trou­ble­some some of the issues we’ve men­tioned become.

Link Globals in Craft

Alright, so we know glob­al vari­ables are often a bad idea, but I trust you to use them judi­cious­ly, so let’s talk about doing just that.

When dis­cussing glob­al vari­ables in this arti­cle, we are not talk­ing about Craft Glob­als, which allow you to make con­tent avail­able glob­al­ly in your templates.

Instead, we’re talk­ing about Twig vari­ables that are glob­al­ly accessible.

A trick some have learned is that you can make a vari­able glob­al­ly acces­si­ble to all of your Twig tem­plates by putting it in a lay­out that every oth­er tem­plate extends from:

{% set someGlobal = "woof" %}

Then you might have a lay­out tem­plate that extends from your _globals.twig template:

{% extends _globals.twig %}

{% block content %}
{% endblock content %}

And now any tem­plate that extends your _layout.twig tem­plate will get any glob­al vari­ables defined in _globals.twig, all the way down the chain.

This lets you nice­ly side-step some of the weird­ness involved with Twig Pro­cess­ing Order & Scope.

But still, there are some lim­i­ta­tions such as child tem­plates not being able to mod­i­fy glob­al variables.

Due to the way Twig works, tem­plates that {% extends %} or {% include %} or {% embed %} oth­er tem­plates can inher­it glob­al vari­ables (called the context in Twig par­lance), but they can’t change them.

Twig tem­plates are passed in a copy of the Twig con­text when they are ren­dered, but any changes to that con­text are dis­card­ed when it is done rendering.

Side­note: If you ever want to see the Twig con­text (aka all of the avail­able vari­ables) in the cur­rent­ly ren­der­ing tem­plate, you can do so via: {{ dump(_context) }}

This is an inten­tion­al design deci­sion in Twig, to help mit­i­gate some of the depen­den­cy com­pli­ca­tions with glob­al vari­ables that we dis­cussed above.

How­ev­er, there are cas­es where it can be frustrating.

This is one of the rea­sons why is why in Craft 4.5 they intro­duced the _​globals global.

Link Leveraging _globals

So what’s the point of this new _globals thing in Craft 4.5 and lat­er, and why would we use it?

_globals is just anoth­er Twig glob­al vari­able that is always avail­able in the Twig con­text that your tem­plates render.

Yes, _globals is a global variable

Under the hood, it’s actu­al­ly a Lar­avel Col­lec­tion object, so all of the Col­lec­tion meth­ods are avail­able, so you can use it as a key-val­ue store for what­ev­er data you like.

Here are some exam­ples from the Craft CMS docs on using the _globals variable:

{% do _globals.set('theme', 'dark') %}
{% do _globals.set({
  red: '#F00',
  green: '#0F0',
  blue: '#00F',
}) %}

{{ _globals.get('theme') }}
{# -> 'dark' #}

{{ _globals.get('red') }}
{# -> '#F00' #}

Since _globals is just a Twig glob­al vari­able, you might won­der why it even exists. You could just cre­ate it your­self in your _globals.twig tem­plate that every­thing else extends via some­thing like:

{% set _globals = {
    theme: 'dark',
    red: '#F00',
    green: '#0F0',
    blue: '#00F',
} %}

This is rough­ly anal­o­gous to what Craft does under the hood to inject the _globals vari­able, with two key distinctions:

  • Since Craft is inject­ing this glob­al vari­able, it is always in the Twig con­text when your tem­plates are ren­dered. Where­as if you include oth­er tem­plates via {% include 'template.twig' only %} or {% include 'template.twig' with {'foo': 'bar'} only %}, vari­ables you’ve set in your _globals.twig tem­plate (or else­where) will not. The _globals Col­lec­tion will also be avail­able in Ele­ment Tem­plates that are ren­dered with the new Ele­ment .render() method added in Craft 5.
  • Because this is a Col­lec­tion object, you can change the val­ues that are in it from oth­er tem­plates, and the changes will per­sist as the rest of your tem­plates render.

To illus­trate the dif­fer­ence, let’s first have a look at some tem­plates that use the tech­nique of set­ting a glob­al in your layout.twig tem­plate that we men­tioned earlier:

{% set color = 'green' %}

{% block header %}
{% endblock header %}

{% block content %}
{% endblock content %}
{% set color = 'blue' %}
{% extends 'globals-example/layout.twig' %}
{% set color = 'red' %}

{% block header %}
    {{ color }}
    {% include 'globals-example/include.twig' only %}
{% endblock %}

{% block content %}
    {{ color }}
{% endblock %}

What do you think the out­put of the above tem­plate will be?

Think about it for a minute, then scroll down.

…the out­put is prob­a­bly not what you’d expect:

green green

Did­n’t we set the color glob­al vari­able to red in our index.twig tem­plate, and then set it to blue in our include.twig tem­plate? Why isn’t the out­put red blue?

This is because the code in the layout.twig tem­plate is processed after the code in the index.twig tem­plate, so while the color vari­able was changed to red, it was then set to green before the block renders.

Then in our include.twig file, the color vari­able was changed to blue, but only in the copy of the con­text that was passed down to it, which is dis­card­ed after it is ren­dered. So we end up with a rather unin­tu­itive result of green green.

Let’s take a look at how we can use the _globals Col­lec­tion to make this a bit better:

{% do _globals.set('color', 'green') %}

{% block header %}
{% endblock header %}

{% block content %}
{% endblock content %}
{% do _globals.set('color', 'blue') %}
{% extends 'globals-example/layout.twig' %}
{% do _globals.set('color', 'red') %}

{% block header %}
    {{ _globals.get('color') }}
    {% include 'globals-example/include.twig' only %}
{% endblock %}

{% block content %}
    {{ _globals.get('color') }}
{% endblock %}

What do you think the out­put of the above tem­plate will be?

Think about it for a minute, then scroll down.

…the out­put is prob­a­bly not what you’d expect:

green blue

Well, at least this is a lit­tle clos­er to what we’re look­ing for, set­ting the color item in the _globals Col­lec­tion in our include.twig tem­plate did stick and set it to blue.

But did­n’t we set the color item in our _globals Col­lec­tion to red? Why isn’t the out­put red blue?

Again, this is because the code in the layout.twig tem­plate is processed after the code in the index.twig tem­plate, but before the code in the blocks in the index.twig template.

We can work around this by adding an indexCode block to our layout.twig & index.twig Twig templates:

{% do _globals.set('color', 'green') %}

{% block indexCode %}
{% endblock indexCode %}

{% block header %}
{% endblock header %}

{% block content %}
{% endblock content %}
{% extends 'globals-example/layout.twig' %}

{% block indexCode %}
    {% do _globals.set('color', 'red') %}
{% endblock %}

{% block header %}
    {{ _globals.get('color') }}
    {% include 'globals-example/include.twig' only %}
{% endblock %}

{% block content %}
    {{ _globals.get('color') }}
{% endblock %}

This caus­es the code to be exe­cut­ed in the order we’re look­ing for, mak­ing the output:

red blue

Link A Good Example

Let’s look at a real-world exam­ple where you might use glob­al vari­ables to change the col­or theme of your website. 

Let’s not get hung up on whether you think doing them­ing via Twig rather than CSS is a good idea (though you very well might if you’re using Tail­wind CSS); the goal is to pro­vide an exam­ple where lever­ag­ing the _globals Col­lec­tion is useful.

Here are the templates:

{% do _globals.set('currentTheme', 'dark') %}
{% do _globals.set('themes', {
    dark: {
        foregroundColor: 'LightBlue',
        backgroundColor: 'DarkRed'
    },
    light: {
        foregroundColor: 'MidnightBlue',
        backgroundColor: 'Salmon'
    },
}) %}

Here we are adding two items to the _globals Col­lec­tion, currentTheme which is the name of the theme that is cur­rent­ly select­ed, and themes which defines our light and dark themes, each with a foregroundColor and backgroundColor prop­er­ty in them.

N.B.: My col­or scheme choic­es are atro­cious. I nev­er claimed to be a designer 🤭

{% include 'theme-example/themes.twig' %}

{% set requestCookies = craft.app.request.cookies %}
{% set themeCookie = requestCookies.get('currentTheme') %}
{% if themeCookie and themeCookie.value != '' %}
    {% do _globals.set('currentTheme', themeCookie.value) %}
{% endif %}

{% block header %}
{% endblock header %}

{% block content %}
{% endblock content %}

Our layout.twig tem­plate imports our themes.twig tem­plate, and then might change the currentTheme val­ue in the _globals Col­lec­tion if it’s set in a cookie.

For more on access­ing cook­ies via Twig, check out the Cut­ting the Cord: Remov­ing Craft CMS 3 Plu­g­ins article.

This cook­ie would prob­a­bly be set via some user pref­er­ence via fron­tend JavaScript. Alter­na­tive­ly, a cus­tom field on the User ele­ment in Craft could be used to indi­cate their preference.

Or you could just set the val­ue based on the cur­rent time of day where they hap­pen to be, or the brows­er col­or scheme pref­er­ence.

What­ev­er floats your boat!

{%- macro getCurrentThemeValue(value) -%}
    {%- set currentThemeName = _globals.get('currentTheme', 'dark') -%}
    {%- set theme = _globals.get('themes')[currentThemeName] -%}
    {{- theme[value] -}}
{%- endmacro -%}

Our macros.twig tem­plate defines the getCurrentThemeValue() macro which makes it more con­ve­nient to pull out the val­ue of a prop­er­ty from the cur­rent theme. This is some­thing we might be doing often, so it makes sense to cre­ate a macro for it.

<button
    style="height:{{ height }}px; width:{{ width }}px; color:{{ foregroundColor }}; background-color:{{ backgroundColor }};">
    {{ title }}
</button>

Next, we have a button.twig tem­plate that ren­ders a but­ton for us. This is most­ly the same as pre­sent­ed ear­li­er, except that we’re allow­ing for both fore­ground & back­ground col­ors to be passed into it.

{% extends 'theme-example/layout.twig' %}

{% from 'theme-example/macros.twig' import getCurrentThemeValue %}

{% block header %}
    {% include 'theme-example/button.twig' with {
        width: 300,
        height: 50,
        foregroundColor: getCurrentThemeValue('foregroundColor'),
        backgroundColor: getCurrentThemeValue('backgroundColor'),
        title: 'Header Button'
    } only %}
{% endblock %}

{% block content %}
    {% include 'theme-example/button.twig' with {
        width: 200,
        height: 100,
        foregroundColor: getCurrentThemeValue('foregroundColor'),
        backgroundColor: getCurrentThemeValue('backgroundColor'),
        title: 'Content Button'
    } only %}
{% endblock %}

And final­ly, we have our index.twig tem­plate that imports our getCurrentThemeValue() macro, and then includes the button.twig com­po­nent tem­plate to ren­der our button.

This is a case where we real­ly do want the theme to be glob­al­ly avail­able, but we’ve done so in a way that makes it acces­si­ble every­where in our tem­plates while still not being tight­ly cou­pled to the com­po­nent tem­plates that we are rendering.

We’re still just pass­ing val­ues into our tem­plate com­po­nents, but we’re fetch­ing them from a glob­al­ly defined theme that can be changed as needed. 

And because we’re using the _globals Col­lec­tion, we could change the theme on a per-tem­plate basis with some­thing like:

    {% do _globals.set('currentTheme','dark' %}

…in a {% block %} in the tem­plate, or in an {% include %} and still have it work.

Link Getting Global

Hope­ful­ly this arti­cle has helped you under­stand the pit­falls of glob­al vari­ables in Twig, and a bit about how things work under the hood.

Globe trotting

This should help you judi­cious­ly use glob­al vari­ables — and lever­age the Craft _globals glob­al vari­able — effec­tive­ly in your projects.

Hap­py globe-trotting!