Date and Time

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05; the Time\Duration example was run on the PHP 8.6 release candidate. Every example uses the library’s loan scenario: loans start on a date, fall due a number of days later, and are measured in days late. Locale-aware formatting (month names in Spanish, and so on) is the job of IntlDateFormatter, covered in Unicode, mbstring and intl.

DateTimeImmutable versus DateTime

PHP has two date-time classes implementing DateTimeInterface:

  • DateTimeImmutable: every modifying method (modify(), add(), setTimezone(), …) returns a new object;

  • DateTime: the same methods change the object in place and return it.

Use DateTimeImmutable everywhere. With DateTime, passing a date to a function that calls modify() silently changes the caller’s date, a classic bug. Since PHP 8.5 the DateTimeImmutable modifiers are marked #[\NoDiscard], so forgetting to use the returned object is reported.

<?php

declare(strict_types=1);

function dueDate(DateTime $borrowed): DateTime
{
    return $borrowed->modify('+21 days');          // also changes the caller's object!
}

$borrowed = new DateTime('2026-10-05');
$due = dueDate($borrowed);
echo $borrowed->format('Y-m-d'), ' ', $due->format('Y-m-d'), "\n";   // 2026-10-26 2026-10-26 -- bug

$borrowed = new DateTimeImmutable('2026-10-05');
$due = $borrowed->modify('+21 days');                                 // a new object
echo $borrowed->format('Y-m-d'), ' ', $due->format('Y-m-d'), "\n";   // 2026-10-05 2026-10-26

Convert between them with DateTimeImmutable::createFromMutable()/createFromInterface() and DateTime::createFromImmutable(), and type parameters as DateTimeInterface when either is acceptable.

Creating and parsing

The constructor accepts a wide range of formats (ISO 8601, @timestamp, relative formats such as tomorrow or next monday) and an optional time zone. Since PHP 8.3 an unparseable string throws DateMalformedStringException (a subclass of DateException); before, it threw a generic Exception.

For input in a known format, use createFromFormat(), which returns false on failure. It is lenient about invalid dates: 31/02/2026 overflows into March with only a warning, so check getLastErrors() (since 8.2 it returns false when there were no warnings or errors).

<?php

declare(strict_types=1);

$madrid = new DateTimeZone('Europe/Madrid');

$fromIso = new DateTimeImmutable('2026-10-05T14:30:00', $madrid);
$fromForm = DateTimeImmutable::createFromFormat('d/m/Y H:i', '26/10/2026 09:00', $madrid);
$fromStamp = DateTimeImmutable::createFromTimestamp(1_791_230_000.5);   // 8.4: int or float, in UTC
echo $fromIso->format('c'), ' | ', $fromForm->format('c'), ' | ', $fromStamp->format('Y-m-d H:i:s.u P'), "\n";

/** Strict parsing: reject overflowing dates such as 31 February. */
function parseLoanDate(string $input): ?DateTimeImmutable
{
    $date = DateTimeImmutable::createFromFormat('!d/m/Y', $input);   // "!" resets unparsed fields to zero
    $errors = DateTimeImmutable::getLastErrors();
    if ($date === false || ($errors !== false && ($errors['warning_count'] + $errors['error_count']) > 0)) {
        return null;
    }
    return $date;
}

var_dump(parseLoanDate('31/02/2026'));                       // NULL -- would have become 2026-03-03
var_dump(parseLoanDate('28/02/2026')?->format('Y-m-d'));     // string(10) "2026-02-28"

try {
    new DateTimeImmutable('31/02/2026');                     // not a supported format at all
} catch (DateMalformedStringException $e) {
    echo get_class($e), "\n";
}

Without a leading ! or | in the format, fields missing from the input are filled with the current date and time, so createFromFormat('Y-m-d', '2026-10-05') contains the current time of day.

Formatting

format() takes the same letters as date(). The predefined constants on DateTimeInterface cover standard formats: ATOM (Y-m-d\TH:i:sP, the safe choice for APIs), RFC3339_EXTENDED (with milliseconds), RFC2822, COOKIE, ISO8601_EXPANDED (8.2, for years beyond 9999).

Letter Meaning Letter Meaning

Y / y

year, 4 / 2 digits

H / G

hour 00-23 / 0-23

m / n

month 01-12 / 1-12

i

minutes 00-59

d / j

day 01-31 / 1-31

s

seconds 00-59

D / l

weekday Mon / Monday

u / v

microseconds / milliseconds

M / F

month Oct / October

e / T / P

time zone id / abbreviation / offset +02:00

N

ISO weekday 1 (Mon) - 7

W

ISO week number

S

ordinal suffix (st, nd)

U

Unix timestamp

t

days in the month

L

leap year (1/0)

<?php

declare(strict_types=1);

$due = new DateTimeImmutable('2026-10-26 09:00', new DateTimeZone('Europe/Madrid'));

echo $due->format('l, jS F Y \a\t H:i T'), "\n";        // Monday, 26th October 2026 at 09:00 CET
echo $due->format(DateTimeInterface::ATOM), "\n";        // 2026-10-26T09:00:00+01:00
echo $due->format('\W\e\e\k W, \d\a\y N'), "\n";         // Week 44, day 1

