Twig icon indicating copy to clipboard operation
Twig copied to clipboard

IntlExtension >= 3.8 throws Exception when formatDateTime is used and $timezone === null

Open TPantelConventus opened this issue 1 year ago • 6 comments

Background

The Exception occurred after I updated from 3.7 to 3.8 and the Workaround for me right now is to downgrade to 3.7.

Description

I use the fomat_datetime filter in many places in my app like this:

{{ presentation.presentationFileUploadedAt | format_datetime('medium', timezone=false, locale=app.request.locale) }}

Before the update of "twig/intl-extra" from 3.7 to 3.8 that worked without any problems, but when i updated to 3.8 it suddenly gave me Excpetions:

An exception has been thrown during the rendering of a template ("Twig\Extra\Intl\IntlExtension::createDateFormatter(): Argument #5 ($timezone) must be of type ?DateTimeZone, bool given, called in /var/www/html/vendor/twig/intl-extra/IntlExtension.php on line 379").

That was wondering me a lot, so I looked into IntlExtension.php to look for the corresponding line:

/**
     * @param \DateTimeInterface|string|null  $date     A date or null to use the current time
     * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
     */
    public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string
    {
        $date = twig_date_converter($env, $date, $timezone);

        $formatterTimezone = $timezone;
        if (null === $formatterTimezone) {
            $formatterTimezone = $date->getTimezone();
        } elseif (\is_string($formatterTimezone)) {
            $formatterTimezone = new \DateTimeZone($timezone);
        }
      // THE NEXT LINE IS LINE 379
  $formatter = $this->createDateFormatter($locale, $dateFormat, $timeFormat, $pattern, $formatterTimezone, $calendar); 

        if (false === $ret = $formatter->format($date)) {
            throw new RuntimeError('Unable to format the given date.');
        }

        return $ret;
    }

So formatDateTime (which the twig filter uses) accepts $timezone === false, but then in line 379 createDateFormatter() is called with $formatterTimezone being false, throwing the Exception, because:

private function createDateFormatter(?string $locale, ?string $dateFormat, ?string $timeFormat, string $pattern, ?\DateTimeZone $timezone, string $calendar): \IntlDateFormatter

The solution would be to modify the formatDateTimeFunction:

/**
     * @param \DateTimeInterface|string|null  $date     A date or null to use the current time
     * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
     */
    public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string
    {
        $date = twig_date_converter($env, $date, $timezone);

        $formatterTimezone = $timezone;
        if (null === $formatterTimezone || false === $formatterTimezone ) {
            $formatterTimezone = $date->getTimezone();
        } elseif (\is_string($formatterTimezone)) {
            $formatterTimezone = new \DateTimeZone($timezone);
        }
      // THE NEXT LINE IS LINE 379
  $formatter = $this->createDateFormatter($locale, $dateFormat, $timeFormat, $pattern, $formatterTimezone, $calendar); 

        if (false === $ret = $formatter->format($date)) {
            throw new RuntimeError('Unable to format the given date.');
        }

        return $ret;
    }

Replacing the twig filter from timezone=false to timezone=null is NOT a valid solution, as it prevents the Exception from being thrown, BUT results in a wrong time being displayed, because the $date variable in the function is depending on $timezone and not $formatterTimezone

TPantelConventus avatar Jan 16 '24 14:01 TPantelConventus

probably related to #3903

/cc @keulinho

xabbuh avatar Jan 16 '24 14:01 xabbuh

What about checking for null and false?

fabpot avatar Jan 17 '24 08:01 fabpot

To be honest from reading the comments I did not really understand if and if so how the behaviour should have beeen different for null and false or if they should always have been treated the same way.

xabbuh avatar Jan 17 '24 08:01 xabbuh

Hey @xabbuh do you try this? {{ presentation.presentationFileUploadedAt | format_datetime('medium', timezone=null, locale=app.request.locale) }} or {{ presentation.presentationFileUploadedAt | format_datetime('medium', locale=app.request.locale) }} (null is the default) Why you set the timezone to "false" instead of "null", because null is already the "undefined" state. I think the formatDateTime method should be type hinted with "?string" to avoid wrong variable types...?! Regards, Tim

lochmueller avatar Feb 06 '24 15:02 lochmueller

Hey @xabbuh do you try this? {{ presentation.presentationFileUploadedAt | format_datetime('medium', timezone=null, locale=app.request.locale) }} or {{ presentation.presentationFileUploadedAt | format_datetime('medium', locale=app.request.locale) }} (null is the default) Why you set the timezone to "false" instead of "null", because null is already the "undefined" state. I think the formatDateTime method should be type hinted with "?string" to avoid wrong variable types...?! Regards, Tim

Because setting the timezone to null (the default) will lead to a different output than setting the timezone to false, which is intended and also documented in the Twig docs:

Timezone By default, the date is displayed by applying the default timezone (the one specified in php.ini or declared in Twig -- see below), but you can override it by explicitly specifying a timezone:

{{ datetime|format_datetime(locale='en', timezone='Pacific/Midway') }}

If the date is already a DateTime object, and if you want to keep its current timezone, pass false as the timezone value:

{{ datetime|format_datetime(locale='en', timezone=false) }}

TPantelConventus avatar Feb 07 '24 07:02 TPantelConventus

Looking at the code it seems that the CoreExtension::dateConverter already takes care of timezone=false (or setting the proper timezone when one is passed and loading the default timezone when null is passed)

With that, it seems extraneous to also attempt handling the timezone again in the format_datetime filter.

fryght avatar Feb 08 '24 09:02 fryght