Travelling through Date and Time, part III

Until now, we looked at the basics and started to work with our dates. But we didn’t deal with timezones, which means all we did was pretty useless. In this part, we’re going to change that.

Okay, so what exactly are timezones? According to Wikipedia, a timezone “is a region of the earth that has uniform standard time, usually referred to as the local time”. And “local time” could be defined like this: “local time is UTC plus the current timezone offset for the considered location”. Note that it says “current offset”, not just “offset”, because a location can have different offsets at different times (e.g. because of daylight savings time). By the way: UTC stands for “Coordinated Universal Time” (actually: “Universal Time, Coordinated”) and is roughly equivalent to the formerly used GMT (Greenwich Mean Time). It is considered to be the “world time”, on which basis all other timezones are calculated. More on this topic can be found on Wikipedia, but I think this will do for our task ;)

In part I and II we completely omitted the fact that we live in timezones. Let’s have a closer look at what happened when we didn’t specify a timezone along with our dates. Or did we? Since PHP 5.3, there is a php.ini setting for the default timezone, e.g.: date.timezone = 'Europe/Berlin'. Actually, if you haven’t set that and are using something like date(), you’ll get a warning. But in case this is set, all DateTime objects without any specific timezone are implicit in the timezone set in the php.ini. So, in my setup, my dates would have been in Europe/Berlin, but the very same script could produce completely different results in your setup. Which could cause problems.

But it’s not all bad. Remember the definiton in the beginning? Every local time is computed by UTC plus current offset. So all things are in relation to UTC. And the unix timestamp, which we got to know in the first part of this series, is always in UTC. This proves really helpful. Let’s see why:
Say we create a DateTime object, this time with a specific timezone, like this:
$dateInBerlin = new DateTime('2010-04-20 10:00:00', new DateTimeZone('Europe/Berlin'));
And another date:
$dateInMoscow = new DateTime('2010-04-20 12:00:00', new DateTimeZone('Europe/Moscow'));
On April 20th, the timezone offset from Berlin to UTC is 2 hours, and from Moscow to UTC is 4 hours, so the difference between Berlin and Moscow timezone is 2 hours. Note that our dates are not exactly the same: One is at 10:00, the other at 12:00. Now, if we call getTimestamp() on both objects, we’ll get the same timestamp (1271750400) for both dates. Why? Because the timestamp consists of seconds elapsed since January 1st 1970 in UTC timezone. And because of the time difference between Berlin and Moscow, the dates are the same when expressed in UTC time.

OK, what does that mean for us? As we already know, FLOW3 stores the DateTime objects in the database as unix timestamps. To convert the DateTime object to an integer, FLOW3 uses the getTimestamp() method. Therefore, whatever timezone our date is in, we’ll get the same timestamp. What happens on reconstitution? The timestamp in the database is set via setTimestamp(). At this point, we’ll have a DateTime which value is the timestamp (UTC) and which expresses this in the default timezone specified in the php.ini.
But because I want to store a timezone along with my events, we need to do something before displaying them. We need to make sure that format() uses my timezone and not the default timezone. It turns out this is actually quite easy to do. DateTime objects have the method setTimezone(), which expects a valid timezone identifier as an argument. It then modifies the timezone, but not the timestamp. This is very important, as format() will output something different now, but getTimestamp() will still be the same. Mission accomplished.

Which means all is well. Or is it? No, not quite. Because if we want to associate a timezone with an event, we’ll probably have a form which allows the input of a date, along with a dropdown of possible timezones. Of course, the user enters the date corresponding to the timezone he selected. So before saving the date, we have to convert the entered date to express the correct timestamp (which is: an UTC expression of the combination of entered date and selected timezone). Note that just setting the timezone via setTimezone() doesn’t work here, because we actually want to modify the timestamp inside the DateTime object. Of course, this conversion has only to take place if we create the DateTime object before we have the timezone information available. On the other hand, if we change the timezone after we set the date, we have to modify the date again.

The last bit shows that timezone handling can get a bit complicated, but what it comes down to is basically to make sure that all dates in the database are in UTC. Once that is ensured, we’re safe. Additionaly, we also solved the problem of different php.ini settings, as our solution is now independent of this setting.

Next up: Localization.

filed under , , & posted on April 22, 2010

Travelling through Date and Time, part II

Now that we covered the basices in part I, let’s actually do something. I’ll explain how to fill the model with data and how to get specific models from the repository in FLOW3.

Before we begin, let’s create some classes: A DemoController which will showcase some of our calendar functionality. Then, we’ll need an AdminController with basic CRUD actions and corresponding Fluid templates. For the Domain, I’m going to use an Event model and an EventRepository. To make it simple, each event will only contain a start date (startDateTime), an end date (endDateTime) and a title (title) and the respective getters and setters.

