My day job involves working on an OSS project called Max Media Manager, written in PHP.
The project involves the collection and presentation of statistics, and thus, dates and times are rather important. Ideally, we want to record all dates and times in the database in UTC, and then simply adjust them according to user preference (ie. show them in UTC if they want, or change the times to be in the user's time zone, so the statistics make sense to the user, wherever they are in the world). However, the project is a fork of a previous OSS project, and as a result, the UI doesn't have separate data access and presentation layers yet, so this goal is a little way off.
In the mean time, then, there are some problems I have been having with dates and times. These basically come about because of a bug in PEAR::Date, which I reported nearly a year ago. The problem is that when PEAR::Date compares two dates, to find out which one is earlier and which one is later (or if they are equal), it needs to deal with the fact that the two dates might be in different timezones. So, it first converts both dates into UTC, so they can be directly compared. (If you are using PHP5, you might want to know about this patch that I submitted for PEAR::Date.)
However, in order to convert dates into UTC, PEAR::Date needs to know if the date it is converting is in a Daylight Savings Time (DST) zone, or not. As part of the method that does this (Date_TimeZone::inDaylightTime()), the environment variable TZ is set in PHP to alter the timezone that PHP is in, so that the date to be converted can be inspected with PHP's localtime() function, and the "tm_isdst" value inspected.
Of course, this means that the TZ environment variable needs to be restored before the Date_TimeZone::inDaylightTime() method returns, otherwise future dates will be in the wrong timezone. Herein lies the problem I'm having.
On most Linux systems that I have come across, the TZ environment variable is not set. This means that a call to getenv("TZ") returns false. Thus, in the Date_TimeZone::inDaylightTime() method, it first tries to get the TZ data, which is "false". It then sets the TZ environment variable to the timezone the date it is testing is in, finds out if the date is in a DST timezone, and then tries to restore the old TZ value. However, because the previous value was found to be false, when a call of putenv("TZ", $env_tz) is made (where $env_tz = false), the end result is that the TZ environment variable is, in fact, set to an empty string (and not NULL, as it was before).
From here on, all new PEAR::Date objects are created in UTC. This is fine if you were in UTC to start with, but for me, when I'm in British Summer Time, it means that after the first date comparison, all new dates after that are 1 hour behind where they should be. I'm sure it's even worse for people not in this timezone, as the difference between what they should be in, and UTC, is even greater. At least I have the timezone right for 6 months of the year (when GMT is the same as UTC).
As PHP lacks the ability to unset environment variables (at least in PHP4, anyway - maybe PHP5 can do this?), about the only solution I can see is to ensure that the TZ environment variable is set at the start of all scripts in the project. Fortunately, we have a nice initialisation system, shamelessly borrowed in concept from Seagull, so doing this is easy.
A couple of smart guys (thanks Matteo and Mark!) on the Max IRC channel suggested the following:
if (getenv('TZ') === false) {
$diff = date('O') / 100;
putenv('TZ=GMT'.($diff > 0 ? '-' : '+').abs($diff));
}This way, the TZ environment variable is set to a timezone that indicates the offset from GMT, based on the user's current timezone, so that we don't have to add a configuration item to the project, specifying which timezone the user is in. Nifty! This has solved the problem, at least under PHP4. I've tested the code in BST, as well as a couple of other timezones, and it seems to work fine. However, there are still, apparently, issues under PHP5, which I want to tackle today.
If anyone has ideas about better ways to work around this issue in PEAR::Date (excluding the "right way" of doing everything in UTC and converting in the presentation layer, of course), I'm all ears...
Update: It turns out the PEAR::Date has a bug similar to the one I mentioned before, but in this case, in the Date_Span::setFromDateDiff() method. A similar patch would work here, or you can ensure that you clone your dates with Date::copy() before using them. As a result, the above solution does indeed work in PHP5, but, as one of the maintainers of PEAR::Date has mentioned, it isn't a thread-safe solution - but who cares? The PEAR::Date package isn't thread safe to begin with. At least this is backwards compatible with the current API.
At work we use PEAR::Date all over the place, and while the package is fine for most things, when it comes to timezone handling and daylight savings time offsets, there are some serious bugs that have been open in the PEAR bugtracker for around one year
Tracked: Feb 01, 17:57
It's almost two years since I raised this bug in PEAR::Date, described in more detail here - and at last, there's been an officical fix!
Tracked: Jun 14, 10:23
Joel writes:"The biggest surprise was how much work it took so that every user sees things in their own time zone."Oh yeah, I hear you. Now imagine that your software has to do that not because you are hosting it for your clients all over the
Tracked: Jul 10, 09:07