Blog / Coding tips

PHP Timezone Europe/London (date_default_timezone_set)

To use UK local time in PHP, set the default timezone to Europe/London with date_default_timezone_set('Europe/London') (or date.timezone in php.ini), prefer DateTimeImmutable with an explicit DateTimeZone, store timestamps in UTC in the database, and convert to London only when you display them. Europe/London observes GMT in winter and BST (UTC+1) in summer, so a fixed +00:00 offset is wrong for half the year. Never assume the server’s default is London — on many hosts it is UTC.

I care about this because UK date formats (dd/mm/yyyy) and British Summer Time show up in almost every form and report I build. The snippets below were run on PHP 8.4; the sample conversion uses 5 Oct 2026 13:47 UTC, which is 14:47 BST in London and 19:47 in Asia/Dhaka.

Set the default timezone

<?php
declare(strict_types=1);

echo date_default_timezone_get(), PHP_EOL;

date_default_timezone_set('Europe/London');
echo date_default_timezone_get(), PHP_EOL;
echo date('Y-m-d H:i:s T'), PHP_EOL;

Put date_default_timezone_set('Europe/London'); in a front controller or bootstrap file so every script agrees. Alternatively set in php.ini:

date.timezone = Europe/London

If you omit both, PHP 8+ typically falls back in a way that may warn or use UTC depending on configuration — either way, be explicit. Calling date() without a known zone is how “it works on my laptop” bugs ship to production.

Europe/London is an IANA timezone name. It includes the historical rules for daylight saving. Prefer it over GB or hard-coded offsets.

GMT vs BST (tested)

<?php
declare(strict_types=1);

$winter = new DateTimeImmutable('2026-01-15 12:00:00', new DateTimeZone('Europe/London'));
$summer = new DateTimeImmutable('2026-07-15 12:00:00', new DateTimeZone('Europe/London'));

echo $winter->format('Y-m-d H:i:s T (P)'), PHP_EOL;
echo $summer->format('Y-m-d H:i:s T (P)'), PHP_EOL;

Output:

2026-01-15 12:00:00 GMT (+00:00)
2026-07-15 12:00:00 BST (+01:00)

Same civil clock time, different offsets. If you store "2026-07-15 12:00:00" without a zone and later interpret it as UTC, you are one hour wrong for users in Britain. That breaks reminders, “open at 9am” logic, and working-day counters (see also my JavaScript working days with UK bank holidays project notes for the calendar side).

Convert UTC ↔ London ↔ Dhaka

<?php
declare(strict_types=1);

$utc = new DateTimeImmutable('2026-10-05 13:47:00', new DateTimeZone('UTC'));
$london = $utc->setTimezone(new DateTimeZone('Europe/London'));
$dhaka  = $utc->setTimezone(new DateTimeZone('Asia/Dhaka'));

echo $utc->format('Y-m-d H:i:s T'), PHP_EOL;
echo $london->format('Y-m-d H:i:s T'), PHP_EOL;
echo $dhaka->format('Y-m-d H:i:s T'), PHP_EOL;
echo $london->format('d/m/Y H:i'), PHP_EOL;

Full CLI dump from the test script (including defaults on this machine):

Default timezone: UTC
date(): 2026-10-05 13:45:46 UTC
After set Europe/London: Europe/London
Winter: 2026-01-15 12:00:00 GMT (+00:00)
Summer: 2026-07-15 12:00:00 BST (+01:00)
UTC:    2026-10-05 13:47:00 UTC
London: 2026-10-05 14:47:00 BST
Dhaka:  2026-10-05 19:47:00 +06
UK display: 05/10/2026 14:47
Naive UTC parse of local-looking string: 2026-03-29T01:30:00+00:00
Explicit Europe/London: 2026-03-29T02:30:00+01:00 -> BST
Stored UTC: 2026-10-05 13:45:46
Shown London: 05 Oct 2026, 14:45 BST

setTimezone returns a new DateTimeImmutable with the same instant and a different wall clock. That is the safe mental model: one instant, many displays.

Store UTC, display London

Recommended pattern for apps (especially if you ever gain users outside the UK):

  1. When saving: convert the user’s input (interpreted in Europe/London, or their preference) to UTC, store Y-m-d H:i:s in UTC or a Unix timestamp.
  2. When showing: load UTC, convert to Europe/London, format for humans.
<?php
declare(strict_types=1);

// User typed a UK local datetime in a form
$local = DateTimeImmutable::createFromFormat(
    'd/m/Y H:i',
    '05/10/2026 14:47',
    new DateTimeZone('Europe/London')
);
if ($local === false) {
    throw new RuntimeException('Invalid date');
}

$forDb = $local->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s');
// INSERT with PDO binding $forDb

