Building a Custom Twig Filter the TDD Way

Share this article

Twig is a powerful, yet easy to master template engine. It is also my personal favorite as all my web development is based on either Symfony or Silex.

Twig Header

Apart from its core syntax ({{ ... }} and {% ... %}), Twig has built-in support for various filters. A filter is like a “converter”. It receives certain original data (a string, a number, a date, etc) and by applying a conversion, outputs the data in a new form (as a string, a number, a date, etc). For example, the number_format filter can convert a number into a more readable one:

{{price|number_format(2, '.', ',')}}

Assuming price is a variable with value 1234567.12345, after the filter operation, the output in the page will be 1,234,567.12: 2 decimal places, “.” as the decimal point and “,” as the thousands separator. This makes it much more readable.

As another example, capitalize will make every first letter of a word in a sentence uppercase and others lowercase:

{{title|capitalize}}

Assuming title is a variable with the value this tutorial is nice, after the filter operation, the output will be This Tutorial Is Nice.

Twig has a number of built-in filters. The full list can be found in its official documentation.

Why Choose a Filter?

Some may argue that the above functionality is also doable in PHP; that is true. We also often find the functionality provided by built-in filters quite limited and insufficient. So, why use a filter?

In an MVC environment, the model layer is responsible for providing data (a book price or an article title, for example). The view layer is responsible for displaying the data. Doing the data conversion, filter-style, in a controller is not advisable because it is against the design role of a controller, and doing it in a model effectively changes the data, which is not good. In my opinion, the view is the only viable option.

Besides, as a particular transformation of data may be requested in many places in a template (as well as in various templates) on the same data from various sources, it is better to call that filter in the template every time such a conversion is required, than to call a function in the controller. The code will be much tidier.

Let’s consider the following code segments comparing using a filter and a PHP function call (using Symfony 2 + Doctrine). We can easily see the differences in elegance and usability.

Filter version:

...
<em>{{ book.title|capitalize }}</em> has {{book.pages|number_format(2, ".", ",")}}
and costs ${{ book.price|number_format(2, ".", ",")}}.  
...

And for this approach, what we do in the controller will be:

$book=$repo->findById('00666');
...
return $this->render('A_Template', ['book'=>$book]);

Find the book (the data) and pass it to the view to display.

But if we use PHP function calls, the code may look like this:

//Using PHP function within a Symfony framework and Doctrine
$book=$repo->findById('00666');
$book['tmpTitle'] = ucwords($book->getTitle);
$book['tmpPage'] = number_format($book->getPages(), 2, ".", ",");
$book['tmpPrice'] = number_format($book->getPrice(), 2, ".", ",");
...
return $this->render('A_Template', ['book'=>$book]);

.. and then in the template

<em>{{ book.tmpTitle }}</em> has {{book.tmpPages}}
and costs ${{ book.tmpPrice}}.

You can see that the filter approach is much cleaner and easier to manage, with no clumsy temp variables in between.

Let’s build a filter

We’ll build a filter to display the publication date/time of a post in a fancier way. That is, instead of saying something like “Posted at 2015-03-14 13:34“, this timestamp will be transformed into something like “Just now“, “A few hours earlier“, “A few days back“, “Quite some time ago“, “Long, long ago“, etc.

We’ll build it in a TDD way. To get introduced to TDD, see this post and the links within it, but the approaches we take in this tutorial should be easy enough to understand even without looking into TDD beforehand.

First, install PHPUnit by executing the following Composer command:

composer global require phpunit/phpunit

This will install the most recent version of PHPUnit globally, making it accessible on your entire machine from any folder. We will use PHPUnit to run the tests and assert that all the expectations are met.

Set expectations

The use case is clear: we want to convert a date/time object (2014-03-19 12:34) into something like “Just now“, “A few hours ago“, “A few days back“, “Quite some time ago“, “Long, long ago“, etc, depending on how far the date/time is from the current moment.

There is no set rule to determine how far away a date/time should be so that it can be displayed as “Quite some time ago“. This is a subjective matter so we will define a customized rule set for our app and these rules will be reflected in our expectations:

