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
- Create dates with DateTimeImmutable from strings and 'now'
- Format dates with the date() format letters
- Do date math with DateInterval, add(), sub() and modify()
- Compare dates and read the gap with diff()
- Parse custom formats with createFromFormat()
- Convert between timezones with DateTimeZone
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)
- "My modify() / add() did nothing" — on an immutable object these return a new value. Capture it: $d = $d->add(...).
- The time is hours off — a timezone issue. Build with an explicit DateTimeZone and convert with setTimezone(); store in UTC.
- The format is wrong (year shows 26, not 2026) — you used lowercase y. Capital Y is the four-digit year. Case matters.
- "DateInterval::__construct(): Unknown or bad format" — your interval string is malformed. Remember P first, and T before hours/minutes: PT90M.
Pro Tips
- 💡 Default to DateTimeImmutable. It eliminates the most common date bug — an object being changed behind your back.
- 💡 Store UTC, display local. Keep every saved timestamp in UTC and convert only when showing it to a user.
- 💡 Use DateTime::ATOM (ISO 8601) for APIs. It's unambiguous and machine-readable: format(DateTime::ATOM).
📋 Quick Reference — Dates
| Syntax | Example | What 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)->days | Gap 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!
- ✅ DateTimeImmutable is the safe default — every change returns a new object
- ✅ format() turns a date into text; case matters (Y vs y)
- ✅ DateInterval + add()/sub()/modify() do date arithmetic
- ✅ diff() measures the gap; ->days is the total
- ✅ Store in UTC and convert with DateTimeZone + setTimezone()
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
- Previous: Type Declarations & Typed Properties
- Next: Working with JSON — Encode and decode JSON for APIs with json_encode and json_decode
- Quick reference: PHP cheat sheet
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.