PHP Time and Timezones
The concept of timezones in PHP has gone through many iterations since PHP 3.x which didn’t have the concept of timezones, of course there were still ways to progromaticaly handle timezones in PHP3 but not as simply as we can today. PHP4 was a marked improvement native support for timezones and PHP5 wraps everything into some tidy classes for us to use.
The concept of time across our earth can often confuse developers, indeed the problem burdened me initially. At the core of time within PHP is the Unix time stamp. A Unix time stamp (or POSIX time) is the number of seconds from 1st January 1970 00:00:00 UTC. For our discussion here about timezones the important part to note is ‘UTC’ which is essentially GMT time. The reason this is important is because as you move round the earth into different timezones UTC is offset by n number of hours, for example San Fransisco is either -7 or -8 hours from UTC. I say ‘either’ because San Fransisco has daylight saving which alters the offset by 1 hour, whilst we are on the topic of daylight the reason for any offset in the first place is because of the suns rotation around the earth. It is sunrise or a new day in New Zealand (which is +12) before it is a new day or sunrise in England (which is either 0/+1).
Once you’ve grasped this concept, that you were no doubt taught in school and subsequently forgot by the next lesson, time because much easier.
Ok so before we get any further ahead of ourselves, you need to tell the server where it is. You could rely on the default configuration of the server, but that would be silly because a) it’s probably not in your control and b) what if you need another server, say in the US to feed a growing market – that server will be in another timezone and thus ruin your code.
date_default_timezone_set('UTC');
The above code tells your server that from now on you want all date related functions (date(), mktime() etc) to be done in that timezone. Read that again because it’s important. Anytime you want to format a Unix time stamp into a legible string for a given timezone you need to wrap it in the below code.
$cache = date_default_timezone_get();
date_default_timezone_set('Australia/Sydney');
$now = date("r");
date_default_timezone_set($cache);
Notice here how we cache the current timezone, we do this because we only want to be setting the default timezone from configuration etc once.
The important thing to remember is just using time() although gives you the time, is not a ‘timezone safe’ operation. For example.
$now = time();
date("r", $now);
Will return the correct time ONLY if you your server is configured to the same timezone you are in. Or put another way if you are sat in England (not in British summer time) and your using localhost as your server (correctly configured to UTC) will the above result in a correct date and time. If your sat in another timezone which has an offset from UTC then this is not really ‘now’ for you. Here are foreign cousins have the advantage because if your never developing on a timezone that is UTC the problem will always be apparent, but for us what you see may only work half the year, as when the clocks change for BST (British summer time) you will now be at UTC +1.
That explains a few problems that your likely to come across. At the core of these problems is the old PHP5.1 style way of calculating time. Everything you’ve seen so far is PHP5.1/4/3. Although the methods are still supported for obvious reasons the new PHP5.2 DateTime classes are the most type safe solutions and should always be the interfaces you use to calculate time.
date_default_timezone_set('UTC');
$now = new DateTime("now");
echo($now->format("r")); // Current time in RFC 2822 Thu, 21 Dec 2000 16:01:07 +0200
The above code creates a DateTime object automatically set for the current time in the default timezone. Below shows how to get the current time in a timezone.
$tz = new DateTimeZone("Australia/Melbourne");
$now = new DateTime("now", $tz);
No switching of the servers timezone required here, which means you can be sure that your fellow developers haven’t forgotten to switch the default timezone back after a date() function. With the above example you can guarantee that the returned time, whatever format you specify will be in the correct timezone. Using the above code as a starting point its easy to adjust the time.
$now->setTime($now->format("H") + 3); // setTime(hours, minutes, seconds);
Above sets the DateTime object 3 hours into the future and printed will return the current time in Melbourne, Australia + 3 hours.
Now back to UTC time, which is great but often causes confusion with timezones. If we format the above DateTime object we will get the same Unix time stamp returned if we have set the DateTimeZone object to ‘Europe/London’.
date_default_timezone_set('UTC');
$now = new DateTime();
echo('Now: '.$now->format('r').' ('.$now->format('U').')<br />'); // Returns: Now: Fri, 16 Apr 2010 20:22:11 +0000 (1271449331)
$tz = new DateTimeZone('Australia/Melbourne');
$now = new DateTime("now", $tz);
echo('Now: '.$now->format('r').' ('.$now->format('U').')<br />'); // Returns: Now: Sat, 17 Apr 2010 06:22:11 +1000 (1271449331)
It’s great to store Unix time stamps and use Unix time stamps, I love them, they are the most versatile way of representing time. However it’s here that unless you have a thorough understanding of time and that Unix is always UTC you will get and cause bugs in your code. To take a Unix time stamp and ensure its used in a timezone safe manner you need to quickly convert that time stamp into a DateTime object set with the correct timezone, so for the seconds returned timezone.
$tz = new DateTimeZone('Australia/Melbourne');
$now = new DateTime("now", $tz);
$now->setTimestamp(1271449331);
The most type safe process to ensure your Unix timestamps are stored and used by client code in the correct way is to provide a timezone safe function within your objects for getting time. The below code shows this.
class SurfEvent
{
private $timestamp;
private $timezone;
function __construct()
{
$this->timestamp = '1271449331'; // Came from the database for example.
$this->timezone = 'Australia/Melbourne';
}
function getDate($format)
{
$tz = new DateTimeZone($this->timezone);
$dt = new DateTime("now", $tz);
$dt->setTimestamp($this->timestamp);
return $dt->format($format);
}
}
The above code ensures a client coder cannot access the raw time stamp and run date() on it without thinking about the timezone of that current time stamp. The above code rightly forces the client coder to provide a format that they want the time stamp in. Although yes you could provide “U” as a format and then run a date() function on that timezone, but adding checking for the passed format is trivial. The above code could just as easily be used to return a RFC 2822 formatted date which includes timezone information and then used to create DateTime objects for comparison. Or you could modify the class to make this easier. I leave it to you to play with.