Lesson 29 of 100
Dates12 min5 checksPHP 8.5

Working with DateTimeImmutable

Create UTC dates with DateTimeImmutable, change them without mutating the original object, and avoid the common pitfalls of month arithmetic.

01 · THE IDEAREAD → TRY → LEARN

Why dates deserve their own class

A date is more than a string. It has a calendar, a time of day and a timezone, and the same text can mean different moments depending on the zone. PHP represents this with DateTimeImmutable. It does the same job as DateTime, with one important difference: methods that change the value return a new object and leave the original alone.

That is the property this lesson is about. When a date object can never change under you, passing it into a function is safe. You will not wonder whether some other piece of code shifted it by a day.

Creating a date in UTC

The constructor is new DateTimeImmutable(string $datetime = "now", ?DateTimeZone $timezone = null). In lessons and tests, never rely on "now". Pass a fixed string and an explicit DateTimeZone, so the result is the same on every machine.

Example
<?php
$date = new DateTimeImmutable('2024-01-31 09:30:00', new DateTimeZone('UTC'));
echo $date->format('Y-m-d H:i:s T'), "\n";
Output
2024-01-31 09:30:00 UTC

The format() method turns the object back into text. In the format string, Y is a four-digit year, m the month, d the day, H:i:s the time and T the timezone abbreviation. Format letters are case-sensitive, so m (month) and i (minutes) are not interchangeable with M and I.

Changes return new objects

Methods such as modify(), add(), sub(), setDate() and setTime() do not edit the object they are called on. The PHP 8.4 manual even marks them so that ignoring the return value is flagged. The bug to watch for is calling $date->modify('+1 day'); as a statement and expecting $date to change. It will not.

Example
<?php
$start = new DateTimeImmutable('2024-01-31 09:30:00', new DateTimeZone('UTC'));
$next = $start->modify('+1 day');
echo $start->format('Y-m-d'), "\n";
echo $next->format('Y-m-d'), "\n";
Output
2024-01-31
2024-02-01

The original stays on 31 January, and the new object holds 1 February. Always assign the result, or chain it into another call.

Calendar arithmetic is not always intuitive

Adding a fixed number of days is predictable. Adding a month is not always, because months have different lengths. If you add one month to 31 January, PHP computes the nonexistent 31 February and lets it overflow into March. In 2024, a leap year, February has 29 days, so the result is 2 March.

Example
<?php
$date = new DateTimeImmutable('2024-01-31', new DateTimeZone('UTC'));
$result = [
    'plus_days' => $date->add(new DateInterval('P30D'))->format('Y-m-d'),
    'plus_month' => $date->add(new DateInterval('P1M'))->format('Y-m-d'),
];
echo json_encode($result), "\n";
Output
{"plus_days":"2024-03-01","plus_month":"2024-03-02"}

DateInterval('P30D') means a period of 30 days. P1M means one month. Pick the unit that matches what you really mean: "30 days later" and "the same day next month" are different questions.

Worked example: a safe helper

Suppose you need a helper that takes a Y-m-d string and returns another Y-m-d string some days later. Work through it in steps:

  1. Build a DateTimeImmutable from the input, with an explicit UTC zone.
  2. Describe the shift as text such as +3 days or -1 days.
  3. Call modify() and keep the returned object.
  4. Format that new object and return the string.

Using sprintf('%+d days', $days) produces a leading sign for both positive and negative values, so one code path handles both directions.

Edge cases and mistakes

  • Discarding the result. $date->modify(...) alone does nothing useful on an immutable object.
  • Missing timezone. Without a zone argument, the constructor uses the script's default timezone, which depends on configuration. State UTC explicitly.
  • Invalid strings. A malformed date string makes the constructor throw an exception, so validate untrusted input before using it.
  • Month-end overflow. Adding months to the 29th, 30th or 31st can land in the following month.
  • Mixing up formats. format('d-m-Y') and format('Y-m-d') give different strings. Choose Y-m-d when strings need to sort correctly.

Exercise contract

Write addDaysUtc(string $date, int $days): string. The input is a date in Y-m-d form. Interpret it as midnight UTC, shift it by $days calendar days (the value may be negative or zero), and return the result as a Y-m-d string. Your function must cross month, year and leap-day boundaries correctly. It should return the new date rather than print it.

Recap

DateTimeImmutable gives you a date value that never changes in place. Give it a fixed input and an explicit UTC zone, assign the result of every modification, and be careful with month arithmetic. Those three habits prevent most date bugs in everyday code.

Before you start

Classes

You’ll practice
  • Create a date with an explicit UTC timezone
  • Return a changed date without mutating the original
Your task

Make addDaysUtc() 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.