Pragmatism in the real world

Overriding the built-in Twig date filter

In one project that I’m working on, I’m using Twig and needed to format a date received from an API. The date string received is of the style “YYYYMMDD”, however date produced an unexpected output.

Consider this:

{{ "20141216"|date('jS F Y') }}

creates the output:

20th May 1976

This surprised me. Then I thought about it some more and realised that the date filter is treating my date string as a unix timestamp. I investigated and discovered the problem in twig_date_converter:

    $asString = (string) $date;
    if (ctype_digit($asString) 
        || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
        $date = '@'.$date;
    }

    $date = new DateTime($date, $defaultTimezone);

This code tests to see if the dates string provided is a number (positive or negative) and then prepends an ‘@’ symbol to the front. This has the effect of informing DateTime‘s constructor to treat $date as a unix timestamp.

Unfortunately, twig_date_converter is a function and so I couldn’t override it, so I wrote my own extension that registers new date, date_modify filters and a new date function in order to solve my problem:

<?php
/*
 * Extension to provide updated date & date_modify filters along with an
 * updated date function which do not auto-convert strings of numbers to
 * a unix timestamp.
 *
 * Code within dateFilter(), modifyFilter() and dateFromString() extracted
 * from Twig/lib/Twig/Extension/Core.php which is (c) 2009 Fabien Potencier
 * and licensed as per https://github.com/twigphp/Twig/blob/master/LICENSE
 */
namespace My\Twig\Extension;

use Twig_Extension;
use Twig_SimpleFilter;
use Twig_SimpleFunction;
use DateTime;
use DateTimeInterface;
use DateTimeImmutable;

class DateExtension extends Twig_Extension
{
    public function getName()
    {
        return 'my_date';
    }

    public function getFilters()
    {
        return array(
            new Twig_SimpleFilter('date', [$this, 'dateFilter'],
                    ['needs_environment' => true]),
            new Twig_SimpleFilter('date_modify', [$this, 'modifyFilter'],
                    ['needs_environment' => true]),
        );
    }

    public function getFunctions()
    {
        return array(
            new Twig_SimpleFunction('date', [$this, 'dateFromString'],
                    ['needs_environment' => true]),
        );
    }

    public function dateFilter($env, $date, $format = null, $timezone = null)
    {
        if (null === $format) {
            $formats = $env->getExtension('core')->getDateFormat();
            $format = $date instanceof DateInterval ? $formats[1] : $formats[0];
        }

        if ($date instanceof DateInterval) {
            return $date->format($format);
        }

        return $this->dateFromString($env, $date, $timezone)->format($format);
    }

    public function modifyFilter($env, $date, $format = null, $timezone = null)
    {
        $date = $this->dateFromString($env, $date, false);
        $date->modify($modifier);

        return $date;
    }

    public function dateFromString($env, $date, $timezone)
    {
        // determine the timezone
        if (!$timezone) {
            $defaultTimezone = $env->getExtension('core')->getTimezone();
        } elseif (!$timezone instanceof DateTimeZone) {
            $defaultTimezone = new DateTimeZone($timezone);
        } else {
            $defaultTimezone = $timezone;
        }

        // immutable dates
        if ($date instanceof DateTimeImmutable) {
            return false !== $timezone ? $date->setTimezone($defaultTimezone) : $date;
        }

        if ($date instanceof DateTime || $date instanceof DateTimeInterface) {
            $date = clone $date;
            if (false !== $timezone) {
                $date->setTimezone($defaultTimezone);
            }

            return $date;
        }

        $date = new DateTime($date, $defaultTimezone);
        if (false !== $timezone) {
            $date->setTimezone($defaultTimezone);
        }

        return $date;
    }
}

This class simply registers new date, date_modify filters and a new date function to replace the ones in Twig core and then is a direct copy of the functions twig_date_format_filter, twig_date_modify_filter and twig_date_converter with the functionality above removed.

I also needed to register this extension with the Twig_Environment using: $env->addExtension(new \My\Twig\Extension\DateExtension()); and I’m done.

{{ "20141216"|date('jS F Y') }}

now correctly outputs:

16th December 2014

While, it’s a shame I can’t just override twig_date_converter, I’m glad that I can re-register the relevant Twig filters and function.

5 thoughts on “Overriding the built-in Twig date filter

  1. You *could*, I suppose, try to trick Twig by adding a '.' (or something that won't affect DateTime's parsing) to the end of your date string – it'll subvert the ctype_digit test thus making DateTime parse it for you.

    But that's no fun of course.

Comments are closed.