Dates & Times
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:
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
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";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:
| Character | Meaning | Example |
|---|---|---|
d / j | Day of the month, with / without leading zero | 05 / 5 |
m / n | Month number, with / without leading zero | 03 / 3 |
M / F | Short / full month name | Nov / November |
Y | Four-digit year | 2026 |
D / l | Short / full weekday name (l is a lowercase L) | Sat / Saturday |
H / G | Hour, 24-hour clock, with / without leading zero | 09 / 9 |
h / g | Hour, 12-hour clock, with / without leading zero | 09 / 9 |
i / s | Minutes / seconds | 30 / 00 |
A | AM or PM | AM |
T | Time zone abbreviation | IST |
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
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";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
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";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
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";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:
$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";
}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
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";
}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/Kolkataor calldate_default_timezone_set(). - Using
DateTimeand changing a date by accident. PreferDateTimeImmutable. - Comparing date strings like
'15/08/2026' < '02/09/2026'. That compares text, not dates. CompareDateTimeImmutableobjects with<and>instead. - Trusting
+1 monthon the 29th, 30th or 31st. Uselast day of next monthwhen 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:ssuits MySQL andDATE_ATOMsuits APIs.- Move dates with
modify()or withadd()andsub()plus aDateIntervallikeP7D. diff()gives the gap between two dates;daysis 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
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";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?
Immutable objects never change; their methods return new objects.
2. Which DateInterval code means two weeks?
Interval codes start with P; units like W for weeks follow the number.
3. Which format string prints Sat 14 Nov 2026?
D is the short weekday, d the day with a zero, M the short month and Y the year.
4. Why compare $date->format('d/m/Y') with the input after createFromFormat()?
PHP accepts impossible dates and moves them forward; comparing catches the change.
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
