Navigation
 Startseite
 Fachbücher
 Anzeigenmarkt
 Forum
 Webmaster News
 Script Newsletter
 Push Meldungen
 Kontakt
 Script Installation
 Php
 Php Tutorials
 Lernpfade
 Webhoster Vergleich
 Impressum

Community-Bereich
 kostenlos Registrieren
 Anmelden
 Benutzerliste

Script Datenbank
 Script Archiv
 Script Top 20
 Screenshots
 Testberichte

Suche
 

Unsere Php Scripts
 Counter Script
 Umfrage Script
 Bilder Upload Script
 Terminverwaltung
 Simple PHP Forum
 RSS Grabber

Tools und Generatoren
 .htpasswd Generator
 md5 Generator
 base64 Generator
 Markdown to HTML
 Colorpicker
 Unix timestamp Tool
 Unit Test Generator
 TLD Liste
 Webkatalog‑Verzeichnis

Webspace-Mieten.d...
Webhosting mit Plesk aus Erfurt: Vier...
https://www.Webspace-Mieten.de
Artfiles.de
Bietet Serviceorientierte...
https://www.Artfiles.de
 
 
 

PHP gmdate(): Datum und Uhrzeit in UTC und GMT ausgeben

Sie befinden sich: Home > Php Tutorial > PHP gmdate(): Datum und Uhr...

PHP gmdate(): Datum und Uhrzeit in UTC und GMT ausgeben
Veröffentlicht am:
14.11.2026
Hits / Besucher:
14
Sprache:
  Deutsch
Tutorial Art:
eigenes
Eingetragen von:
Merkliste:
 
Beschreibung

Ein Onlineshop schickt für seine Produktbilder einen Expires-Header, und am Ende steht brav „GMT“. Trotzdem halten Browser die Bilder eine Stunde länger für frisch als geplant, im Sommer sogar zwei. Der Fehler steckt in einem einzigen Funktionsnamen: Die Zeile benutzt date(), wo PHP gmdate() hingehört. Wie man mit dem richtigen Datum einen Last-Modified-Header samt 304-Antwort baut, zeigt das Tutorial zu getlastmod(), und einen UTC-Zeitstempel aus Tag, Monat und Uhrzeit erzeugt gmmktime() aus dem Tutorial zu mktime(). Hier geht es um PHP gmdate() selbst: was es anders macht als date(), welche Formate es für Header, Schnittstellen und Datenbanken liefert und wo es still falsche Werte erzeugt.

Illustration zum Tutorial: PHP gmdate(): Datum und Uhrzeit in UTC und GMT ausgeben

Jede Ausgabe in diesem Text ist gemessen, unter Windows und Linux mit PHP 8.4 und für Versionsfragen zusätzlich mit PHP 8.1 bis 8.5. Die HTTP-Beispiele liefen gegen einen eigenen Testserver.

PHP date() gegen PHP gmdate(): eine Sekunde, zwei Uhrzeiten

Ein Unix-Zeitstempel zählt die Sekunden seit dem 1. Januar 1970, 00:00 Uhr UTC. Eine Zeitzone hat er nicht. Erst beim Formatieren entscheidet PHP, als welche Uhrzeit die Sekunde erscheint. date() nimmt die eingestellte Zeitzone samt Sommerzeit, PHP gmdate() nimmt immer UTC. Das Handbuch fasst es in einem Satz: identisch mit date(), nur dass die Zeit als Greenwich Mean Time zurückkommt.

<?php

date_default_timezone_set('Europe/Berlin');
$ts = 1784109600; /* 15.07.2026, 10:00 UTC */

echo date('d.m.Y H:i T', $ts), PHP_EOL;
/* 15.07.2026 12:00 CEST */
echo gmdate('d.m.Y H:i T', $ts);
/* 15.07.2026 10:00 GMT */

Dieselbe Sekunde unter fünf Einstellungen von date.timezone, ausgegeben mit dem Format H:i:s T P:

date.timezone date() PHP gmdate()
UTC 10:00:00 UTC +00:00 10:00:00 GMT +00:00
Europe/Berlin 12:00:00 CEST +02:00 10:00:00 GMT +00:00
America/New_York 06:00:00 EDT -04:00 10:00:00 GMT +00:00
Asia/Kolkata 15:30:00 IST +05:30 10:00:00 GMT +00:00
Europe/Helsinki 13:00:00 EEST +03:00 10:00:00 GMT +00:00

Die rechte Spalte bewegt sich nie. Und selbst in der ersten Zeile, mit der Zeitzone UTC, sind beide Ausgaben nicht gleich. Das Formatzeichen T liefert bei date() den Text UTC, bei gmdate() dagegen GMT. Gemeint ist für PHP dieselbe Zeit, nur das Etikett unterscheidet sich.

Das Beispiel aus dem PHP-Handbuch zeigt den Effekt am Jahreswechsel. mktime() baut Mitternacht am 1. Januar 1998 in Ortszeit, PHP gmdate() zeigt dieselbe Sekunde in UTC:

<?php

date_default_timezone_set('Europe/Berlin');
$neujahr = mktime(0, 0, 0, 1, 1, 1998);
echo date('M d Y H:i:s', $neujahr), PHP_EOL;
/* Jan 01 1998 00:00:00 */
echo gmdate('M d Y H:i:s', $neujahr);
/* Dec 31 1997 23:00:00 */

In Berlin liegt Mitternacht eine Stunde vor Mitternacht UTC, die Sekunde gehört in UTC also noch zum Jahr 1997. Mit der Zeitzone Europe/Helsinki erscheint genau die Ausgabe aus dem Handbuch, Dec 31 1997 22:00:00. Die Formatzeichen sind dieselben wie bei date(). Einige liefern unter gmdate() aber immer denselben Wert, gemessen am 26.10.2026 um 10:00 Uhr UTC:

Zeichen PHP gmdate() date() in Berlin Bedeutung
e UTC Europe/Berlin Name der Zeitzone
T GMT CET Kürzel der Zeitzone
P +00:00 +01:00 Abstand zu UTC
p Z +01:00 wie P, bei null Abstand aber Z
O +0000 +0100 Abstand ohne Doppelpunkt
Z 0 3600 Abstand in Sekunden
I 0 0 1 bei Sommerzeit, unter UTC nie
v 000 000 Millisekunden, immer null
F October October Monatsname, immer englisch

Welche der beiden Funktionen passt, entscheidet nicht der Geschmack, sondern der Empfänger des Werts:

flowchart TD
    A[Zeitstempel ausgeben] --> B{Wer liest ihn?}
    B -->|Mensch vor Ort| C[Ortszeit mit Zeitzone]
    B -->|Header, Log, API, DB| D[gmdate in UTC]
    B -->|Dauer in Sekunden| E{Unter 24 Stunden?}
    C --> F[date oder DateTimeImmutable]
    D --> G[gleich auf jedem Server]
    E -->|ja| H[gmdate mit H:i:s]
    E -->|nein| I[selbst rechnen]

Die Signatur im Original: Format als String, Timestamp als int

PHP gmdate() erwartet einen Format-String und optional einen Zeitstempel als Integer:

gmdate(string $format, ?int $timestamp = null): string


/* format - the format of the outputted date
string, same characters as in the
manual of the date() function
timestamp - optional int unix timestamp, the
number of seconds since the unix
epoch (01-01-1970 00:00:00 GMT);
null means the current time
return - returns a formatted date string.
Identical to date(), except that the time
returned is greenwich mean time (GMT, for
php the same as UTC) and not the current
local time of the default timezone.
Daylight savings never apply, I prints 0.
Names of the day and the month are always
english, not according to locale settings. */

Fehlt der Zeitstempel oder steht dort null, nimmt PHP gmdate() die aktuelle Zeit, also den Wert von time(). Seit PHP 8.0 ist null ausdrücklich erlaubt. Eine Kommazahl wie 1.5 wird abgeschnitten und meldet Deprecated: Implicit conversion from float 1.5 to int loses precision, ein Text wie 'abc' wirft TypeError: gmdate(): Argument #2 ($timestamp) must be of type ?int, string given. Einen Fehlerwert kennt die Funktion nicht, sie gibt immer einen String zurück.

Das Jahr 2038 ist auf einem 64-Bit-System kein Thema. Die alte Grenze von 32 Bit liegt bei 2147483647 Sekunden, und PHP rechnet einfach weiter:

<?php

