skip to content

Dates & Time Zones

DateTimeImmutable, DateTimeZone and DateInterval model moments, zones and durations, and createFromFormat parses input. Interviewers ask about mutation bugs, month overflow and the default zone.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In PHP, what is the difference between DateTime and DateTimeImmutable, and what bug does calling modify() on a shared DateTime cause?

level: juniorimportance: must knowfreq 55%

answer

  1. same API, different return
  2. DateTime changes $this
  3. Immutable returns a new object
  4. both implement DateTimeInterface
  5. 8.5 warns on a discarded result

basics

~20 s

DateTime's modify(), add(), sub(), setDate() and setTimezone() change the object itself and return it; DateTimeImmutable's return a new object and leave the original alone. So $end = $start->modify('+7 days') on a DateTime silently moves $start too.

solid answer

~40 s

Both classes implement `DateTimeInterface` and share almost the same API, but `DateTime` is mutable: `modify()`, `add()`, `sub()`, `setDate()`, `setTime()` and `setTimezone()` change the object and return that same object. `DateTimeImmutable` returns a new instance from each of them and never changes the original. The classic bug is `$end = $start->modify('+7 days')` with a `DateTime`: `$end` and `$start` are the same object, both now a week later. The same happens when a function receives a `DateTime` and adjusts it, because objects are passed as handles and the caller sees the change. The mirror bug with the immutable class is calling `$date->modify('+1 day');` and ignoring the result; since PHP 8.5 those methods carry `#[\NoDiscard]`, so that line raises a warning. Modern code defaults to `DateTimeImmutable` and type-hints `DateTimeInterface`.

code

php · 14 lines
php
<?php
declare(strict_types=1);

$start = new DateTime('2026-03-01');
$end = $start->modify('+7 days');          // same object as $start
echo $start->format('Y-m-d'), "\n";       // 2026-03-08: start moved
var_dump($start === $end);                  // bool(true)

$from = new DateTimeImmutable('2026-03-01');
$to = $from->modify('+7 days');             // new object
echo $from->format('Y-m-d'), ' -> ', $to->format('Y-m-d'), "\n";
// 2026-03-01 -> 2026-03-08

$from->modify('+1 day');                    // PHP 8.5: warning, result discarded

go deeper

for a junior

Recall that DateTime methods change the object itself while DateTimeImmutable methods return a new object, and that you must assign that result.

for a middle

Explain the aliasing bug from $end = $start->modify(...), why objects passed to functions share the change, and how createFromMutable() and createFromInterface() help.

for a senior

Set team defaults: DateTimeImmutable internally, DateTimeInterface in signatures, and treat the PHP 8.5 NoDiscard warning as a real bug report.

for a principal

Frame immutability as a design choice for shared values, and plan how a large code base with DateTime on entities moves to immutable dates safely.

