Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Making Twig Macros Work Like Functions
Learn how to make Twig macros return values you can manipulate, just like functions in other programming languages
Twig Macros are a way you can abstract bits of reusable code away into neat little template fragments that work much like functions do in other programming languages.
This lets you embrace Don’t Repeat Yourself (DRY) software development principles, which can make your code easier to manage.
There is a catch, however
Twig macros don’t return values like functions typically do; instead, they just output a result as a string.
This makes them a little less useful than a function, because you might want to use macros to compute some kind of value that you can use later on in multiple places, rather than just being output.
This article will show you how you can use Twig macros to return values, just like real functions.
Let’s start with an example 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 parameter orders, and calculates the total sales by multiplying each order’s price by its quantity.
The orders data structure could come from a database query, a GraphQL query, a Collection, or wherever.
If we output the result of this macro with our contrived data passed in like this:
<span>
{{ _self.calculateSales(orders) }}
</span>
The HTML result will look something like this:
<span> 191.93
</span>
Link Whitespace Control
You may be wondering what the weird spacing is all about. Well, that’s because of the spacing in the actual code that makes up our Twig macro.
Without that spacing in our code, it’d be difficult for humans to read. Fortunately, HTML doesn’t care about the extra spacing, but let’s tighten it up by using whitespace control:
{%- 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 output look a little prettier for us, and also the result is starting to look a little bit more like a function result now that the whitespace is gone:
<span>
191.93
</span>
If you don’t feel like adding all of the {%- -%} and {{- -}} whitespace control to your macro, you can also just do this:
{{ _self.calculateSales(orders) | spaceless }}
This uses the spaceless filter to remove the whitespace, 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 useful enough already for some scenarios, it’s still not returning a value. It’s just outputting a string.
What if we wanted to call calculateSales() in multiple places, and then do some sort of computation based on the result, such as calculating sales tax, and then presenting a grand total?
Some might argue that this is out of Twig’s “presentational” role, and data should be injected into the template in a form that’s ready for display.
While I agree with this general sentiment, there are many real-world situations I’ve run into where Twig macros would be so much more useful 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 variable total is set to the output 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 rendered template code.
So we need to get just the value of the content property from this object. Alas, content is a private property, so we can’t just do total.content… but we can leverage PHP’s __toString() magic method (which is implemented 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 variable, as we can see from dump():
"191.93"
In fact, if we use the spaceless filter instead of the whitespace control operators ({{- -}}), we don’t even need __toString() because the spaceless filter converts 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 variable, as we can see from dump():
"191.93"
Even though this is a string, but thanks to Twig & PHP being loosely typed, we can still do calculations using this string value, and it’ll coerce them to the right type:
{{ total * 0.08 }}
…and it will spit out the appropriate calculation result for you.
Link Includes work too!
Tangentially, you can also use the same technique for the Twig include function. Here’s the template we include (which looks suspiciously 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 variable as well:
{% set total = include('calculateSales.twig') | spaceless %}
The total variable is now set to the result of our include’d template.
We don’t even need the __toString() magic in this case, because include returns the rendered string result already.
You can then do what you like with the total variable, just like in our previous example:
<span>
{{ total * 0.08 }}
</span>
This will output the same calculated result as our macro!
Link Getting More Explicit
While letting Twig magically coerce strings for you works fine… many Content Management Systems (CMS) where Twig is used have added explicit type coercion filters.
Craft CMS (and others), for example added the following filters:
These filters will coerce string into the type we’re looking for, which makes things a bit more explicit:
{% set total = _self.calculateSales(orders) | spaceless | float %}
…and now if we dump() the value of our total variable, we’ll see that it’s an actual floating point number, not a string:
191.93
While this last step isn’t strictly necessary, I like being more explicit about types, and I think it makes the Twig macros feel a little bit more like actual functions.
Link Returning non-scalar values
But what if you need to return a non-scalar value such as an array or an object from your macros/includes?
No problem, we can just leverage the json_encode / json_decode filters to make this work.
In your macro or include, when you output the result, pipe it to the json_encode filter 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 template, use the json_decode filter to transform the JSON back into the non-scalar value:
{% set saleItems = _self.onSaleItems(inventory) | spaceless | json_decode %}
Link Wrapping up
While the example here uses numeric calculations, being able to set the result of a Twig macro or include to a variable opens up a world of possibilities beyond that.
Hopefully, this makes your Twig templates DRY-er than ever.
Enjoy!