echo gmdate('d-m-Y H:i:s', 2147483647), PHP_EOL;
/* 19-01-2038 03:14:07 */
echo gmdate('d-m-Y H:i:s', 2147483648), PHP_EOL;
/* 19-01-2038 03:14:08 */
echo gmdate('d-m-Y', -2147483649);
/* 13-12-1901 */

Negative Zeitstempel stehen für die Zeit vor 1970 und funktionieren ebenso. Eine Funktion gmtime() wie in C gibt es in PHP nicht, function_exists('gmtime') liefert false. Ihre Aufgabe übernimmt PHP gmdate(). Das ältere gmstrftime() läuft zwar noch, meldet unter PHP 8.4 aber Function gmstrftime() is deprecated since 8.1, use IntlDateFormatter::format() instead.

Lokal richtig, auf dem Server falsch: date.timezone und Docker

Eine Vereinsverwaltung zieht in einen Docker-Container. Die Administratorin setzt dort die Umgebungsvariable TZ=Europe/Berlin, damit die Uhrzeiten stimmen. Danach zeigt die Anwendung im Sommer jede Uhrzeit zwei Stunden zu früh an. Gemessen in einem frischen PHP-Container ohne php.ini:

<?php

var_dump(getenv('TZ'));
/* string(13) "Europe/Berlin" */
echo date_default_timezone_get();
/* UTC */

PHP liest die Variable TZ nicht. Es nimmt zuerst, was date_default_timezone_set() festlegt, danach date.timezone aus der php.ini und sonst UTC. Gewarnt wird dabei nicht, das galt in allen gemessenen Versionen von PHP 8.1 bis 8.5. Auffällig wird nur ein Tippfehler in der Einstellung, ab PHP 8.2 schon beim Start: Warning: PHP Startup: Invalid date.timezone value 'Mars/Phobos', using 'UTC' instead. PHP 8.1 meldet ihn erst beim ersten Datumsaufruf, mit dem Zusatz we selected the timezone 'UTC' for now. Auf dem Entwicklungsrechner fällt das Problem oft gar nicht auf. Die getestete Windows-Installation stand auf date.timezone=UTC, dort liefern date() und gmdate() bis auf das Formatzeichen T dieselbe Uhrzeit.

Die Zeitzone gehört deshalb in die php.ini des Containers oder an den Anfang der Anwendung:

date.timezone = Europe/Berlin
<?php

date_default_timezone_set('Europe/Berlin');

$zone = date_default_timezone_get();
if ($zone !== 'Europe/Berlin') {
error_log('Zeitzone ist ' . $zone);
}
echo $zone;
/* Europe/Berlin */

Am besten läuft diese Prüfung in einem Health-Check, damit der nächste Umzug auffällt, bevor Kunden falsche Termine sehen. Jede Stelle, die PHP gmdate() benutzt, hat der Umzug dagegen nicht berührt: Diese Werte waren vorher richtig und sind es danach.

Expires eine Stunde daneben: HTTP-Datum und Cookies mit PHP gmdate()

Zurück zum Shop aus der Einleitung. Die Entwicklerin hat die Zeile für den Cache-Header aus einem Forum übernommen: date('D, d M Y H:i:s') . ' GMT'. Der Server läuft mit Europe/Berlin. Gegen einen lokalen Testserver gemessen, schickt die Zeile für 10:00 Uhr UTC am 26.10.2026 den Wert Mon, 26 Oct 2026 11:00:00 GMT, im Juli wären es zwei Stunden mehr. Kein Fehler, keine Warnung. Liest man den Wert mit strtotime() zurück, liegt er genau 3600 Sekunden neben der gemeinten Zeit, und genau so lange halten Browser die Datei zu lange für gültig.

date() formatiert in Ortszeit, das angehängte „GMT“ behauptet etwas anderes. Richtig ist das HTTP-Datum nur mit PHP gmdate():

<?php

const HTTP_DATUM = 'D, d M Y H:i:s \G\M\T';
$ts = 1793008800; /* 26.10.2026, 10:00 UTC */

header('Expires: ' . gmdate(HTTP_DATUM, $ts));
/* Expires: Mon, 26 Oct 2026 10:00:00 GMT */

date_default_timezone_set('Europe/Berlin');
$falsch = date('D, d M Y H:i:s', $ts) . ' GMT';
echo $falsch;
/* Mon, 26 Oct 2026 11:00:00 GMT */