## Two classes, one API PHP's date extension ships two concrete classes for a point in time: **`DateTime`** and **`DateTimeImmutable`**. Both implement **`DateTimeInterface`**, both are created the same way (`new DateTimeImmutable('2026-03-01 09:00', $zone)`), and both have `format()`, `getTimestamp()`, `diff()` and friends. The difference is entirely in the methods that change the date. | Method | `DateTime` | `DateTimeImmutable` | |---|---|---| | `modify('+1 day')` | changes `$this`, returns `$this` | returns a new object | | `add()` / `sub()` | changes `$this` | returns a new object | | `setDate()` / `setTime()` | changes `$this` | returns a new object | | `setTimezone()` | changes `$this` | returns a new object | | Ignoring the return value | still works | the change is lost | ## The mutable-object bug Because a `DateTime` method returns the same object it changed, this code looks right and is wrong: 1. `$start = new DateTime('2026-03-01');` 2. `$end = $start->modify('+7 days');` 3. Both variables now hold the **same object**, set to 8 March. The start date is gone. The same trap appears across function boundaries. PHP passes objects as **handles**: a function that receives a `DateTime` and calls `modify()` on it changes the caller's object too. A helper such as `endOfBillingPeriod(DateTime $d)` can quietly move the invoice date stored on an entity. With `DateTime` the only protection is to `clone` before changing, which every caller has to remember. ## The immutable-object bug, and the 8.5 warning `DateTimeImmutable` removes the aliasing problem, but it has its own mistake: writing `$date->modify('+1 day');` on a line by itself. The method returns the new date, the result is thrown away, and `$date` is unchanged. Since **PHP 8.5**, the changing methods of `DateTimeImmutable` (`modify()`, `add()`, `sub()`, `setTimezone()`, `setTime()`, `setDate()` and others) are marked with the new **`#[\NoDiscard]`** attribute. Calling one and discarding the result emits a warning that the return value should be used, because the method does not modify the object itself. If you really mean to discard it, a `(void)` cast says so explicitly. ## Converting between them - `DateTimeImmutable::createFromMutable($dateTime)` makes an immutable copy of a `DateTime`. - `DateTime::createFromImmutable($immutable)` goes the other way. - `createFromInterface()` on either class accepts any `DateTimeInterface`. This lets you keep `DateTimeImmutable` inside your code while still accepting or returning `DateTime` where a library insists on it. ## What to use - Default to **`DateTimeImmutable`** for new code: values that cannot change under you are easier to share, cache and reason about. - Type-hint parameters as **`DateTimeInterface`** when you only read the date, so callers may pass either class. - Treat any remaining `DateTime` as a local working object, and `clone` it before handing it out. ## Where the mutable bug hides in real code The aliasing bug rarely looks as obvious as two lines in a row. Typical hiding places: - A **getter that returns an internal `DateTime`**, such as `$order->getCreatedAt()`. Any caller that calls `modify()` on the result changes the order itself. - A **shared instance** created once, for example a "today" object kept in a service or a static cache and adjusted by one caller for its own calculation. - A **loop** that reuses one `DateTime` to generate a schedule and stores it in an array on each pass: every array element ends up as the same object with the last date. With `DateTimeImmutable` each of these becomes safe by construction, because nobody can change the object they were given. That is the real argument for immutability: it removes a whole category of action at a distance, not just one typo. ## A quick review checklist 1. Search for `new DateTime(` and ask whether the object can ever escape the function that created it. 2. Check getters that return dates: they should return `DateTimeImmutable`, or a clone. 3. Check every `modify()`, `add()` and `sub()` on an immutable date: its result must be assigned. ## Interview summary The expected answer names the mechanism, not just the verdict: `DateTime` methods mutate and return `$this`, so assigning the result creates an alias; `DateTimeImmutable` methods return a fresh object, so the result must be assigned. Mentioning the PHP 8.5 `#[\NoDiscard]` warning shows you know the immutable class's failure mode as well.

  • A method accepts DateTime $due and calls $due->modify('+1 day') to compute a reminder date. What goes wrong for the caller?
    Objects are passed as handles, so the method changes the caller's own `DateTime`: the due date stored on the invoice or entity moves forward by a day. The fix is to accept `DateTimeInterface`, convert with `DateTimeImmutable::createFromInterface()` and work on the immutable copy, or at least `clone` before modifying.
  • Why does PHP 8.5 warn about $date->modify('+1 day'); on a DateTimeImmutable but not on a DateTime?
    The immutable class's changing methods are marked `#[\NoDiscard]` in PHP 8.5, because their only effect is the returned object, so discarding it is almost always a bug. A `DateTime` call changes the object in place, so ignoring its return value is normal and carries no attribute.

saying these in an interview costs you the question

  • DateTime::modify() returns a modified copy and leaves the original alone
  • DateTimeImmutable::modify() changes the object in place
  • Passing a DateTime to a function gives the function its own copy
  • Immutable dates cannot be shifted, so they are only for constants
  • DateTime and DateTimeImmutable cannot be converted into each other
open as a page

In PHP, how do the default time zone, DateTimeZone and setTimezone() decide what a date object shows, and how should apps store and display times?

level: middleimportance: must knowfreq 45%

basics

~20 s

Every PHP date object has a zone: the one passed in, else date_default_timezone_set(), else the date.timezone ini value, else UTC. setTimezone() keeps the instant and changes only the wall-clock view. Store UTC plus the user's zone ID.

open as a page

In PHP, how does DateTimeImmutable::createFromFormat() parse input, and why can a date like 2026-02-30 or a format without '!' give a surprising result?

level: middleimportance: should knowfreq 38%

basics

~20 s

createFromFormat() parses against an explicit format and returns false when it cannot. Fields the format omits take the current time unless it starts with ! or ends with |, and February 30 rolls into March with only a warning.

open as a page

In PHP, what does DateTimeImmutable::diff() return, why does $interval->format('%d') often give the wrong day count, and how does DatePeriod iterate a range?

level: middleimportance: should knowfreq 30%

basics

~20 s

diff() returns a DateInterval split into years, months, days and time, plus days, the total day count. %d prints only the days component; %a prints the total. DatePeriod steps from start to end, excluding the end unless INCLUDE_END_DATE is set.

open as a page

A PHP billing job renews monthly subscriptions with modify('+1 month'), and customers who signed up on January 31 are billed on March 3; why, and how do you compute renewals correctly across time zones?

level: seniorimportance: should knowfreq 35%

basics

~20 s

modify('+1 month') only increments the month number: January 31 becomes February 31, which overflows to March 3 in 2026. Compute each renewal from the original signup day, clamped to the target month's length, in the customer's time zone, then convert to UTC.

open as a page