Boundary bugs: resetting date values in PHP

— 3 minute read

PHP is—like most languages—full of quality-of-life features found through everyday experience. (And lots of googling.)

Still, I've somehow missed this handy ability in the regular ol' DateTime class for a literal decade:

You can reset any tokens not passed in a format with an exclamation mark:

// 2021-10-23 15:27:18
DateTime::createFromFormat('Y-m-d', '2021-10-23');

// 2021-10-23 00:00:00
DateTime::createFromFormat('!Y-m-d', '2021-10-23');

This initially might seem unworthy of much attention, but it can result in hard-to-debug issues, especially when fetching results from a DB based on those DateTime values.

Boundary bugs and querying based on dates permalink

I'd wager that for any given codebase, there are many, many bugs that can be found just below/at/above the boundary of a query. This is doubly-so with DateTimes.

Let's say you're creating and persisting an Entry of some sort:

// ...

$entry->date = new DateTime('now');

// ...

You created the Entry at 10 AM. Three days later—around 2 PM—you have a need to see Entries from the last three days. No problem; that's an easy one:

$threeDaysEarlier = (new DateTime('now'))->modify('-3 days');

$entries = Entry::find()->where('date', '>=', $threeDaysEarlier);

You probably want that Entry you made at 10 AM—but it's not going to be returned.

There's a bug hidden between our mental logic and what's actually happening.

As humans, we typically think of "three days earlier" as "starting from midnight today, the previous 72 hours". However, our code is actually querying for the exact 72 hours previous to execution.

Here's our code, now corrected to match our assumption:

// Fun fact: DateTime accepts relative references, but
// you'll need to be cautious about timezones.

$threeDaysEarlier = (new DateTime('today'))->modify('-3 days');

$entries = Entry::find()->where('date', '>=', $threeDaysEarlier);

In practice permalink

Generally speaking, it's a good idea to reset any tokens you're not passing to the DateTime object—just drop the ! in there when you're formatting (or use a relative reference that returns at midnight like we did above) and your codebase will be a little closer to matching your mental logic.