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.