DateTimeImmutable vs DateTime
debt(d7/e3/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints.tools field is not specified, meaning no standard linter or SAST tool is called out. Accidentally mutating a shared DateTime or ignoring the return value of modify() on a DateTimeImmutable produces no compiler or runtime error — the bug silently passes through. Only careful code review or a test that checks date values after a shared-object operation will catch it.
Closest to 'simple parameterised fix' (e3). The quick_fix states: replace 'new DateTime()' with 'new DateTimeImmutable()' everywhere, and update any modify() call-sites to capture the return value. This is a mechanical find-and-replace pattern across a codebase, but it is localised per call-site rather than requiring deep architectural changes. It rates slightly above e1 because multiple files are touched and type-hints (DateTime → DateTimeInterface) must also be updated.
Closest to 'persistent productivity tax' (b5). The applies_to contexts are web and cli — broad PHP usage. Any project that passes DateTime objects through multiple layers of business logic carries the ongoing risk of accidental mutation. Developers must remember to use DateTimeImmutable, update type-hints to DateTimeInterface, and avoid date_create() helpers. This is a recurring tax on every developer who writes date-handling code, not just a single component.
Closest to 'serious trap' (t7). The misconception field highlights a performance red herring (devs avoid DateTimeImmutable thinking it's slower), but the deeper trap from common_mistakes is more dangerous: calling modify() on a DateTimeImmutable and silently discarding the result looks exactly like the DateTime mutation pattern but does nothing. Additionally, type-hinting DateTime instead of DateTimeInterface silently rejects valid DateTimeImmutable values. These contradict how mutation works in DateTime and how type-hints behave with related classes, making this a serious trap for competent developers unfamiliar with the distinction.
Also Known As
TL;DR
Explanation
DateTime and DateTimeImmutable both implement DateTimeInterface and accept the same constructor arguments and format strings. The difference is mutation: DateTime::modify(), add(), sub(), and setDate() mutate the object and return $this. DateTimeImmutable::modify() returns a new object with the change applied — the original is unchanged. This matters when a date is passed to a function that calls modify() — with DateTime, the caller's date changes too; with DateTimeImmutable, it cannot. The DateTimeInterface type hint accepts both, so you can write functions that work with either while defaulting to the immutable variant.
Common Misconception
Why It Matters
Common Mistakes
- Calling modify() on a DateTimeImmutable and ignoring the return value — the call does nothing visible because the original is unchanged and the new object is discarded.
- Type-hinting DateTime instead of DateTimeInterface — this rejects DateTimeImmutable instances; use DateTimeInterface to accept both.
- Using date_create() helper functions which return DateTime not DateTimeImmutable — use 'new DateTimeImmutable()' or DateTimeImmutable::createFromFormat().
- Storing mutable DateTime objects in value objects or entities — any code that holds the same reference can accidentally change the date.
Code Examples
<?php
// ❌ DateTime mutation bug — modify() changes the original
function addBusinessDays(DateTime $date, int $days): DateTime
{
for ($i = 0; $i < $days; $i++) {
$date->modify('+1 day'); // Mutates the caller's DateTime!
if ((int) $date->format('N') >= 6) {
$date->modify('+2 days');
}
}
return $date;
}
$orderDate = new DateTime('2026-01-10');
$dueDate = addBusinessDays($orderDate, 3);
// $orderDate is now '2026-01-15' — silently mutated!
<?php
// ✅ DateTimeImmutable — originals are never modified
function addBusinessDays(DateTimeImmutable $date, int $days): DateTimeImmutable
{
$current = $date;
for ($i = 0; $i < $days; $i++) {
$current = $current->modify('+1 day'); // Returns new object
if ((int) $current->format('N') >= 6) {
$current = $current->modify('+2 days');
}
}
return $current;
}
$orderDate = new DateTimeImmutable('2026-01-10');
$dueDate = addBusinessDays($orderDate, 3);
// $orderDate is still '2026-01-10' — unchanged