Preface: I tested this in CE 2.3.6, but according to Github the relevant code is unchanged since years.

Expected behaviour in M1

Consider this code and its output in M1 (

$coreDate = Mage::getSingleton('core/date');
    "gmtTimestamp()\t" . $coreDate->gmtTimestamp() . "\n" .
    "timestamp()\t" . $coreDate->timestamp() . "\n" .
    "gmtDate()\t" . $coreDate->gmtDate() . "\n" .
    "date()\t\t" . $coreDate->date() . "\n";


gmtTimestamp()  1609489364
timestamp()     1609492964
gmtDate()       2021-01-01 08:22:44
date()          2021-01-01 09:22:44

As you see, it returns the time in GMT/UTC and the local timezone (Europe/Berlin, UTC+1) as expected.

Wrong behaviour in CE 2.3.6

Now have a look at the equivalent (?) code in CE 2.3.6:

$this->formatOutput('DateTime->date()', $dateTime->date());
$this->formatOutput('DateTime->gmtDate()', $dateTime->gmtDate());


DateTime->date()      2021-01-01 08:36:33
DateTime->gmtDate()   2021-01-01 08:36:33

As you see, there is no difference between date / gmtDate or timestamp/gmtTimestamp.
Calling date_default_timezone_set('Europe/Berlin') prior to the test changes the last four lines to the local time, including gmtDate().

Long version with debug info:

$this->formatOutput('$dateTime', get_class($dateTime));
$this->formatOutput('$timeZone', get_class($timeZone));
$this->formatOutput('date.timezone', ini_get('date.timezone'));
$this->formatOutput('date.timezone', ini_get('date.timezone'));
$this->formatOutput('date_default_timezone_get()', date_default_timezone_get());
$this->formatOutput('TimeZone->getDefaultTimezone()', $timeZone->getDefaultTimezone());
$this->formatOutput('TimeZone->getConfigTimezone()', $timeZone->getConfigTimezone());
$this->formatOutput('DateTime->getGmtOffset()', $dateTime->getGmtOffset());
$this->formatOutput('DateTime->timestamp()', $dateTime->timestamp());
$this->formatOutput('DateTime->gmtTimestamp()', $dateTime->gmtTimestamp());
$this->formatOutput('DateTime->date()', $dateTime->date());
$this->formatOutput('DateTime->gmtDate()', $dateTime->gmtDate());
$this->formatOutput('DateTime->date("Y-m-d H:i:s", time())', $dateTime->date('Y-m-d H:i:s', time()));
$this->formatOutput('DateTime->gmtDate("Y-m-d H:i:s", time())', $dateTime->gmtDate('Y-m-d H:i:s', time()));


$dateTime                                    Magento\Framework\Stdlib\DateTime\DateTime
$timeZone                                    Magento\Framework\Stdlib\DateTime\Timezone
date.timezone                                Europe/Berlin
date.timezone                                Europe/Berlin
date_default_timezone_get()                  UTC
TimeZone->getDefaultTimezone()               UTC
TimeZone->getConfigTimezone()                Europe/Berlin
DateTime->getGmtOffset()                     3600
DateTime->timestamp()                        1609490193
DateTime->gmtTimestamp()                     1609490193
DateTime->date()                             2021-01-01 08:36:33
DateTime->gmtDate()                          2021-01-01 08:36:33
DateTime->date("Y-m-d H:i:s", time())        2021-01-01 08:36:33
DateTime->gmtDate("Y-m-d H:i:s", time())     2021-01-01 08:36:33

Reason for the wrong behaviour

This is the method Magento\Framework\Stdlib\DateTime\DateTime->date():

 * Converts input date into date with timezone offset
 * Input date must be in GMT timezone
 * @param  string $format
 * @param  int|string $input date in GMT timezone
 * @return string
public function date($format = null, $input = null)
    if ($format === null) {
        $format = 'Y-m-d H:i:s';
    $result = date($format, $this->timestamp($input));
    return $result;

The PhpDoc clearly states it should work like I expected it to.
The handling of timezones is expected to be handled by the method timestamp():

 * Converts input date into timestamp with timezone offset
 * Input date must be in GMT timezone
 * @param  int|string $input date in GMT timezone
 * @return int
public function timestamp($input = null)
    switch (true) {
        case ($input === null):
            $result = $this->gmtTimestamp();
        case (is_numeric($input)):
            $result = $input;
        case ($input instanceof \DateTimeInterface):
            $result = $input->getTimestamp();
            $result = strtotime($input);

    $date = $this->_localeDate->date($result);

    return $date->getTimestamp();

Again, the PhpDoc confirms the expected behaviour.
The handling of timezones is passed further on to $this->_localeDate->date(), which creates a \DateTime object with the given timezone and then sets the given UTC timestamp (see the last line):

 * @inheritdoc
public function date($date = null, $locale = null, $useTimezone = true, $includeTime = true)
    $locale = $locale ?: $this->_localeResolver->getLocale();
    $timezone = $useTimezone
        ? $this->getConfigTimezone()
        : date_default_timezone_get();

    switch (true) {
        case (empty($date)):
            return new \DateTime('now', new \DateTimeZone($timezone));
        case ($date instanceof \DateTime):
            return $date->setTimezone(new \DateTimeZone($timezone));
        case ($date instanceof \DateTimeImmutable):
            return new \DateTime($date->format('Y-m-d H:i:s'), $date->getTimezone());
        case (!is_numeric($date)):
            $timeType = $includeTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE;
            $formatter = new \IntlDateFormatter(
                new \DateTimeZone($timezone)

            $date = $this->appendTimeIfNeeded($date, $includeTime);
            $date = $formatter->parse($date) ?: (new \DateTime($date))->getTimestamp();

    return (new \DateTime(null, new \DateTimeZone($timezone)))->setTimestamp($date);

So this is basically what happens:

/** @var int $utcTimestamp */
$datetime = new \DateTime(null, new \DateTimeZone('Europe/Berlin'));
$localTimestamp = $datetime->getTimestamp();

Of course, that can't work.


Is it recommended to handle the conversion oneself by adding and substracting $dateTime->getGmtOffset() and then using the standard date()?
It's possible to use $timeZone->date()->format(), but that would only work in one direction, UTC → local.
What are the best practices for time zone handling in M2?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.