Let’s start with the AdminController. The question that arises is: How to deal with our DateTime objects? Obviously, we can’t just display a DateTime object in a form textfield, and on the other end, we’ll receive a string from the form instead of a DateTime object. It would be easy if we could get and set a string for the date and also the time (I think it’s good to have two separate textfields for date and time). To be able to do this, let’s create the needed getters and setters: getStartDate/getEndDate and getStartTime/getEndTime. How do they work? The get*-methods return a formatted version of the DateTime objects (e.g. $this->startDateTime->format('Y-m-d');), the set*-methods receive a string from the form and call setDate/setTime on the DateTime objects (e.g. $this->startDateTime->setDate($year, $month, $day); – of course, the variables have to be extracted from the string). For all of this to work, we have to make sure that the DateTime objects are never null, so we have to initialize them in the constructor.

Problem solved, so now on to the repository. Here we need to create our own methods which return subsets of events. This is actually a bit tricky and I won’t go into much detail here. But let’s have a look at how it is supposed to work generally. I would suggest having one method which can return all events between a beginning and an end (of course, it should work with no beginning or no end as well). We’ll call it getBetween. This method would be called by several wrappers which themselves are easier to use in a controller. E.g., it would be handy to have getIn (which allows us to specify only, for example, a year and the method determines beginning and end automatically), getLatest (skipping beginning and setting end to the current date) and getUpcoming (skipping end and using the current date as end).

And how does getBetween work? Or more to the point, how does it compare the dates? As we’ve seen in part I, FLOW3 stores all DateTime objects as unix timestamps in the database, which means our method getBetween has to create timestamps from the given arguments beginning and end, which then can be compared to the values in the database.

filed under , , & posted on April 20, 2010

Travelling through Date and Time, part I

This is the first part of a series (of yet-to-be-determined length) dealing with date and time in PHP (and FLOW3).

This first part will cover the very basics of this complex and often very weird topic. Please note that what you’ll read here is often not the whole story, and will be completed in the next parts.

So why am I doing this? Well, I’m trying to put together a calendar package for FLOW3. The calendar should be able to handle events which have a beginning and an optional end. A beginning consists of a date (year, month and day) and, again optional, a time (hour and minute, no seconds). The same applies for the end of an event. Furthermore, when creating an event, the admin should be able to specify the timezone of the dates entered (one timezone for both dates).

This blog post will only deal with the handling of date and time in PHP and FLOW3, I’ll leave the timezone handling for another time.
So, what do we have? Basically, when using date and time in PHP, we use DateTime. DateTime was introduced in PHP 5.2 and provides a way of dealing with, well, date and time. Unfortunately, DateTime is a bit weird. For example, there are at least 3 options to set the date and time of a DateTime objects.
One is to hand it a UNIX timestamp (since 5.3, via setTimestamp). This is the number of elapsed seconds since January 1st 1970, 00:00. In PHP, an integer is used for timestamps. On 32-bit systems, this will mean we’ll run into problems in 2038 because too many seconds will have passed by then to be hold in an 32-bit signed integer. Of course, probably all systems will be 64-bit by then, so not to worry on the PHP part here in my opinion.
A second option is to create the DateTime object with a string like 2010-04-15 17:06:02. You can use any string that you could also pass to strtotime. When you create a DateTime object with no arguments, the string "now" is assumed and you’ll get an object which represents the current date and time.
Weird enough, there is no method similar to setTimestamp to do the same thing later on an object, you have to use the static method DateTime::createFromFormat or use something like the add/sub methods to change the DateTime object later on …
Thirdly, you can use setDate and setTime to modify parts of a DateTime object.

Ok, now on to the next topic: Gettting the date and time back from the object. format() exists for this purpose, which takes a format string (with the same options as for date()). Unfortunately, there is no locale aware format method, but again, more on this in another blog post.

With this covered, what about FLOW3? Basically, handling DateTime objects is baked right into the persistence layer. But how is it persisted? Well, let’s take a step back and have a look at MySQL for a second.
There, you have several options: You can store a regular INTEGER (unix timestamp), or a DATETIME value (and some other options as well). The latter stores a STRING representation of a date and time, but has the advantage that you can do several operations in SQL like comparisons. The INTEGER version of course has the disadvantage that it could run into the 2038 problem mentiond earlier.
Now, back to FLOW3: The persistence backend is generating all SQL queries from the query object, so we can’t use the date/time comparisons. Also, some of these things are MySQL only anyway, so usage of them is not an option for FLOW3 which aims to be more or less backend independent. That means FLOW3 turns the DateTime object into a unix timestamp before persisting it (in an INTEGER field), and converts it back upon reconstitution.

Enough for today. The plan is to write more about handling and comparing DateTime values in FLOW3 in the next part. The third part will probably deal with timezones and then, we’ll have a look at localization.

filed under , , & posted on April 15, 2010