Is your application prepared to support different locales? Do you provide translations and different number formats based on a user’s regional conditions? If so, how about time and calendar dates? Do you keep track of the UTC Offset and the Daylight Saving Time (DST) relative to the time zone your users live in, and are you able to convert dates properly to different time zones? This article will show you how to utilize Zend_Date
to make your application aware of all this.
Michael, Tim and an 8 hour offset
Why would you want to convert dates to different time zones once they have been persisted in the data storage? Let’s say you are a London based developer and one of your clients works with this software you’ve written, a tool for creating, managing and sharing documents. This company is about to expand globally, and you’re excited that they just ordered a few more licenses for their new office in San Francisco. Michael, the former head of the financial department and one of the core users of your document sharing tool, will be the company’s first long time employee heading to the US, and a week before he moves, he introduces Tim – his replacement for the London based office – to the functionality of your software.
“See”, Michael says, “once you have finished working with the document, you click the ‘share’ button and it will automatically end up in my inbox for pending reviews.” He leans back, crossing the arms behind his head. “It’s that easy!”
A few days later – Tim finally got his own user account set up by the company’s admin – Michael moves from London to San Francisco. Your application did its job really good over the past months, and everyone’s pleased with how smooth everything’s working, and how convenient it is to access and manage documents from any computer with a working internet connection[1].
What nobody recognized yet – your application stored Michael’s (and everyone else’s) documents along with the absolute date of the time zone of London (GMT, that is), and he now continues to use your software in San Francisco. And he’s pretty confused about the strange dates that confront him now while looking at his document workspace. “Bananas”, he thinks.
“This one draft here”, he takes a close look at the monitor while circling the mouse pointer over the document entry, “I know Tim created it and shared it with me last monday, but that was clearly not at 08:00 PM.”
He scratches his head. “I remember receiving this draft around noon, or was it the other day? I went home around 07:00 PM, there’s no way I could have received the draft later on. Something must be broken… Or am I stressed out?”
So what’s happening here? Well, he moved to the pacific time zone which relies upon Pacific Standard Time (PST). And this time zone is exactly 8 hours behind the time zone he previously lived in (ignoring Daylight Saving Time for now).
Clicking somewhat lost through your application in search for a reasonable explanation, he notices that the software does not consider him being in a different time zone. Furthermore: He doesn’t find any option that would let him select a different time zone other than the one he lived in – and obviously where the software was developed.
It’s definitely time for some improvements. We all want our software to be used across the globe, don’t we?
Confused? Don’t worry, Zend_Date is here to help. As part of the Zend Framework, Zend_Date
allows for convenient access to operations related to calculations, formats and transformations of dates.
Preparing your application
It is really important for software to be aware of the time zone it is currently serving as soon as there are any calculations related to dates and times involved.
Wondering whether your software is affected by this problem? Are dates and times part of any entity in your software which need to be persisted? If you can answer this question with “Yes!” (or if your first thought was “ah, crap…”), then you have already given yourself the answer: To a certain degree, it is possible that your software messes with the users’ feelings as soon as the time zone changes – whether it’s the time zone of your PHP environment or the time zone a user lives in. The following will give you some food for thought for your next planning meeting[2].
There are a few steps you should take into account when adding time zone support to your application, and all of them should be implemented in one or another way in your software:
- Make the time zone globally configurable
- Provide a default time zone and let the user choose the timezone he currently lives in
- Set the default time zone for further date calculations for each request
Make the time zone globally configurable
The time zone setting should be globally configurable without even touching PHP’s or the system’s configuration. When working with Zend_Framework
, this can easily be done by using Zend_Config, a powerful component which helps in parameterizing your software. There are a lot of good tutorials out there so I won’t dive too deep into how to set up an application with Zend_Config
, and give you a simple example instead:
[environment] ; Specify the default time zone for the application here. For a list of ; timezones supported by PHP, see http://de.php.net/manual/en/timezones.php date.timezone.default = Europe/Berlin
You can access the configuration by simply loading the ini-File using Zend_Config_Ini
, and then query the settings accordingly.
// load the configuration $config = new Zend_Config_Ini('/path/to/config/file'); // read out the time zone setting $timezone = $config->environment->date->timezone->default;
Following this approach, we are now able to easily specify a default time zone value for our application that should later be used. This is our very first step to gain more independency from our server’s presets and escape the scourge of unwanted configuration locks.
Let the user choose a time zone
There are a lot of ways we can map a time zone to a user: We could bind this property to a user object which is available through the request lifetime for each and every signed in user, or we could create an individual settings-object, which – again – can be mapped 1:1 to a user.
For a list of available and supported time zones in PHP, you can visit http://de.php.net/manual/en/timezones.php. But if you want to create a drop-down box with a set of all time zones to choose from, it would be best to create this list automatically by using PHP’s native DateTimeZone object, which is available for PHP >= 5.2:
$timezones = DateTimeZone::listIdentifiers(); for ($i=0, $len = count($timezones); $i < $len; $i++) { echo $timezones[$i] . "\n"; }
The remaining work shouldn’t be too much of a challenge – we provide a settings section in our application where the user can specify the desired time zone. It’s simple as that.
Set up the time zone for each request
We’re almost there – just one important step is missing: How do we tell which time zone the application should serve? Quite simple – put the application in a time zone context by applying best practices[3]! And the best practice would be to call date_default_timezone_set() in your bootstrapper, with the value of either the default time zone – or, if available – the user’s manually chosen time zone or the one you tried to autodetect.
// code to read out the users time zone from his settings, // assuming that our user object provides a "getTimezone()" method $timezone = $user->getTimezone(); $result = @date_default_timezone_set($timezone);
The 0 meridian for your data storage
We have just enhanced our application with support for serving different time zones, but how exactly would we want to store the dates/times in the underlying data storage of our application? By storing the data without any time offset[4]! We use UTC/GMT as the preferred time zone for all of our date values.
There are various steps involved in making sure we are putting the proper UTC date to our data storage – and to be able to serve this dates properly to different time zones:
- Choosing the MySQL data type for our dates
- Converting user/system generated dates to UTC
- Converting UTC dates back to a specific time zone
Choosing the MySQL data type for our dates
Storing UTC dates in our data storage boosts the (locale) portability of our software. However, we have to think about how our dates will be stored. We will take a look at two common alternatives:
- Saving dates and times as Unix timestamps
- Saving dates and times in a
datetime
field
Both of them have advantages and disadvantages:
Unix Timestamp
A Unix timestamp is an integer value which represents the elapsed seconds since 1970-01-01 00:00:00(GMT)
. This is the so called “Unix Epoch”. On 32-bit systems, the valid range of values reach from 1970-01-01 00:00:00 UTC
to 2038-01-19 03:14:07 UTC
(the upper limit denoted by the year 2038 is commonly referred to as the Year 2038 problem). The short range of 68 years which can be represented by unsigned 32-bit timestamps and the fact that you cannot use dates before 1970 (except for 64-bit systems where Unix timestamps are stored as signed 64-bit integers, or on most 32-bit systems where timestamps are stored as signed integers, supporting a range from 1901-12-13 20:45:54 UTC
to 2038-01-19 03:14:07 UTC
) narrows the use cases for timestamps somewhat down[6].
However, when dealing with “transient” dates, such as dates belonging to log entries or such, timestamps are proven to work. Oh, and it’s hard to figure out what date exactly a 4-byte long value represents without doing some good old math, don’t you think?[7]
datetime Fields
datetime
fields in MySQL require dates to be in the format YYYY-MM-dd HH:mm:ss
(example: 1999-05-03 20:15:00
) which makes it not only perfect readable, but also allows for complex date calculations by using MySQL’s built in functionality. The supported range for datetime
values is 1000-01-01 00:00:00
to 9999-12-31 23:59:59
. This covers a wide range of dates[9] and is perfect for storing UTC datetime values.
Getting that UTC date ready for Mysql
Since we’ve decided to store only UTC dates in our database, we go now back to Zend_Date
and take a look at how we convert any date string to UTC date.
// we assume that the variable $date holds the date we want to add in // UTC to the data storage // the following works with date formats that include time zone offsets, // e.g. Fri, 19 Jan 2007 22:08:13 +0100 (CET) // read out the currently used timezone $date = 'Fri, 19 Jan 2007 22:08:13 +0100 (CET)'; $oldZone = date_default_timezone_get(); // set the new timezone to UTC to give PHP a hint what strtotime() has to // do date_default_timezone_set('UTC'); assert(date_default_timezone_get() === 'UTC'); // strtotime() is much more forgiving to date strings which // do not comply to the exact standard $dvalue = @strtotime($date); // reset default timezone to previous value date_default_timezone_set($oldZone); assert(date_default_timezone_get() === $oldZone); // get back to Zend Date $dateObject = new Zend_Date(); try { $dateObject->set(($dvalue === false ? $date : $dvalue)); } catch (Zend_Date_Exception $e) { // fall back to default date to have a value at last $dateObject->set('1970-01-01 00:00:00'); } $dateObject->setTimezone('UTC'); $result = $dateObject->get('YYYY-MM-dd HH:mm:ss'); assert($result === '2007-01-19 21:08:13'); echo $result;
Note our first call to strtotime()
. This is a native PHP function which is much more forgiving than Zend_Date
when it comes to parsing date strings which do not comply to international standards. However, strtotime()
can fail, and if it does, we delegate the parsing of the date to Zend_Date
. It does not use strtotime()
itself, but instead tokenizes the passed string for gathering all the information it needs. It even uses the BC Math extension (if available), which comes quite handy in case strtotime()
failed with a date which is out of bounds. We’ll get to that later.
If parsing the date failed, we catch the exception and fall back to a default value of 1970-01-01 00:00:00
, but it’s up to you if you let the exception bubble up or replace the date with any value you want. It mainly depends on your use case and how sensitive your data is to wrong date values[11].
The last step is to put Zend_Date
into the UTC timezone, and – based on that – convert the string to the format YYYY-MM-dd HH:mm:ss
, which is the required format for MySQL datetime
fields.
Let’s go back to strtotime()
for a second. You should’ve noted that we temporarily set another timezone (UTC) so the internal date conversion knows what kind of output we excpect.
This might confuse you at first, so let’s go through it in detail: Let’s assume we have the following date that needs to be converted to UTC: Fri, 19 Jan 2007 22:08:13 +0100 (CET)
. The last part of the string includes the offset to the UTC time zone “+0100 (CET)
” (means: CET is 1 hour ahead of UTC when no DST is available, otherwise it would be + 2 hrs), and the first part “Fri, 19 Jan 2007 22:08:13
” is actually the local date that should be put into the UTC time zone and stored to the data storage. By telling strtotime()
it should generate a Unix timestamp from that date and setting the default timezone to UTC, the offset is calculated against the local date. If, however, you’d set the default timezone to America/Los_Angeles
, the offset to UTC (-08:00) for this time zone (PST) is computed against the UTC date of the local date. See a few examples:
// show which timezone our script runs in echo date_default_timezone_get() . "\n"; // outputs the converted date, depending on the timezone initially set echo "1: ".date("Y-m-d H:i:s", strtotime('Fri, 19 Nov 2007 22:08:13 +0100 (CET)')) . "\n"; // lets have a look at DST. Set the timezone to a value of which we know // that it uses DST date_default_timezone_set('Europe/Berlin'); assert(date_default_timezone_get() === 'Europe/Berlin'); // no DST from end of Okt to end of March in Europe/Berlin, // so the following will output 2007-01-19 22:08:13 $d = date("Y-m-d H:i:s", strtotime('Fri, 19 Jan 2007 22:08:13 +0100 (CET)')); assert($d === '2007-01-19 22:08:13'); echo "2: $d \n"; // summertime, i.e. CEST in Europe/Berlin, for example during August // the following will output 2007-08-19 23:08:13, since the timezone is specified // as CET, but during August, DST is active, so 1 hour wil be added $d = date("Y-m-d H:i:s", strtotime('Sun, 19 Aug 2007 22:08:13 +0100 (CET)')); assert($d === '2007-08-19 23:08:13'); echo "3: $d \n"; // now specify CEST as a timezone - this tells that the date is in Central European // Summer Time (2 hrs ahead of UTC) // the output will be 2007-08-19 22:08:13 $d = date("Y-m-d H:i:s", strtotime('Sun, 19 Aug 2007 22:08:13 +0200 (CEST)')); assert($d === '2007-08-19 22:08:13'); echo "4: $d \n"; // changing the timezone to UTC date_default_timezone_set('UTC'); assert(date_default_timezone_get() === 'UTC'); // ouputs 2007-01-19 21:08:13, the date converted to UTC time zone $d = date("Y-m-d H:i:s", strtotime('Fri, 19 Jan 2007 22:08:13 +0100 (CET)')); echo "5: $d \n"; assert($d === '2007-01-19 21:08:13'); // changing the timezone to America/Los_Angeles date_default_timezone_set('America/Los_Angeles'); assert(date_default_timezone_get() === 'America/Los_Angeles'); // outputs 2007-01-19 13:08:13, the specified date in the time // zone of Los Angeles (America) // calculate UTC date: // (Fri, 19 Jan 2007 22:08:13 - (+01:00)) = Fri, 19 Jan 2007 21:08:13 +00:00 (UTC) // now substract 08:00 (PST is 8 hrs behind UTC) from the UTC date // (Fri, 19 Jan 2007 21:08:13 +00:00 (UTC)) - (08:00)) // == Fri, 19 Jan 2007 13:08:13 -08:00 (PST) $d = date("Y-m-d H:i:s", strtotime('Fri, 19 Jan 2007 22:08:13 +0100 (CET)')); echo "6: $d \n"; assert($d === '2007-01-19 13:08:13'); // test this the same way around: date_default_timezone_set('Europe/Berlin'); assert(date_default_timezone_get() === 'Europe/Berlin'); // outputs 2007-01-19 22:08:13 $d = date("Y-m-d H:i:s", strtotime('Fri, 19 Jan 2007 13:08:13 -08:00 (PST)')); echo "7: $d \n"; assert($d === '2007-01-19 22:08:13'); // Output generated: // [your timezone] // 1: [date based on your time zone] // 2: 2007-01-19 22:08:13 // 3: 2007-08-19 23:08:13 // 4: 2007-08-19 22:08:13 // 5: 2007-01-19 21:08:13 // 6: 2007-01-19 13:08:13 // 7: 2007-01-19 22:08:13
It’s quite easy once you get the hang of it. The most important part is to relate to UTC all the time, and simply take the offsets of other time zones into account during converting. By using the UTC timezone, you can safely and easily display them in other time zones.
Being soft and flexible
Now that you have prepared your application to store UTC dates in the format YYYY-MM-dd HH:mm:ss
, you are able to display them properly based on the time zone you have specified by using date_default_timezone_set()
. We return to Zend_Date
now to convert UTC dates to the default time zone cosen by you or your application’s user, as mentioned above.
Let’s first have a look at an error-prone solution and see why this is not the right way to do it:
//creating Zend_Date and pass our datetime value from the data storage to // the constructor $dateObject = new Zend_Date('2007-01-19 14:08:13'); echo $date->get('YYYY-MM-dd HH:mm:ss');
So why is this wrong? Well, first off we’re simply passing a string to Zend_Date
which provides no further information about its time zone, and therefor its offset (remember our date string “Fri, 19 Jan 2007 22:08:13 +0100 (CET)
“? The timezone and offset information at its end helps functions like strtotime()
when they look for any additional information for which region in the world the date was created for). Thus, Zend_Date
will fall back to the timezone it has detected when it internally queried the return value of date_default_timezone_get()
, and uses this value for converting.
Secondly, we’re not specifying the target time zone the date should be converted to. Guess what? By echoing the return value of Zend_date::get()
in this case, the converted value will be the same as the value we just passed to Zend_Date::__construct()
(whereas getting the same value is not an error: This happens when an input date with zero offset is converted to a timezone which in turn does not have an offset itself[13]).
Let’s have a look at another example:
//create Zend_Date and pass the datetime value to the constructor $dateObject = new Zend_Date('2007-01-19 14:08:13'); // set the time zone $dateObject->setTimezone(date_timezone_default_get()); echo $dateObject->get('YYYY-mm-dd HH:ii:ss');
Well, we’re not doing anything different here, are we? We simply do a call to Zend_Date::setTimezone()
to set the Zend_Date
-object to the time zone it was already set to. It already checks the value of date_timezone_default_get()
internally. Relax. Take a break. And then have a look at a working example.
// create Zend_Date object $dateObject = new Zend_Date(); // put the Date_Object into the UTC time zone $dateObject->setTimezone('UTC'); // set the UTC date the date object must work with $dateObject->set('2007-01-19 14:08:13'); // now, set the time zone of the object to the time zone our application runs in $dateObject->setTimezone(date_default_timezone_get()); // display the UTC date converted to our time zone echo $dateObject->get('YYYY-MM-dd HH:mm:ss');
Wrapping things up
Adding time zone support to your PHP application isn’t too much of a hassle, and not only will the user benefit from it, but so do you by eliminating a potential source for errors.
There aren’t too many steps involved in adding time zone support, and it’s best done at a very early implementation cycle to spare you the problems that occur when persisted data has to be converted back to UTC if you do not know which time zones the dates originally belong to.
As mentioned earlier – if you need to make your code work on each and every (32-bit) OS where PHP is available and you need to consider those OSs where PHP only supports unsigned 32-bit timestamps, you need to check whether dates which get processed with PHP internal functions are in between 1970-01-01
and 2038-01-19
. In this case, you have to use a different approach than the one this article is about, since Zend_Date
assumes that your target system supports at least signed 32-bit integers when working time related functions (PHP supporting only timestamps as unsigned 32-bit timestamps is a known problem which has been fixed since 5.1.0).
An additional “soft” approach when deploying your application would be to require the availability of the BC Math extension, so Zend_Date
can take advantage of it.
The best configuration for a PHP system that covers a large range of dates would be a 64-bit OS. Your webpage should be guaranteed to run even in the far away future.
Oh, and did I mention the Unit Tests you should write for your date manipulating functions to get started in the first place?
Happy PHPing!
- They were specially excited about phone calls during the weekends when their division manager would ask them to check a document for them. Write once, run anywhere. ↩
- You are an agilist, aren’t you? ↩
- They are proven to work ↩
- You remember that 8 hour offset from San Francisco – London that ended up in the data storage? You don’t want that! ↩
- Should you be working for the LHC, then the fractional second difference should definitely matter to you. ↩
- Think about birthdates. Even Facebook’s userbase does not only exist of people like “awesomesauce_1988″ and the like ↩
- Unless your first name is Sheldon and you tend to embrace your genius to the fullest ↩
- Assuming you converted the date to a timestamp properly ↩
- Watch out, PHP programmers working in history-related institutions! ↩
- Take a look at
Zend_Locale_Math
to understand howZend_Date
utilizesBC Math
↩ - Ever wondered how these emails from 1970 end up in your inbox? ↩
- How many email clients do you know, and how many of them are looking out to push their own proprietary standard through? ↩
- London Calling ↩