How long ago To be regarded as
< 1 minute Just now
< 10 minutes Minutes ago
< 1 hour Within an hour
< 16 hours A few hours ago
< 24 hours Within one day
< 3 days Some time back
< 10 days Ages ago
> 10 days From Mars

Let’s translate these expectations into a test script so that we can test it with PHPUnit. This script is saved in src/AppBundle/Tests/Twig/timeUtilTest.php:

<?php

namespace AppBundle\Tests\Twig;
use AppBundle\Twig\AppExtension;

class timeUtilTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider tsProvider
     */
    public function testtssFilter($testTS, $expect)
    {
        $tu=new AppExtension();
        $output=$tu->tssFilter($testTS);
        $this->assertEquals($output, $expect);
    }
    
    public static function tsProvider()
    {
        return [
            [date_sub(new \DateTime(), new \DateInterval("PT50S")), "Just now"],
            [date_sub(new \DateTime(), new \DateInterval("PT2M")), "Minutes ago"],
            [date_sub(new \DateTime(), new \DateInterval("PT57M")), "Within an hour"],
            [date_sub(new \DateTime(), new \DateInterval("PT13H1M")), "A few hours ago"],
            [date_sub(new \DateTime(), new \DateInterval("PT21H2M")), "Within one day"],
            [date_sub(new \DateTime(), new \DateInterval("P2DT2H2M")), "Some time back"],
            [date_sub(new \DateTime(), new \DateInterval("P6DT2H2M")), "Ages ago"],
            [date_sub(new \DateTime(), new \DateInterval("P13DT2H2M")), "From Mars"],
        ];
    }
}

If we run this test now:

phpunit -c app/

The test won’t run because we have not defined the AppBundle\Twig\AppExtension yet. We can quickly create a skeleton file: src/AppBundle/Twig/AppExtension.php. It can be as simple as this:

namespace AppBundle\Twig;

class AppExtension extends \Twig_Extension
{
    public function getFilters()
    {
        return [
            new \Twig_SimpleFilter('tss', [$this, 'tssFilter']),
        ];
    }
    
    public function getName()
    {
        return 'app_extension';
    }

	public function tssFilter(\DateTime $timestamp)
    {
    	// to be implemented 
    }
}

Now we can run the test script. All tests (expectations) will fail because we have not done anything to implement the tssFilter function.

NOTE: Symfony2 works very well with PHPUnit. With the default Symfony2 setup, there is a phpunit.xml.dist file in the project’s app folder. The above command will automatically use that file as the configuration file for PHPUnit. Normally, no further adjustment is needed.

The full code of the tssFilter function is listed below:

public function tssFilter(\DateTime $timestamp)
    {
        $TSS=['Just now','Minutes ago','Within an hour','A few hours ago','Within one day','Some time back','Ages ago', 'From Mars'];

        $i=-1;
        $compared = new \DateTime();
        
        $ts1=$timestamp->getTimestamp();
        $co1=$compared->getTimestamp();
        
        $diff=$ts1-$co1;
        if($diff<0 ) // Diff is always <0, so always start from index 0
        {
            $i++;
        }
        
        if($diff<-1*60 ) //within one minute
        {
            $i++;
        }
        
        if($diff<-10*60) // within ten minues
        {
            $i++;
        }
        if($diff<-60*60)
        {
            $i++;
        }
        
        if($diff<-16*60*60)
        {
            $i++;
        }
        
        if($diff<-24*60*60)
        {
            $i++;
        }
        
        if($diff<-3*24*60*60)
        {
            $i++;
        }
        
        if($diff<-10*24*60*60)
        {
            $i++;
        }
        
        return $TSS[$i];
    }

The code will reside in tssFilter. It accepts a DateTime object so that the program can determine which string in $TSS should be returned based on far timestamp is from now.

That’s it! Run the test, and everything should pass!

Integrate it into Symfony

The tssFilter is still isolated from the Symfony framework. To use it in our template, we need to register it in the services.yml file:

services:
    app.twig_extension:
        class: AppBundle\Twig\AppExtension
        tags:
            - { name: twig.extension }

We must provide the fully qualified name of the filter class: AppBundle\Twig\AppExtension.

Finally, we can use it like this in our Twig template:

