Let’s acknowledge something right out of the gate: working with dates and times can be a slog. Not a year goes by without some app breaking due to Daylight Saving Time or an assistant not realizing that March 31st exists.
For those of us working in PHP—especially more recent versions of the language—dates and times don’t need to be a source of pain. Instead of strtotime()
this and date()
that, we have functionality baked into PHP that dramatically simplifies the work of parsing, converting, and formatting dates and times.
Meet the DateTimeInterface
PHP’s datetime extension (built into PHP by default) defines the DateTimeInterface
which, as you might have guessed from its name, is an interface related to dates and times. Who says naming things has to be hard?
PHP also ships with two implementations of this interface: DateTime
and DateTimeImmutable
. Once again, their differences should be pretty clear from the description on the tin: a DateTimeImmutable
object functions exactly like a DateTime
except that it cannot be modified. Instead, attempting to modify a DateTimeImmutable
object will result in a new DateTimeImmutable
instance.
There are good use-cases for both implementations, but as a general rule of thumb you should default to DateTimeImmutable
: this prevents operations from manipulating the underlying date/time directly, which becomes increasingly important if (for example) you’re passing a datetime through a series of middleware layers. DateTime
should really only be used in cases where you need to manipulate the date and/or time within the same instance of the object.
Let’s Make a Date(Time)!
The constructors of the DateTime
classes are pretty familiar if you’ve used PHP’s strtotime()
function, as they works in much the same way:
1 2 3 4 5 6 7 8 9 10 11 |
new DateTimeImmutable('now'); #=> 2024-07-19 00:35:00 +0000 new DateTimeImmutable('2024-07-19'); #=> 2024-07-19 00:00:00 +0000 new DateTimeImmutable('2024-07-04 12:00:00'); #=> 2024-07-04 12:00:00 +0000 new DateTimeImmutable('2 days ago'); #=> 2024-07-17 00:35:00 +0000 |
Notice that all of the examples above are in Universal Coordinated Time (UTC): because we didn’t specify a time zone, PHP defaulted to the system time zone which—on a reasonable, *nix-based server—will be UTC.
We specify time zones by constructing DateTimeZone
objects:
1 2 3 4 5 |
new DateTimeImmutable('now', new DateTimeZone('America/New_York')); #=> 2024-07-19 20:35:00 -0400 new DateTimeImmutable('now', new DateTimeZone('America/Los_Angeles')); #=> 2024-07-19 17:35:00 -0700 |
Note that the constructor will also attempt to automatically set the time zone if it can be inferred from the date string:
1 2 3 4 5 |
new DateTimeImmutable('2024-07-04 12:00:00 -0400'); #=> 2024-07-04 12:00:00 -0400 new DateTimeImmutable('2024-07-04 12:00:00 -0400', new DateTimeZone('America/Los_Angeles')); #=> 2024-07-04 12:00:00 -0400 |
If we have a Unix timestamp, we can work with those, too!
1 2 3 4 5 6 7 |
$now = time(); new DateTimeImmutable("@{$now}"); #=> 2024-07-19 00:35:00 +0000 new DateTimeImmutable("@{$now}", new DateTimeZone('America/New_York')); #=> 2024-07-19 00:35:00 +0000 |
Notice that Unix timestamps are prefixed with an ampersat (“@”, or “at sign”) character. Additionally, the time zone argument is ignored, as Unix timestamps are meant to represent the number of seconds that have passed since 1970-01-01 00:00:00 +0000
, also known as the Unix Epoch.
If you know the format a date is supposed to be in, you can also parse it using the static createFromFormat()
method:
1 2 3 4 5 |
DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2024-07-19 00:35:00'); #=> 2024-07-19 00:35:00 +0000 DateTimeImmutable::createFromFormat('Y-m-d H:i:s', 'July 19, 2024'); #=> bool(false) |
Working with DateTime Objects
Once you have a valid DateTime
object, there are a whole slew of options at your fingertips. Some of the most common include:
Modifying & Comparing DateTimes
Imagine you run an ecommerce store with a 30 day return policy, and need to determine whether or not an order is eligible for returns:
1 2 3 4 5 6 7 8 |
$order_date = new DateTimeImmutable('2024-06-12 14:00:00'); $today = new DateTimeImmutable(); if ($order_date->modify('+30 days') >= $today) { // Return window is open. } else { // Return window is closed. } |
There are a few things to note about that example:
- Using the
modify()
method, we can passstrtotime()
-like strings DateTime
objects can be compared with one another out of the box
In that example, I chose to add 30 days to the original order date, but this could have just as easily been reversed:
1 2 3 4 5 6 7 8 |
$order_date = new DateTimeImmutable('2024-06-12 14:00:00'); $thirty_days_ago = new DateTimeImmutable('30 days ago'); if ($order_date <= $thirty_days_ago) { // The order was in the last 30 days. } else { // The order was more than 30 days ago. } |
While both of those examples work, PHP also includes the DateInterval
class, which is designed to handle situations just like this:
1 2 3 4 5 6 7 8 9 |
$order_date = new DateTimeImmutable('2024-06-12 14:00:00'); $return_window = new DateInterval('P30D'); $today = new DateTimeImmutable(); if ($order_date->add($return_window) >= $today) { // Return window is still open. } else { // The return window has closed. } |
Note the “P30D” argument passed to the DateInterval
constructor: this may seem confusing at first, but it represents a period of 30 days. A full explanation can be found in the docs, but for your convenience:
Designator | Description |
---|---|
Y | Years |
M | Months |
D | Days |
W | Weeks (7 days) |
H | Hours |
M | Minutes |
S | Seconds |
Notice that “M” is used for both months and minutes: every time period will start with “P”, and anything that comes after “T” (if present) will be interpreted as time:
1 2 3 4 5 |
P2M15D # Two months, fifteen days P1Y4M # One year, four months P1WT15M # One week and fifteen minutes P4MT4M # Four months and four minutes PT30M # Thirty minutes |
Formatting Dates & Times
When you’re finally ready to render a date (or save it to a database), call on DateTimeInterface::format()
. It accepts all the arguments you’re used to from PHP’s date()
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$dt = new DateTimeImmutable('2024-07-04 12:00:00'); // Typical YYYY-MM-DD HH:MM:SS form for database storage $dt->format('Y-m-d H:i:s'); #=> 2024-07-04 12:00:00 // Atom $dt->format(DateTimeInterface::ATOM); #=> 2024-07-04T12:00:00+00:00 // Unix timestamp $dt->format('U'); #=> 1720094400 // RFC 2822/5322 $dt->format('r'); #=> Thu, 04 Jul 2024 12:00:00 +0000 |
Time Zone Conversions & Localization
One of the really nice parts about PHP’s datetime functionality is that it understands a ton of different time zones. Developers operating primarily in North America and the European Union have it easy, but not every country has it so easy.
Did you know that Asia/Kabul (Afghanistan) is UTC +4:30? Or that Pacific/Chatham (Chatham Islands of New Zealand) is UTC +12:45 or UTC +13:45, depending on Daylight Saving Time?
There are a entire countries that can’t be properly addressed using an integer UTC offset, and you don’t want to be responsible for keeping track of it all. Fortunately, the Internet Assigned Numbers Authority (IANA) maintains a database of all of this information, which is shipped with PHP.
Since PHP is aware of time zones and how they relate to UTC, we can also use DateTime
objects to localize dates and times for the user:
1 2 3 4 5 6 7 8 9 10 |
$now = new DateTimeImmutable('now'); $user_tz = new DateTimeZone('America/New_York'); printf( "Right now it's %s here in UTC, but %s in %s!", $now->format('g:ia'), $now->setTimezone($user_tz)->format('g:ia'), $user_tz->getName() ); #=> Right now, it's 2:30pm here in UTC, but 10:30am in America/New_York! |
As if that wasn’t handy enough, I’ve been burying the lede: using PHP DateTime objects means you don’t need to worry about Daylight Saving Time!
1 2 3 4 5 6 7 |
$tz = new DateTimeZone('America/New_York'); $fourth_of_july = new DateTimeImmutable('2024-07-04 12:00:00', $tz); #=> Thu, 04 Jul 2024 12:00:00 -0400 $last_christmas = new DateTimeImmutable('2023-12-25 12:00:00', $tz); #=> Mon, 25 Dec 2023 12:00:00 -0500 |
Note that I didn’t need to specify that Christmas falls during Eastern Standard Time (EST, UTC -5:00) and the 4th of July is in Eastern Daylight Time (EDT, UTC -4:00). Instead, I told it “hey, create objects representing these dates for the America/New_York time zone” and PHP figured out the rest!
Working with DateTime Objects vs Date Strings & Integer Timestamps
Let’s face it: working with date strings and Unix timestamps can be messy, and doing math with them is even worse. How many times have you seen (or written) something like this?
1 2 3 4 5 6 |
$date = strtotime('2024-03-17 12:00:00'); $seven_days = 60 * 60 * 24 * 7; $one_week_later = $date + $seven_days; printf('One week later is %s', date('F jS', $one_week_later)); #=> One week later is March 24th |
It’s a trivial example, but is still not the friendliest to read. What are the numbers going into $seven_days
?† Why does $date
hold a Unix timestamp?
These problems only multiply when you start passing date strings and/or Unix timestamps around as arguments for functions, because these strings and integers lack context. Additionally, every time you convert a date to a timestamp or back again, you run the risk of losing precision (at best) or confusing the local time zone for UTC. DateTime
objects, on the other hand, act as PHP value objects that represent a specific date and time in a specific time zone.
The next time you find yourself converting dates to timestamps and vice versa, do yourself a favor and consider a DateTime
instance instead!
Only the First Date!
This post only scratches the surface of what PHP’s datetime library offers. Whether it’s dealing with recurring events or handling errors with date parsing, PHP’s datetime functionality makes working with dates and times much easier.
If you need even more flexibility, you might also consider the Carbon library, which also uses the DateTimeInterface
under the hood (though, in my experience, does add some overhead).
Now go forth and save the date!
† 60 seconds * 60 minutes * 24 hours * 7 days, but you’d know that at a glance if you were using stevegrunwell/time-constants!
Leave a Reply