and

ABSTRACT

PHP provides some nice date manipulation functions that work very well in combination with each other. However, they only handle dealing with the server's timezone. Adding the feature for shifting dates to a user-defined timezone can be a very unpleasant experience, as we know first hand. In this article, we discuss the problems we encountered, and present our solution.

Introduction

We work for a software development company, CommNav, who amongst other things has developed a PHP based portal framework and application suite. Until recently it relied solely on PHP's built in date and timestamp manipulation functions for handling date and time values. However, in our most recent release in order to allow our users to specify their own timezones we found it necessary to use our own functions. Doing this turned out to be a bit more complicated than we at first imagined.
By allowing the user to specify a timezone, we unwittingly opened a Pandora's Box, out of which flew shrieking timestamp demons who tormented us, and drove us to the brink of insanity. We decided to write this article to serve as a warning post for travelers of this path. In it, we discuss our implementation prior to the change, our misconceptions about how PHP's time functions work, what PHP's time functions actually do, and our solution to the whole mess.
Why use Timestamps?
When we initially designed our portal, the choice was made to store time values in the database as timestamps. This means that as soon as a user enters a date, it is converted from a text string to a timestamp for internal manipulation and storage, and only converted back to human-readable form when being displayed back to the user. Using timestamps it is possible to easily manipulate the date with a combination of simple math and the powerful functions provided by PHP ('date' and 'mktime').
For example, finding midnight of any given day is a simple matter of applying this equation: $timestamp - ($timestamp % (24*60*60)). It is also handy to use the very flexible 'mktime' function. It allows the programmer to do strange things like input "January -1, 2000" and expect a timestamp representing "December 31, 1999." The 'date' function is also very powerful and allows for formatting of a date string from any timestamp. But, lurking behind their seemingly benign facade is a maelstrom of debugging horrors.

The Inadequacy of PHP Time Functions

PHP's 'mktime' and 'date' functions work well as a pair without the help of any other timestamp manipulation routines, but only if the application in which they are used is concerned solely with display and entry of time in the servers timezone. If an application needs to handle entry from a timezone other than that in which the server is located something more than 'mktime' and 'date' is required.
Two things are required to accomplish this: a location independent format for storing time in the database, and methods to translate to and from that format into the user's local time. Simply deciding to store all timestamps in the database as seconds since the epoch (12:00AM January 1st 1970) in GMT is sufficient for determining the location independent time format. However, writing methods to translate to and from that format is a bit more difficult. It is not a simple task to translate a value like "2:00AM August 18th 1982" into a single integer. There are all kinds of nasty issues involving leap years, daylight savings, and such that must be dealt with to get the timestamp value. Fortunately as we already mentioned PHP has a nice set of functions to do all that for you. However a very specific knowledge of what those functions do is necessary in order to use them to convert the timestamps they return into a location independent format.
Unfortunately to many people the PHP documentation will not be very helpful on this point. Although to an enlightened few who are already well versed in the wonders of *nix timestamps it may be clear from reading it what 'mktime' actually returns; for most of us the answer is not so obvious.

The Big Mess

The problem as we saw it was this: there are two distinct possibilities of what 'mktime' returns (probably more, but we doubt that any of them are sensible). Either 'mktime' returns a timestamp which represents seconds since the epoch in the local timezone or it returns a timestamp which represents seconds since the epoch in GMT.
If 'mktime' returns a timestamp in local time then the conversion to a timestamp in GMT would be a simple one. Because under these assumptions 'mktime' is not applying an offset (both the input and output are relative to the server) all we need to do to convert the output is to apply the user's offset. If on the other hand 'mktime' returns a timestamp which represents seconds since the epoch in GMT then the conversion of the timestamp is not so simple. Remember that any conversion applied by 'mktime' is always performed relative to the server's timezone, so even though 'mktime' under these assumption would try to return a time in GMT it would fail because it would apply the server's offset rather than the user's offset. To solve this problem we would have to figure out the difference between the server's offset and the user's offset, in other words, the user's offset from the server, and then subtract that value from the timestamp returned by 'mktime'.
Hypothesis 1 and 2
Hopefully, this hasn't confused anyone to the point of madness like it did us. If it has, hang on. The solution is quite simple. As all you true hardcore *nix wizards out there already know, timestamps are always stored as seconds since the epoch in GMT. In fact the epoch is defined as being in GMT. A a technicality for sure, but an important one. If you keep in mind that all PHP functions deal with timestamps in GMT then your timestamp problems will be greatly reduced. Also, if it helps you can demonstrate that this is the case; just run the following code:

<?php

echo mktime(0,0,0,1,1,1970);