Vor header() darf keine Ausgabe stehen. Im getesteten Linux-Container mit output_buffering=0 kam nach einem einzelnen echo die Warnung Cannot modify header information - headers already sent, und der Header fehlte in der Antwort. Unter Windows lief derselbe Code am Testserver ohne Warnung durch, weil die php.ini dort einen Ausgabepuffer von 4096 Bytes setzt. Auch so entsteht der Satz „lokal läuft es“.

Naheliegend wäre statt der eigenen Konstante die eingebaute DATE_RFC7231 mit genau diesem Muster. Mit PHP gmdate() liefert sie auch das richtige Ergebnis. PHP 8.5 meldet aber bei jeder Verwendung Deprecated: Constant DATE_RFC7231 is deprecated since 8.5, as this format ignores the associated timezone and always uses GMT, ebenso für DateTimeInterface::RFC7231. Die Begründung beschreibt genau den Fehler aus dem Shop: Das Muster schreibt immer „GMT“, auch wenn die Uhrzeit davor Ortszeit ist. Mit display_errors=1 und ohne Ausgabepuffer ist die Meldung selbst schon Ausgabe: Am Testserver unter PHP 8.5 folgte headers already sent, und der Expires-Header fehlte ganz, unter 8.4 kam derselbe Header an. Die eigene Konstante HTTP_DATUM lief unter PHP 8.4 und 8.5 ohne Meldung. Wer seine Tests schon vor dem Update mit PHP 8.5 laufen lässt, sieht die Abkündigung rechtzeitig.

Für Cookies muss man meist gar nichts formatieren. setcookie() nimmt den Zeitstempel und schreibt das Ablaufdatum selbst, am Testserver gemessen als expires=Mon, 26 Oct 2026 10:00:00 GMT, also ebenfalls in GMT. PHP gmdate() braucht nur, wer den Set-Cookie-Header von Hand zusammensetzt. Vorsicht bei der Konstante DATE_COOKIE: Sie erzeugt ein anderes Muster, mit gmdate() gemessen Monday, 26-Oct-2026 10:00:00 GMT.

