Dates and time zones in PHP
Tags: php, date, timezones
Published: 2020-07-27 07:37:02

Working on an existing codebase means you can get unexpected surprises, like having dates being represented as strings inside application logic. We decided to migrate this to PHP's [`DateTime`](https://www.php.net/manual/en/class.datetime.php) objects. While migrating some of these string to `DateTime` instances in our codebase I came across code where we create a DateTime object based on data from our database and from API responses. The time zones of our application and our website are one and the same, but some APIs tend to send their data in [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time). And our default time zone is Europe/Amsterdam. What can possibly go wrong?!

## Naive
The code I found used a naive solution. Although it might look good - we add a time zone, right? - the result is not what I expected:

```php
// somewhere in our application bootstrap
date_default_timezone_set('Europe/Amsterdam');

// somewhere deep in our application
$date = \DateTimeImmutable::createFromFormat(
    \DateTime::ATOM,
    '2020-07-23T12:00:00+00:00'
);

// let's output
print $date->format('c');
```

This will print `2020-07-23T12:00:00+00:00`. Nice! Now let's show this to our users. Users typically don't care for time zones, they just want to know what the date and time is *for them*. You might want to localize this, but for now I will use the format `YYYY-MM-DD HH:MM:SS`. So what will be the output of `$date` using this format when in Europe/Amsterdam? `2020-07-23 12:00:00`. Hmmmz, that's not right. I expected this to be `2020-07-23 14:00:00`, seeing that Amsterdam is in UTC + 2:00.

**NB** I'm using [`DateTimeImmutable`](https://www.php.net/manual/en/class.datetimeimmutable.php) in my examples and not `DateTime`. This is deliberate. As `DateTime` objects are mutable by default you can find yourself in unexpected situations. An example is when you pass a `DateTime` object to a different class or function. You don't know what this class or function will do with that object and if it decides to use [`add`](https://www.php.net/manual/en/datetime.add.php) or [`sub`](https://www.php.net/manual/en/datetime.sub.php) your instance will be affected:

```php
function getUpcomingOpeningHours(DateTime $start): array
{
    $database->query(
        '/* query */',
        [
            'start' => $start,
            'end' => $start->add(new DateInterval('P7D'))
        ]
    );
}


$date = new DateTime();
$openingHours = getUpcomingOpeningHours($date);

print "Upcoming opening hours since ${date->format('Y-m-d')}: ";
```

This will now print the date 7 days into the future. Surprise!

## Specify a time zone 1
A look at the documentation of [`DateTime::createFromFormat`](https://www.php.net/manual/en/datetime.createfromformat.php) shows it accepts a third parameter; an instance of `DateTimeZone`:

```php
// set the default time zone
date_default_timezone_set('Europe/Amsterdam');

// create a date object
$date = \DateTimeImmutable::createFromFormat(
    \DateTime::ATOM,
    '2020-07-23T12:00:00+00:00',
    new \DateTimeZone(date_default_timezone_get())
);

// show the user
print $date->format('Y-m-d H:i:s');
```

This will print `2020-07-23 12:00:00`. Still wrong. But why? From the documentation:

> The timezone parameter and the current timezone are ignored when the time parameter either contains a UNIX timestamp (e.g. 946684800) or specifies a timezone (e.g. 2010-01-28T15:00:00+02:00).

This means that this approach only works if the date time string you are receiving is set in a specific time zone, but does not have a time zone indication in the date time string. In my personal opinion that is an undesirable situation, but I have seen far worse:

```php
// set the default time zone
date_default_timezone_set('Europe/Amsterdam');

// create a date object
$date = \DateTimeImmutable::createFromFormat(
    'Y-m-d H:i:s',
    '2020-07-23 12:00:00',
    new \DateTimeZone(date_default_timezone_get())
);

// show the user
print $date->format('Y-m-d H:i:s');
```

This prints `2020-07-23 12:00:00`; [winning](https://www.youtube.com/watch?v=pipTwjwrQYQ)! But wait, this example does not make any sense. If you create a `DateTime` object without any time zone specification it will use the default time zone. What happens if we specify a different time zone?

```php
// set the default time zone
date_default_timezone_set('Europe/Amsterdam');

// create a date object
$date = \DateTimeImmutable::createFromFormat(
    'Y-m-d H:i:s',
    '2020-07-23 12:00:00',
    new \DateTimeZone('UTC')
);

// show the user
print $date->format('Y-m-d H:i:s');
```

This prints `2020-07-23 12:00:00`. That is still not what we expect.

## Specify a time zone 2
So, what to do? We need to tell the `DateTime` explicitly what time zone we're expecting:

```php
// set the default time zone
date_default_timezone_set('Europe/Amsterdam');

// create a date object
$date = \DateTimeImmutable::createFromFormat(
    \DateTime::ATOM,
    '2020-07-23T12:00:00+00:00'
)->setTimeZone(new \DateTimeZone(date_default_timezone_get()));

// show the user
print $date->format('Y-m-d H:i:s');
```

This will print `2020-07-23 14:00:00`. Which is correct since at July 23rd 2020 Europe/Amsterdam is `+02:00`.

I'm using the functions [`date_default_timezone_set()`](https://www.php.net/manual/en/function.date-default-timezone-set.php) and [`date_default_timezone_get()`](https://www.php.net/manual/en/function.date-default-timezone-get.php). Unless you let your users specify their time zone themselves I would recommend using these functions - or abstractions of them - in your code for this. By consistently using `date_default_timezone_get()` you make sure your code will not break once it gets decided your application should also become available in territories with a different time zone. You need to make sure that somewhere in the bootstrap of your application you specify the correct time zone, and your entire application should follow you in the time zone migration.

## tl;dr
When you're building `DateTime` objects from date time strings and you want to use it to display a human readable date time format always specify the time zone of the object by using [`DateTime::setTimezone()`](https://www.php.net/manual/en/datetime.settimezone.php). And to ensure you don't end up showing incorrect times it is always a good idea to either store or submit a timestamp, which is by definition in UTC - but can only represent dates after the [unix epoch](https://en.wikipedia.org/wiki/Unix_time), or a date time with a time zone indication.