Don't use DateTimeInterface, I beg you
In this article, I'll get into why PHP's DateTimeInterface
is problematic and why developers should think twice before using it.
So let's see what makes it such a pet peeve of mine, shall we?
(TL;DR: Use DateTimeImmutable
instead.)
Introductionπ
PHPβs DateTimeInterface
provides a common interface for DateTime
and DateTimeImmutable
.
At first glance, this might seem like a good idea, offering users the flexibility to type-hint against a shared interface.
Upon closer inspection, DateTimeInterface
introduces more problems than it solves, and thereβs a (seemingly abandoned) RFC to remove it from PHP.
To quote the RFC:
This sort of behavior is what may become yet another of those quirks in the language that depreciate PHP.
I wholeheartedly agree.
Where DateTimeInterface
falls shortπ
It cannot be implemented by user-defined classesπ
This is mostly what's been described in the RFC.
Unlike typical interfaces, DateTimeInterface
is restricted to the internal PHP classes DateTime
and DateTimeImmutable
.
User-defined classes cannot implement it, limiting its usefulness and making it the only interface in PHP that I know of that exhibits such special behavior.
This is a result of DateTimeInterface
being an implementation detail of ext/date
.
Here's an example:
<?php
class MyDateTime implements \DateTimeInterface
{
}
This is what you'll see when you attempt to run it:
$ php --version
PHP 8.4.2 (cli) (built: Dec 17 2024 15:31:31) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.4.2, Copyright (c) Zend Technologies
with Zend OPcache v8.4.2, Copyright (c), by Zend Technologies
$ php test.php
Fatal error: DateTimeInterface can't be implemented by user classes in /home/lukas/test.php on line 3
"You're holding it wrong"π
Typically, when using interfaces, the user should not need to worry about the specific implementation of the object being operated on.
However, DateTimeInterface
does not abide by this rule.
Rather than being a proper abstraction, it exposes the differences of its implementations without resolving the inconsistencies, leaving developers in a position where they must handle the case of differing underlying implementations anyway.
DateTime
and DateTimeImmutable
behaviors conflictπ
Let's get some basics out of the way.
The core distinction between DateTime
and DateTimeImmutable
is, obviously, mutability.
For instance:
- With
DateTime
, calling$dateTime->modify('+1 week')
alters the instance in place. - With
DateTimeImmutable
, calling$dateTime->modify('+1 week')
returns a new instance, leaving the original unchanged.
This applies to any method that changes anything about the object, such as setTimezone
, setTime
, ...
This difference affects how methods on DateTimeInterface
objects must be called.
Meaning, if you intend to modify the object, you will have to reassign it just in case you received a DateTimeImmutable
object.
Or you have to explicitly clone it in case you received a DateTime
object and don't want it to change in place.
This is unnecessarily tedious and undermines the purpose of having a shared interface in the first place.
Be careful with gettersπ
If you write a function that could return a mutable DateTime
object, you might simultaneously allow the caller of that function to change that value in place, potentially causing unintended side effects.
The distinction between DateTime
and DateTimeImmutable
should not existπ
Itβs unclear to me why developers should need to decide whether they want to choose a mutable or immutable datetime class. While I obviously believe the immutable variant is superior for pretty much every use case, PHP should just have committed to a single approach instead of supporting both options and papering over it with an interface. Oh well.
Alright Mr. Smarty Pants, lemme see the codeπ
Suppose you want to write a simple class to schedule a task on a calendar.
Consider the following code:
<?php
class Task
{
public function __construct(
private string $what,
private \DateTimeInterface $when,
private bool $active = true,
) {
}
// Postpone to next week.
public function procrastinate(): void {
if ($this->when instanceof \DateTime) {
// I can just call modify and it works!
$this->when->modify('+1 week');
} else if ($this->when instanceof \DateTimeImmutable) {
// I need to reassign the object explicitly.
$this->when = $this->when->modify('+1 week');
} else {
// This is theoretically unnecessary, but it helps with static
// analysis. It may also make bugs more noticeable if PHP ever
// introduces a new class that implements DateTimeInterface.
throw \LogicException('Unexpected DateTimeInterface implementation');
}
}
// This *might* give the caller the ability to mutate $this->when.
public function getWhen(): \DateTimeInterface {
return $this->when;
}
// TODO: The rest of the owl.
}
As you can see, the way methods are called on the object differ between implementations of DateTimeInterface
.
The caller should not have to care about the implementation, otherwise we might as well be using mixed
and instanceof
everywhere.
(Of course, the code could have been shorter by reassigning $this->when
regardless of the implementation, but it just exists to illustrate the problem.)
Current statusπ
To quote the RFC:
Implementation None, so far. Joe Watkins told me that he has a patch for it.
To put this into context, the RFC originally proposed to remove it by the time PHP 7 came around. Where did the years go...
Anyway, regarding usage, a large part of the PHP community does not seem to have caught on to the fact that this interface is a mistake.
As an example, the widely used Symfony Maker Bundle generates DateTimeInterface
entity fields.
The way forwardπ
Prefer DateTimeImmutable
π
If you can help it, use DateTimeImmutable
.
It is by far the most sane option.
In my mind, a datetime object is a very basic data type, much like integers or strings, so immutability should be the default. This approach makes it easier to reason about code and reduces potential bugs.
What if I have to use DateTimeInterface
?π
If you, for example, need to implement an external interface in which a method receives a DateTimeInterface
, you should make sure that you reassign the object every time you intend to mutate it, and explicitly clone the object every time you intend to create a modified copy.
This makes behaviors between DateTime
and DateTimeImmutable
more consistent, but it obviously doesn't fix the problems outlined in this article.
Conclusionπ
DateTimeInterface
is not a meaningful or useful abstraction.
Attempting to unify DateTime
and DateTimeImmutable
is a pointless task, and it introduces more complexity than it removes.
The best course of action for developers is to avoid DateTimeInterface
and instead use DateTimeImmutable
wherever possible.
A tiny fraction of the PHP community has recognized these issues years ago, as evidenced by the aforementioned RFC to drop DateTimeInterface
, however clearly not everyone has caught on.
Until PHP itself resolves this design flaw (which would be a serious backward compatibility break), developers can mitigate the impact by enforcing immutability and leaving DateTime
and DateTimeInterface
behind.