ZetCode

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.

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.

create.php
<?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.

immutable.php
<?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.

diff.php
<?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.

interval.php
<?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.

period.php
<?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.

tz.php
<?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.

fixed.php
<?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

My name is Jan Bodnar and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.

List all PHP tutorials.