Literal letters in a format must be escaped with a backslash, which is easy to forget. The names are always English; use IntlDateFormatter for other languages, since strftime() is deprecated.

Modifying dates

modify() accepts relative formats: +21 days, -1 week, next monday, last day of next month, first friday of january 2027, midnight, noon. add() and sub() take a DateInterval. Month arithmetic overflows: one month after 31 January is 3 March (or 2 March in a leap year), because 31 February does not exist; use last day of next month when you mean "end of the month".

<?php

declare(strict_types=1);

$borrowed = new DateTimeImmutable('2026-10-05 14:30');

echo $borrowed->modify('+21 days')->format('Y-m-d'), "\n";                 // 2026-10-26
echo $borrowed->modify('next monday')->format('Y-m-d l'), "\n";            // 2026-10-12 Monday
echo $borrowed->modify('last day of next month')->format('Y-m-d'), "\n";   // 2026-11-30
echo (new DateTimeImmutable('2026-01-31'))->modify('+1 month')->format('Y-m-d'), "\n";   // 2026-03-03 -- overflow
echo $borrowed->setDate(2027, 1, 15)->setTime(9, 0)->format('Y-m-d H:i'), "\n";         // 2027-01-15 09:00

DateInterval and diff

A DateInterval is a duration expressed in calendar units (years, months, days, hours, minutes, seconds, microseconds). It is constructed from an ISO 8601 duration string (P1M2DT3H) or returned by diff(), which also fills days with the total number of days and invert with the sign.

<?php

declare(strict_types=1);

$due = new DateTimeImmutable('2026-10-26');
$returned = new DateTimeImmutable('2026-11-08 16:00');

$late = $due->diff($returned);
printf("%d days late (%s)\n", $late->days, $late->format('%m month(s), %d day(s), %h hour(s)'));   // 13 days late
printf("fine: %.2f EUR\n", $late->invert === 0 ? $late->days * 0.25 : 0);                         // fine: 3.25 EUR

$renewal = new DateInterval('P14D');                      // ISO 8601: period of 14 days
echo $due->add($renewal)->format('Y-m-d'), "\n";          // 2026-11-09
echo $due->sub(DateInterval::createFromDateString('3 weeks'))->format('Y-m-d'), "\n";   // 2026-10-05

DateInterval cannot be compared or normalised on its own (is P1M more than P30D?); compare the resulting dates instead, or use Time\Duration for exact durations (below).

DatePeriod

DatePeriod iterates over recurring dates: a start, an interval, and either an end date or a number of recurrences. DatePeriod::INCLUDE_END_DATE (8.2) includes the end; EXCLUDE_START_DATE skips the start. Since PHP 8.3 DatePeriod::createFromISO8601String() parses ISO 8601 repeating intervals (the old constructor form with a string is deprecated).

<?php

declare(strict_types=1);

// Weekly reminder e-mails for the next three weeks (start + 3 recurrences)
$reminders = new DatePeriod(new DateTimeImmutable('2026-10-05'), new DateInterval('P1W'), 3);
foreach ($reminders as $date) {
    echo $date->format('D d M'), "\n";
}

// Every opening day between two dates, inclusive
$days = new DatePeriod(new DateTimeImmutable('2026-10-05'), new DateInterval('P1D'),
    new DateTimeImmutable('2026-10-11'), DatePeriod::INCLUDE_END_DATE);
$opening = array_filter(iterator_to_array($days), fn (DateTimeImmutable $d): bool => (int) $d->format('N') <= 5);
echo count($opening), " opening days\n";                                    // 5 opening days

// ISO 8601 repeating interval (8.3)
foreach (DatePeriod::createFromISO8601String('R2/2026-10-05T09:00:00Z/P1D') as $d) {
    echo $d->format('Y-m-d H:i e'), "\n";
}

Time zones and daylight saving time

Every date-time has a time zone. Without one, PHP uses date.timezone (or date_default_timezone_set()), which defaults to UTC. Rules that work:

  • Store and compute in UTC, convert to the user’s zone only for display.

  • Use zone identifiers (Europe/Madrid), never fixed offsets or abbreviations (+01:00 or CET do not know about daylight saving time).

  • Comparisons (<, ==, diff()) work across zones: they compare instants.

  • Around DST changes, local times can be missing (spring forward) or ambiguous (fall back).

<?php

declare(strict_types=1);

$madrid = new DateTimeZone('Europe/Madrid');
$loanStart = new DateTimeImmutable('2026-10-05 14:30', $madrid);

$utc = $loanStart->setTimezone(new DateTimeZone('UTC'));
echo $utc->format('c'), ' ', $utc == $loanStart ? 'same instant' : 'different', "\n";   // 2026-10-05T12:30:00+00:00 same instant
echo $loanStart->setTimezone(new DateTimeZone('America/New_York'))->format('H:i T'), "\n";   // 08:30 EDT

// Spring forward: 02:00-02:59 does not exist on 2026-03-29 in Madrid
$beforeChange = new DateTimeImmutable('2026-03-29 01:30', $madrid);
echo $beforeChange->modify('+1 hour')->format('H:i T'), "\n";       // 03:30 CEST

