skip to content

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%

answer

  1. every date object carries a zone
  2. date_default_timezone_set, then date.timezone, then UTC
  3. an offset in the string wins
  4. setTimezone keeps the instant
  5. store UTC, convert at the edge

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.

solid answer

~40 s

A `DateTime` or `DateTimeImmutable` is an instant plus a time zone. If the constructor gets no `DateTimeZone`, PHP uses the default zone: the value set by `date_default_timezone_set()`, otherwise the `date.timezone` ini setting, otherwise `UTC`. A zone or offset written in the string itself (`2026-03-29T10:00:00+02:00`) or a Unix timestamp (`@1767225600`) overrides both. `setTimezone()` keeps the same instant and changes the wall-clock reading; creating a new object with another zone for the same wall-clock text gives a different instant. Named zones such as `Europe/Berlin` follow daylight-saving rules, fixed offsets like `+01:00` do not. `time()` and `getTimestamp()` are zone-independent. So: store UTC or timestamps, keep each user's zone ID separately, and convert with `setTimezone()` only for display or local-calendar logic.

code

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

date_default_timezone_set('UTC');

$meeting = new DateTimeImmutable('2026-06-01 09:00', new DateTimeZone('Europe/Berlin'));
$utc = $meeting->setTimezone(new DateTimeZone('UTC'));

echo $meeting->format(DATE_ATOM), "\n"; // 2026-06-01T09:00:00+02:00
echo $utc->format(DATE_ATOM), "\n";     // 2026-06-01T07:00:00+00:00
var_dump($meeting == $utc);              // bool(true): same instant
var_dump($meeting->getTimestamp() === $utc->getTimestamp()); // bool(true)

$fromOffset = new DateTimeImmutable('2026-06-01T09:00:00+05:30', new DateTimeZone('UTC'));
echo $fromOffset->getTimezone()->getName(), "\n"; // +05:30: the string wins

go deeper

for a junior

Recall that each date object has a time zone, that date_default_timezone_set() sets the default, and that time() returns a zone-independent timestamp.

for a middle

Explain the default-zone lookup order, why an offset in the string overrides the argument, and the difference between setTimezone() and constructing in another zone.

for a senior

Enforce storage in UTC with a separate zone identifier per user, do calendar arithmetic in the user's zone, and remove any reliance on server defaults.

for a principal

Define the organisation's time contract across services and databases, including which layer converts zones and how zone identifiers are validated.

## Every date object carries a zone In PHP a `DateTime` or `DateTimeImmutable` is not just a string of digits: it is an **instant** on the global timeline plus a **time zone** used to show and calculate it as local wall-clock time. Two objects can print different hours and still be the same moment. ## Where the zone comes from When you create a date object, PHP picks its zone in this order: 1. A zone or offset **inside the date string** (`2026-03-29T10:00:00+02:00`, `... Europe/Paris`) or a Unix timestamp written as `@1767225600`, which is always UTC. In these cases the `$timezone` argument is ignored. 2. The **`DateTimeZone` argument** you pass to the constructor or to `createFromFormat()`. 3. The **default zone**, which the engine resolves as: - the value set at runtime with `date_default_timezone_set()`; - otherwise the `date.timezone` ini setting; - otherwise **`UTC`**, the built-in fallback. Because the default can differ between a developer laptop, the CLI and the web server, relying on it is a common source of "works locally" bugs. Many teams set `date.timezone = UTC` explicitly and pass zones where they matter. ## setTimezone() versus a new object These two operations look similar and are not: | Operation | Instant | Wall-clock text | |---|---|---| | `$utc->setTimezone(new DateTimeZone('Asia/Tokyo'))` | unchanged | shifts to Tokyo time | | `new DateTimeImmutable('2026-03-01 09:00', new DateTimeZone('Asia/Tokyo'))` | 09:00 in Tokyo | as written | | same string with `Europe/London` | a different instant | as written | `setTimezone()` answers "what time is it there right now"; constructing with a zone answers "which moment is 9 o'clock there". ## Named zones, offsets and abbreviations `DateTimeZone` accepts three kinds of value: - **Identifiers** such as `Europe/Berlin` or `America/New_York`: they carry the full rule history, including daylight saving time. Use these for people and places. - **Fixed offsets** such as `+01:00`: always the same offset, no DST. Fine for parsing a timestamp that came with an offset, wrong for a user's calendar. - **Abbreviations** such as `CET` or `EST`: ambiguous and DST-unaware; avoid them for storage. An unknown name throws `DateInvalidTimeZoneException` since PHP 8.3 (earlier versions threw a generic `Exception`). ## Zone-independent values - `time()` returns the current Unix timestamp as an `int`, the same number everywhere on Earth. - `getTimestamp()` and `format('U')` return the instant, not the local time. - Comparing two date objects with `<`, `>` or `==` compares instants, even when their zones differ. ## Daylight-saving edges In zones with daylight saving time, some local times do not exist (the hour skipped in spring) and some occur twice (the hour repeated in autumn). PHP does not fail on either: it resolves the wall-clock text to one valid instant. That is convenient for display but dangerous for scheduling, because a job planned for a skipped or repeated hour runs at a time nobody chose. Two practical consequences: - Schedule recurring jobs in UTC, or at local times outside the transition window. - When a local time matters legally or financially, test it against the transition dates, which `DateTimeZone::getTransitions()` lists for a zone. `DateTimeZone::listIdentifiers()` returns every valid identifier, which is the right source for a zone picker or for validating a stored zone name. ## A storage rule that holds up 1. **Store instants in UTC** (or as timestamps) in the database. 2. **Store each user's zone identifier** as a separate string, such as `Europe/Berlin`. 3. **Convert at the edges**: `setTimezone()` for display, and do calendar logic ("tomorrow at 9", "end of month") in the user's zone before converting back to UTC. 4. **Never store local wall-clock text without its zone**: it cannot be turned back into an instant.

  • Why is storing a user's zone as +01:00 worse than storing Europe/Berlin?
    A fixed offset is only correct for part of the year. Berlin uses +01:00 in winter and +02:00 in summer, so any future date computed with a stored +01:00 is an hour off during daylight saving time. The identifier carries the rules, so `setTimezone(new DateTimeZone('Europe/Berlin'))` picks the right offset for each date.
  • Two DateTimeImmutable objects show 09:00 and 07:00. Can == still be true?
    Yes. Comparison operators on date objects compare the instants, not the printed text. `09:00+02:00` in Berlin and `07:00+00:00` in UTC are the same moment, so `==` is `true` and `getTimestamp()` returns the same integer for both.

An instant is the moment a photo was taken; the zone is which wall clock appears in the picture. setTimezone() swaps the clock in the photo, not the moment it was taken.

saying these in an interview costs you the question

  • setTimezone() moves the date to a different moment in time
  • PHP has no default zone unless date.timezone is set
  • The $timezone argument wins over an offset inside the string
  • time() returns a different number in every time zone
  • A fixed offset like +01:00 handles daylight saving time