Dates & Times (DateTime)

Reviewed & published by Brayan K

Dates are deceptively tricky — timezones, leap years, daylight saving, formatting. PHP's DateTimeImmutable handles all of it for you. Learn to create, format, do arithmetic on, and compare dates safely, and you'll never reach for raw timestamp maths again.

Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Creating Dates

Build a date by passing a string to new DateTimeImmutable(...). It accepts ISO dates ('2026-01-15'), full timestamps, and even friendly phrases like 'now', 'next friday' or '+2 weeks'. To turn an object back into text, call format() with format letters.

<?php
// DateTimeImmutable is the modern, safe way to handle dates in PHP.
// "Immutable" means every change returns a NEW object — the original
// is never altered, which avoids a whole class of bugs.

// "now" — the current moment.
$now = new DateTimeImmutable('now');

// A specific date and time.
$launch = new DateTimeImmutable('2026-01-15 09:30:00');

// Natural-language strings work too.
$friday = new DateTimeImmutable('next friday');

// format() turns an object into a string using format letters.
echo $launch->format('Y-m-d') . "\n";          // 2026-01-15
echo $launch->format('l, j F Y') . "\n";        // Thursday, 15 January 2026
echo $launch->format('H:i') . "\n";            // 09:30
?>

2️⃣ Formatting with format()

The format string is a recipe of single letters, where case matters: Y is a four-digit year, m a zero-padded month, d a zero-padded day, H:i:s 24-hour time. Any letter you want printed literally must be escaped with a backslash — that's why the ISO example below has \T.

<?php
$dt = new DateTimeImmutable('2026-06-18 14:05:09');

// The most common format letters:
echo $dt->format('Y') . "\n";   // 2026   four-digit year
echo $dt->format('m') . "\n";   // 06     month, zero-padded
echo $dt->format('d') . "\n";   // 18     day, zero-padded
echo $dt->format('H:i:s') . "\n"; // 14:05:09  24-hour time
echo $dt->format('D') . "\n";   // Thu    short weekday
echo $dt->format('A') . "\n";   // PM     AM/PM
echo $dt->format('Y-m-d\\TH:i:sP') . "\n";   // 2026-06-18T14:05:09+00:00 (ISO 8601)
?>

3️⃣ Date Arithmetic

To move a date forwards or backwards, describe the span with a DateInterval and call add() or sub(). The interval string starts with P (period) and uses T before time parts: P10D is ten days, PT2H30M is two and a half hours. For quick phrases, modify('+3 days') is even handier. Because the object is immutable, each call returns a new date and leaves the original alone.

<?php
// DateInterval describes a span of time: P = period, T = time part.
// P1Y = 1 year, P2M = 2 months, P10D = 10 days, PT2H30M = 2h30m.

$start = new DateTimeImmutable('2026-06-18');

// modify() and add()/sub() return NEW objects (immutable!).
$inTenDays = $start->add(new DateInterval('P10D'));
$lastMonth = $start->sub(new DateInterval('P1M'));
$soon      = $start->modify('+3 days');

echo $inTenDays->format('Y-m-d') . "\n";   // 2026-06-28
echo $lastMonth->format('Y-m-d') . "\n";   // 2026-05-18
echo $soon->format('Y-m-d') . "\n";        // 2026-06-21

// The original is untouched:
echo $start->format('Y-m-d') . "\n";       // 2026-06-18
?>

4️⃣ Comparing Dates with diff()

To find the gap between two dates, call $a->diff($b), which returns a DateInterval you can read or format. Use ->days for the true total number of days, the broken-down ->y/->m/->d for years/months/days, and ->invert (which is 1) to tell direction. You can also compare two date objects directly with <, > and ==.

<?php
// diff() compares two dates and returns a DateInterval describing the gap.
$birth = new DateTimeImmutable('2000-03-01');
$today = new DateTimeImmutable('2026-06-18');

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

// Read parts off the interval, then format the whole thing.
echo $age->y . " years, " . $age->m . " months\n";  // 26 years, 3 months
echo "Total days: " . $age->days . "\n";            // Total days: 9605
echo $age->format('%y years and %d days') . "\n";    // 26 years and 17 days