// Later, read $row['created_at'] assumed UTC
$shown = (new DateTimeImmutable($row['created_at'] ?? $forDb, new DateTimeZone('UTC')))
    ->setTimezone(new DateTimeZone('Europe/London'))
    ->format('d M Y, H:i T');

Use PDO prepared statements for the insert. Format UK-style with d/m/Y for forms and d M Y for prose. For currency on the same invoices, a separate concern is JavaScript currency formatting in the browser — keep server and client zones consistent.

DateTimeImmutable vs date() / strtotime

date() and strtotime() use the default timezone. They are fine for quick scripts once the default is set. For application code I prefer DateTimeImmutable because:

  • The timezone is visible in the constructor.
  • Methods return new objects (no accidental mutation).
  • createFromFormat with a zone parses UK form input reliably.
<?php
$naive = new DateTimeImmutable('2026-03-29 01:30:00'); // uses default zone
$explicit = new DateTimeImmutable('2026-03-29 01:30:00', new DateTimeZone('Europe/London'));

Around the spring-forward hour, civil times can be skipped or ambiguous. Explicit zones plus storing UTC remove most of the drama. If you need recurring “every Monday at 9:00 London time”, libraries like Laravel’s Carbon build on the same PHP timezone database — the rules above still apply.

Bootstrap checklist for a UK PHP app

  1. date.timezone = Europe/London in php.ini or date_default_timezone_set in bootstrap.
  2. Database columns: UTC (DATETIME/TIMESTAMP with a clear convention) or Unix integers.
  3. Forms: label “UK time”, parse with Europe/London.
  4. APIs: prefer ISO-8601 with offset (c format) so clients are not guessing.
  5. Logs: UTC is easier to merge across servers; convert when showing to humans.
  6. Sessions and “last active” times: same UTC discipline (session login).
  7. Emails: format the London time in the body string; the mail server’s clock is a separate topic from mail() contact forms.

Common mistakes

  1. Hard-coding +00:00 for Britain year-round.
  2. Storing “local” strings with no zone, then deploying to a UTC server.
  3. Mixing strtotime('now') with a later date_default_timezone_set.
  4. Parsing 01/02/2026 as February in US libraries and January in UK forms — be explicit with createFromFormat('d/m/Y', ...).
  5. Using time() for display without formatting through a timezone.
  6. Forgetting that BST changes the offset for calendar maths (“add 24 hours” ≠ “add one calendar day” near transitions).
  7. Comparing formatted strings instead of comparing timestamps or DateTimeImmutable objects.

FAQ

Should I set Europe/London or UTC as the default?

Either works if you are disciplined. Many teams default PHP to UTC and convert to Europe/London only in the presentation layer so servers and queues stay consistent. UK-only apps often default to Europe/London for convenience. Pick one convention and document it.

How do I list timezones in PHP?

DateTimeZone::listIdentifiers() returns the IANA list. For a UK-only product you may not need a picker; hard-code London and say so in the UI.

Does MySQL TIMESTAMP convert zones?

TIMESTAMP columns convert between the session time_zone and UTC storage. That can surprise you if the MySQL session zone differs from PHP’s. Many apps use DATETIME and treat the value as UTC in application code to keep one source of truth. Whichever you choose, test inserts and reads explicitly.

How do I show times for a user in Dhaka and a user in London?

Store UTC. Keep a timezone preference per user (Asia/Dhaka, Europe/London). On output, setTimezone(new DateTimeZone($userTz)). Do not store two copies of the same instant.

Why is my scheduled job an hour wrong in summer?

The cron expression or scheduler is probably using the server’s UTC clock while you mentally planned BST. Either schedule in UTC and convert the civil time yourself, or run the scheduler in Europe/London and document it. Re-check after the last Sunday in March and October.

Is date_default_timezone_set enough for the whole request?

It sets the default for date functions in that process. It does not change MySQL’s session time zone or JavaScript’s Date behaviour in the browser. Align each layer.

Formatting helpers you will reuse

<?php
declare(strict_types=1);

function uk_datetime(DateTimeInterface $utcInstant): string
{
    return (new DateTimeImmutable('now', new DateTimeZone('UTC')))
        ->setTimestamp($utcInstant->getTimestamp())
        ->setTimezone(new DateTimeZone('Europe/London'))
        ->format('d/m/Y H:i');
}

function parse_uk_form_datetime(string $value): DateTimeImmutable
{
    $dt = DateTimeImmutable::createFromFormat(
        '!d/m/Y H:i',
        $value,
        new DateTimeZone('Europe/London')
    );
    $errors = DateTimeImmutable::getLastErrors();
    if ($dt === false || ($errors['warning_count'] ?? 0) || ($errors['error_count'] ?? 0)) {
        throw new InvalidArgumentException('Use UK time as dd/mm/yyyy hh:mm');
    }
    return $dt;
}

The ! in the format resets unused fields so you do not inherit the current clock’s seconds by accident. Always check getLastErrors() — createFromFormat can return a DateTimeImmutable even when the input was partially nonsense.

