Dates & Times

PHP · Lesson 27 of 50 7 min read Updated

Train departures, exam schedules, delivery dates, the "posted 3 days ago" under a comment: dates are everywhere in web apps. They are also full of traps, such as time zones, months with 28 to 31 days, and the many ways people write a date.

In this lesson you will use PHP's DateTimeImmutable class to create dates in the right time zone, format them for Indian readers, add and subtract periods, find the gap between two dates, and safely read dates that users type.

Create a date in the right time zone

A time zone says how far local clock time is from UTC (Coordinated Universal Time, the world's reference clock). India uses one zone, named Asia/Kolkata, which is UTC+05:30.

If you do not say which zone you mean, PHP uses the date.timezone setting from php.ini, which on many servers is UTC or some other city. Then "today" can be yesterday for your users. So always pass the zone explicitly, or set a default at the start of your app:

PHP
date_default_timezone_set('Asia/Kolkata');

Create dates with the DateTimeImmutable class. The first argument is a date string, the second is a DateTimeZone:

PHP
<?php
declare(strict_types=1);

$ist = new DateTimeZone('Asia/Kolkata');
$exam = new DateTimeImmutable('2026-11-14 09:30', $ist);

echo $exam->format('d/m/Y') . "\n";
echo $exam->format('l, j F Y') . "\n";
echo $exam->format('g:i A') . "\n";
echo $exam->format('D d M Y, H:i') . "\n";
echo $exam->format(DATE_ATOM) . "\n";
Output
14/11/2026
Saturday, 14 November 2026
9:30 AM
Sat 14 Nov 2026, 09:30
2026-11-14T09:30:00+05:30

For the current moment, use new DateTimeImmutable('now', $ist).

Format characters

format() replaces each letter with part of the date. These are the ones you will use most:

CharacterMeaningExample
d / jDay of the month, with / without leading zero05 / 5
m / nMonth number, with / without leading zero03 / 3
M / FShort / full month nameNov / November
YFour-digit year2026
D / lShort / full weekday name (l is a lowercase L)Sat / Saturday
H / GHour, 24-hour clock, with / without leading zero09 / 9
h / gHour, 12-hour clock, with / without leading zero09 / 9
i / sMinutes / seconds30 / 00
AAM or PMAM
TTime zone abbreviationIST

Use Y-m-d H:i:s when you store a date in MySQL, and DATE_ATOM (an ISO 8601 format) when you send dates to other programs, for example in JSON.

Why "Immutable"?

PHP has two date classes: DateTime and DateTimeImmutable. The difference matters:

PHP
<?php
declare(strict_types=1);

$a = new DateTime('2026-10-29');
$b = $a->modify('+1 day');
echo $a->format('Y-m-d') . ' ' . $b->format('Y-m-d') . "\n";

$c = new DateTimeImmutable('2026-10-29');
$d = $c->modify('+1 day');
echo $c->format('Y-m-d') . ' ' . $d->format('Y-m-d') . "\n";
Output
2026-10-30 2026-10-30
2026-10-29 2026-10-30

DateTime::modify() changes the original object, so $a silently moved to the 30th. DateTimeImmutable::modify() leaves the original alone and returns a new object. With mutable dates, a function that calculates a due date can secretly change the order date you passed in. Use DateTimeImmutable everywhere and this bug cannot happen.

Adding and subtracting time

There are two ways to move a date. modify() takes English phrases such as +5 days or next monday. add() and sub() take a DateInterval, which is written in a compact code: P (period) followed by numbers and units, Y years, M months, W weeks, D days, then T and H hours, M minutes, S seconds. So P1Y6M is one year and six months, and PT90M is 90 minutes.

PHP
<?php
declare(strict_types=1);

$ist = new DateTimeZone('Asia/Kolkata');
$order = new DateTimeImmutable('2026-10-29 16:45', $ist);

$delivery = $order->modify('+5 days');
$returnBy = $delivery->add(new DateInterval('P7D'));
$warranty = $order->add(new DateInterval('P1Y6M'));

echo 'Ordered:   ' . $order->format('d M Y') . "\n";
echo 'Delivery:  ' . $delivery->format('d M Y') . "\n";
echo 'Return by: ' . $returnBy->format('d M Y') . "\n";
echo 'Warranty:  ' . $warranty->format('d M Y') . "\n";
Output
Ordered:   29 Oct 2026
Delivery:  03 Nov 2026
Return by: 10 Nov 2026
Warranty:  29 Apr 2028

The difference between two dates

diff() returns a DateInterval that describes the gap. Its y, m and d properties hold years, months and days, and days holds the total number of days.

PHP
<?php
declare(strict_types=1);

$ist = new DateTimeZone('Asia/Kolkata');
$birth = new DateTimeImmutable('2004-03-18', $ist);
$today = new DateTimeImmutable('2026-10-29', $ist);

$age = $birth->diff($today);
echo "Ananya is {$age->y} years, {$age->m} months and {$age->d} days old.\n";
echo 'That is ' . $age->days . " days in total.\n";

$event = new DateTimeImmutable('2026-11-14', $ist);
echo $today->diff($event)->format('%a days to Children\'s Day') . "\n";
Output
Ananya is 22 years, 7 months and 11 days old.
That is 8260 days in total.
16 days to Children's Day

In DateInterval::format(), %a is the total days, and %y, %m, %d are the parts. If the second date is earlier than the first, the interval's invert property is 1.

To repeat a date, for example a class every Monday, use DatePeriod. It gives every date from a start, in steps of an interval, up to (but not including) an end date:

PHP
$start = new DateTimeImmutable('2026-11-02');
$end = new DateTimeImmutable('2026-11-30');

foreach (new DatePeriod($start, new DateInterval('P1W'), $end) as $day) {
    echo 'Class on ' . $day->format('D d M') . "\n";
}
Output
Class on Mon 02 Nov
Class on Mon 09 Nov
Class on Mon 16 Nov
Class on Mon 23 Nov

Reading dates that users type

Indian users usually type dates as DD/MM/YYYY, but PHP reads 15/08/2026 in American month-first order, or not at all. Use createFromFormat() to say exactly which format you expect:

PHP
<?php
declare(strict_types=1);

function parseIndianDate(string $input): ?DateTimeImmutable
{
    $date = DateTimeImmutable::createFromFormat('!d/m/Y', $input, new DateTimeZone('Asia/Kolkata'));

    if ($date === false || $date->format('d/m/Y') !== $input) {
        return null;
    }
    return $date;
}

foreach (['15/08/2026', '31/02/2026', '2026-08-15'] as $input) {
    $date = parseIndianDate($input);
    echo $input . ' => ' . ($date ? $date->format('Y-m-d') : 'invalid') . "\n";
}
Output
15/08/2026 => 2026-08-15
31/02/2026 => invalid
2026-08-15 => invalid

createFromFormat() returns false when the text does not match the format. But 31/02/2026 does match, and PHP quietly rolls it over to 3 March. Formatting the result again and comparing it with the input catches that. The ! at the start of the format sets the time to midnight; without it, PHP fills in the current time.

If you pass nonsense to the constructor, as in new DateTimeImmutable('next fortnight-ish'), PHP 8.3 and later throw a DateMalformedStringException. Catch it when the string comes from outside your code.

Common mistakes

  • Not setting a time zone. Dates come out hours wrong. Pass Asia/Kolkata or call date_default_timezone_set().
  • Using DateTime and changing a date by accident. Prefer DateTimeImmutable.
  • Comparing date strings like '15/08/2026' < '02/09/2026'. That compares text, not dates. Compare DateTimeImmutable objects with < and > instead.
  • Trusting +1 month on the 29th, 30th or 31st. Use last day of next month when that is what you mean.

Key points

  • Always use a time zone; India is Asia/Kolkata (UTC+05:30).
  • Prefer DateTimeImmutable: its methods return new objects and never change the original.
  • format() turns a date into text; Y-m-d H:i:s suits MySQL and DATE_ATOM suits APIs.
  • Move dates with modify() or with add() and sub() plus a DateInterval like P7D.
  • diff() gives the gap between two dates; days is the total number of days.
  • Parse user input with createFromFormat() and check the result strictly.

Exercise

A library lends books for 14 days and charges a fine of ₹5 for each day late. Write a script with an issue date of 20/10/2026 (typed in Indian format) and a return date of 08/11/2026. Print the due date as 03 Nov 2026 style, the number of days late, and the fine.

Show solution
PHP
<?php
declare(strict_types=1);

$ist = new DateTimeZone('Asia/Kolkata');
$issued = DateTimeImmutable::createFromFormat('!d/m/Y', '20/10/2026', $ist);
$returned = DateTimeImmutable::createFromFormat('!d/m/Y', '08/11/2026', $ist);

$due = $issued->add(new DateInterval('P14D'));
$daysLate = $returned > $due ? $due->diff($returned)->days : 0;

echo 'Due date: ' . $due->format('d M Y') . "\n";
echo "Days late: $daysLate\n";
echo 'Fine: ₹' . ($daysLate * 5) . "\n";
Output
Due date: 03 Nov 2026
Days late: 5
Fine: ₹25

add() returns a new date for the due date, DateTimeImmutable objects can be compared with >, and diff()->days gives the whole number of days between them.

Quick Quiz

1. What does $b = $a->modify('+1 day'); do when $a is a DateTimeImmutable?

2. Which DateInterval code means two weeks?

3. Which format string prints Sat 14 Nov 2026?

4. Why compare $date->format('d/m/Y') with the input after createFromFormat()?

Learn PHP with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call