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'.

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.
In slightly more verbose terms:
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.
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.