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.
Source: DateTimeImmutable and DateTime
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 |
|---|---|---|---|
|
year, 4 / 2 digits |
|
hour 00-23 / 0-23 |
|
month 01-12 / 1-12 |
|
minutes 00-59 |
|
day 01-31 / 1-31 |
|
seconds 00-59 |
|
weekday Mon / Monday |
|
microseconds / milliseconds |
|
month Oct / October |
|
time zone id / abbreviation / offset |
|
ISO weekday 1 (Mon) - 7 |
|
ISO week number |
|
ordinal suffix ( |
|
Unix timestamp |
|
days in the month |
|
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.
Source: DateTimeInterface::format and DateTimeInterface constants
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).
Source: DateInterval and DateTimeInterface::diff
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";
}
Source: DatePeriod and DatePeriod::createFromISO8601String
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:00orCETdo 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
Source: DateTimeZone and List of Supported Timezones
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" : '';
Source: DateTimeImmutable::setMicrosecond and hrtime
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)
Source: RFC: Duration class
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)
Source: Date/Time Functions
Where older books and tutorials are outdated
-
strftime()withsetlocale()(Beak ch. 3, Powers ch. 16, Kromann ch. 12) is deprecated since 8.1; useIntlDateFormatter. -
DateTimeeverywhere: books use the mutable class; preferDateTimeImmutable. -
Error handling:
DateMalformedStringException(8.3) andgetLastErrors()returningfalse(8.2) are newer than every book; books check for a genericExceptionor ignore invalid dates. -
createFromTimestamp(),getMicrosecond()/setMicrosecond()(8.4),DatePeriod::createFromISO8601String()andINCLUDE_END_DATE(8.2/8.3), PSR-20 andTime\Duration(8.6) are absent. -
mktime()without arguments and theis_dstparameter were removed in 8.0.