Appointments that span the BST change

If a meeting is “every week at 10:00 London time”, add seven calendar days in the London zone, not +604800 seconds from a UTC instant, when you want to keep the civil time stable. Conversely, for “exactly 48 hours later”, add an interval in UTC. Decide which meaning the product needs; banks and universities often want civil time in Europe/London.

Interop with JavaScript

Browsers parse date strings inconsistently. Prefer sending ISO-8601 with offset from PHP (format('c')) after converting to London or UTC explicitly. For display-only UK formatting in JS, see format date dd/mm/yyyy. Do not ship a naive new Date('2026-10-05') and expect midnight in London — that form is treated as UTC in modern engines.

Servers in other countries

Your VPS may be in Frankfurt or New York while users are in Britain. That is fine. PHP’s timezone database does not care where the machine sits. Set the default (or convert explicitly) and keep the OS clock in NTP sync. Cron in UTC plus conversion is usually clearer than changing the system timezone on the server.

Comparing dates the right way

<?php
$a = new DateTimeImmutable('2026-10-05 14:47:00', new DateTimeZone('Europe/London'));
$b = new DateTimeImmutable('2026-10-05 13:47:00', new DateTimeZone('UTC'));
var_export($a == $b); // true — same instant
var_export($a->format('c') === $b->format('c')); // false — different strings

Compare objects or timestamps, not formatted strings. Sorting log lines alphabetically by d/m/Y also fails (month/day chaos); sort by UTC instants then format for display.

Business “today” in London

<?php
$todayLondon = new DateTimeImmutable('today', new DateTimeZone('Europe/London'));
$start = $todayLondon->setTime(0, 0);
$end = $todayLondon->setTime(23, 59, 59);
// convert $start/$end to UTC for querying a UTC column

“Today” depends on the zone. A request at 01:00 BST on Tuesday is still Monday evening in UTC — using UTC midnight for a UK newspaper site would be wrong.

Documentation one-liner for your README

Write: “All database timestamps are UTC. PHP default timezone is Europe/London for formatting. Forms accept UK civil time.” Future you will thank present you when DST confusion returns in March.

Worked example: event start in a database

User enters 20/06/2026 19:30 as the start of a meetup in London.

<?php
$startLocal = parse_uk_form_datetime('20/06/2026 19:30');
// 2026-06-20 19:30:00 BST
$startUtc = $startLocal->setTimezone(new DateTimeZone('UTC'));
// 2026-06-20 18:30:00 UTC
$pdo->prepare('INSERT INTO events (title, starts_at) VALUES (?, ?)')
    ->execute(['PHP meetup', $startUtc->format('Y-m-d H:i:s')]);

On the event page:

<?php
$utc = new DateTimeImmutable($row['starts_at'], new DateTimeZone('UTC'));
echo $utc->setTimezone(new DateTimeZone('Europe/London'))->format('l j F Y, g:ia T');
// Saturday 20 June 2026, 7:30pm BST

Visitors in Dhaka can convert the same UTC row to Asia/Dhaka without you storing a second column. That is why UTC storage wins once you leave a single-zone hobby script.

PHPUnit tip

Freeze time in tests with a fixed DateTimeImmutable injected into your services, or libraries that allow clock mocking. Do not call new DateTimeImmutable('now') deep inside domain code without a way to override it — DST regression tests become impossible.

Quick reference: format characters for UK output

| Format | Example (London) | Use | |--------|------------------|-----| | d/m/Y H:i | 05/10/2026 14:47 | Forms and compact UI | | d M Y, H:i T | 05 Oct 2026, 14:47 BST | Readable prose | | c | 2026-10-05T14:47:00+01:00 | APIs | | U | Unix timestamp | Storage alternative |

Combine with DateTimeZone('Europe/London') on the way out and UTC on the way in. If you only remember one sentence from this article, remember that.

DST transition dates

In the UK, clocks go forward on the last Sunday in March and back on the last Sunday in October. PHP’s timezone database knows the exact instants. Your job is not to hard-code those dates — your job is to use Europe/London so PHP applies them. Hard-coded “BST means +1” tables in application code rot.

Takeaway

Set or convert with Europe/London, store UTC when data might leave one country, format d/m/Y for British humans, and never hard-code a year-round +00:00 for the UK. The tested winter/summer pair at the top of this article is the proof that the offset moves — your code should move with it.

Why not just date_default_timezone_set everywhere?

Setting the default is convenient for date() and strtotime(). Explicit DateTimeZone arguments are safer when code is copied between projects with different defaults. Use both: a London default for the UK app, and explicit zones at system boundaries (database UTC, user preferences, API payloads).

Further reading

Be explicit about Europe/London, store UTC when data might travel, and format d/m/Y for British users. That removes most of the “works in January, breaks in July” class of bugs.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes