Andrew Welch · Insights · #twig #craftcms #macro

Published , updated · 5 min read ·


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

Making Twig Macros Work Like Functions

Learn how to make Twig macros return val­ues you can manip­u­late, just like func­tions in oth­er pro­gram­ming languages

Twig Macros are a way you can abstract bits of reusable code away into neat lit­tle tem­plate frag­ments that work much like func­tions do in oth­er pro­gram­ming languages.

This lets you embrace Don’t Repeat Your­self (DRY) soft­ware devel­op­ment prin­ci­ples, which can make your code eas­i­er to manage.

There is a catch, however

Twig macros don’t return val­ues like func­tions typ­i­cal­ly do; instead, they just out­put a result as a string.

This makes them a lit­tle less use­ful than a func­tion, because you might want to use macros to com­pute some kind of val­ue that you can use lat­er on in mul­ti­ple places, rather than just being output.

This arti­cle will show you how you can use Twig macros to return val­ues, just like real functions.

Let’s start with an exam­ple Twig macro that we’ll build upon:

{% macro calculateSales(orders) %}
    {% set result = 0.0 %}
    {% for order in orders %}
        {% set result = result + order['price'] * order['quantity'] %}
    {% endfor %}
    {{ result }}
{% endmacro %}

This calculateSales() macro takes one para­me­ter orders, and cal­cu­lates the total sales by mul­ti­ply­ing each order’s price by its quantity.

The orders data struc­ture could come from a data­base query, a GraphQL query, a Col­lec­tion, or wherever. 

If we out­put the result of this macro with our con­trived data passed in like this:

    <span>
        {{ _self.calculateSales(orders) }}
    </span>

The HTML result will look some­thing like this:

    <span>                                                191.93
</span>

Link Whitespace Control

You may be won­der­ing what the weird spac­ing is all about. Well, that’s because of the spac­ing in the actu­al code that makes up our Twig macro.

With­out that spac­ing in our code, it’d be dif­fi­cult for humans to read. For­tu­nate­ly, HTML does­n’t care about the extra spac­ing, but let’s tight­en it up by using white­space con­trol:

{%- macro calculateSales(orders) -%}
    {%- set result = 0.0 -%}
    {%- for order in orders -%}
        {%- set result = result + order['price'] * order['quantity'] -%}
    {%- endfor -%}
    {{- result -}}
{%- endmacro -%}

That’ll make the out­put look a lit­tle pret­ti­er for us, and also the result is start­ing to look a lit­tle bit more like a func­tion result now that the white­space is gone:

    <span>
        191.93
    </span>

If you don’t feel like adding all of the {%- -%} and {{- -}} white­space con­trol to your macro, you can also just do this:

    {{ _self.calculateSales(orders) | spaceless }}

This uses the spaceless fil­ter to remove the white­space, and is less finicky than adding all of those -s to your Twig.

Link Returning a Value from a Twig macro

While this might be use­ful enough already for some sce­nar­ios, it’s still not return­ing a val­ue. It’s just out­putting a string.

What if we want­ed to call calculateSales() in mul­ti­ple places, and then do some sort of com­pu­ta­tion based on the result, such as cal­cu­lat­ing sales tax, and then pre­sent­ing a grand total?

Some might argue that this is out of Twig’s pre­sen­ta­tion­al” role, and data should be inject­ed into the tem­plate in a form that’s ready for display.

While I agree with this gen­er­al sen­ti­ment, there are many real-world sit­u­a­tions I’ve run into where Twig macros would be so much more use­ful if they could return values.

So let’s figure out how to return a value from a Twig macro

We can do this by using our old friend the set tag:

    {% set total = _self.calculateSales(orders) %}

Now the Twig vari­able total is set to the out­put of our Twig macro calculateSales()!

It’s still just a string, as we can see when we dump() it out:

Twig\Markup {#1684 ▼
  -content: "191.93"
  -charset: "UTF-8"
}

Woah wait a minute, that’s not a string, that’s a Twig\Markup object that Twig wraps around your ren­dered tem­plate code.

So we need to get just the val­ue of the content prop­er­ty from this object. Alas, content is a pri­vate prop­er­ty, so we can’t just do total.content… but we can lever­age PHP’s __toString() mag­ic method (which is imple­ment­ed by the Twig\Markup class) to do it:

    {% set total = _self.calculateSales(orders).__toString() %}

…and now we have just a plain old string in our total vari­able, as we can see from dump():

"191.93"

In fact, if we use the space­less fil­ter instead of the white­space con­trol oper­a­tors ({{- -}}), we don’t even need _​_​toString() because the space­less fil­ter con­verts the Twig\Markup object to a string for us!

    {% set total = _self.calculateSales(orders) | spaceless %}

…and again, we now have just a plain old string in our total vari­able, as we can see from dump():

"191.93"

Even though this is a string, but thanks to Twig & PHP being loose­ly typed, we can still do cal­cu­la­tions using this string val­ue, and it’ll coerce them to the right type:

    {{ total * 0.08 }}

…and it will spit out the appro­pri­ate cal­cu­la­tion result for you.

Link Includes work too!

Tan­gen­tial­ly, you can also use the same tech­nique for the Twig include func­tion. Here’s the tem­plate we include (which looks sus­pi­cious­ly like our calculateSales() macro from above):

{% set result = 0.0 %}
{% for order in orders %}
    {% set result = result + order['price'] * order['quantity'] %}
{% endfor %}
{{ result }}

We can just set this to a vari­able as well:

    {% set total = include('calculateSales.twig') | spaceless %}

The total vari­able is now set to the result of our included tem­plate.

We don’t even need the __toString() mag­ic in this case, because include returns the ren­dered string result already.

You can then do what you like with the total vari­able, just like in our pre­vi­ous example:

    <span>
        {{ total * 0.08 }}
    </span>

This will out­put the same cal­cu­lat­ed result as our macro!

Link Getting More Explicit

While let­ting Twig mag­i­cal­ly coerce strings for you works fine… many Con­tent Man­age­ment Sys­tems (CMS) where Twig is used have added explic­it type coer­cion filters.

Craft CMS (and oth­ers), for exam­ple added the fol­low­ing filters:

These fil­ters will coerce string into the type we’re look­ing for, which makes things a bit more explicit:

    {% set total = _self.calculateSales(orders) | spaceless | float %}

…and now if we dump() the val­ue of our total vari­able, we’ll see that it’s an actu­al float­ing point num­ber, not a string:

191.93

While this last step isn’t strict­ly nec­es­sary, I like being more explic­it about types, and I think it makes the Twig macros feel a lit­tle bit more like actu­al functions.

Link Returning non-scalar values

But what if you need to return a non-scalar val­ue such as an array or an object from your macros/​includes?

No prob­lem, we can just lever­age the json_encode / json_decode fil­ters to make this work.

In your macro or include, when you out­put the result, pipe it to the json_encode fil­ter to encode it as JSON:

{% set result = [] %}
{% for item in inventory %}
    {% if item['onSale'] %}
    	{% set result = result | merge([item]) %}
    {% endif %}
{% endfor %}
{{ result | json_encode }}

And then in your tem­plate, use the json_decode fil­ter to trans­form the JSON back into the non-scalar value:

    {% set saleItems = _self.onSaleItems(inventory) | spaceless | json_decode %}

Link Wrapping up

While the exam­ple here uses numer­ic cal­cu­la­tions, being able to set the result of a Twig macro or include to a vari­able opens up a world of pos­si­bil­i­ties beyond that.

Hope­ful­ly, this makes your Twig tem­plates DRY-er than ever.

Enjoy!