Working with Time Zones in CakePHP
Rule #1 of working with time zones is: don’t have users pick abbreviations like “GMT”. Instead, have them select their location or zoneinfo zone name. Abbreviated time zones are fickle. Look at England for example: its abbreviated time zone is currently “BST”, and in a few months it’ll be “GMT”, but its zone name is always Europe/London.
Here’s a helper to produce a $form->inputable array of times for various locations in the pattern “4:20 PM – New York City, United States”.
1 <?php
2 class TimezonesHelper extends AppHelper {
3
4 function show() {
5 $zones = array(
6 'Pacific/Apia' => 'Apia, Upolu, Samoa', // UTC-11:00
7 'US/Hawaii' => 'Honolulu, Oahu, Hawaii, United States', // UTC-10:00
8 'US/Alaska' => 'Anchorage, Alaska, United States', // UTC-09:00
9 'US/Pacific' => 'Los Angeles, California, United States', // UTC-08:00
10 'US/Mountain' => 'Phoenix, Arizona, United States', // UTC-07:00
11 'US/Central' => 'Chicago, Illinois, United States', // UTC-06:00
12 'US/Eastern' => 'New York City, United States', // UTC-05:00
13 'America/Santiago' => 'Santiago, Chile', // UTC-04:00
14 'America/Sao_Paulo' => 'São Paulo, Brazil', // UTC-03:00
15 'Atlantic/South_Georgia' => 'South Georgia, S. Sandwich Islands', // UTC-02:00
16 'Atlantic/Cape_Verde' => 'Praia, Cape Verde', // UTC-01:00
17 'Europe/London' => 'London, United Kingdom', // UTC+00:00
18 'UTC' => 'Universal Coordinated Time (UTC)', // UTC+00:00
19 'Europe/Paris' => 'Paris, France', // UTC+01:00
20 'Africa/Cairo' => 'Cairo, Egypt', // UTC+02:00
21 'Europe/Moscow' => 'Moscow, Russia', // UTC+03:00
22 'Asia/Dubai' => 'Dubai, United Arab Emirates', // UTC+04:00
23 'Asia/Karachi' => 'Karachi, Pakistan', // UTC+05:00
24 'Asia/Dhaka' => 'Dhaka, Bangladesh', // UTC+06:00
25 'Asia/Jakarta' => 'Jakarta, Indonesia', // UTC+07:00
26 'Asia/Hong_Kong' => 'Hong Kong, China', // UTC+08:00
27 'Asia/Tokyo' => 'Tokyo, Japan', // UTC+09:00
28 'Australia/Sydney' => 'Sydney, Australia', // UTC+10:00
29 'Pacific/Noumea' => 'Nouméa, New Caledonia, France', // UTC+11:00
30 );
31 $dateTime = new DateTime('now');
32 foreach($zones as $zone => $name) {
33 $zoneObject = new DateTimeZone($zone);
34 $dateTime->setTimezone($zoneObject);
35 $zones[$zone] = $dateTime->format('g:i A - ').$name;
36 }
37 return $zones;
38 }
39
40 }
41 ?>
Call it from your view like this:
1 <?php e($form->input('time_zone', array(
2 'options' => $timezones->show(),
3 'default' => 'US/Eastern',
4 ))) ?>
Rule #2 is have your entire app and database work from one time zone, applying offsets only when interacting with users (e.g., in view files). You can go with your server’s default time zone, but if you’d like to secure your app’s use of UTC, follow these steps:
- Getting the app itself to use UTC is easy: add the line
date_default_timezone_set('UTC');toward the top of /app/cofig/bootstrap.php. Update! As of CakePHP 1.3, there’s a date_default_timezone_set setting commented out in /app/config/core.php - Having MySQL report dates in UTC—and if you followed the first step, this one is important when you
SELECT NOW()or do any date math—is a little more involved. I did it by copying /cake/libs/model/datasources/dbo/dbo_mysql.php to /app/models/datasources/dbo/dbo_mysql.php and adding the line$this->_execute("SET time_zone = '+00:00'");to DboMysql::connect, just after it sets the encoding (line 387 in the current stable version of CakePHP 1.2). I was tempted to run the query directly from AppModel instead, but I’m not sure how robust that solution would be.
Again, the above steps are optional. Whatever time zone your app uses, you’ll need the current user’s GMT/UTC offset for the “userOffset” parameter in CakePHP’s Time helper. You may be tempted to calculate the value of this offset and store it in the database for each user, but remember daylight savings: UTC offsets aren’t static, so generate them in an afterFind (retrieve) instead of a beforeSave (create, update):
1 <?php
2 class User extends AppModel {
3
4 function afterFind($results) {
5 foreach ($results as $i => $user) {
6 $user_zone = new DateTimeZone($user['User']['time_zone']);
7 $user_time = new DateTime('now', $user_zone);
8 $results[$i]['User']['utc_offset'] = $user_zone->getOffset($user_time)/60/60;
9 }
10
11 return $results;
12 }
13
14 }
15 ?>
If you use AuthComponent and the above afterFind, the current user’s UTC offset will be stored in Session.Auth.User.utc_offset, so you can easily apply its inverse before handling user input.
The above methods sacrifice performance for versatility and reliability. For scalability, you may want to cache UTC offsets, perhaps directly in the user table. But some variation on the above will give you the greatest amount of accuracy for the greatest number of time zones.