DateTime Class

Su Subhalakshmi Chandran Updated 05 Nov 2026
8 min read ·Lesson 239 of 248

What Are the DateTime Classes?

In simple terms: PHP's DateTime classes represent dates and times as objects. An object knows its date, time, and time zone, and provides methods to format it, move it forward or backward, compare it with other dates, and calculate differences.

Technically: The date extension provides a family of classes:

ClassPurpose
DateTimeImmutableA date and time that never changes; every modification returns a new object
DateTimeA date and time that is modified in place
DateTimeInterfaceThe interface implemented by both (use it in type declarations)
DateTimeZoneA time zone, such as Asia/Kolkata
DateIntervalA duration, such as "1 month and 2 days"
DatePeriodA recurring series of dates

Why Use Objects Instead of Timestamps?

  • Time zones are part of the object, so conversions are explicit and correct.
  • Month and year arithmetic ("add 1 month") is handled by the calendar, not by counting seconds.
  • Differences in years, months, and days are calculated correctly, including leap years and daylight saving time.
  • Methods make code readable: $date->modify('+1 week'), $a->diff($b)->y.

DateTimeImmutable vs DateTime

The two classes have almost identical methods. The difference is what happens when you modify them:

<?php
$start = new DateTime('2026-10-15');
$end = $start->modify('+7 days');

echo "DateTime:          start " . $start->format('Y-m-d') . ", end " . $end->format('Y-m-d') . "\n";

$start = new DateTimeImmutable('2026-10-15');
$end = $start->modify('+7 days');

echo "DateTimeImmutable: start " . $start->format('Y-m-d') . ", end " . $end->format('Y-m-d') . "\n";

Output

DateTime:          start 2026-10-22, end 2026-10-22
DateTimeImmutable: start 2026-10-15, end 2026-10-22

Why This Matters

With DateTime, modify() changes the object itself and returns it, so $start and $end are the same object — the start date was silently moved. This is a common source of bugs, especially when a date object is passed to a function that "only calculates something" but modifies it.

DateTimeImmutable returns a new object for every change and leaves the original untouched. Use DateTimeImmutable by default; this lesson uses it throughout.


Creating Date Objects

From a Date String

<?php
$tz = new DateTimeZone('Asia/Kolkata');

$a = new DateTimeImmutable('2026-10-15 09:00', $tz);
$b = new DateTimeImmutable('tomorrow', $tz);        // relative formats work too
$c = new DateTimeImmutable('@1791541200');           // from a Unix timestamp (always UTC)

echo $a->format('Y-m-d H:i T') . "\n";
echo $c->format('Y-m-d H:i:s P') . "\n";
echo $c->setTimezone($tz)->format('Y-m-d H:i:s P') . "\n";

Output

2026-10-15 09:00 IST
2026-10-09 10:20:00 +00:00
2026-10-09 15:50:00 +05:30
  • The second argument sets the time zone; without it, the default time zone is used.
  • A string starting with @ is a Unix timestamp. It is always interpreted as UTC (the time zone argument is ignored), and setTimezone() converts it for display.
  • new DateTimeImmutable() without arguments gives the current date and time.

From a Known Format: createFromFormat()

User input such as 15/10/2026 should be parsed with an explicit format:

<?php
$withTime = DateTimeImmutable::createFromFormat('d/m/Y', '15/10/2026');
$midnight = DateTimeImmutable::createFromFormat('!d/m/Y', '15/10/2026');

echo $midnight->format('Y-m-d H:i:s') . "\n";
var_dump($withTime->format('H:i:s') === '00:00:00');

Output (the second line is false unless you run it exactly at midnight)

2026-10-15 00:00:00
bool(false)

Without !, any fields not in the format (here, the time) are taken from the current time. The ! at the start resets all unspecified fields to zero, which is what you want for dates without times.

Strict Validation of Parsed Dates

createFromFormat() returns false for input that does not match the format, but it accepts impossible dates and rolls them over:

<?php
function parseIndianDate(string $input): ?DateTimeImmutable
{
    $date = DateTimeImmutable::createFromFormat('!d/m/Y', $input);

    return ($date !== false && $date->format('d/m/Y') === $input) ? $date : null;
}

foreach (['15/10/2026', '31/02/2026', '2026-10-15', '5/10/2026'] as $input) {
    $date = parseIndianDate($input);
    echo str_pad($input, 12) . ($date?->format('D, d M Y') ?? 'invalid') . "\n";
}

Output

15/10/2026  Thu, 15 Oct 2026
31/02/2026  invalid
2026-10-15  invalid
5/10/2026   invalid
  • 31/02/2026 would be accepted by createFromFormat() as 3 March 2026 (31 days counted from 1 February). Formatting the result back and comparing it with the input detects this.
  • 2026-10-15 does not match the format, so createFromFormat() returns false.
  • 5/10/2026 is rejected by the round-trip check because the format expects two-digit days; use j/n/Y if single digits should be allowed.