Vorbeugen lässt sich mit einer einfachen Regel: date() nie mit einem festen „GMT“, „UTC“ oder „Z“ kombinieren. Eine Suche im Projekt nach Zeilen, in denen date( und GMT zusammen stehen, findet solche Stellen in Sekunden. Sendet der Server zusätzlich Cache-Control: max-age, werten Caches laut HTTP-Standard RFC 9111 nur diesen Wert aus und ignorieren Expires. Der falsche Wert trifft dann nur Caches, die allein Expires kennen.

Die API lehnt +00:00 ab: ISO 8601 mit Z

Eine Spedition meldet Sendungen an einen Partner, dessen Schnittstelle Zeitpunkte als ISO 8601 mit Z am Ende verlangt. Der Entwickler nimmt gmdate('c') und bekommt 2026-10-26T10:00:00+00:00. Fachlich ist das dieselbe Zeit, die Schnittstelle lehnt den Wert trotzdem ab. Dazu fehlen Millisekunden: gmdate('v') liefert gemessen immer 000, gmdate('u') immer 000000.

Die Formatzeichen c und P schreiben den Abstand zu UTC aus. Sekundenbruchteile kann PHP gmdate() nicht kennen, weil ein Zeitstempel eine ganze Zahl ist. Für das Z gibt es zwei Wege, das Literal \Z und das Formatzeichen p, das seit PHP 8.0 bei null Abstand Z schreibt:

<?php

$ts = 1793008800;
echo gmdate('Y-m-d\TH:i:s\Z', $ts), PHP_EOL;
/* 2026-10-26T10:00:00Z */
echo gmdate('Y-m-d\TH:i:sp', $ts), PHP_EOL;
/* 2026-10-26T10:00:00Z */
echo gmdate(DATE_ATOM, $ts);
/* 2026-10-26T10:00:00+00:00 */

Für Millisekunden braucht es ein Objekt, das Sekundenbruchteile trägt. DateTimeImmutable in UTC mit dem Formatzeichen v liefert sie, und der Rückweg vom String mit Z zum Zeitstempel klappt ohne Verlust:

<?php

$utc = new DateTimeZone('UTC');
$jetzt = new DateTimeImmutable('now', $utc);
echo $jetzt->format('Y-m-d\TH:i:s.vp'), PHP_EOL;
/* z. B. 2026-10-26T10:00:00.123Z */

$d = new DateTimeImmutable('2026-10-26T10:00:00Z');
var_dump($d->getTimestamp() === 1793008800);
/* bool(true) */

Das Format sollte als Konstante an genau einer Stelle im Projekt stehen. Ein kleiner Test, der prüft, ob jeder ausgehende Zeitpunkt auf Z endet, verhindert, dass jemand später wieder c einsetzt.

Dauern: warum aus 25 Stunden 01:00:00 wird

Ein Laufverein zeigt auf seiner Seite die Zeiten eines 24-Stunden-Staffellaufs, ein Stadtwerk die Laufzeit des nächtlichen Imports. Beide haben die Dauer in Sekunden und formatieren sie mit einer Datumsfunktion. Mit date() stimmt es nie, mit PHP gmdate() nur eine Zeit lang:

<?php

date_default_timezone_set('Europe/Berlin');
echo gmdate('H:i:s', 3725), PHP_EOL; /* 01:02:05 */
echo gmdate('H:i:s', 90000), PHP_EOL; /* 01:00:00 */
echo date('H:i:s', 3725); /* 02:02:05 */

Beide Funktionen formatieren einen Zeitpunkt, keine Dauer. date() zeigt die Ortszeit am 1. Januar 1970, in Berlin also eine Stunde zu viel, in New York sogar 20:02:05. gmdate() zeigt die Uhrzeit des Tages in UTC und stimmt deshalb bis 86399 Sekunden. Ab 86400 Sekunden beginnt ein neuer Tag, und die Stunden zählen still von vorn. 90000 Sekunden, also 25 Stunden, erscheinen als 01:00:00. Eine negative Differenz von 60 Sekunden ergibt 23:59:00. Es gibt keine Warnung, der Wert sieht plausibel aus.

Eine Dauer rechnet man deshalb selbst aus. Die Funktion arbeitet mit intdiv() und beherrscht auch negative Werte:

<?php

function dauer(int $sek): string
{
$vz = $sek < 0 ? '-' : '';
$sek = abs($sek);
return $vz . sprintf(
'%02d:%02d:%02d',
intdiv($sek, 3600),
intdiv($sek % 3600, 60),
$sek % 60
);
}
echo dauer(3725), PHP_EOL; /* 01:02:05 */
echo dauer(90000), PHP_EOL; /* 25:00:00 */
echo dauer(-60); /* -00:01:00 */

PHP gmdate() bleibt für Dauern nur dort richtig, wo der Wert sicher unter einem Tag liegt. Die Tageslänge im Tutorial zu date_sunset() ist so ein Fall: Zwischen Aufgang und Untergang desselben Tages liegen immer weniger als 24 Stunden. Wer Tage mit ausweisen will, kann mit DateInterval und dem Platzhalter %a arbeiten.

Zeitumstellung: doppelte 02:30 im Log

Ein Dienst holt rund um die Uhr Zählerstände ab und schreibt jede Abfrage mit date('Y-m-d H:i:s') ins Log. In der Nacht zum 25.10.2026 stellt Deutschland die Uhr von Sommer- auf Winterzeit zurück. Am Morgen stehen zwei Blöcke mit 02:30 Uhr im Protokoll, und welche Zeile zuerst kam, lässt sich nicht mehr sagen:

<?php

date_default_timezone_set('Europe/Berlin');
foreach ([1792888200, 1792891800] as $t) {
echo date('H:i T', $t), ' | ';
echo gmdate('Y-m-d\TH:i:sp', $t), PHP_EOL;
}
/* 02:30 CEST | 2026-10-25T00:30:00Z
02:30 CET | 2026-10-25T01:30:00Z */

Zwei Sekunden, die eine Stunde auseinanderliegen, ergeben in Ortszeit dieselbe Uhrzeit. Ohne das Kürzel T im Format sind sie nicht zu unterscheiden. Wer den Text später mit strtotime('2026-10-25 02:30') zurückliest, bekommt gemessen die zweite Stunde, also 01:30 Uhr UTC. Im März fehlt die Stunde dafür ganz: strtotime('2026-03-29 02:30') ergibt 03:30 Uhr Sommerzeit. Der Ortstag 25.10.2026 hatte gemessen 25 Stunden.

Mit PHP gmdate() und dem Format aus dem Abschnitt zu ISO 8601 bleibt jede Zeile eindeutig, das zeigt die rechte Spalte der Ausgabe. Als Text sortiert stehen die UTC-Zeilen außerdem in der richtigen Reihenfolge, weil Jahr, Monat, Tag und Uhrzeit von groß nach klein folgen. Protokolle gehören deshalb in UTC, die Ortszeit kommt erst beim Anzeigen dazu.

gmdate() und MySQL: UTC_TIMESTAMP statt NOW()

Ein Backend schreibt Ereignisse teils per SQL mit NOW(), teils mit Werten aus PHP. Beim Abgleich passen die Zeiten nicht zusammen, mal um eine, mal um zwei Stunden. In der lokalen Docker-Umgebung lieferten vier Zeitquellen in derselben Sekunde zwei verschiedene Uhrzeiten:

SELECT UTC_TIMESTAMP(), NOW();

/* 2026-10-26 11:45:56 | 2026-10-26 12:45:56 */

PHP gmdate() ergab in derselben Sekunde 2026-10-26 11:45:56, date() im Container mit Berliner Zeit 12:45:56. NOW() folgt der Zeitzone der Datenbanksitzung, date() der von PHP. Auf einem anderen Server stimmen beide womöglich nicht mehr überein, und der Unterschied fällt erst beim Vergleich auf. Sicher ist die Kombination aus gmdate() auf der einen und UTC_TIMESTAMP() auf der anderen Seite:

<?php

$sql = 'INSERT INTO ereignis (zeit, text)'
. ' VALUES (?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([gmdate('Y-m-d H:i:s'), 'Import']);

$v = $pdo->query('SELECT TIMESTAMPDIFF(SECOND,'
. ' zeit, UTC_TIMESTAMP()) FROM ereignis');
echo $v->fetchColumn();
/* 0 */

Das Beispiel lief gegen eine temporäre Tabelle in der lokalen MySQL-Datenbank, die Differenz war 0 Sekunden. Der Wert wird als Parameter gebunden, nie in den SQL-Text geklebt. In einer Tabelle sollten nie Werte aus NOW() und Werte aus PHP nebeneinander stehen. Ein Spaltenkommentar wie „UTC“ erspart dem nächsten Entwickler das Raten.

Die Spalte zeit hatte dort den Typ DATETIME, und das ist kein Detail. Eine Spalte vom Typ TIMESTAMP rechnet beim Schreiben von der Zeitzone der Sitzung nach UTC um und beim Lesen zurück. Gemessen: Der UTC-Wert 2026-10-26 10:00:00, unter Berliner Sitzungszeit in beide Spaltentypen geschrieben, erschien nach SET time_zone = '+00:00' in der TIMESTAMP-Spalte als 09:00:00, in der DATETIME-Spalte weiter als 10:00:00. Vorsicht auch mit UNIX_TIMESTAMP(): Es deutet selbst einen DATETIME-Wert in der Sitzungszeitzone und lieferte unter Berliner Zeit für beide Spalten 1793005200 statt 1793008800. Für Werte aus PHP gmdate() passt deshalb diese Definition:

CREATE TABLE ereignis (

zeit DATETIME NOT NULL COMMENT 'UTC',
text VARCHAR(100) NOT NULL
);

Wenn gmdate() die falsche Wahl ist: Tagesgrenze und Monatsnamen

Wer gelernt hat, immer UTC zu verwenden, rechnet manchmal den falschen Tag aus. Ein Shop gruppiert den Tagesumsatz mit gmdate('Y-m-d'). Eine Bestellung um 23:30 Uhr UTC am 15.07. ist in Berlin schon 01:30 Uhr am 16.07.:

<?php

date_default_timezone_set('Europe/Berlin');
$bestellt = 1784158200; /* 15.07.2026 23:30 UTC */
echo gmdate('Y-m-d', $bestellt), PHP_EOL;
/* 2026-07-15 */
echo date('Y-m-d', $bestellt);
/* 2026-07-16 */

Kunde und Buchhaltung erwarten den 16.07. PHP gmdate() kennt nur den UTC-Tag, für Menschen zählt der Tag vor Ort. Die Lösung trennt beides sauber: Den Ortstag bildet DateTimeImmutable in Europe/Berlin, für die Abfrage der in UTC gespeicherten Daten werden seine Grenzen nach UTC umgerechnet. Am Tag der Zeitumstellung kommt dabei die 25. Stunde von selbst mit:

<?php

$zone = new DateTimeZone('Europe/Berlin');
$utc = new DateTimeZone('UTC');
$tag = new DateTimeImmutable('2026-10-25', $zone);
$von = $tag->setTimezone($utc);
$bis = $tag->modify('+1 day')->setTimezone($utc);
echo $von->format('Y-m-d H:i:s'), PHP_EOL;
/* 2026-10-24 22:00:00 */
echo $bis->format('Y-m-d H:i:s');
/* 2026-10-25 23:00:00 */

Die beiden Werte gehen als zeit >= ? AND zeit < ? in die Abfrage. Die zweite Falle betrifft Texte für Menschen. Eine Bestätigungsmail soll „Montag, 26. Oktober“ zeigen, PHP gmdate() schreibt Monday, 26. October 2026. Auch ein vorheriges setlocale() hilft nicht: Unter Windows meldete setlocale(LC_ALL, 'de-DE') die deutsche Locale als gesetzt, die Ausgabe blieb englisch. Die gewohnte Schreibweise de_DE lieferte dort sogar false. date() und gmdate() ignorieren die Locale vollständig. Deutsche Namen liefert IntlDateFormatter, der die Zeitzone als Parameter bekommt:

<?php

$fmt = new IntlDateFormatter(
'de_DE',
IntlDateFormatter::NONE,
IntlDateFormatter::NONE,
'UTC',
null,
"EEEE, d. MMMM y, HH:mm 'UTC'"
);
echo $fmt->format(1793008800);
/* Montag, 26. Oktober 2026, 10:00 UTC */

Das Beispiel braucht die Erweiterung intl. Im Linux-Container war sie vorhanden. In der getesteten Windows-Installation fehlte sie, dort endete der Aufruf mit Uncaught Error: Class "IntlDateFormatter" not found. Wie man Datumsangaben je Sprache formatiert, zeigt das Tutorial zu datefmt_create(). Die Faustregel aus dem Diagramm gilt also auch hier: PHP gmdate() für Maschinen, die Ortszeit und die Sprache des Lesers für Menschen.

GMT+0000 in der Mail: DateTimeImmutable neben PHP gmdate()

Viele Teams stellen auf Objekte um und erzeugen sie aus einem Zeitstempel mit dem Präfix @. Das Ergebnis überrascht beim ersten Blick in eine Mail:

<?php

$ts = 1793008800;
$roh = new DateTimeImmutable('@' . $ts);
echo $roh->format('e T'), PHP_EOL;
/* +00:00 GMT+0000 */

$utc = $roh->setTimezone(new DateTimeZone('UTC'));
echo $utc->format('e T'), PHP_EOL;
/* UTC UTC */

$a = $utc->format('Y-m-d H:i:s');
$b = gmdate('Y-m-d H:i:s', $ts);
var_dump($a === $b);
/* bool(true) */

Ein Objekt aus @ trägt nur den Abstand +00:00, keine benannte Zone, deshalb steht in Texten plötzlich GMT+0000. createFromFormat('U', ...) verhält sich gemessen genauso. Erst setTimezone() mit UTC macht daraus die Zone UTC, danach stimmt die Ausgabe mit PHP gmdate() überein. Eine kleine Fabrikfunktion für UTC-Objekte erspart die verstreuten @-Aufrufe.

Welches Werkzeug passt wann? PHP gmdate() ist kurz, braucht kein Objekt und reicht für Header, Logzeilen und Datenbankwerte. DateTimeImmutable lohnt sich, sobald gerechnet, in eine andere Zone gewechselt oder mit Millisekunden gearbeitet wird.

Praxis: ein Bericht-Endpunkt mit PHP gmdate()

Zum Schluss ein Endpunkt, der alle Fallen auf einmal hatte. bericht.php liefert den Stand des nächtlichen Imports als JSON. Die alte Fassung setzte Expires mit date() und angehängtem „GMT“, schrieb Logzeilen in Ortszeit und zeigte die Importdauer mit gmdate('H:i:s'). Die neue Fassung sammelt die Zeitlogik in einer kleinen Datei zeit.php, die zusammen mit bericht.php im öffentlichen Ordner public liegt:

<?php

const HTTP_DATUM = 'D, d M Y H:i:s \G\M\T';

function http_datum(int $ts): string
{
return gmdate(HTTP_DATUM, $ts);
}

function iso_utc(?int $ts = null): string
{
return gmdate('Y-m-d\TH:i:sp', $ts);
}

function ortszeit(int $ts, string $zone): string
{
$d = new DateTimeImmutable('@' . $ts);
$d = $d->setTimezone(new DateTimeZone($zone));
return $d->format('d.m.Y H:i T');
}
/* dazu dauer() aus dem Abschnitt zu Dauern */

Der Endpunkt nimmt als Bezugszeit $_SERVER['REQUEST_TIME'], damit Header, Log und JSON dieselbe Sekunde zeigen:

<?php

require __DIR__ . '/zeit.php';

$start = $_SERVER['REQUEST_TIME'];
header('Cache-Control: public, max-age=3600');
header('Expires: ' . http_datum($start + 3600));

file_put_contents(
dirname(__DIR__) . '/logs/bericht.log',
iso_utc($start) . ' bericht erzeugt' . PHP_EOL,
FILE_APPEND | LOCK_EX
);

header('Content-Type: application/json');
echo json_encode([
'erzeugt' => iso_utc($start),
'db_wert' => gmdate('Y-m-d H:i:s', $start),
'anzeige' => ortszeit($start, 'Europe/Berlin'),
'import_dauer' => dauer(93784),
]);
/* {"erzeugt":"2026-10-26T12:03:28Z",
"db_wert":"2026-10-26 12:03:28",
"anzeige":"26.10.2026 13:03 CET",
"import_dauer":"26:03:04"} */

Am Testserver kam eine Antwort mit Date: Mon, 26 Oct 2026 12:03:28 GMT und Expires: Mon, 26 Oct 2026 13:03:28 GMT, genau eine Stunde später und passend zu max-age=3600. Im Log stand 2026-10-26T12:03:28Z bericht erzeugt. Die JSON-Antwort ist im Kommentar zur besseren Lesbarkeit umbrochen, die Uhrzeiten hängen vom Abruf ab. Das Log liegt bewusst außerhalb des Web-Roots im Ordner logs neben public. In der ersten Fassung lag es neben bericht.php und war am Testserver als /bericht.log mit Status 200 abrufbar, jetzt antwortet dieselbe Adresse mit 404. Den Ordner logs muss es vorher geben, sonst meldet file_put_contents() Failed to open stream: No such file or directory. Die Importdauer von 93784 Sekunden erscheint als 26:03:04, die alte Fassung hätte 02:03:04 gezeigt. Unter Windows und unter PHP 8.5 lief der Endpunkt identisch und ohne Meldung. Nur das Feld anzeige spricht Ortszeit, alles andere ist UTC, und der Server darf dafür beliebig eingestellt sein.

Fazit

PHP gmdate() formatiert einen Zeitstempel in UTC, unabhängig von date.timezone, von der Sommerzeit und vom Server. Damit ist es das passende Werkzeug für alles, was Maschinen lesen: das HTTP-Datum mit eigener Formatkonstante statt der in PHP 8.5 abgekündigten DATE_RFC7231, ISO 8601 mit Z über das Formatzeichen p, eindeutige Logzeilen in der Nacht der Zeitumstellung und Datenbankwerte, die zu UTC_TIMESTAMP() passen.

Still falsch wird es an drei Stellen. date() mit angehängtem „GMT“ liegt eine oder zwei Stunden daneben. Dauern ab 24 Stunden zählen unter PHP gmdate() von vorn. Und für Menschen ist der UTC-Tag oft der falsche, dort gehören Ortszeit, Ortstag und deutsche Monatsnamen hin. Wer bei jedem Zeitwert zuerst fragt, wer ihn liest, trifft die Wahl zwischen beiden Funktionen fast immer richtig.

 

Tags:

 

 

Kommentare (0)

Noch keine Kommentare. Sei der Erste!

Melde dich an, um einen Kommentar zu schreiben.
Bücherregal mit drei Büchern: 'PHP 4 - Grundlagen und Profiwissen' von Hanser Verlag, 'Webdesign in a Nutshell' von O'Reilly Verlag, und 'Webgestaltung' von Galileo Computing.