The simplest way to keep business logic out of your Twig templates.
All this plugin does is registers a Twig extension—everything else is up to you! With Twig Toolbox, you can finally start to clean up the mess that accumulates at the top of every template…
Twig Toolbox lets you inject custom filters, functions, globals, and tests into Craft’s template engine.
To get started, install the plugin from the Craft Plugin Store, or use Composer:
composer require oofbar/craft-twig-toolbox
php craft plugins/install twig-toolbox
Then, copy this into config/twig-toolbox.php
:
<?php return [
'filters' => [],
'functions' => [],
'globals' => [],
'tests' => [],
];
💡 If you’re in the mood for a challenge, writing your own Twig extension is a great way to get familiar with custom module development.
Let’s look at some examples of how each language feature can be used.
→ Documentation on Twig Filters
The filters
key should contain alphanumeric keys and functions or callables as values. Filters always have at least one argument!
<?php return [
'filters' => [
'salePrice' => function(float $price): float {
return $price * 0.9;
},
],
];
<div class="product">
<div class="sku">{{ product.sku }}</div>
<span class="price price--default">{{ product.price | money }}</span>
<span class="price price--members">{{ product.price | salePrice | money }}</span>
</div>
→ Documentation on Twig Functions
Each item in the functions
array should have an alphanumeric key, and a function as its value. The function needs to declare expected arguments, and should explicitly return a value, if appropriate.
😄 You can use virtually any Craft API in a function!
<?php return [
'functions' => [
'getDeals' => function(): array {
return Entry::find()
->section('products')
->onSale(true)
->all();
},
'log' => function(mixed $message): void {
Craft::getLogger()->log($message);
},
],
];
{# Use to fetch data for a loop... #}
{% for deal in getDeals() %}
<div class="deal">
<div class="title">{{ deal.title }}</div>
<div class="expiry">{{ deal.saleEndDate | date('short') }}</div>
</div>
{% else %}
{# ...or just do something silently! #}
{% do log('We didn’t show a user any deals!') %}
<div class="empty">Sorry, there is nothing on sale right now.</div>
{% endfor %}
→ Documentation on Twig Globals
Globals are best used sparingly, and only for simple values. The new custom config in Craft 4 is a near equivalent!
⚠️ Be mindful of what you are assigning to a global! Calling some Craft or Plugin APIs can cause a race condition as the system initializes.
<?php return [
'globals' => [
'cutoffTime' => (new \DateTime)->modify('midnight'),
],
];
<h2>Prices are valid until {{ cutoffTime | date }}!</h2>
→ Documentation on Twig Tests
Tests are sort of like functions, but only available when using Twig’s is
operator. They can do a lot to make your templates read more clearly—especially when the logic behind it is convoluted.
<?php
use craft\elements\User;
return [
'tests' => [
'expensive' => function(float $value): bool {
return $value > 10.0;
},
'member' => function(User $user): bool {
return $user->isInGroup('members');
},
],
];
{% set image = product.image.one() %}
{% if product.price is expensive %}
<img src="{{ image.url }}" class="shiny-effect">
{% else %}
<img src="{{ image.url }}">
{% endif %}
PHP has a special “type” for callable values. This includes the anonymous functions or “closures” we’ve used so far, in addition to a few other syntaxes that make it simple to add proxies to native PHP and Craft functions:
use craft\helpers\Number;
return [
'filters' => [
// Built-in PHP functions:
'chunk' => 'array_chunk',
// Craft helper proxy:
'roman' => [Number::class, 'upperRoman'],
],
];
Some of the functions above could be made even more flexible by accepting the special mixed
type, or a union type. For example, the expensive
test could do some type checking and normalization like this...
<?php
use craft\elements\Entry;
return [
'tests' => [
'expensive' => function(float|Entry $value): bool {
// Normalize an Entry into a scalar field value:
if ($value instanceof Entry) {
$value = $value->price;
}
return $value > 10.0;
},
],
];
...then, the template can read a bit more fluidly:
{% if product is not expensive %}
<button>Buy two!</button>
{% endif %}
filters
and functions
can take arguments to customize their behavior. If you find yourself adding a number of similar helpers, take a moment to consider how they could be consolidated and parameterized with one or more arguments.
Consider how Twig can help you generate HTML, rather than trying to build it up yourself!
<?php return [
'functions' => [
'bem' => function(string $base, array $flags): string {
$classNames = [$base];
// Create BEM-style class names, ignoring empty flags:
foreach (array_filter($flags) as $flag) {
$classNames[] = "{$base}--{$flag}";
}
return join(' ', array_unique($classNames));
},
],
];
<div class="{{ bem('product', [
product is expensive ? 'expensive' : null,
currentUser is member ? 'member-pricing' : null,
]) }}">
{{ product.title }}
</div>
If you’re having trouble getting started, create an issue on GitHub and we’ll do our best to help out! If you need support on a project-specific task (like finding the appropriate Craft APIs), we recommend posing it to the broader community.