// Days in a loan are calendar days, independent of DST
$d1 = new DateTimeImmutable('2026-10-20', $madrid);
$d2 = new DateTimeImmutable('2026-10-27', $madrid);                 // DST ends on 2026-10-25
echo $d1->diff($d2)->days, ' days, ', ($d2->getTimestamp() - $d1->getTimestamp()) / 3600, " hours\n";   // 7 days, 169 hours

Microseconds and measuring durations

DateTime objects have microsecond precision; PHP 8.4 added getMicrosecond() and setMicrosecond(). For measuring elapsed time, do not subtract wall-clock dates: the system clock can jump. Use hrtime(), a monotonic high-resolution clock in nanoseconds:

<?php

declare(strict_types=1);

$stamp = (new DateTimeImmutable('2026-10-05 14:30:00'))->setMicrosecond(250_000);   // 8.4
echo $stamp->format('H:i:s.u'), ' ', $stamp->getMicrosecond(), "\n";              // 14:30:00.250000 250000

$start = hrtime(true);                               // nanoseconds, monotonic
$titles = array_map(fn (int $i): string => "Title {$i}", range(1, 50_000));
usort($titles, strcmp(...));
$elapsedMs = (hrtime(true) - $start) / 1e6;
echo $elapsedMs > 0 ? "sorted in a measurable time\n" : '';

Testable code with PSR-20 ClockInterface

Code that calls new DateTimeImmutable() (meaning "now") directly cannot be tested deterministically. PSR-20 defines Psr\Clock\ClockInterface with a single now(): DateTimeImmutable method: production code receives a system clock, tests a frozen one. Symfony’s Clock component, lcobucci/clock and Laravel’s Date/Carbon test helpers (Carbon::setTestNow(), $this->travelTo()) provide implementations.

<?php

declare(strict_types=1);

// The interface as defined by PSR-20 (normally installed with: composer require psr/clock)
interface ClockInterface { public function now(): DateTimeImmutable; }

final class SystemClock implements ClockInterface
{
    public function now(): DateTimeImmutable { return new DateTimeImmutable('now', new DateTimeZone('UTC')); }
}

final class FrozenClock implements ClockInterface
{
    public function __construct(private DateTimeImmutable $at) {}
    public function now(): DateTimeImmutable { return $this->at; }
}

final class LoanDesk
{
    public function __construct(private ClockInterface $clock) {}

    public function daysLate(DateTimeImmutable $due): int
    {
        $diff = $due->diff($this->clock->now());
        return $diff->invert === 1 ? 0 : $diff->days;
    }
}

$desk = new LoanDesk(new FrozenClock(new DateTimeImmutable('2026-11-08')));
echo $desk->daysLate(new DateTimeImmutable('2026-10-26')), "\n";   // 13 -- always, in every test run

Source: PSR-20: Clock

Time\Duration (upcoming in PHP 8.6)

PHP 8.6 adds Time\Duration, an immutable, exact length of time stored as seconds and nanoseconds — unlike DateInterval, which mixes calendar units. It has named constructors (fromSeconds(), fromMilliseconds(), fromMinutes(), fromHours(), fromIso8601DurationString(), …), arithmetic (add(), sub(), multiplyBy(), divideBy(), negate(), absolute()) and Duration::compare().

<?php

declare(strict_types=1);

// requires PHP 8.6
use Time\Duration;

$grace = Duration::fromMinutes(90)->add(Duration::fromSeconds(30, 0));
var_dump($grace->seconds);                                                           // int(5430)
var_dump(Duration::compare(Duration::fromHours(1), Duration::fromIso8601DurationString('PT60M')));   // int(0)

Legacy functions

The procedural functions — time(), date(), mktime(), strtotime(), checkdate(), getdate() — work with Unix timestamps and the default time zone. They remain fine for quick scripts (date('Y')), but strtotime() returns false on failure (easy to miss) and timestamps carry no time zone. Prefer the classes in application code.

<?php

declare(strict_types=1);

var_dump(checkdate(2, 29, 2028), checkdate(2, 29, 2027));     // bool(true) bool(false)
echo date('Y-m-d', strtotime('2026-10-05 +1 week')), "\n";     // 2026-10-12
var_dump(strtotime('not a date'));                             // bool(false)

Where older books and tutorials are outdated

  • strftime() with setlocale() (Beak ch. 3, Powers ch. 16, Kromann ch. 12) is deprecated since 8.1; use IntlDateFormatter.

  • DateTime everywhere: books use the mutable class; prefer DateTimeImmutable.

  • Error handling: DateMalformedStringException (8.3) and getLastErrors() returning false (8.2) are newer than every book; books check for a generic Exception or ignore invalid dates.

  • createFromTimestamp(), getMicrosecond()/setMicrosecond() (8.4), DatePeriod::createFromISO8601String() and INCLUDE_END_DATE (8.2/8.3), PSR-20 and Time\Duration (8.6) are absent.

  • mktime() without arguments and the is_dst parameter were removed in 8.0.