Handle Dates The Right Way In PHP
You may hear here and there than PHP date and time API is a bit of a mess and indeed everything is not perfect, but it’s not a reason to get your own code and application logic messy. PHP is still powerful enough for any app to handle date and time in a standard way easy to maintain and get the behavior you really expect (no surprise with exotic users timezones, summer time 30/31 month overflow, leap years, and so on.).
Use The Right Settings
First, set the default timezone to UTC.
You also can change this setting from php.ini with the date.timezone entry. If you use a framework, it would probably have a dedicated config (in Laravel, it’s
'timezone' property in config/app.php. But one sure thing is you must set it, you should not work with an unknown timezone and for a better compatibility of your app on any server/hosting setting it at the application level is safer).
Why UTC? There are so many good reasons to use UTC over others I dedicated an article to this topic:
Be lazy and preserve the data
As often in programming, you should wait the last moment to transform data, rather than doing all the work in once at the beginning. Regarding dates, it means you should postpone transformations (formatting, conversions, translations, and so on.) of the date and time data as long as you can.
Solution A: Send it raw to the client
Supposing you get date-time like ‘2018–05–12 23:16:46.123456’ from your DB or any input (can be with or without milliseconds/microseconds, date and time can be separated by a space or a “T” and it can be suffixed with a timezone but as said sooner, you should simply have UTC in your DB and as default timezone in PHP), then you can output the string JS needs with the following:
You will get ‘2018–05–12T23:16:46.123456Z’ this can be passed to the native Date JS class:
On creating the Date object, the browser/front-end will shift automatically the date and time from UTC (because “Z” means UTC) to the local timezone.
Then .toLocaleString will format it using the client settings (country/region).
Quick note about DateTimeImmutable. PHP provides both DateTime and DateTimeImmutable, so you can decide if you date object is mutable or not. Immutable date is often safer because instead of modifying the current object, any modification methods will produce a new object so can still use the new object and the original, like in this example:
Solution B: Format it with PHP
For many reasons, you may not apply the solution A. First if you don’t have a front-end (sending e-mails, CLI output, etc.)
In this case, you will need to choose in which timezone and format/language display with PHP, it can be read from the browser settings, manually selected by the user via a form, but as this part is not our topic, let just say we have them in variables.
$lang = 'en_US';
$timezone = 'Europe/Paris';
You may think those US + Europe mixed settings are contradictory but they are not. I chose them on purpose to make it clear you can’t guess one from the other.
$lang is “In which language the user want you to speak to him?”,
$timezone is “Where is the user?” so it answers different questions.
Concretely, we’ll use
$lang to translate words if needed (Monday/January/“st”) and select a format: DD/MM/YYYY in fr_FR, MM/DD/YYYY in en_US, DD.MM.YYYY in en_UK, this part mostly depends on the language region but not the region you are, if you live in Chicago and travel for a week to London, you will probably not change your computer settings to display dates as DD.MM.YYYY you will still get dates displayed in en_US.
However you will get the time in the Europe/London timezone.
Let see first how to handle it with vanilla PHP:
This code smells? Yes indeed, let’s be honest date and time stuffs are not the strong point of PHP. You can get it a bit better by installing the intl extension and use the
IntlDateFormatter class (http://php.net/manual/en/class.intldateformatter.php). But else you this very procedural-style code.
This do the job, it will search for the right language and format (%x is the preferred date format and %X the preferred time format) in the language packages installed on the machine, then it displays it with the right timezone.
But there is a weak point: here we try to en_US.UTF-8, en_US.utf8, en_US then finally en because languages name depends on installed packages (OS/utf8 support) and each machines can have more or less different languages available. So we’re not sure what language exactly we will get and even if any of them is available.
Carbon to the rescue
To get a reliable internationalization you would need to embed translations in every language you want to support, and to get it right in your dates object you would need to extend the DateTime(Immutable) class. Thankfully, you don’t need to reinvent the wheel, there are libraries that yet embed translations and formats and can handle most needs for dates. We will take Carbon, a widely used one and if you use Laravel, you already have it and dates you get from your models are actually Carbon instances. Else you can easily install it with composer (https://getcomposer.org/).
Refer to the doc to install or upgrade to the last version: https://carbon.nesbot.com/
If you have a Laravel version < 5.8, you’re on a Carbon 1 version by default, but you can find in the documentation main page how to get Carbon 2 working with your Laravel version.
So now, I’ll assume you have Carbon 2.10 or upper. You can now use both
\Carbon\CarbonImmutable classes for both respectively
DateTimeImmutable. As any PHP class name with namespace, you can put
use Carbon\Carbon; and/or
use Carbon\CarbonImmutable; at the top of a file and then simply call
CarbonImmutable later in your file.
Carbon handle JSON stringification so our first example become:
And the json_encode example with multiple dates in it simply becomes:
Then now for internationalization and timezone, we will rely on internal Carbon translations (see all available locales here: https://carbon.nesbot.com/docs/#api-localization):
You have multiple possibilities but
calendar is a very user-friendly way to display date and time, if the time is in the current day, you will get “Today at 3:54 PM”, if it’s in the current week and in the past “Monday at 11:30 AM”, if it’s farther, simply “01/16/2019” and all words and format will match the
Then, if you have a JS front-end, you can display it the same way thanks to moment.js (https://momentjs.com/):
So you get the same display of your date in every output.
From PHP side you can still shorten things a bit using
In every case, you get it with
$lang taken into account. In this example we assume those variables are set, if they can be null, you must provide fallback values, example:
Want the same front-side? Moment.js can do it too:
What about user inputs?
OK, so now we know how to send and display dates from the server to the client, let’s talk about sending dates and times from the client to the server.
You can ask dates with many form tools (free text input, year-months-day selectors) or use the HTML5 inputs (type=”date” and type=”time”) supported by real browsers missing in Internet Explorer, Safari and Opera Mini. You can also google datepicker (+ any library you use if so) and find tons of super fancy pickers.
So depending on your tool you may retrieve numbers, strings or a Date object. Let say first, strings and numbers are not proper to be sent raw to the server. When a user enter a date and time, he means (even if he don’t realize it) this date-time in his own timezone. So the server will have to interpret those strings and number using a user timezone he will have to guess/ask/detect. Don’t do that. Just speak to your server the universal time language (UTC). So from your front-end, take those inputs and convert them into a Date object. Like we did before but this time, we won’t specify a timezone (no trailing
Z) so the browser will create what we need a local Date (with the current device timezone).
Now we have a local
Date, you just have to get the ISO string from it with a simple
date.toISOString(). Taking the data above (2018–01–25 12:30), it will give you “2018–01–25T04:30:00.000Z” if you are in Chicago, “2018–01–25T11:30:00.000Z” if you are in Paris, etc. And that’s what we want, we need to care about the user’s timezone. Note that toISOString also exists on moment date, so you would get the exact same result using
moment instead of
That string output have just to be sent to PHP (still using UTC as default input timezone) and if you want save it somewhere (let say a database), you can simply use format:
.u if you don’t need to store milli/microseconds. Then you get your string ready to be stored in a DATETIME SQL column. You can use
CarbonImmutable instead of
DateTimeImmutable as the format method is the same.
A small note about some exceptions.
Sometimes you want a user to pick a date only (year, month, day) but no hour/minute. In this case, you must be clear about the meaning of this date. For example, if i pick 2018–01–24 as an holiday in my calendar, I mean I won’t be at the office for the whole day (from 00:00:00 to 23:59:59, office timezone), more often by a date, we mean the whole day in a given timezone (often the browser timezone). If my office is in San Fransisco, for someone in Sidney, that means he won’t be able to call me from 2018–01–24 19:00:00 to 2018–01–25 18:59:59, if you add the opening hours, you can see that for a remote colleague in Sidney, I’m reachable the 24 but not the 25, for someone in Paris, I’m there the morning but not the afternoon. So what is just a day in your timezone is a range between 2 moments for the rest of the world. My recommendation in this case is to save the beginning of this range 2018–01–24 00:00 in San Fransisco, so you save 2018–01–24 09:00 (UTC) in you DB. Then you can easily retrieve this moment and add 1 day (use
->modify('1 day')in PHP) to get the end of the range, then use the user timezone to display this range in a way he can understand.
In other case you pick a date only because the time is known (like picking a lunch date), in this case, you can simply assume the time is 12:00 (local hour) and append it to get your whole date-time string.
Sometimes a user have to pick a date that is not in his own timezone. If you want to book an hotel in Tokyo, you will pick your arrival date (and/or time) using Tokyo timezone. In this case, you can “remove” the user timezone when you create the
So when you will call
toISOString(), you will basically get exactly what pass as a date constructor.
Then, you can cut this timezone and force an other one server-side:
Of course you will also have to display this date with Tokyo timezone (it can be precised explicitly with a mention like “Tokyo time”).
A last case: an unknown timezone. It should be very rare, most often when you think you’re in this case, you could in fact find the timezone with some business analysis. But the date of birth is a good example. If I’m born at 1970–01–01 01:23 in Rome, when I’m in New York, I should say “I’m born at 1969–12–31 19:23” or I should precise I’m born in Rome. The more often, you just say the date (that is in fact a 24-hours approximation) and you don’t say the place (that is an other a 26-hours approximation as timezones in Earth go from -12h to +14h), so you’re giving a very imprecise information (1970–01–01 can be from this date midnight at GMT-12 to 23:59 GMT+14) and despite this 50-hours vagueness, everyone is pretty OK to take your birth date as is and display it as you give it to everyone that would be allowed to see it. In this case, we would take it assuming it’s UTC (as in the Tokyo hotel example), then we will take it as is with no timezone change sever-side and still save it with no change. Finally, we will display this date in UTC timezone.
Have an other case? First try to rationalize it as much as you can as it can probably be handled with methods mentioned above. Else post it as a comment and I will try to add it as an example in this article.