{{post.author|capitalize}} posted "{{post.title|capitalize}}" (posted {{post.creation|tss}})

The filter name (tss) is derived from src/AppBundle/Twig/AppExtension.php file’s tssFilter() function name and like with other Symfony components, “Filter” is stripped.

Wrapping up

In this quick tutorial, we covered a few things:

  1. Twig filters and why it is better to use them than pure PHP calls.
  2. How to build a custom filter the TDD way with PHPUnit.
  3. How to integrate a filter into the Symfony framework.

Leave your comments and thoughts below and share your achievements!

Frequently Asked Questions on Building Custom Twig Filter

What is a Twig filter and why is it important in Symfony?

A Twig filter is a feature in the Twig templating engine that allows you to modify variables. It’s a crucial part of Symfony, a PHP framework, because it provides a way to manipulate data before rendering it in a template. This can include formatting dates, translating text, or even complex operations like sorting an array. By creating custom Twig filters, you can tailor your data manipulation to fit your specific project needs, enhancing the flexibility and functionality of your Symfony application.

How do I create a custom Twig filter in Symfony?

Creating a custom Twig filter in Symfony involves defining a new Twig extension. This extension should contain a method that performs the desired data manipulation. You then need to register this extension as a service in your Symfony application. Once registered, you can use your custom filter in any Twig template just like you would use a built-in filter.

What is Test-Driven Development (TDD) and how does it apply to building a custom Twig filter?

Test-Driven Development (TDD) is a software development methodology where you write tests before writing the code that passes those tests. In the context of building a custom Twig filter, you would first write a test that defines the expected behavior of your filter. Then, you write the filter code, continually refining it until it passes the test. This approach ensures that your filter works as expected and can help catch any potential issues early in the development process.

How can I test my custom Twig filter?

You can test your custom Twig filter using PHPUnit, a testing framework for PHP. First, you need to write a test case that defines the expected output of your filter for a given input. Then, you run this test using PHPUnit. If the actual output of your filter matches the expected output, the test passes. Otherwise, the test fails, indicating that there’s an issue with your filter that needs to be addressed.

Can I use custom Twig filters in Drupal?

Yes, you can use custom Twig filters in Drupal. Drupal uses the Twig templating engine, so any custom filters you create can be used in your Drupal templates. However, you need to ensure that your filter is properly registered as a service in your Drupal application, just like you would in a Symfony application.

What are some common use cases for custom Twig filters?

Custom Twig filters can be used for a wide range of data manipulation tasks. For example, you might create a filter that formats a date in a specific way, or a filter that translates text into a different language. You could also create a filter that sorts an array based on a specific criterion, or a filter that converts a string into a slug for use in a URL. The possibilities are virtually endless.

How can I debug my custom Twig filter?

Debugging a custom Twig filter can be done using various tools and techniques. One common approach is to use var_dump() or print_r() to output the value of variables at different points in your filter code. You can also use a tool like Xdebug, which provides a full-featured debugging environment for PHP.

Can I create a custom Twig filter that takes multiple arguments?

Yes, you can create a custom Twig filter that takes multiple arguments. When defining your filter, you simply need to specify additional parameters for your filter method. These parameters will then be available as arguments when you use your filter in a Twig template.

Can I use custom Twig filters with other templating engines?

Custom Twig filters are specific to the Twig templating engine and cannot be used with other templating engines. However, many other templating engines have similar features that allow you to manipulate data before rendering it in a template.

How can I improve the performance of my custom Twig filter?

There are several ways to improve the performance of your custom Twig filter. One approach is to ensure that your filter code is as efficient as possible. This might involve optimizing your algorithms or reducing the number of operations your filter performs. Another approach is to use caching to store the results of your filter, reducing the need to recompute these results for each request.

Taylor RenTaylor Ren
View Author

Taylor is a freelance web and desktop application developer living in Suzhou in Eastern China. Started from Borland development tools series (C++Builder, Delphi), published a book on InterBase, certified as Borland Expert in 2003, he shifted to web development with typical LAMP configuration. Later he started working with jQuery, Symfony, Bootstrap, Dart, etc.

BrunoSfilterfilteringfiltersOOPHPPHPsymfonysymfony frameworksymfony2tddtwig
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week