Learn how to use the time_diff function to display a date on the views with a time ago format and support for i18n to implement an human friendly date-time formatting.

How to use the time_diff and ago (time ago) functions on Dates for Twig views in Symfony 3

On many web applications and websites you are able to see dates and read them easily without knowing which day is today or without knowing which day is the 21 of May, January etc. This feature is achieved usually in the browser using JavaScript. So what's the problem? The libraries for formatting aren't (usually) lightweight and you will end up with at least 3 libraries to achieve something like that.  In case you want to implement such a feature on your application and you don't need to update the dates dinamically on the view, you can make this task on your own server.

In this article we'll show you 2 ways to display dates in the time ago format: using the KnpTimeBundle (a) or implementing the Twig_Extensions_Extension_Date extension of Twig. Both of them provide the same functionality but the implementation of 1 may be longer for some developers, so it's up to you which method is easier to implement for you.

A. The Bundle (easy) way

If you don't have to worry about the size of your application and you are allowed to install third party bundles, then it will be easier to achieve your goal (a lot easier):

1. Install and register the KnpTimeBundle

We are talking about the KnpTimeBundle. This bundle does one simple job: takes dates and return friendly "2 hours ago"-type messages. To install this bundle in your Symfony project execute the following command on the terminal:

composer require knplabs/knp-time-bundle

For more information about this bundle, please visit the official repository at Github here. Once the installation of the bundle finishes, be sure to enable the Bundle in the Kernel of Symfony (app/AppKernel.php):

<?php

// app/AppKernel.php

public function registerBundles()
{
    $bundles = [
        // ...
        new Knp\Bundle\TimeBundle\KnpTimeBundle(),
        // ...
    ];

    // ...
}

And you're ready to continue with the next step.

2. Add App Locale and Enable translator

Usually, on every default Symfony application the locale already exists and it's by default en (english). This value is used in your entire application, not only in the bundle. In case it doesn't exists provide the locale parameter with the identifier of your own language:

Note

There are a lot of supported languages, so be sure to check the translations files to see which languages are supported.

parameters:
    locale: en
    # Or the locale of your language e.g : es,de,nl,pt,pl etc

As next, you need to enable the translator because it's usually commented, so be sure that you translator setting in the app/config/config.yml file is uncommented and the fallbacks uses the previous declared locale parameter:

framework:
    translator: { fallbacks: ['%locale%'] }

Once this is made, you will be able to use the ago filter of the bundle to display the readable description of the time difference.

3. Using the Twig ago filter

The KnpTimeBundle exposes a new twig filter namely ago. This filter expects as target a DateTime object that refers to $since (the origin date) and an optional parameter $to that specifies from where should the difference should be made (another DateTime object), for example:

