Handling TimeZones in CakePHP

Timezones with CakePHP

The web is a global resource for anything, anywhere, any timezone. But often times we are careless in consideration for people in other areas of the world. The different timezones offset people's schedules from one area to the next. When handling user profiles (or anything for that matter) where date and time is involved we can do a little magic with our CakePHP app to make things a little more convenient for our users. How do we handle this you ask?

First lets say when a user logs in to their profile they can click to edit the profile, selects his/her timezone, and clicks save. Relative to GMT we can calculate offsets of each timezone, where GMT would = 0. So for instance Eastern Timezone for New York, US would be -5 since its 5 ours BEHIND GMT. So lets start by generating a dropdown list with cake with all the different timezones, then save this list to the users profile. If you are using Auth with CakePHP, you will likely have a users table, and we'll add a decimal field called "timezone" to that table. Fortunately there is a helper to assist us with this dropdown in the bakery, Timezone Helper. By taking a look at the source you'll notice all the labels associated with an offset "-5.0" or "+3.0". Also look a little closer and you might notice a few have actual decimal (Kabul is +4.5). So first things first, go ahead and install that Helper as those instructions say. Add it to your Edit view for users to edit their profile. Once included you just add the drop down to your form with:

echo $timezone->select('timezone');

Now each user should have a timezone attached to their profile. Now the trick to timezones is that no matter what the timezone of our server is, we just want to convert it to display to the user in that user's preferred timezone. On the reverse side, if a user enters a datetime, we convert it back to the timezone of the server.

Lets start our with the user viewing a date from the server, in our view we want to convert it to the user's preferred timezone set to their profile (which should now be stored in their session, I'll demonstrate with Auth)

$time->format('F j, Y g:i:s a', $mydate, null, $session->read('Auth.User.timezone'));

Using the Time helper in CakePHP, the format function accepts a fourth parameter which indicates the timezone, which is extremely helpful. So this is all you need to do to display a time from the server to a user.

Next lets say the user is about to edit a field from the server in a form. First we need to select the existing data from the database, we'll convert it to the user's timezone, and place it in an input field to be edited. Then once the user saves the data we'll simply convert it back to the server's timezone. (There a bunch of "one liners" in doing all of these tasks). So if you have your Time helper in your controller when you get the data from the database, go ahead and convert it to the new timezone just like we did before:

$data['Model']['mydate'] = $time->format('Y-n-d H:i:sP', $data['Model']['mydate'], null, $this->Auth->user('timezone'));

Now in your view for the form there are a few different scenarios. 1) You use Cake's default inputs for editing date times, with the 6 dropdowns, or 2) You use a plain input and use a date/time picker of some sort. Lets first handle the first scenario, Cakes default 6 input datetime. The data has been submitted, validated and we're about to place it back in the database, we need to convert it back to server timezone. But wait! the form field is broken down into an array of six elements: year, month, day, hour, minute, meridian. Well we can create a nice little function that will convert this array into a datetime, and pass it our timezone:

function dateTimeArrayToServerTZString($dt, $tz=0){
    $tz = (floatval($tz) >= 0)? '+'. number_format(floatval($tz),2,':','') : number_format(floatval($tz),2,':','');
    $dt['min'] = str_pad($dt['min'], 2, "0", STR_PAD_LEFT);

    $loc = $dt['year'] .'-'. $dt['month'] .'-'. $dt['day'] .' '. $dt['hour'] .':'. $dt['min'] .' '. $dt['meridian'] .' '. $tz;

    $datetime = new DateTime($loc);
    $datetime->setTimezone(new DateTimeZone(date('e', time())));

    return $datetime->format('Y-n-d H:i:sP');

Now right before I insert my data into the database I just need to call this function on that datetime array:

$this->data['Model']['mydate'] = dateTimeArrayToServerTZString($this->data['Model']['mydate'], $this->Auth->user('timezone'));

Now $this->data['Model']['mydate'] will be a string, and cake still knows how to handle that just fine. Now for the second scenerio, a user submitting a string. This isn't too hard at all, we just use our time helper and convert that puppy back to the server time.

$this->data['Model']['mydate'] = $time->format('Y-n-d H:i:sP', $this->data['Model']['mydate'], null, -5);

Ok, so I've hardcoded in the timezone to -5 (Eastern Timezone), what if I want this to be dynamic to where ever my server is? Well the Time helper has a function which isn't mentioned in the book called serverOffset(), which will return a number of the offset. If you're not using the helper and just want to get the timezone of the server its a simple one-liner:

date('Z', time())

Thats all there is to handling timezones in your cakePHP site, or any site for that matter. Just keep in mind as you go to convert any times for the user's pleasure. You have any tips or alternatives?