PHPのDateTimeZoneで日付を変換するときの落とし穴

DateTimeZoneは、DateTimeやDateTimeImmutableに紐づけるタイムゾーン情報を表すクラスです。日付を「どの地域の時刻として扱うか」を指定するために使います。

タイムゾーンまわりは、動かしてみると動くので後回しにしがちですが、サーバーとDBとアプリでタイムゾーン設定がずれていると、本番でだけ時刻が9時間ずれる、みたいな事故につながります。今日はDateTimeZoneを使うときに私がハマったところを中心に書いておきます。

DateTimeZoneはどう指定する?

コンストラクタにタイムゾーン識別子の文字列を渡すだけです。

$tz = new DateTimeZone('Asia/Tokyo');
$dt = new DateTime('2026-09-22 10:00:00', $tz);

echo $dt->format('Y-m-d H:i:s P');
// 2026-09-22 10:00:00 +09:00

識別子は’Asia/Tokyo’や’UTC’のようなIANAタイムゾーンデータベースの名前を使うのが基本です。’JST’のような略称もPHPの内部データ上は解釈されることがありますが、略称は一つの時刻に複数の地域が対応していたり、サマータイムの扱いが曖昧だったりして信頼性が低いとされています。地域名で書く、というのを最初に決めておくと迷いません。

なぜsetTimezoneで時刻がずれて見えるのか

DateTimeオブジェクトのsetTimezoneは、内部で保持している「絶対時刻」を変えずに、表示するタイムゾーンだけを差し替えるメソッドです。ここを誤解していると、時刻が勝手にずれたように見えて混乱します。

$dt = new DateTime('2026-09-22 10:00:00', new DateTimeZone('Asia/Tokyo'));
$dt->setTimezone(new DateTimeZone('UTC'));

echo $dt->format('Y-m-d H:i:s P');
// 2026-09-22 01:00:00 +00:00

これはバグではなく仕様通りの挙動です。同じ瞬間を指したまま、表示形式だけ東京時間からUTCに変換されています。「日本時間の10時」と「UTCの10時」は違う瞬間なので、setTimezoneをかけると表示上の時刻が変わるのは当然というわけです。ここを「なぜか時刻がずれた」と勘違いしてバグ報告を上げてしまうケースを何度か見たことがあります。

date_default_timezone_setに頼りすぎると起きること

PHPにはdate_default_timezone_set()でスクリプト全体のデフォルトタイムゾーンを設定する方法もあります。タイムゾーンを指定せずにDateTimeを生成すると、このデフォルト値が使われます。

date_default_timezone_set('Asia/Tokyo');

$dt = new DateTime('2026-09-22 10:00:00');
// デフォルトのAsia/Tokyoが使われる

お手軽なぶん、ライブラリを跨いで動くコードやCLIバッチなど、実行環境のphp.ini設定に依存する場面で厄介です。ini側のdate.timezoneが未設定だとE_WARNINGが出ますし、チームの別のメンバーが別の環境で動かすと暗黙のデフォルトが変わって結果がずれる、ということも起こり得ます。個人的には、DateTimeを生成するたびに明示的にDateTimeZoneを渡すほうが、後から読む人にとって親切だと思っています。

DBに保存するときはUTC統一が無難

アプリ内部やDBへの保存はUTCで統一し、ユーザーへの表示のときだけローカルタイムゾーンに変換する、という設計が定番です。

$storedAt = new DateTimeImmutable('now', new DateTimeZone('UTC'));
// DBにはUTCのまま保存

$displayAt = $storedAt->setTimezone(new DateTimeZone('Asia/Tokyo'));
echo $displayAt->format('Y-m-d H:i');
// 表示用に東京時間へ変換

保存とロジックの基準を一つに揃えておくと、サーバーの地域が変わったり多言語対応でユーザーごとにタイムゾーンが違ったりしても、変換のタイミングが表示直前の一箇所に集約されて見通しがよくなります。逆に、保存時のタイムゾーンがバラバラだと、日付をまたぐ集計処理などで地味に厄介なバグを踏むことになりがちです。

DateInterval::diffで日数がずれて見える理由

タイムゾーンが違う2つのDateTimeをdiff()で比較すると、単純な引き算では出てこないズレが出ることがあります。

$a = new DateTime('2026-09-22 23:00:00', new DateTimeZone('Asia/Tokyo'));
$b = new DateTime('2026-09-22 23:00:00', new DateTimeZone('UTC'));

$diff = $a->diff($b);
echo $diff->h . '時間' . $diff->i . '分';
// 9時間0分

diff()は内部的に絶対時刻同士の差を計算するので、表示上は同じ「23:00:00」でもタイムゾーンが違えば9時間の差として扱われます。ローカル時刻の見た目だけで比較すると混乱するので、比較する2つのDateTimeが同じタイムゾーンを指しているかを先に確認する癖をつけておくと安全です。

まとめ

DateTimeZoneまわりは、コードとしては数行で済むぶん、設計判断のほうが重要になる領域だと思います。保存はUTCに統一し、表示直前だけローカルタイムゾーンに変換する、date_default_timezone_setに頼らず明示的にDateTimeZoneを渡す、setTimezoneは絶対時刻を変えずに表示だけを変えるものだと理解しておく。このあたりを押さえておけば、本番環境でだけ時刻がずれるという事故はかなり減らせるんじゃないかと思います。

よくある質問

Q. DateTimeZoneに’JST’のような略称は使えますか?
A. 値としては解釈されることがありますが、略称は複数の地域が同じ表記を共有していたりサマータイムの扱いが曖昧だったりして信頼性が低いとされています。’Asia/Tokyo’のようなIANAタイムゾーンデータベースの識別子を使うのが安全です。

Q. setTimezoneを呼ぶとDateTimeが指す瞬間そのものが変わりますか?
A. 変わりません。setTimezoneは内部で保持する絶対時刻はそのままに、表示に使うタイムゾーンだけを差し替えます。format()で出てくる時刻表記が変わるのはそのためです。

Q. DBに日時を保存するときのタイムゾーンはどう決めればいいですか?
A. UTCで統一して保存し、ユーザーに表示する直前だけローカルタイムゾーンへ変換する設計が無難です。保存側のタイムゾーンが揃っていれば、集計やdiff()での比較でも余計なズレに悩まされにくくなります。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です