{#
    In this example we convert the now string to a date
    The date can be retrieven from the controller etc.
#}
{% set myDate = "now"|date %}

{#
    Modify our date by removing 4 days
#}
{% set myDate = myDate|date_modify('-4 day') %}

{# Displays according to your locale: 
    4 days ago 
    vor 4 Tagen
    hace 4 días
    etc
#}
{{ myDate|ago}}


{#
    And if you need to differentiate the date from another day but not now
    provide the first argument:
#}

{% set fromTomorrow = "now"|date_modify('+1 day') %}

{# Displays according to your locale: 
    5 days ago
    vor 5 Tagen
    hace 5 días
    etc
#}
{{ myDate|ago(fromTomorrow)}}

Easy isn't ?

B. The self-implemented way

If you are happy today and have time to waste, then you may learn something new. The self implemented way takes a little more to implement but if you aren't able to install a third party bundle, then this is your best option.

1. Create Twig_Extensions_Extension_Date extension

The first you will need to do is to create in your project the Twig_Extensions_Extension_Date extension in your project. This class refers to the official date extension of Twig by Fabien Potencier. The advantages of the following class is that its able to use the translator module of Symfony too, so it's something similar to the bundle of the first step.

Create the Extension file namely Twig_Extensions_Extension_Date.php on some directory of your application and change the namespace according to yours. In this case, we are creating the class inside the Extensions folder in the AppBundle:

<?php

namespace AppBundle\Extensions;

/**
 * This file is part of Twig.
 *
 * (c) 2014 Fabien Potencier
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
use Symfony\Component\Translation\TranslatorInterface;
use Twig_Environment;
use Twig_SimpleFilter;
use Twig_Extension;

/**
 * @author Robin van der Vleuten <[email protected]>
 */
class Twig_Extensions_Extension_Date extends Twig_Extension
{
    public static $units = array(
        'y' => 'year',
        'm' => 'month',
        'd' => 'day',
        'h' => 'hour',
        'i' => 'minute',
        's' => 'second',
    );

    /**
     * @var TranslatorInterface
     */
    private $translator;

    public function __construct(TranslatorInterface $translator = null)
    {
        $this->translator = $translator;
    }

    /**
     * {@inheritdoc}
     */
    public function getFilters()
    {
        return array(
            new Twig_SimpleFilter('time_diff', array($this, 'diff'), array('needs_environment' => true)),
        );
    }

    /**
     * Filter for converting dates to a time ago string like Facebook and Twitter has.
     *
     * @param Twig_Environment $env  a Twig_Environment instance
     * @param string|DateTime  $date a string or DateTime object to convert
     * @param string|DateTime  $now  A string or DateTime object to compare with. If none given, the current time will be used.
     *
     * @return string the converted time
     */
    public function diff(Twig_Environment $env, $date, $now = null)
    {
        // Convert both dates to DateTime instances.
        $date = twig_date_converter($env, $date);
        $now = twig_date_converter($env, $now);

        // Get the difference between the two DateTime objects.
        $diff = $date->diff($now);

        // Check for each interval if it appears in the $diff object.
        foreach (self::$units as $attribute => $unit) {
            $count = $diff->$attribute;

            if (0 !== $count) {
                return $this->getPluralizedInterval($count, $diff->invert, $unit);
            }
        }

        return '';
    }

    protected function getPluralizedInterval($count, $invert, $unit)
    {
        if ($this->translator) {
            $id = sprintf('diff.%s.%s', $invert ? 'in' : 'ago', $unit);

            return $this->translator->transChoice($id, $count, array('%count%' => $count), 'date');
        }

        if (1 !== $count) {
            $unit .= 's';
        }

        return $invert ? "in $count $unit" : "$count $unit ago";
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'date';
    }
}

2. Add App Locale and Enable translator

Usually, on every default Symfony application the locale already exists and it's by default en (english). This value is used in your entire application, not only in the bundle. In case it doesn't exists provide the locale parameter with the identifier of your own language:

parameters:
    locale: en
    # Or the locale of your language e.g : es,de,nl,pt,pl etc

As next, you need to enable the translator because it's usually commented, so be sure that you translator setting in the app/config/config.yml file is uncommented and the fallbacks uses the previous declared locale parameter:

framework:
    translator: { fallbacks: ['%locale%'] }

Once this is made, you will be able to use the translator module on your extension.

3. Register extension

As next, proceed to register the extension with the translator service as argument and providing the path to the class of the previous step:

services:   
    twig.extension.date:
        # the namespace with the name of the Twig Extensions created class
        class: AppBundle\Extensions\Twig_Extensions_Extension_Date
        arguments: ["@translator"]
        tags:
          -  { name: twig.extension }

4. Creating translation files

As next you need to create the translation files, however, for lazy developers like me, there are always a way to make it everything easy. The translation files in this case will be in the xliff format because we can copy the translations of the KnpTimeBundle and so we won't need to write our own translation files. However, note that the namespace in KnpTimeBundle is time, but in this extension is date due to the providen name of the service twig.extension.date.

Go to the translation files of the KnpTimeBundle here and choose the one(s) you need and copy them in to the app/Resources/translations/ folder of your project (if the translations folder doesn't exist, then create it). For example, the following file (app/Resources/translations/date.de.xliff) provides the translation for our dates in German:

Important

Note that the id of every trans-unit is a string. In the repository of KnpTimeBundle the ids are numbers, so be sure to change the id by the content of the source attribute, otherwise Symfony won't find any item to translate. The name of the xliff files need to follow the filename pattern (in this case) date.<lang-identifier>.xliff.

<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext" original="file.ext">
        <body>
            <trans-unit id="diff.ago.year">
                <source>diff.ago.year</source>
                <target>vor einem Jahr|vor %count% Jahren</target>
            </trans-unit>
            <trans-unit id="diff.ago.month">
                <source>diff.ago.month</source>
                <target>vor einem Monat|vor %count% Monaten</target>
            </trans-unit>
            <trans-unit id="diff.ago.day">
                <source>diff.ago.day</source>
                <target>vor %count% Tag|vor %count% Tagen</target>
            </trans-unit>
            <trans-unit id="diff.ago.hour">
                <source>diff.ago.hour</source>
                <target>vor einer Stunde|vor %count% Stunden</target>
            </trans-unit>
            <trans-unit id="diff.ago.minute">
                <source>diff.ago.minute</source>
                <target>vor einer Minute|vor %count% Minuten</target>
            </trans-unit>
            <trans-unit id="diff.ago.second">
                <source>diff.ago.second</source>
                <target>vor einer Sekunde|vor %count% Sekunden</target>
            </trans-unit>
            <trans-unit id="diff.empty">
                <source>diff.empty</source>
                <target>jetzt</target>
            </trans-unit>
            <trans-unit id="diff.in.second">
                <source>diff.in.second</source>
                <target>in einer Sekunde|in %count% Sekunden</target>
            </trans-unit>
            <trans-unit id="diff.in.hour">
                <source>diff.in.hour</source>
                <target>in einer Stunde|in %count% Stunden</target>
            </trans-unit>
            <trans-unit id="diff.in.minute">
                <source>diff.in.minute</source>
                <target>in einer Minute|in %count% Minuten</target>
            </trans-unit>
            <trans-unit id="diff.in.day">
                <source>diff.in.day</source>
                <target>in einem Tag|in %count% Tagen</target>
            </trans-unit>
            <trans-unit id="diff.in.month">
                <source>diff.in.month</source>
                <target>in einem Monat|in %count% Monaten</target>
            </trans-unit>
            <trans-unit id="diff.in.year">
                <source>diff.in.year</source>
                <target>in einem Jahr|in %count% Jahren</target>
            </trans-unit>
        </body>
    </file>
</xliff>

5. Use time_diff filter in the views

In the same way the ago filter of the KnpTimeBundle does, the time_diff filter expects as target a DateTime object that refers to $since (the origin date) and an optional parameter $to that specifies from where should the difference should be made (another DateTime object), for example:

{#
    In this example we convert the now string to a date
    The date can be retrieven from the controller etc.
#}
{% set myDate = "now"|date %}

{#
    Modify our date by removing 4 days
#}
{% set myDate = myDate|date_modify('-4 day') %}

{# Displays according to your locale: 
    4 days ago 
    vor 4 Tagen
    hace 4 días
    etc
#}
{{ myDate|time_diff}}

{#
    And if you need to differentiate the date from another day but not now
    provide the first argument:
#}

{% set fromTomorrow = "now"|date_modify('+1 day') %}

{# Displays according to your locale: 
    5 days ago
    vor 5 Tagen
    hace 5 días
    etc
#}
{{ myDate|time_diff(fromTomorrow)}}

Happy coding !


Senior Software Engineer at Software Medico. Interested in programming since he was 14 years old, Carlos is a self-taught programmer and founder and author of most of the articles at Our Code World.

Sponsors