// invert is 1 if the second date is BEFORE the first.
$past = $today->diff($birth);
echo "Inverted? " . $past->invert . "\n";           // Inverted? 1
?>

5️⃣ Parsing & Timezones

When a string isn't in a standard format — say 18/06/2026 — tell PHP the layout with createFromFormat(). For correct times, attach a DateTimeZone when you build the date, then setTimezone() to convert it for display. Storing in UTC and converting only at render time is the pattern that keeps you out of daylight-saving trouble.

<?php
// Parse a string of one format with createFromFormat(), and set a timezone.
$dt = DateTimeImmutable::createFromFormat('d/m/Y', '18/06/2026');
echo $dt->format('Y-m-d') . "\n";   // 2026-06-18

// Timezones: build with an explicit zone, then convert.
$utc = new DateTimeImmutable('2026-06-18 12:00:00', new DateTimeZone('UTC'));
$ny  = $utc->setTimezone(new DateTimeZone('America/New_York'));

echo $utc->format('H:i T') . "\n";  // 12:00 UTC
echo $ny->format('H:i T')  . "\n";  // 08:00 EDT  (UTC-4 in summer)
?>

Now you try — fill in each ___ using the 👉 hint, then run it and check against the Output panel.

<?php
// 🎯 YOUR TURN — fill in each blank marked ___ , then run it.

$d = new DateTimeImmutable('2026-12-25');

// 1) Print just the year:
echo $d->format(___) . "\n";        // 👉 the format letter for a 4-digit year

// 2) Add 7 days using a DateInterval:
$week = $d->add(new DateInterval(___));   // 👉 the period string for 7 days
echo $week->format('Y-m-d') . "\n";       // should print: 2027-01-01

// ✅ Expected output:
//    2026
//    2027-01-01
?>

🧩 Reorder Challenge

These lines compute a date one week from a start date — but they're scrambled. Put them in the order that prints 2026-06-25.

B → C → D → A

Create the start date (B) and the interval (C) — order between these two doesn't matter — then add them to get a new date (D), and finally format it (A). You can't reference $next before it exists.

🧠 Quick Recall — Predict the Output

