01 · THE IDEAREAD → TRY → LEARN
Why immutable dates?
Date arithmetic is a classic source of surprises. With a mutable date object, one call such as modify() changes the object in place, so any other code holding that object sees the change. DateTimeImmutable avoids this. It behaves like DateTime, except that every method that would modify the value returns a new object and leaves the original alone.
That gives you a simple rule: the original is a fixed value, and every calculation produces a fresh result that you must capture in a variable.
Make the input deterministic
For repeatable results, always give the constructor a fixed date string and an explicit DateTimeZone('UTC'). Never rely on "now" or on the server's default timezone when you want predictable output. The calculation then depends only on its inputs.
A second habit is to return plain strings such as Y-m-d from your functions instead of date objects. Strings are easy to compare, test and serialise as JSON.
Modifying without changing the original
The first example shows the core idea. modify('+1 day') returns a new object, while $start still holds its old value.
$utc = new DateTimeZone('UTC');
$start = new DateTimeImmutable('2024-01-31 10:00:00', $utc);
$next = $start->modify('+1 day');
echo $start->format('Y-m-d'), PHP_EOL;
echo $next->format('Y-m-d'), PHP_EOL;2024-01-31 2024-02-01
A frequent mistake is to write $start->modify('+1 day'); on its own line and expect $start to change. It will not. The new object is returned and, if you ignore it, lost.
Month arithmetic and overflow
Adding days is straightforward, but months have different lengths. Adding one month to 31 January gives the nonexistent date 31 February, which PHP rolls over into March. If you want the end of the next month instead, say so with a relative phrase.
$utc = new DateTimeZone('UTC');
$start = new DateTimeImmutable('2024-01-31', $utc);
$overflow = $start->add(new DateInterval('P1M'));
$clamped = $start->modify('last day of next month');
echo json_encode([$overflow->format('Y-m-d'), $clamped->format('Y-m-d')]), PHP_EOL;["2024-03-02","2024-02-29"]
Because 2024 is a leap year, February has 29 days, so the overflow lands on 2 March. The last day of next month modifier lands on 29 February.
Intervals and differences
add() and sub() take a DateInterval. The period string P7D means seven days. diff() compares two dates and returns a DateInterval; its days property is the total number of whole days between them.
$utc = new DateTimeZone('UTC');
$a = new DateTimeImmutable('2024-02-28', $utc);
$b = new DateTimeImmutable('2024-03-05', $utc);
echo $a->diff($b)->days, PHP_EOL;
echo $b->sub(new DateInterval('P7D'))->format('D, d M Y'), PHP_EOL;6 Tue, 27 Feb 2024
Notice that the leap day is counted automatically; you never have to know which years are leap years.
Worked plan for the exercise
You will write shiftDate(string $date, int $days): string. The input is a Y-m-d string and a whole number of days, which may be negative or zero. The steps are:
- Build a
DateTimeImmutablefrom the date plus' 00:00:00', with the UTC timezone. - Create the shifted date with
modify(). The formatsprintf('%+d days', $days)always includes a sign, so5becomes+5 daysand-1becomes-1 days. - Store the object that
modify()returns, then return itsformat('Y-m-d').
Exercise contract
- Function name:
shiftDate - Arguments: a date string like
2024-02-28and an integer day count. - Return value: a string in
Y-m-dform. - Boundaries to cover: a leap day, a month change, a year change, a negative shift and a zero shift.
The tests compare the returned strings, so nothing depends on the current time or on the machine running the code.
Recap
DateTimeImmutablemethods return new objects; always keep the return value.- Pass fixed input and an explicit UTC timezone to stay deterministic.
- Month arithmetic can overflow; use phrases like
last day of next monthwhen you mean it. diff()gives an interval, and->daysis the total day count.- Return formatted strings from functions so results are simple to test.
Functions · Arrays · Types
- Use UTC and fixed dates; return formatted strings, not date objects
- Implement and test a deterministic PHP 8.4 function