The trap: DateTime edits itself
Every modify(), add(), or sub() call on a DateTime object changes the object in place and returns the same instance. There is no copy. The original reference now points at a different point in time.

That is usually not what you want when a date is passed into a helper, stored in an array, or reused later in the same request.
The bug you don’t see
function billingCycleStart(DateTime $d): DateTime
{
// intent: return the 1st of the month without touching the caller's date
return $d->modify('first day of this month');
}
$invoiceDate = new DateTime('2026-08-21');
$start = billingCycleStart($invoiceDate);
echo $invoiceDate->format('Y-m-d'); // "2026-08-01" <- the caller's date was mutated!
echo $start->format('Y-m-d'); // "2026-08-01" <- same object, same value
$invoiceDate silently moved from the 21st to the 1st. Any later code that expected the 21st is now wrong, and the two variables are literally the same object, so debugging by inspection is hopeless.
Why it bites harder in collections
$dates = [];
$base = new DateTime('2026-01-01');
for ($i = 0; $i < 3; $i++) {
$dates[] = $base->modify("+{$i} month");
}
// every slot is the SAME object holding the LAST value
foreach ($dates as $d) {
echo $d->format('Y-m-d') . PHP_EOL; // 2026-03-01 three times
}
You pushed three references to one mutable object, then advanced it three times. The array ends up holding three copies of the final date.
The fix: DateTimeImmutable
DateTimeImmutable has the exact same API, but every mutating call returns a new object and leaves the original untouched.
function billingCycleStart(DateTimeImmutable $d): DateTimeImmutable
{
return $d->modify('first day of this month'); // new object, $d is safe
}
$invoiceDate = new DateTimeImmutable('2026-08-21');
$start = billingCycleStart($invoiceDate);
echo $invoiceDate->format('Y-m-d'); // "2026-08-21" <- unchanged
// $start === "2026-08-01"
// collections now behave as expected
$base = new DateTimeImmutable('2026-01-01');
$dates = [];
for ($i = 0; $i < 3; $i++) {
$dates[] = $base->modify("+{$i} month");
}
// 2026-01-01, 2026-02-01, 2026-03-01
When you still need the mutable one
- You are doing many chained edits on a throwaway local date and want to avoid allocating new objects (rare; the GC cost is negligible for normal request volumes).
- You interoperate with a library that type-hints
DateTimeand mutates internally — wrap it, or clone before handing it over:clone $date.
For everything else — request dates, DB row timestamps, dates passed through helpers — default to DateTimeImmutable. It makes “the date I received” and “the date I computed” two different, predictable values.
Takeaway
DateTimemutates in place;modify()/add()/sub()return the same instance.DateTimeImmutablereturns a new instance; the original stays put.- Prefer
DateTimeImmutableunless you have a concrete reason not to. - If you must share a
DateTime, pass acloneso callers can’t rewrite your value.