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.

Mutable vs immutable PHP date handling

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 DateTime and 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

  • DateTime mutates in place; modify()/add()/sub() return the same instance.
  • DateTimeImmutable returns a new instance; the original stays put.
  • Prefer DateTimeImmutable unless you have a concrete reason not to.
  • If you must share a DateTime, pass a clone so callers can’t rewrite your value.