Timezone Handling
debt(d7/e7/b7/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints list phpstan, psalm, and carbonphp as tools — these are specialist static analysis tools that can catch some patterns (e.g. date() without explicit timezone), but many timezone bugs (storing local time, ambiguous abbreviations, DST transitions) only manifest at runtime when the server timezone changes or DST kicks in. They won't catch semantic errors like storing local time that happens to look valid.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix says 'store all timestamps in UTC' but the common_mistakes reveal that stored local timestamps must be migrated in the database, date_default_timezone_set() calls must be audited across web/cli/queue contexts, and DateTime must be replaced with DateTimeImmutable throughout. This touches database schemas, application logic, and all display layers — a cross-cutting fix spanning multiple files and components.
Closest to 'strong gravitational pull' (e7). The applies_to covers all three PHP contexts (web, cli, queue-worker) and the tags include both database and i18n. Every feature involving dates — billing, scheduling, notifications — must navigate the UTC storage/local display split. The choice shapes how every timestamp-related operation is written and reviewed, imposing a persistent tax across all work streams.
Closest to 'serious trap' (t7). The misconception field states explicitly: 'Using local server timezone for PHP and MySQL is fine — server timezone changes (DST, migration) silently change stored times.' This contradicts common developer intuition (storing what users see seems natural), and the failure is silent in production. It doesn't reach t9 because UTC-first is a well-known best practice that most experienced developers eventually learn, but the 'obvious' naive approach is reliably wrong.
Also Known As
TL;DR
Explanation
Always store and compute in UTC; convert for display. PHP's DateTimeImmutable with DateTimeZone handles conversions. Database: use DATETIME with UTC application-level enforcement (MySQL), or TIMESTAMPTZ (PostgreSQL, which stores UTC and converts automatically). DST transitions create ambiguous times (1:30am exists twice) and gaps (2:30am may not exist). The IANA timezone database (America/New_York, not EST) handles DST rules correctly; three-letter abbreviations (EST, CET) are ambiguous.
Diagram
flowchart TD
USER_INPUT[User enters: 2026-03-15 14:00] --> AMBIG{Which timezone?}
AMBIG -->|assumed UTC| UTC_STORE[Store as UTC in DB<br/>2026-03-15T14:00:00Z]
AMBIG -->|assumed local| WRONG[Wrong time stored!<br/>timezone lost forever]
UTC_STORE --> DISPLAY[Display to user]
DISPLAY --> CONVERT[Convert UTC to user timezone<br/>Europe/London = 14:00 BST]
subgraph PHP_Best_Practice
DI2[DateTimeImmutable with timezone]
UTC2[Always store UTC]
PREF[User timezone preference<br/>in profile settings]
DI2 --> UTC2 --> PREF
end
style WRONG fill:#f85149,color:#fff
style UTC_STORE fill:#238636,color:#fff
style UTC2 fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Storing local timestamps in the database — server TZ changes silently shift all stored times.
- Using date_default_timezone_set() to a non-UTC timezone for storage — always store UTC; convert only for display.
- Three-letter timezone abbreviations (EST, IST) — ambiguous; IST is India Standard, Israel Standard, and Irish Standard Time.
- Not using DateTimeImmutable — DateTime::modify() mutates the object; DateTimeImmutable always returns a new instance.
Code Examples
// Storing local time — breaks when server TZ changes:
date_default_timezone_set('America/New_York');
$created = date('Y-m-d H:i:s'); // Local time stored in DB
// Server moved to UTC: all existing times are now wrong
// Mutable DateTime — shared reference bug:
$start = new DateTime('2026-01-01');
$end = $start->modify('+30 days'); // $start is ALSO modified!
// UTC storage, local display:
date_default_timezone_set('UTC');
$created = (new DateTimeImmutable())->format('Y-m-d H:i:s'); // Stored as UTC
// Display in user's timezone:
$utc = new DateTimeImmutable('2026-01-01 12:00:00', new DateTimeZone('UTC'));
$local = $utc->setTimezone(new DateTimeZone('America/New_York'));
echo $local->format('Y-m-d H:i:s T'); // '2026-01-01 07:00:00 EST'