Working with date & time in PHP can sometimes be annoying, leading to unexpected bugs in the code:
$startedAt = new DateTime('2019-06-30 10:00:00'); $finishedAt = $startedAt->add(new DateInterval('PT3M')); var_dump($startedAt->format('Y-m-d H:i:s')); //2019-06-30 10:03:00 ❌ var_dump($finishedAt->format('Y-m-d H:i:s')); //2019-06-30 10:03:00 ✅
$finishedAt are 3 minutes forward in time, because methods such as
modify() also change
DateTime object they were called on before returning it. In the above example, this certainly isn't the desired behaviour.
We can fix this by copying reference object before acting on it, like so:
$startedAt = new DateTime('2019-06-30 10:00:00'); $finishedAt = clone $startedAt; $finishedAt->add(new DateInterval('PT3M'));
Every time I encounter
clone in PHP code things start to smell as it is usually about someone hacking someone else's bad code design. In this particular case it was used to avoid mutating behavior, but it makes the code ugly and introduces unnecessary noise.
Alternatively, this could be solved by converting original
DateTime instance to
$startedAt = new DateTime('2019-06-30 10:00:00'); $finishedAt = DateTimeImmutable::createFromMutable($startedAt)->add(new DateInterval('PT3M'));
But why not using
DateTimeImmutable from the beginning?
Uncompromising use of DateTimeImmutable
Instead of manually applying defensive techniques in order to prevent unexpected mutation when passing around date/time objects, use
DateTimeImmutable that encapsulates those techniques, making your code more reliable.
$startedAt = new DateTimeImmutable('2019-06-30 10:00:00'); $finishedAt = $startedAt->add(new DateInterval('PT3M')); var_dump($startedAt->format('Y-m-d H:i:s')); //2019-06-30 10:00:00 ✅ var_dump($finishedAt->format('Y-m-d H:i:s')); //2019-06-30 10:03:00 ✅
In most contexts, concept of a date is treated as a value, we compare dates by their values, and when we modify a date it becomes a different date. All this perfectly matches the definition of a Value Object, and one important characteristic of value objects is that they are immutable.
Verbose coding style
Immutability forces you to explicitly reassign a
DateTimeImmutable object every time you act on it, because it never modifies itself but a new copy is returned. After years of working with mutable
DateTime, and due to the fact that mutability is the default in imperative programming languages, it is hard to get rid of bad mutating habits and conform to the new coding style that enforces reassignment:
$this->expiresAt = $this->expiresAt->modify('+1 week');
Static analysis tools such as PHPStan and one of its extensions can warn us if we misuse
DateTimeImmutable by omitting assignment.
Yet this cognitive bias towards mutability is suppressed when we perform arithmetic operations on primitive values, for example:
$a + 3;. On its own, this gets perceived as an pointless statement that is clearly missing reassignment:
$a = $a + 3; or
$a += 3;. Would not it be lovely if we could use something similar in the case of value objects?
Some programming languages features a syntactic sugar called operator overloading that allows for implementing operators in user-defined types and classes, so that they behave much like the primitive data types. I would not mind if PHP steals this trick from another programming language that would allow us to write our code this way:
$this->expiresAt += '1 week';
Some people argue that performance-wise, it is better to use
DateTime when calculations are done within a single scope of execution. That is a valid point, but unless you are doing hundreds of operations, and given that references to the old
DateTimeImmutable object will be garbage collected, in most practical scenarios memory consumption should not be a concern.
Carbon is a super popular library that extends PHP's date/time API with a rich set of functionality. To be more accurate, it extends API of a mutable
DateTime class, which conflicts with the aim of this blog post.
So if you like working with Carbon, but you favor immutability, I suggest you consider Chronos. It is a standalone library that was originally based on Carbon, focusing on providing immutable date/time objects by default, but it also ships with mutable variants in case you need them.
Edit (05/07/2019): It turns out that Carbon does have an immutable date/time variant, which is a big plus on its account. Yet, the reason I gave Chronos advantage is that unlike Carbon, it encourages and promotes immutability by default, both in code and documentation, and that is a crucial factor with regard to the message of this post.
DateTimeImmutable was first introduced back in the ancient PHP 5.5, and to my surprise many developers are discovering it only now. Use
DateTimeImmutable by default whenever possible, also bearing in mind some of the tradeoffs I've described, which I consider to be more a matter of habit and shift in mindset.