PHP DateTime
last modified October 22, 2026
In this article, we show how to work with dates and times in PHP using the
DateTime family of classes: DateTime,
DateTimeImmutable, DateInterval,
DatePeriod and DateTimeZone.
The object-oriented API replaces the older date,
strtotime and mktime functions for anything beyond
trivial cases.
It handles time zones, daylight saving time and calendar arithmetic correctly, which is difficult to do by hand with timestamps.
Main Features of the DateTime classes
The date and time API is built from five classes.
- DateTime: a mutable date and time; methods such as
modifychange the object. - DateTimeImmutable: the same API, but every change returns a new object.
- DateInterval: a span of time such as one month and two days.
- DatePeriod: iterates over dates at a regular interval.
- DateTimeZone: a time zone with its daylight saving rules.
Creating dates and formatting
A date object can be created from a string or from a string in an exact format. The format method turns it back into text.
<?php
date_default_timezone_set('UTC');
$a = new DateTime('2026-03-15 14:30:00');
$b = new DateTimeImmutable('2026-03-15 14:30:00');
$c = DateTime::createFromFormat('d/m/Y H:i', '25/12/2026 18:00');
echo $a->format('Y-m-d H:i:s'), "\n";
echo $b->format('l, F j, Y'), "\n";
echo $c->format(DATE_ATOM), "\n";
echo $a->format('D, d M y'), "\n";
echo $a->getTimestamp(), "\n";
var_dump(DateTime::createFromFormat('Y-m-d', 'not a date'));
The constructor accepts many date formats. The createFromFormat method parses an exact format and returns false when the input does not match.
The format method uses the same characters as the date function: Y is the four-digit year, m the month, d the day, l the day name and F the month name.
Predefined constants such as DATE_ATOM give standard formats. We set the default time zone to UTC to make the output reproducible.
$ php create.php 2026-03-15 14:30:00 Sunday, March 15, 2026 2026-12-25T18:00:00+00:00 Sun, 15 Mar 26 1773585000 bool(false)
Mutable versus immutable
PHP has two date classes with the same methods. They differ in whether the methods change the object or return a new one.
<?php
$mutable = new DateTime('2026-01-31');
$mutable->modify('+1 month');
echo $mutable->format('Y-m-d'), "\n";
$start = new DateTimeImmutable('2026-01-31');
$next = $start->add(new DateInterval('P1D'));
echo $start->format('Y-m-d'), "\n";
echo $next->format('Y-m-d'), "\n";
function addWeek(DateTime $d): void
{
$d->modify('+1 week'); // changes the caller's object
}
$d = new DateTime('2026-05-01');
addWeek($d);
echo $d->format('Y-m-d'), "\n";
DateTime changes in place. The first line also shows a surprise of calendar arithmetic: January 31 plus one month is "February 31", which overflows to March 3.
With DateTimeImmutable the original value stays intact and the result is a new object.
The addWeek function shows the danger of mutable objects: it silently modified the caller's date. Prefer DateTimeImmutable by default, and type-hint DateTimeInterface so functions accept both.
$ php immutable.php 2026-03-03 2026-01-31 2026-02-01 2026-05-08
Differences between dates
The diff method computes the time between two dates. In the example, we compute the age of a person.
<?php
$birth = new DateTimeImmutable('1985-07-23');
$today = new DateTimeImmutable('2026-10-22');
$diff = $birth->diff($today);
echo $diff->y, " years, ", $diff->m, " months, ", $diff->d, " days\n";
echo "Total days: ", $diff->days, "\n";
echo $diff->format('%y years, %m months, %d days'), "\n";
echo $diff->invert ? "birth is in the future\n" : "birth is in the past\n";
var_dump($birth < $today);
var_dump(max($birth, $today)->format('Y-m-d'));
The diff method returns a DateInterval with the year, month and day components. The days property holds the total number of days.
The invert property is 1 when the second date is earlier. DateTime objects can be compared directly with the comparison operators and with max and min.
$ php diff.php 41 years, 2 months, 29 days Total days: 15066 41 years, 2 months, 29 days birth is in the past bool(true) string(10) "2026-10-22"
Intervals and relative modifications
Dates can be moved forward and backward by intervals or by relative expressions written in English.
<?php
$date = new DateTimeImmutable('2026-01-15 09:00');
$interval = new DateInterval('P1M2DT3H'); // 1 month, 2 days, 3 hours
echo $date->add($interval)->format('Y-m-d H:i'), "\n";
echo $date->sub($interval)->format('Y-m-d H:i'), "\n";
$i = DateInterval::createFromDateString('3 weeks ago');
echo $date->add($i)->format('Y-m-d'), "\n";
echo $date->modify('last day of this month')->format('Y-m-d'), "\n";
echo $date->modify('next monday')->format('Y-m-d'), "\n";
echo $date->modify('first day of next month midnight')->format('Y-m-d H:i'), "\n";
A DateInterval is built from an ISO 8601 duration. P1M2DT3H is one month, two days and three hours; the T separates the date part from the time part. An interval can also be built from a relative string with createFromDateString.
The modify method understands expressions like last day of this month, next monday and first day of next month midnight.
$ php interval.php 2026-02-17 12:00 2025-12-13 06:00 2025-12-25 2026-01-31 2026-01-19 2026-02-01 00:00
DatePeriod
A DatePeriod generates a sequence of dates, which we can iterate with foreach.
<?php
$start = new DateTimeImmutable('2026-10-26');
$end = new DateTimeImmutable('2026-11-09');
$period = new DatePeriod($start, new DateInterval('P1D'), $end);
$workdays = 0;
foreach ($period as $day) {
if ($day->format('N') < 6) {
$workdays++;
}
}
echo "Working days: $workdays\n";
// the start date plus 3 recurrences, one week apart
$weekly = new DatePeriod($start, new DateInterval('P1W'), 3);
foreach ($weekly as $monday) {
echo $monday->format('D Y-m-d'), "\n";
}
A DatePeriod takes a start date, an interval and either an end date or the number of recurrences. The end date is exclusive.
The first loop counts working days over two weeks. It tests the ISO weekday number N, which is 1 for Monday to 7 for Sunday.
The second period has three recurrences, so it yields four dates including the start.
$ php period.php Working days: 10 Mon 2026-10-26 Mon 2026-11-02 Mon 2026-11-09 Mon 2026-11-16
Time zones
A date always belongs to a time zone. In this example, we convert a meeting time between zones and cross a daylight saving change.
<?php
$meeting = new DateTimeImmutable('2026-10-22 15:00', new DateTimeZone('Europe/Bratislava'));
foreach (['Europe/Bratislava', 'America/New_York', 'Asia/Tokyo', 'UTC'] as $name) {
$local = $meeting->setTimezone(new DateTimeZone($name));
printf("%-18s %s (%s)\n", $name, $local->format('Y-m-d H:i'), $local->format('T P'));
}
// DST transition: clocks go forward at 02:00 on 2026-03-29 in Europe/Bratislava
$tz = new DateTimeZone('Europe/Bratislava');
$before = new DateTimeImmutable('2026-03-29 01:30', $tz);
$after = $before->add(new DateInterval('PT1H'));
echo $before->format('H:i T'), ' -> ', $after->format('H:i T'), "\n";
We create a meeting in Bratislava and show it in other zones with setTimezone. The moment in time is the same; only its representation changes. The T format character shows the abbreviation and P the offset from UTC.
The last lines cross a daylight saving change. Adding one hour to 01:30 CET gives 03:30 CEST, because 02:30 does not exist that night.
Store timestamps in UTC and convert to the user's zone only for display.
$ php tz.php Europe/Bratislava 2026-10-22 15:00 (CEST +02:00) America/New_York 2026-10-22 09:00 (EDT -04:00) Asia/Tokyo 2026-10-22 22:00 (JST +09:00) UTC 2026-10-22 13:00 (UTC +00:00) 01:30 CET -> 03:30 CEST
Leap years, ages and errors
The final example handles leap years, birthdays on February 29, and invalid input.
<?php
function isLeapYear(int $year): bool
{
return (new DateTimeImmutable("$year-01-01"))->format('L') === '1';
}
function age(string $birth, string $on): int
{
return (new DateTimeImmutable($birth))->diff(new DateTimeImmutable($on))->y;
}
foreach ([1900, 2000, 2024, 2026] as $y) {
echo $y, ': ', isLeapYear($y) ? 'leap' : 'common', "\n";
}
echo age('2000-02-29', '2026-02-28'), "\n";
echo age('2000-02-29', '2026-03-01'), "\n";
try {
new DateTimeImmutable('31st of Februember');
} catch (DateMalformedStringException $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
The L format character is 1 for a leap year.
For a person born on February 29, the age on February 28 of a common year is still the earlier one. They turn a year older on March 1.
The constructor throws an exception for an invalid date string. Since PHP 8.3, it is the more specific DateMalformedStringException; earlier versions threw a plain Exception. Wrap user-supplied input in try/catch.
$ php fixed.php 1900: common 2000: leap 2024: leap 2026: common 25 26 DateMalformedStringException: Failed to parse time string (31st of Februember) at position 0 (3): Unexpected character
Summary
In this article, we have worked with DateTime and DateTimeImmutable, computed differences, built intervals and periods, and converted between time zones.
Author
List all PHP tutorials.