Extending Twig in Symfony 4

In the last article we have covered twig templating basics concepts. Now it’s time to extend functionalities, which can be very useful. Twig has a fair amount of built-in functions, tags, filters and operators but in some cases, you will find that not sufficient. We will cover how to extend twig and what can be extended with examples.
How extending works
For extending twig, you will need twig extensions bundle. It’s a separate bundle from twig. It can be installed via composer:
composer require twig/extensions
We will write our code in src/Twig/folder since latest best practices are oriented for the bundle-less applications (https://medium.com/@fabpot/symfony-4-best-practices-b4bbd6a9c994). Once you have installed twig/extensions bundle, you can start writing your code for extending twig. To extend twig we will create our class TestExtension in src/Twig folder, class will also extend Twig\Extension\AbstractExtension class. Abstract class extends Twig_Extension, which implements Twig_ExtensionInterface. Let’s take a look at the interface, it will show us what can be extended (filters, tests, functions and operators):
interface Twig_ExtensionInterface
{
/**
* Returns the token parser instances to add to the existing list.
*
* @return Twig_TokenParserInterface[]
*/
public function getTokenParsers();
/**
* Returns the node visitor instances to add to the existing list.
*
* @return Twig_NodeVisitorInterface[]
*/
public function getNodeVisitors();
/**
* Returns a list of filters to add to the existing list.
*
* @return Twig_Filter[]
*/
public function getFilters();
/**
* Returns a list of tests to add to the existing list.
*
* @return Twig_Test[]
*/
public function getTests();
/**
* Returns a list of functions to add to the existing list.
*
* @return Twig_Function[]
*/
public function getFunctions();
/**
* Returns a list of operators to add to the existing list.
*
* @return array<array> First array of unary operators, second array of binary operators
*/
public function getOperators();
}
Extend with class
Above we have already defined our class name, so now let’s create src/Twig/ TestExtension.php script and define the class inside. Today we will extend functionality with one new filter, function and test. For demonstration, we’ll create filter, test and function.
Functions extend
Imagine you have numbers array and you need an average value to be used in twig template. Since there is no built-in function for that, let’s create one. We already have class (TestExtension), now we need to implement our logic in getFunctions method. First, we need to provide functions array inside getFunctions method, to provide new function we need to instantiate Twig\TwigFunction class with name (twig function name) and function (TestExtension class function name and class instance) to call, in our case name is average and function is averageFunction. Next, we need to create averageFunction with numbers array in parameter, and last we need to return average value calculated with array_sum and items count.
How to use in template:
Array ({{ dump(values) }}) average value: {{ average(values) }}
Here is final result:
public function getFunctions()
{
return array(
new TwigFunction('average', array($this, 'averageFunction')),
);
}
public function averageFunction($numbers = array())
{
return array_sum($numbers) / count($numbers);
}
Tests extend
There is always a need to test if the email is valid. Maybe not so much in twig, but you never know. We will create emailvalid twig function to check if the email is valid. First, we’ll define match pattern in the class constant:
const EMAIL_PATTERN = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/i";
Next is to add our test in getTests function array, for that we need to instantiate Twig\TwigTest class with “emailvalid” as name and emailValid as function in class. After setup last part is to create function emailValid and match, here is final result:
public function getTests()
{
return array(
new TwigTest('emailvalid', array($this, 'emailValid')),
);
}
public function emailValid($email = '')
{
return preg_match(self::EMAIL_PATTERN, $email);
}
Twig use:
Email ([email protected]) valid: {% if '[email protected]' is emailvalid %}valid{% else %}invalid{% endif %}
Email (drdisrespect@slickdaddyclub) valid: {% if 'drdisrespect@slickdaddyclub' is emailvalid %}valid{% else %}invalid{% endif %}
Filters extend
The last example is extending filters, it’s similar to previous extends. We need to add our filter in getFilters array, for that we will provide Twig\TwigFilter class instance with name (moneyTest) and function moneyFilter for class function name. Next, we need to create moneyFilter function with the number in parameter and logic for money format. For demonstration we will use PHP built-in money_format function wit locale set on en_US (Important: on debian/ubuntu ‘en_US’ is not a valid locale – you need ‘en_US.UTF-8’ or ‘en_US.ISO-8559-1’.). Here is final result:
public function getFilters()
{
return array(
new TwigFilter('moneyTest', array($this, 'moneyFilter')),
);
}
public function moneyFilter($number = 0)
{
//On debian/ubuntu 'en_US' is not a valid locale - you need 'en_US.UTF-8' or 'en_US.ISO-8559-1'.
setlocale(LC_MONETARY, 'en_US.UTF-8');
return money_format('%(#10n', $number) . "\n";
}
Twig usage:
Number 2968.68 filtered in US dollars: {{ 2968.68 | moneyTest }}
Final code
<?php
/**
* Twig extension example for article @ inchoo.net
*/
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
class TestExtension extends AbstractExtension
{
const EMAIL_PATTERN = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/i";
public function getFunctions()
{
return array(
new TwigFunction('average', array($this, 'averageFunction')),
);
}
public function averageFunction($numbers = array())
{
return array_sum($numbers) / count($numbers);
}
public function getTests()
{
return array(
new TwigTest('emailvalid', array($this, 'emailValid')),
);
}
public function emailValid($email = '')
{
return preg_match(self::EMAIL_PATTERN, $email);
}
public function getFilters()
{
return array(
new TwigFilter('moneyTest', array($this, 'moneyFilter')),
);
}
public function moneyFilter($number = 0)
{
//On debian/ubuntu 'en_US' is not a valid locale - you need 'en_US.UTF-8' or 'en_US.ISO-8559-1'.
setlocale(LC_MONETARY, 'en_US.UTF-8');
return money_format('%(#10n', $number) . "\n";
}
}
twig:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
</head>
<body>
<p>Array ({{ dump(values) }}) average value: {{ average(values) }}</p>
<p>Email ([email protected]) valid: {% if '[email protected]' is emailvalid %}valid{% else %}invalid{% endif %}</p>
<p>Email (drdisrespect@slickdaddyclub) valid: {% if 'drdisrespect@slickdaddyclub' is emailvalid %}valid{% else %}invalid{% endif %}</p>
<p>Number 2968.68 filtered in US dollars: {{ 2968.68 | moneyTest }}</p>
</body>
</html>
Conclusion
Extending Twig is fairly easy and it gives no limit what can be done in templates. Be aware to not overextend with something that not belong in templates (for example data preparation which should be used in controllers or models). Symfony has some built-in additional twig functions, you can check them here: https://symfony.com/doc/current/reference/twig_reference.html. Because of its almost unlimited extend potential and easy use twig can save you a lot of time and resources, not only in Symfony. I encourage you to try twig and twig extensions bundle, let me know your thoughts in a comment.
1 comment
uuuuuh.. Zoran
i see what you did there…. slick daddy approves.. great article btw.