2026-06-18 — the object is immutable, so modify() returns a new object that was never captured. The original is unchanged. (You'd need $d = $d->modify(...).)

2. What does this print?

10 — ->days is the total number of days between the two dates.

3. What does format('jS') output for the 3rd?

3rd — j is the day without a leading zero and S adds the English ordinal suffix.

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Dates

SyntaxExampleWhat It Does
new DateTimeImmutable()new DateTimeImmutable('now')Create a date
->format()$d->format('Y-m-d')Date → string
->add() / ->sub()$d->add(new DateInterval('P1D'))Move forward/back
->modify()$d->modify('+3 days')Phrase-based shift
->diff()$a->diff($b)->daysGap between two dates
createFromFormat()::createFromFormat('d/m/Y', $s)Parse a custom format
->setTimezone()$d->setTimezone(new DateTimeZone('UTC'))Convert timezone

Mini-Challenge: Days-Until Countdown

No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments.

<?php
// 🎯 MINI-CHALLENGE: "days until" countdown.
// No code is filled in — work from the steps below, then run it.
//
// 1. Make $today = new DateTimeImmutable('2026-06-18').
// 2. Make $event = new DateTimeImmutable('2026-07-04').
// 3. Use $today->diff($event) to get the interval.
// 4. echo two lines:
//       Event: 2026-07-04   (use ->format on $event)
//       Days to go: 16      (read ->days off the interval)
//
// ✅ Expected output:
//    Event: 2026-07-04
//    Days to go: 16

// your code here
?>

🎉 Lesson Complete!

Practice quiz

Why is DateTimeImmutable preferred over the mutable DateTime?

  • It is faster
  • It supports more timezones
  • Every operation returns a new object, leaving the original untouched
  • It uses less memory

Answer: Every operation returns a new object, leaving the original untouched. DateTimeImmutable returns a brand-new object from every operation and never alters the original, avoiding a whole class of bugs.

What prints? $d = new DateTimeImmutable('2026-06-18'); $d->modify('+1 day'); echo $d->format('Y-m-d');

  • 2026-06-18
  • 2026-06-19
  • 2026-06-17
  • It throws an error

Answer: 2026-06-18. On an immutable object, modify() returns a NEW object that was never captured. The original is unchanged, so it prints 2026-06-18.

In a format string, what does a capital Y produce versus a lowercase y?

  • Both give four digits
  • Y is two digits, y is four digits
  • y is the month
  • Y is four digits (2026), y is two digits (26)

Answer: Y is four digits (2026), y is two digits (26). Case matters: capital Y is a four-digit year (2026); lowercase y is two digits (26).

Which method returns a DateInterval describing the gap between two dates?

  • gap()
  • diff()
  • between()
  • minus()

Answer: diff(). $a->diff($b) returns a DateInterval you can read or format.

On the interval from diff(), what does the ->days property give?

  • The total absolute number of days between the two dates
  • Leftover days after whole years/months
  • The day-of-month number
  • Always 0

Answer: The total absolute number of days between the two dates. ->days is the true total number of days, whereas ->d is only the leftover days after whole years and months are accounted for.

What is the DateInterval period string for seven days?

  • D7
  • 7D
  • P7D
  • PT7D

Answer: P7D. Interval strings start with P (period); P7D is seven days. T appears only before time parts like hours/minutes.

Which prefix marks the time portion of a DateInterval string, as in PT2H30M?

  • P
  • T
  • H
  • I

Answer: T. P starts the period; T precedes the time part, so PT2H30M is 2 hours 30 minutes.

Which method parses a string in a custom layout like '18/06/2026'?

  • strtotime()
  • parse()
  • fromString()
  • createFromFormat()

Answer: createFromFormat(). DateTimeImmutable::createFromFormat('d/m/Y', '18/06/2026') tells PHP the exact layout to parse.

What does the ->invert property of a DateInterval indicate?

  • The number of leap years
  • It is 1 when the second date is earlier than the first
  • Whether the interval is negative days
  • The timezone offset

Answer: It is 1 when the second date is earlier than the first. ->invert is 1 when the second date passed to diff() is before the first, telling you the direction.

What is the recommended pattern for storing and displaying times across timezones?

  • Store local time, never convert
  • Use the server default everywhere
  • Store everything in UTC and convert to local only when rendering
  • Avoid DateTimeZone entirely

Answer: Store everything in UTC and convert to local only when rendering. Store and compute in UTC, then convert with setTimezone() only at display time, to avoid daylight-saving and server-location surprises.

Continue this course

Frequently asked questions

Should I use DateTime or DateTimeImmutable in PHP?

Prefer DateTimeImmutable for almost everything. With the mutable DateTime, methods like modify() and add() change the object in place, so if you pass it to a function that modifies it, your original silently changes too — a classic source of bugs. DateTimeImmutable returns a brand-new object from every operation and leaves the original untouched, which makes your code predictable and safe to share. The two classes share the same methods, so switching is usually just changing the class name.

Why does my date show the wrong time — is it a timezone problem?

Almost always, yes. If you don't specify a timezone, PHP uses the server's default (date_default_timezone_get()), which may not be what you expect. Construct dates with an explicit new DateTimeZone('UTC') and convert for display with setTimezone(). A reliable pattern is to store and compute everything in UTC, then convert to the user's local zone only when you render it. This avoids daylight-saving and server-location surprises entirely.

What's the difference between format() characters like Y, y, m, and M?

Case matters in format strings. Capital Y is a four-digit year (2026); lowercase y is two digits (26). Lowercase m is a zero-padded month number (06); capital M is a short month name (Jun) and F is the full name (June). Lowercase d is a zero-padded day (08) while j is the day with no leading zero (8). When a format looks wrong, the cause is usually a wrong-case letter — keep php.net's date() page handy until they stick.

How do I find the number of days between two dates?

Call diff() on one date passing the other: $a->diff($b) returns a DateInterval. Read $interval->days for the total absolute number of days between them, or use the broken-down ->y, ->m, ->d for years/months/days. The ->invert property is 1 when the second date is earlier than the first. Note that ->days is the true total, whereas ->d is only the leftover days after whole years and months are accounted for.

Related lessons