Lesson 95 of 100
Dates15 min5 checksPHP 8.5

Calculate with DateTimeImmutable

Learn to do date arithmetic safely with DateTimeImmutable, fixed UTC input and formatted string results.

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.

Example
<?php
$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;
Output
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.

Example
<?php
$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;
Output
["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.

Example
<?php
$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;
Output
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:

  1. Build a DateTimeImmutable from the date plus ' 00:00:00', with the UTC timezone.
  2. Create the shifted date with modify(). The format sprintf('%+d days', $days) always includes a sign, so 5 becomes +5 days and -1 becomes -1 days.
  3. Store the object that modify() returns, then return its format('Y-m-d').

Exercise contract

  • Function name: shiftDate
  • Arguments: a date string like 2024-02-28 and an integer day count.
  • Return value: a string in Y-m-d form.
  • 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

  • DateTimeImmutable methods 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 month when you mean it.
  • diff() gives an interval, and ->days is the total day count.
  • Return formatted strings from functions so results are simple to test.
Before you start

Functions · Arrays · Types

You’ll practice
  • Use UTC and fixed dates; return formatted strings, not date objects
  • Implement and test a deterministic PHP 8.4 function
Your task

Make shiftDate() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.