?>
It should return something other than 0 (unless you somehow live in GMT) thus indicating that PHP is applying the servers offset to translate the time values passed into 'mktime' to a timestamp that is relative to GMT.
What this means in terms of our previous discussion is that our second hypothesis was the correct one. 'mktime' returns a timestamp representing seconds since the epoch, the epoch being in GMT of course. Just in case there is any lingering confusion, below is a description of what PHP's date functions do.
Actual PHP Behavior chart
In slightly more verbose terms:
gmtime, mktime, gmdate and date charts

Our Solution

Our solution to these problems was to write wrapper functions around the existing PHP functions. These functions act as mediators between PHP's date handling functions and our portal framework. Since we are dealing with user timezones and not necessarily the server's timezone, the functions take a timezone offset as a parameter. This offset need not be the same as the server's offset, which is what makes these functions useful. Now, by using these functions, each user of our portal can have a different timezone offset.
They are designed to work for both incoming and outgoing date formats. For example, when saving to the database, a user enters a date in their local time, and it is converted to GMT using their offset. The GMT timestamp is stored in the database or otherwise used in the application. When reading date fields from the database, we know that they will all be relative to GMT, so we can safely apply the user's timezone again to prepare the date for display to the user. See the diagram below.
User to Database back to User
As mentioned earlier, by definition Unix timestamps are always measured relative to GMT. However, with the interesting things we are doing with timezone shifting, there is a point during our conversion from the user's local time when the timestamp returned from PHP is not actually in GMT. It is necessary to shield the developers from this by encapsulating all of that in the internals of these functions.
Notice the call to 'gmmktime' in our function, 'new_mktime'. This function is a useful alternative to the normal 'mktime'. Essentially, it expects the input to be date information relative to GMT, and it outputs a timestamp also relative to GMT. We take advantage of the fact that this function applies no server offset to the timestamp and treat the input and output as if it is relative to the user's timezone. Now we are dealing with a timestamp that is not in GMT. Before anyone can notice, we quickly apply the user's offset and return the GMT timestamp. The same thing happens with 'new_date', only in reverse.

<?php
    
/*
    **  new_mktime()
    **
    **  Same as PHP's mktime but with a $tz parameter which
    **  is the timezone for converting the timestamp to GMT.
    **  If the user is on the east coast of the USA, this
    **  would be "-0400" in summer, and "-0500" in winter.
    */
    
function new_mktime($hr,$mi,$se,$mo,$dy,$yr,$tz) {
        
$timestamp gmmktime($hr,$mi,$se,$mo,$dy,$yr);
        
$offset = (60 60) * ($tz 100); // Seconds from GMT
        
$timestamp $timestamp $offset;
        return 
$timestamp;
    }

    
/*
    **  new_date()
    **
    **  This is also the same format as PHP's date function,
    **  but with the additional timezone parameter which
    **  specifies the user's timezone.
    */
    
function new_date($format$timestamp$tz) {
        
$offset = (60 60) * ($tz 100); // Seconds from GMT
        
$timestamp $timestamp $offset;
        return 
gmdate($format$timestamp);
    }
?>
BEWARE: The PHP function, 'gmmktime' takes an optional 7th parameter called 'is_dst.' This does not do specifically what the documentation says it does. The use of this flag tells PHP whether or not to apply the server's daylight savings time offset to the GMT timestamp. You will have unexpected results by not using the default (-1). In a perfect world, 'gmmktime' would not allow this parameter to be specified.
You may have noticed that throughout this article, we have glossed over a rather important issue. We are ignoring the fact that users will often need a daylight savings time offset applied to their timezone. In the functions above, the timezone parameter is assuming that the daylight savings time offset has already been applied if necessary. In reality, to fully implement this solution, it will be necessary to write other functions that detect if the timezone is in daylight savings time, and if so, to apply the 1-hour offset manually. This could be handled in new_mktime and new_date, but for simplicity, we opted to leave that as an exercise for the reader.

In Conclusion

It is easy to manipulate date values which are stored as timestamps, and conveniently PHP provides a nice set of functions for converting to and from them. Unfortunately, PHP's functions only understand the server's timezone. It is possible to use PHP's time functions to handle other timezone's, but to do so one needs a clear understanding of exactly what they do. At first this may seem daunting, but with the realization that all PHP's time functions assume that timestamps are in GMT, their interpretation becomes relatively simple. With this knowledge, we created our own solution that provides a consistent way for applications to allow user specified timezones to be applied. Utilizing the fact that the gmmktime function applies no offest in creating timestamps we were able to structure our functions so as to allow specification of an arbitrary offset. Though we did gloss over some important problems such as how to properly determine daylight savings offets we hope that this article will put you on the right path towards developing your own timezone neutral applications.