Invalid Date Strings Throw Exceptions

<?php
try {
    $date = new DateTimeImmutable('not a date');
} catch (Exception $e) {
    echo get_class($e) . ": " . $e->getMessage() . "\n";
}

Output (PHP 8.3+)

DateMalformedStringException: Failed to parse time string (not a date) at position 0 (n): The timezone could not be found in the database

Since PHP 8.3, the constructor throws the more specific DateMalformedStringException (a subclass of Exception); earlier versions throw a plain Exception. Catching Exception works in both. The message refers to time zones because the parser tried to interpret the unknown word as a time zone name.


Changing Dates

modify() With Relative Formats

<?php
$date = new DateTimeImmutable('2026-10-15 09:00');

echo $date->modify('+1 week')->format('Y-m-d') . "\n";
echo $date->modify('next friday')->format('Y-m-d') . "\n";
echo $date->modify('last day of this month')->format('Y-m-d') . "\n";
echo $date->modify('first day of next month')->format('Y-m-d') . "\n";

Output

2026-10-22
2026-10-16
2026-10-31
2026-11-01

modify() accepts the same expressions as strtotime().

add() and sub() With DateInterval

A DateInterval is created from an ISO 8601 duration string: P starts the period, followed by years (Y), months (M), weeks (W), and days (D); T starts the time part with hours (H), minutes (M), and seconds (S).

<?php
$start = new DateTimeImmutable('2026-10-15 09:00');

echo $start->add(new DateInterval('P1M2DT3H'))->format('Y-m-d H:i') . "\n";
echo $start->sub(new DateInterval('PT90M'))->format('Y-m-d H:i') . "\n";
echo $start->add(new DateInterval('P2W'))->format('Y-m-d') . "\n";

Output

2026-11-17 12:00
2026-10-15 07:30
2026-10-29
Duration StringMeaning
P1D1 day
P2W2 weeks
P1M2D1 month and 2 days
P1Y1 year
PT2H30M2 hours 30 minutes
PT90M90 minutes

Note that M means months before T and minutes after it.

The End-of-Month Trap

<?php
$date = new DateTimeImmutable('2026-01-31');

echo $date->modify('+1 month')->format('Y-m-d') . "\n";
echo $date->modify('last day of next month')->format('Y-m-d') . "\n";

Output

2026-03-03
2026-02-28

Adding one month to 31 January gives "31 February", which does not exist; PHP rolls over to 3 March (2026 is not a leap year). For monthly billing or reports, use expressions such as last day of next month, or base calculations on a fixed day number that exists in every month.


Comparing Dates

Date objects can be compared directly with comparison operators:

<?php
$deadline = new DateTimeImmutable('2026-10-20 23:59:59', new DateTimeZone('Asia/Kolkata'));
$submitted = new DateTimeImmutable('2026-10-20 18:30:00', new DateTimeZone('UTC'));

var_dump($submitted < $deadline);
var_dump($submitted == $deadline);

Output

bool(false)
bool(false)

The comparison uses the actual moment in time, regardless of time zone. 18:30 UTC on 20 October is midnight (00:00) on 21 October in India — after the deadline, so the submission is late, even though "18:30" looks earlier than "23:59". Never compare dates as formatted strings in different time zones.


Calculating Differences: diff()

$a->diff($b) returns a DateInterval describing the difference between two dates in calendar units:

<?php
$birthDate = new DateTimeImmutable('1998-03-20');
$today = new DateTimeImmutable('2026-10-15');

$age = $birthDate->diff($today);

echo "Age: {$age->y} years\n";
echo $age->format('%y years, %m months, %d days') . "\n";
echo $age->format('Total: %a days') . "\n";

$overdue = $today->diff(new DateTimeImmutable('2026-10-10'));
echo $overdue->format('%R%a days') . "\n";
var_dump($overdue->invert);

Output

Age: 28 years
28 years, 6 months, 25 days
Total: 10436 days
-5 days
int(1)
Property / FormatMeaning
->y, ->m, ->d, ->h, ->i, ->sYears, months, days, hours, minutes, seconds
->days / %aTotal number of days
->invert / %R1 / - if the second date is earlier

$birthDate->diff($today)->y is the correct way to calculate an age — it accounts for leap years and whether the birthday has already occurred this year.


Recurring Dates: DatePeriod

<?php
$firstEmi = new DateTimeImmutable('2026-11-05');
$monthly = new DateInterval('P1M');

