PHP DateTime: Modern Best Practices
Ever struggled with PHP's datetime handling? You're not alone. PHP's evolved significantly since the early days, and today's DateTime
class (plus the amazing Carbon library) makes working with timestamps actually enjoyable. Let's dive into everything from basic operations to the advanced timezone magic you'll need in production.
The DateTime Class
PHP's native DateTime
class (introduced in PHP 5.2, enhanced in PHP 8+) offers object-oriented datetime handling.
Creating DateTime Objects
<?php
// Current time
$now = new DateTime();
echo $now->format('Y-m-d H:i:s'); // 2024-01-15 19:00:00
// Current time in UTC
$utcNow = new DateTime('now', new DateTimeZone('UTC'));
// From specific date
$date = new DateTime('2024-01-15 19:00:00');
// From Unix timestamp
$timestamp = 1705341600;
$fromTimestamp = new DateTime("@$timestamp");
echo $fromTimestamp->format('Y-m-d H:i:s'); // 2024-01-15 19:00:00
// From format string
$formatted = DateTime::createFromFormat('Y-m-d H:i:s', '2024-01-15 19:00:00');
// Parse various formats
$dates = [
new DateTime('2024-01-15'),
new DateTime('January 15, 2024'),
new DateTime('+1 day'),
new DateTime('next Monday'),
new DateTime('first day of next month'),
];
foreach ($dates as $date) {
echo $date->format('Y-m-d') . PHP_EOL;
}
Converting to Timestamps
<?php
$date = new DateTime('2024-01-15 19:00:00');
// To Unix timestamp
$timestamp = $date->getTimestamp();
echo $timestamp; // 1705341600
// To formatted string
echo $date->format('Y-m-d H:i:s'); // 2024-01-15 19:00:00
echo $date->format('c'); // 2024-01-15T19:00:00+00:00 (ISO 8601)
echo $date->format(DateTime::ATOM); // 2024-01-15T19:00:00+00:00
echo $date->format(DateTime::RFC3339); // 2024-01-15T19:00:00+00:00
// Custom formats
echo $date->format('F j, Y g:i A'); // January 15, 2024 7:00 PM
echo $date->format('l, F j, Y'); // Monday, January 15, 2024
Format Characters Reference
<?php
/*
Date Formats:
Y = Year (4 digits) → 2024
y = Year (2 digits) → 24
m = Month (01-12) → 01
n = Month (1-12) → 1
M = Month short name → Jan
F = Month full name → January
d = Day (01-31) → 15
j = Day (1-31) → 15
l = Day name → Monday
D = Day short name → Mon
Time Formats:
H = Hour 24h (00-23) → 19
h = Hour 12h (01-12) → 07
i = Minute (00-59) → 00
s = Second (00-59) → 00
u = Microseconds → 000000
A = AM/PM → PM
a = am/pm → pm
Timezone Formats:
e = Timezone identifier → America/New_York
T = Timezone abbreviation → EST
P = UTC offset → +05:00
O = UTC offset (no colon) → +0500
Z = Offset in seconds → 18000
Special:
c = ISO 8601 → 2024-01-15T19:00:00+00:00
r = RFC 2822 → Mon, 15 Jan 2024 19:00:00 +0000
U = Unix timestamp → 1705341600
*/
$date = new DateTime('2024-01-15 19:00:00', new DateTimeZone('America/New_York'));
echo "ISO 8601: " . $date->format('c') . PHP_EOL;
echo "RFC 2822: " . $date->format('r') . PHP_EOL;
echo "Custom: " . $date->format('l, F j, Y \a\t g:i A T') . PHP_EOL;
// Output: Monday, January 15, 2024 at 7:00 PM EST
DateTime vs. DateTimeImmutable
Here's where things get interesting. PHP 5.5+ gave us DateTimeImmutable
, and honestly? It's a game-changer for avoiding those sneaky bugs that come from accidentally modifying dates.
The Mutability Problem (And Why It'll Bite You)
<?php
// ❌ DateTime is mutable (dangerous!)
$date = new DateTime('2024-01-15');
$tomorrow = $date->modify('+1 day');
// Both variables now point to the same modified date!
echo $date->format('Y-m-d'); // 2024-01-16
echo $tomorrow->format('Y-m-d'); // 2024-01-16
// ✅ DateTimeImmutable creates new instances
$date = new DateTimeImmutable('2024-01-15');
$tomorrow = $date->modify('+1 day');
// Original remains unchanged
echo $date->format('Y-m-d'); // 2024-01-15
echo $tomorrow->format('Y-m-d'); // 2024-01-16
Best Practice: Use DateTimeImmutable
<?php
// ✅ RECOMMENDED: Use DateTimeImmutable everywhere
class Event
{
private DateTimeImmutable $scheduledAt;
public function __construct(DateTimeImmutable $scheduledAt)
{
$this->scheduledAt = $scheduledAt;
}
public function reschedule(string $modifier): self
{
// Returns new Event with modified date (original unchanged)
return new self($this->scheduledAt->modify($modifier));
}
public function getScheduledAt(): DateTimeImmutable
{
return $this->scheduledAt;
}
}
// Usage
$event = new Event(new DateTimeImmutable('2024-01-15 19:00:00'));
$rescheduled = $event->reschedule('+1 week');
echo $event->getScheduledAt()->format('Y-m-d'); // 2024-01-15
echo $rescheduled->getScheduledAt()->format('Y-m-d'); // 2024-01-22
Timezone Handling
DateTimeZone Class
<?php
// Create timezone object
$utc = new DateTimeZone('UTC');
$nyc = new DateTimeZone('America/New_York');
$tokyo = new DateTimeZone('Asia/Tokyo');
// Current time in different timezones
$utcTime = new DateTimeImmutable('now', $utc);
$nycTime = new DateTimeImmutable('now', $nyc);
$tokyoTime = new DateTimeImmutable('now', $tokyo);
echo "UTC: " . $utcTime->format('Y-m-d H:i:s T') . PHP_EOL;
echo "NYC: " . $nycTime->format('Y-m-d H:i:s T') . PHP_EOL;
echo "Tokyo: " . $tokyoTime->format('Y-m-d H:i:s T') . PHP_EOL;
// List all available timezones
$timezones = DateTimeZone::listIdentifiers();
echo "Total timezones: " . count($timezones) . PHP_EOL;
// Filter timezones by region
$americanZones = DateTimeZone::listIdentifiers(DateTimeZone::AMERICA);
$europeanZones = DateTimeZone::listIdentifiers(DateTimeZone::EUROPE);
$asianZones = DateTimeZone::listIdentifiers(DateTimeZone::ASIA);
// Get timezone info
$tz = new DateTimeZone('America/New_York');
$transitions = $tz->getTransitions();
$location = $tz->getLocation();
print_r([
'country_code' => $location['country_code'],
'latitude' => $location['latitude'],
'longitude' => $location['longitude'],
]);
Converting Between Timezones
<?php
// Create datetime in one timezone
$nycTime = new DateTimeImmutable(
'2024-01-15 14:00:00',
new DateTimeZone('America/New_York')
);
// Convert to different timezones
$utcTime = $nycTime->setTimezone(new DateTimeZone('UTC'));
$tokyoTime = $nycTime->setTimezone(new DateTimeZone('Asia/Tokyo'));
$londonTime = $nycTime->setTimezone(new DateTimeZone('Europe/London'));
echo "NYC: " . $nycTime->format('Y-m-d H:i:s P') . PHP_EOL;
echo "UTC: " . $utcTime->format('Y-m-d H:i:s P') . PHP_EOL;
echo "Tokyo: " . $tokyoTime->format('Y-m-d H:i:s P') . PHP_EOL;
echo "London: " . $londonTime->format('Y-m-d H:i:s P') . PHP_EOL;
// Output:
// NYC: 2024-01-15 14:00:00 -05:00
// UTC: 2024-01-15 19:00:00 +00:00
// Tokyo: 2024-01-16 04:00:00 +09:00
// London: 2024-01-15 19:00:00 +00:00
Default Timezone Configuration
<?php
// Set default timezone (php.ini: date.timezone)
date_default_timezone_set('UTC');
// Get current default
$defaultTz = date_default_timezone_get();
echo "Default timezone: $defaultTz" . PHP_EOL;
// Best practice: Always set to UTC
date_default_timezone_set('UTC');
// Then convert to user's timezone when displaying
function displayInUserTimezone(DateTimeImmutable $dt, string $userTz): string
{
$userTime = $dt->setTimezone(new DateTimeZone($userTz));
return $userTime->format('Y-m-d H:i:s T');
}
$utcNow = new DateTimeImmutable('now', new DateTimeZone('UTC'));
echo displayInUserTimezone($utcNow, 'America/New_York');
DateTime Arithmetic
Modify Method
<?php
$date = new DateTimeImmutable('2024-01-15 19:00:00');
// Add/subtract time
$tomorrow = $date->modify('+1 day');
$nextWeek = $date->modify('+1 week');
$nextMonth = $date->modify('+1 month');
$in2Hours = $date->modify('+2 hours');
// Complex modifications
$complex = $date->modify('+1 month +2 weeks +3 days');
// Relative formats
$dates = [
$date->modify('next Monday'),
$date->modify('last Friday'),
$date->modify('first day of next month'),
$date->modify('last day of this month'),
$date->modify('first Monday of next month'),
];
foreach ($dates as $d) {
echo $d->format('Y-m-d l') . PHP_EOL;
}
// Subtract time
$yesterday = $date->modify('-1 day');
$lastWeek = $date->modify('-1 week');
DateInterval Class
<?php
// Create intervals
$oneDay = new DateInterval('P1D'); // Period: 1 Day
$oneWeek = new DateInterval('P1W'); // Period: 1 Week
$oneMonth = new DateInterval('P1M'); // Period: 1 Month
$oneYear = new DateInterval('P1Y'); // Period: 1 Year
$twoHours = new DateInterval('PT2H'); // Period: Time 2 Hours
$complex = new DateInterval('P1Y2M3DT4H5M6S'); // 1 year, 2 months, 3 days, 4 hours, 5 minutes, 6 seconds
/*
DateInterval Format:
P = Period designator
T = Time designator (separates date from time)
Date parts:
Y = Years
M = Months (before T)
D = Days
W = Weeks
Time parts (after T):
H = Hours
M = Minutes (after T)
S = Seconds
*/
// Add interval
$date = new DateTimeImmutable('2024-01-15');
$future = $date->add($oneMonth);
echo $future->format('Y-m-d'); // 2024-02-15
// Subtract interval
$past = $date->sub($oneWeek);
echo $past->format('Y-m-d'); // 2024-01-08
// Create interval from string
$interval = DateInterval::createFromDateString('3 days 2 hours');
$result = $date->add($interval);
Calculating Differences
<?php
$start = new DateTimeImmutable('2024-01-15 19:00:00');
$end = new DateTimeImmutable('2024-01-20 21:30:00');
// Get difference
$diff = $start->diff($end);
// Access components
echo "Years: " . $diff->y . PHP_EOL; // 0
echo "Months: " . $diff->m . PHP_EOL; // 0
echo "Days: " . $diff->d . PHP_EOL; // 5
echo "Hours: " . $diff->h . PHP_EOL; // 2
echo "Minutes: " . $diff->i . PHP_EOL; // 30
echo "Seconds: " . $diff->s . PHP_EOL; // 0
// Total days (includes fractional part)
echo "Total days: " . $diff->days . PHP_EOL; // 5
// Formatted output
echo $diff->format('%d days, %h hours, %i minutes') . PHP_EOL;
// Output: 5 days, 2 hours, 30 minutes
// Check if negative (past)
if ($diff->invert) {
echo "Date is in the past";
}
// Practical example: Age calculation
function calculateAge(DateTimeImmutable $birthdate): int
{
$now = new DateTimeImmutable();
$age = $birthdate->diff($now);
return $age->y;
}
$birthdate = new DateTimeImmutable('1990-05-15');
echo "Age: " . calculateAge($birthdate) . " years";
Carbon: The Better DateTime Library (Seriously, You'll Love It)
Let's be real—Carbon isn't just "better," it's like DateTime went to finishing school. The fluent API? Chef's kiss. The human-readable syntax? Exactly what we always wanted.
Installation
composer require nesbot/carbon
Basic Usage
<?php
use Carbon\Carbon;
// Current time (timezone-aware by default)
$now = Carbon::now();
$utcNow = Carbon::now('UTC');
$nycNow = Carbon::now('America/New_York');
echo $now . PHP_EOL; // 2024-01-15 19:00:00
// Create from various sources
$date = Carbon::create(2024, 1, 15, 19, 0, 0);
$fromTimestamp = Carbon::createFromTimestamp(1705341600);
$fromFormat = Carbon::createFromFormat('Y-m-d H:i:s', '2024-01-15 19:00:00');
$fromString = Carbon::parse('2024-01-15 19:00:00');
// Parsing flexibility
$dates = [
Carbon::parse('2024-01-15'),
Carbon::parse('January 15, 2024'),
Carbon::parse('15-Jan-2024'),
Carbon::parse('2024-01-15T19:00:00Z'),
];
// Today, tomorrow, yesterday
$today = Carbon::today(); // 2024-01-15 00:00:00
$tomorrow = Carbon::tomorrow(); // 2024-01-16 00:00:00
$yesterday = Carbon::yesterday(); // 2024-01-14 00:00:00
Fluent Modifiers
<?php
use Carbon\Carbon;
$date = Carbon::create(2024, 1, 15, 19, 0, 0);
// Add time
$tomorrow = $date->copy()->addDay();
$nextWeek = $date->copy()->addWeek();
$nextMonth = $date->copy()->addMonth();
$in2Hours = $date->copy()->addHours(2);
// Subtract time
$yesterday = $date->copy()->subDay();
$lastWeek = $date->copy()->subWeek();
// Chaining
$future = $date->copy()
->addDays(5)
->addHours(3)
->addMinutes(30);
// Set specific components
$modified = $date->copy()
->setYear(2025)
->setMonth(6)
->setDay(15)
->setTime(12, 0, 0);
// Start/end of period
$startOfDay = $date->copy()->startOfDay(); // 2024-01-15 00:00:00
$endOfDay = $date->copy()->endOfDay(); // 2024-01-15 23:59:59
$startOfMonth = $date->copy()->startOfMonth(); // 2024-01-01 00:00:00
$endOfMonth = $date->copy()->endOfMonth(); // 2024-01-31 23:59:59
$startOfYear = $date->copy()->startOfYear(); // 2024-01-01 00:00:00
echo "Start of month: " . $startOfMonth . PHP_EOL;
echo "End of month: " . $endOfMonth . PHP_EOL;
Human-Readable Differences
<?php
use Carbon\Carbon;
$now = Carbon::now();
// Past dates
$past = Carbon::now()->subDays(5);
echo $past->diffForHumans(); // "5 days ago"
$recent = Carbon::now()->subMinutes(30);
echo $recent->diffForHumans(); // "30 minutes ago"
// Future dates
$future = Carbon::now()->addDays(3);
echo $future->diffForHumans(); // "3 days from now"
// Relative to another date
$date1 = Carbon::parse('2024-01-15');
$date2 = Carbon::parse('2024-01-20');
echo $date1->diffForHumans($date2); // "5 days before"
// Absolute differences
$diff = $now->diffInDays($past); // 5
$diff = $now->diffInHours($recent); // 0 (less than 1 hour)
$diff = $now->diffInMinutes($recent); // 30
$diff = $now->diffInSeconds($recent); // 1800
Comparison Methods
<?php
use Carbon\Carbon;
$date = Carbon::parse('2024-01-15');
$now = Carbon::now();
// Comparison operators
$isBefore = $date->lt($now); // less than (<)
$isBeforeOrEqual = $date->lte($now); // less than or equal (<=)
$isAfter = $date->gt($now); // greater than (>)
$isAfterOrEqual = $date->gte($now); // greater than or equal (>=)
$isEqual = $date->eq($now); // equal (==)
$isNotEqual = $date->ne($now); // not equal (!=)
// Named methods
$isPast = $date->isPast();
$isFuture = $date->isFuture();
$isToday = $date->isToday();
$isYesterday = $date->isYesterday();
$isTomorrow = $date->isTomorrow();
// Day of week checks
$isMonday = $date->isMonday();
$isWeekend = $date->isWeekend();
$isWeekday = $date->isWeekday();
// Range checks
$inRange = $date->between($start, $end);
$inRangeExclusive = $date->between($start, $end, false);
// Practical example
if ($now->isWeekend() && $now->hour >= 9 && $now->hour < 17) {
echo "It's the weekend during business hours!";
}
Timezone Conversion with Carbon
<?php
use Carbon\Carbon;
// Create in one timezone
$nycTime = Carbon::create(2024, 1, 15, 14, 0, 0, 'America/New_York');
// Convert to different timezones
$utcTime = $nycTime->copy()->setTimezone('UTC');
$tokyoTime = $nycTime->copy()->setTimezone('Asia/Tokyo');
$londonTime = $nycTime->copy()->setTimezone('Europe/London');
echo "NYC: " . $nycTime->format('Y-m-d H:i:s T') . PHP_EOL;
echo "UTC: " . $utcTime->format('Y-m-d H:i:s T') . PHP_EOL;
echo "Tokyo: " . $tokyoTime->format('Y-m-d H:i:s T') . PHP_EOL;
echo "London: " . $londonTime->format('Y-m-d H:i:s T') . PHP_EOL;
// Shorthand methods
$utc = $nycTime->copy()->utc(); // Alias for setTimezone('UTC')
$local = $utcTime->copy()->local(); // Convert to default timezone
Handling DST Transitions
Ever had a recurring event that mysteriously jumps an hour? Welcome to DST. But don't worry—we've got patterns that'll handle these transitions gracefully.
DST-Aware Arithmetic
<?php
use Carbon\Carbon;
// Spring forward: March 10, 2024, 2:00 AM → 3:00 AM in New York
// Attempting to create 2:30 AM (doesn't exist)
$tz = new DateTimeZone('America/New_York');
try {
$nonExistent = new DateTimeImmutable('2024-03-10 02:30:00', $tz);
echo $nonExistent->format('Y-m-d H:i:s T') . PHP_EOL;
// Automatically adjusted to 03:30:00 EDT
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
// With Carbon
$dstTransition = Carbon::create(2024, 3, 10, 2, 30, 0, 'America/New_York');
echo $dstTransition->format('Y-m-d H:i:s T') . PHP_EOL;
// 2024-03-10 03:30:00 EDT (automatically adjusted)
// Fall back: November 3, 2024, 2:00 AM → 1:00 AM
// 1:30 AM occurs twice
$ambiguous = Carbon::create(2024, 11, 3, 1, 30, 0, 'America/New_York');
echo $ambiguous->format('Y-m-d H:i:s T') . PHP_EOL;
// 2024-11-03 01:30:00 EDT (first occurrence)
// Check if DST is active
$date = Carbon::create(2024, 7, 15, 12, 0, 0, 'America/New_York');
echo "DST active: " . ($date->dst ? 'Yes' : 'No') . PHP_EOL;
// DST active: Yes
Recurring Events and DST
<?php
use Carbon\Carbon;
function scheduleRecurringEvent(Carbon $start, int $occurrences = 5): array
{
$events = [];
$current = $start->copy();
for ($i = 0; $i < $occurrences; $i++) {
$events[] = [
'occurrence' => $i + 1,
'utc' => $current->copy()->utc()->toIso8601String(),
'local' => $current->toIso8601String(),
'offset' => $current->format('P'),
'dst' => $current->dst,
];
// Add 1 week (handles DST automatically)
$current->addWeek();
}
return $events;
}
// Example spanning DST transition
$start = Carbon::create(2024, 3, 3, 2, 0, 0, 'America/New_York');
$events = scheduleRecurringEvent($start, 3);
foreach ($events as $event) {
echo sprintf(
"Week %d: %s (DST: %s, Offset: %s)\n",
$event['occurrence'],
$event['local'],
$event['dst'] ? 'Yes' : 'No',
$event['offset']
);
}
// Output:
// Week 1: 2024-03-03T02:00:00-05:00 (DST: No, Offset: -05:00)
// Week 2: 2024-03-10T03:00:00-04:00 (DST: Yes, Offset: -04:00) ← Adjusted!
// Week 3: 2024-03-17T02:00:00-04:00 (DST: Yes, Offset: -04:00)
Database Integration
Storing Timestamps
<?php
use Carbon\Carbon;
// PDO with MySQL/PostgreSQL
class EventRepository
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
// Store as UTC timestamp
public function save(string $title, Carbon $scheduledAt): int
{
$stmt = $this->pdo->prepare("
INSERT INTO events (title, scheduled_at, timezone, created_at)
VALUES (:title, :scheduled_at, :timezone, :created_at)
");
$stmt->execute([
'title' => $title,
'scheduled_at' => $scheduledAt->copy()->utc()->toDateTimeString(),
'timezone' => $scheduledAt->timezone->getName(),
'created_at' => Carbon::now()->utc()->toDateTimeString(),
]);
return (int) $this->pdo->lastInsertId();
}
// Retrieve and convert to timezone
public function find(int $id, string $timezone = 'UTC'): ?array
{
$stmt = $this->pdo->prepare("
SELECT id, title, scheduled_at, timezone, created_at
FROM events
WHERE id = :id
");
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
// Parse UTC timestamp and convert to requested timezone
$scheduledAt = Carbon::parse($row['scheduled_at'], 'UTC')
->setTimezone($timezone);
$createdAt = Carbon::parse($row['created_at'], 'UTC')
->setTimezone($timezone);
return [
'id' => $row['id'],
'title' => $row['title'],
'scheduled_at' => $scheduledAt,
'original_timezone' => $row['timezone'],
'created_at' => $createdAt,
];
}
// Query by date range
public function findInRange(Carbon $start, Carbon $end): array
{
$stmt = $this->pdo->prepare("
SELECT id, title, scheduled_at, timezone
FROM events
WHERE scheduled_at BETWEEN :start AND :end
ORDER BY scheduled_at ASC
");
$stmt->execute([
'start' => $start->copy()->utc()->toDateTimeString(),
'end' => $end->copy()->utc()->toDateTimeString(),
]);
return array_map(function ($row) {
return [
'id' => $row['id'],
'title' => $row['title'],
'scheduled_at' => Carbon::parse($row['scheduled_at'], 'UTC'),
'original_timezone' => $row['timezone'],
];
}, $stmt->fetchAll(PDO::FETCH_ASSOC));
}
}
// Usage
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'password');
$repo = new EventRepository($pdo);
// Save event
$eventId = $repo->save(
'Team Meeting',
Carbon::create(2024, 1, 15, 14, 0, 0, 'America/New_York')
);
// Retrieve in user's timezone
$event = $repo->find($eventId, 'America/Los_Angeles');
echo $event['scheduled_at']->format('Y-m-d H:i:s T');
Laravel Eloquent Integration
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class Event extends Model
{
protected $fillable = ['title', 'scheduled_at', 'timezone'];
// Automatically cast to Carbon instances
protected $casts = [
'scheduled_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
// Accessor: Convert to user's timezone
public function getScheduledAtInTimezoneAttribute(): Carbon
{
return $this->scheduled_at
->copy()
->setTimezone($this->timezone ?? 'UTC');
}
// Mutator: Store as UTC
public function setScheduledAtAttribute($value): void
{
$this->attributes['scheduled_at'] = Carbon::parse($value)
->utc()
->toDateTimeString();
}
// Scope: Events in date range
public function scopeInRange($query, Carbon $start, Carbon $end)
{
return $query->whereBetween('scheduled_at', [
$start->copy()->utc(),
$end->copy()->utc(),
]);
}
// Scope: Upcoming events
public function scopeUpcoming($query)
{
return $query->where('scheduled_at', '>', Carbon::now()->utc());
}
}
// Usage
$event = Event::create([
'title' => 'Team Meeting',
'scheduled_at' => Carbon::create(2024, 1, 15, 14, 0, 0, 'America/New_York'),
'timezone' => 'America/New_York',
]);
// Retrieve with timezone conversion
echo $event->scheduled_at_in_timezone->format('Y-m-d H:i:s T');
// Query upcoming events
$upcoming = Event::upcoming()->get();
Testing Time-Dependent Code
Carbon's Test Helpers
<?php
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;
class SessionTest extends TestCase
{
public function testSessionExpiry(): void
{
// Freeze time to specific moment
Carbon::setTestNow(Carbon::create(2024, 1, 15, 19, 0, 0));
$session = new Session(expiresInSeconds: 3600);
$this->assertTrue($session->isValid());
// Travel forward 59 minutes (still valid)
Carbon::setTestNow(Carbon::create(2024, 1, 15, 19, 59, 0));
$this->assertTrue($session->isValid());
// Travel forward 61 minutes (expired)
Carbon::setTestNow(Carbon::create(2024, 1, 15, 20, 1, 0));
$this->assertFalse($session->isValid());
// Reset time
Carbon::setTestNow();
}
public function testDSTTransition(): void
{
// Before DST (EST)
Carbon::setTestNow(Carbon::create(2024, 3, 9, 12, 0, 0, 'America/New_York'));
$now = Carbon::now('America/New_York');
$this->assertEquals(-5, $now->offsetHours);
$this->assertFalse($now->dst);
// After DST (EDT)
Carbon::setTestNow(Carbon::create(2024, 3, 11, 12, 0, 0, 'America/New_York'));
$now = Carbon::now('America/New_York');
$this->assertEquals(-4, $now->offsetHours);
$this->assertTrue($now->dst);
Carbon::setTestNow();
}
protected function tearDown(): void
{
// Always reset test time after each test
Carbon::setTestNow();
parent::tearDown();
}
}
Mocking DateTime for Testing
<?php
use PHPUnit\Framework\TestCase;
class DateTimeTest extends TestCase
{
public function testWithMockedTime(): void
{
$fixedTime = new DateTimeImmutable('2024-01-15 19:00:00', new DateTimeZone('UTC'));
// Inject clock into class under test
$service = new TimeService($fixedTime);
$result = $service->isBusinessHours();
$this->assertTrue($result);
}
}
// Service with injectable time
class TimeService
{
private DateTimeImmutable $now;
public function __construct(?DateTimeImmutable $now = null)
{
$this->now = $now ?? new DateTimeImmutable();
}
public function isBusinessHours(): bool
{
$hour = (int) $this->now->format('H');
return $hour >= 9 && $hour < 17;
}
public function getCurrentTime(): DateTimeImmutable
{
return $this->now;
}
}
Best Practices
1. Always Use UTC for Storage
<?php
use Carbon\Carbon;
// ✅ CORRECT: Store as UTC
class EventService
{
public function createEvent(string $title, string $datetime, string $timezone): void
{
$scheduledAt = Carbon::parse($datetime, $timezone)->utc();
// Store UTC timestamp
DB::table('events')->insert([
'title' => $title,
'scheduled_at' => $scheduledAt->toDateTimeString(),
'timezone' => $timezone,
]);
}
public function displayEvent(int $id, string $userTimezone): string
{
$event = DB::table('events')->find($id);
// Convert to user's timezone for display
$scheduledAt = Carbon::parse($event->scheduled_at, 'UTC')
->setTimezone($userTimezone);
return $scheduledAt->format('Y-m-d H:i:s T');
}
}
2. Use DateTimeImmutable or Carbon
<?php
use Carbon\Carbon;
// ❌ BAD: Mutable DateTime
class Event
{
private DateTime $scheduledAt;
public function reschedule(string $modifier): void
{
$this->scheduledAt->modify($modifier); // Mutates original!
}
}
// ✅ GOOD: Immutable
class Event
{
private DateTimeImmutable $scheduledAt;
public function reschedule(string $modifier): self
{
$newScheduledAt = $this->scheduledAt->modify($modifier);
return new self($newScheduledAt); // Returns new instance
}
}
// ✅ BEST: Carbon with copy()
class Event
{
private Carbon $scheduledAt;
public function reschedule(string $modifier): self
{
$newScheduledAt = $this->scheduledAt->copy()->modify($modifier);
return new self($newScheduledAt);
}
}
3. Validate DateTime Inputs
<?php
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
function parseDateTime(mixed $input, ?string $timezone = null): Carbon
{
try {
// Handle various input types
if ($input instanceof Carbon) {
return $timezone ? $input->copy()->setTimezone($timezone) : $input->copy();
}
if ($input instanceof DateTimeInterface) {
$carbon = Carbon::instance($input);
return $timezone ? $carbon->setTimezone($timezone) : $carbon;
}
if (is_numeric($input)) {
// Unix timestamp
if ($input < 0) {
throw new InvalidArgumentException('Timestamp cannot be negative');
}
// Check if milliseconds (larger than year 2286)
if ($input > 10000000000) {
$input = $input / 1000;
}
return Carbon::createFromTimestamp($input, $timezone ?? 'UTC');
}
if (is_string($input)) {
return Carbon::parse($input, $timezone ?? 'UTC');
}
throw new InvalidArgumentException('Invalid datetime input type: ' . gettype($input));
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Invalid datetime format: ' . $e->getMessage());
}
}
// Usage
$dates = [
parseDateTime('2024-01-15 19:00:00'),
parseDateTime(1705341600),
parseDateTime(new DateTime('2024-01-15')),
parseDateTime(Carbon::now()),
];
4. Handle Timezones Explicitly
<?php
use Carbon\Carbon;
// ❌ BAD: Implicit system timezone
$date = Carbon::now();
// ✅ GOOD: Explicit timezone
$date = Carbon::now('UTC');
$date = Carbon::now('America/New_York');
// ✅ BEST: Configuration-driven
class DateTimeFactory
{
private string $defaultTimezone;
public function __construct(string $defaultTimezone = 'UTC')
{
$this->defaultTimezone = $defaultTimezone;
}
public function now(?string $timezone = null): Carbon
{
return Carbon::now($timezone ?? $this->defaultTimezone);
}
public function parse(string $datetime, ?string $timezone = null): Carbon
{
return Carbon::parse($datetime, $timezone ?? $this->defaultTimezone);
}
}
Common Pitfalls
1. Mutating DateTime Objects
<?php
// ❌ WRONG
$date = new DateTime('2024-01-15');
$tomorrow = $date->modify('+1 day');
// Both variables now reference the modified date!
echo $date->format('Y-m-d'); // 2024-01-16
echo $tomorrow->format('Y-m-d'); // 2024-01-16
// ✅ CORRECT
$date = new DateTimeImmutable('2024-01-15');
$tomorrow = $date->modify('+1 day');
echo $date->format('Y-m-d'); // 2024-01-15
echo $tomorrow->format('Y-m-d'); // 2024-01-16
2. Ignoring Timezone Context
<?php
// ❌ WRONG: Ambiguous
$date = new DateTime('2024-01-15 14:00:00'); // Which timezone?
// ✅ CORRECT: Explicit timezone
$date = new DateTime('2024-01-15 14:00:00', new DateTimeZone('America/New_York'));
// ✅ BETTER: Use Carbon
use Carbon\Carbon;
$date = Carbon::create(2024, 1, 15, 14, 0, 0, 'America/New_York');
3. Forgetting to Copy Carbon Objects
<?php
use Carbon\Carbon;
// ❌ WRONG
$date = Carbon::now();
$tomorrow = $date->addDay(); // Modifies $date!
echo $date->toDateString(); // 2024-01-16
echo $tomorrow->toDateString(); // 2024-01-16
// ✅ CORRECT
$date = Carbon::now();
$tomorrow = $date->copy()->addDay(); // Creates new instance
echo $date->toDateString(); // 2024-01-15
echo $tomorrow->toDateString(); // 2024-01-16
Performance Tips
Caching Timezone Objects
<?php
class TimezoneCache
{
private static array $cache = [];
public static function get(string $timezone): DateTimeZone
{
if (!isset(self::$cache[$timezone])) {
self::$cache[$timezone] = new DateTimeZone($timezone);
}
return self::$cache[$timezone];
}
}
// Usage
$tz = TimezoneCache::get('America/New_York');
$date = new DateTimeImmutable('now', $tz);
Batch Conversions
<?php
use Carbon\Carbon;
function convertTimestampsBatch(array $timestamps, string $targetTimezone): array
{
// Create timezone object once
$tz = new DateTimeZone($targetTimezone);
return array_map(
fn($ts) => Carbon::createFromTimestamp($ts, $tz),
$timestamps
);
}
$timestamps = [1705341600, 1705428000, 1705514400];
$converted = convertTimestampsBatch($timestamps, 'America/New_York');
Wrapping Up: Your PHP DateTime Journey
Look, I'll be honest with you. When I first started working with PHP datetimes, I made every mistake in this guide. Mutating DateTimes? Check. Ignoring timezones? Yep. Forgetting about DST? Oh, absolutely.
But here's what I've learned over the years:
Your DateTime Toolkit:
DateTimeImmutable
should be your default (seriously, just use it)DateTimeZone
keeps you sane across continentsDateInterval
makes math actually make sense- Carbon makes everything beautiful
The Rules That Actually Matter:
- UTC for storage. Always. No exceptions.
DateTimeImmutable
or Carbon'scopy()
saves you from debugging nightmares- Validate inputs like your production uptime depends on it (because it does)
- Be explicit with timezones—your future self will thank you
- Test with frozen time. Trust me on this.
- DST will surprise you. Be ready.
- Cache those timezone objects. Your servers will thank you.
Master these patterns, and you'll build applications that handle time correctly, no matter where your users are or what edge cases they throw at you. And isn't that the whole point?
Further Reading
- Complete Guide to Unix Timestamps - Foundation of time representation
- ISO 8601 Standard Explained - Date format standard
- Timezone Conversion Best Practices - Cross-language timezone patterns
- Testing Time-Dependent Code - PHP testing strategies
- Database Timestamp Storage - Store PHP timestamps in databases
Building PHP applications with datetime? Contact us for consultation.