$schedule = new DatePeriod($firstEmi, $monthly, 5);   // start + 5 recurrences

$loanAmount = 60000;
$installments = 6;
$emi = $loanAmount / $installments;

foreach ($schedule as $number => $date) {
    printf("EMI %d: %s  ₹%s\n", $number + 1, $date->format('D, d M Y'), number_format($emi));
}

Output

EMI 1: Thu, 05 Nov 2026  ₹10,000
EMI 2: Sat, 05 Dec 2026  ₹10,000
EMI 3: Tue, 05 Jan 2027  ₹10,000
EMI 4: Fri, 05 Feb 2027  ₹10,000
EMI 5: Fri, 05 Mar 2027  ₹10,000
EMI 6: Mon, 05 Apr 2027  ₹10,000

How It Works

  • DatePeriod generates dates starting at $firstEmi and repeating every $monthly interval.
  • The third argument is the number of recurrences after the start date, so 5 produces six dates in total. Alternatively, pass an end date instead of a number.
  • When the start date is a DateTimeImmutable, the generated dates are DateTimeImmutable objects too.

(This simplified schedule divides the amount evenly without interest; real loan schedules also calculate interest, often with the reducing-balance method.)


Common Mistakes

Using DateTime and Modifying Shared Objects

Effect: Dates change unexpectedly in other parts of the code.

Fix: Use DateTimeImmutable.

Parsing Without ! in createFromFormat()

Effect: Dates carry the current time, so comparisons and equality checks fail.

Fix: Start formats with ! (or |) when no time is given.

Accepting Rolled-Over Dates

Effect: 31/02/2026 is accepted as 3 March.

Fix: Format the parsed date back and compare it with the input.

Adding Months to the 29th–31st

Effect: Dates skip into the following month.

Fix: Use last day of ... expressions or adjust to the end of the month explicitly.

Calculating Ages by Dividing Days by 365

Effect: Off-by-one ages around birthdays and leap years.

Fix: Use diff()->y.


Best Practices

  • Use DateTimeImmutable and type-hint parameters with DateTimeInterface.
  • Always create dates with an explicit DateTimeZone.
  • Parse user input with createFromFormat('!...') and validate it strictly.
  • Use DateInterval and diff() for calendar arithmetic instead of counting seconds.
  • Use DatePeriod for schedules and recurring dates.

Official reference: https://www.php.net/manual/en/class.datetimeimmutable.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else User-defined Functions Slicing Strings Function Arguments PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning MySQL Database MySQL Connect MySQL Create DB MySQL Create Table MySQL Insert Data MySQL Get Last ID MySQL Insert Multiple MySQL Prepared MySQL Select Data MySQL Where Debugging PHP Project: Image Upload Gallery $_SESSION $_ENV PHP RegEx RegEx Patterns Heredoc and Nowdoc RegEx Modifiers preg_match preg_replace preg_split PHP RegEx Functions Nullable Types Mixed and Never Types Nullsafe Operator PHP Attributes PHP Fibers Readonly Classes Property Hooks (PHP 8.4) Asymmetric Visibility (PHP 8.4) Reflection API SPL Data Structures Composer PHP Form Handling PDO Transactions PDO Error Handling PHP XML Parsers PHP SimpleXML Parser PHP SimpleXML - Get PHP XML Expat Parser PHP DOM Parser PHP XMLReader PHP XMLWriter AJAX Intro PHP Class Constants Encryption with Sodium Recursive Functions Variable Functions Anonymous Functions Closures Arrow Functions First-class Callable Syntax PHP Arrays Indexed Arrays Associative Arrays Create Arrays Variables Scope Static Variables Variable Variables PHP Echo / Print PHP printf / sprintf PHP Data Types PHP Booleans PHP NULL PHP Strings String Functions array_map / array_filter / array_reduce Array Functions PHP Superglobals $GLOBALS What Is `$_SERVER` ? $_REQUEST $_POST $_GET $_FILES $_COOKIE PHP Type Juggling MySQL Order By MySQL Delete Data MySQL Update Data MySQL Limit Data MySQL Joins with PHP Pagination with PHP PDO Introduction PDO Connect PDO Prepared Statements PDO Fetch Modes PSR-4 Autoloading Installing Extensions (PECL / PIE) OPcache & Performance PHP-FPM Project: Contact Form with Email Project: Login & Registration System Project: CRUD App with MySQL Project: Shopping Cart with Sessions Project: REST API with PHP DateTime Class Timezones PHP Include Include vs Require PHP File Handling PHP File Open/Read PHP File Create/Write File Delete / Rename / Copy PHP Directories PHP CSV Files
Ask about this post
AI Ask about this post

Ask questions about DateTime Class and get answers drawn from it.

Signed-in readers only.