PHP の DateTime とタイムゾーンの話

PHP の DateTime における、コンストラクタとタイムゾーンの関係ついて解説します。

実は「指定したはずのタイムゾーンが無視される」挙動をすることもあり、意外と油断なりません。その点についてもしっかりと解説してきます。

# 前提条件
この記事における PHP のデフォルトタイムゾーンは Asia/Tokyo とします。これは DateTime でタイムゾーンが未指定だったときのデフォルト値として使われます。

# Case 1. now にタイムゾーンを指定する
まず最初に、現在日時（now）で DateTime を作ったときの実例を見てみましょう。

現在日時は「日本時間で 2025年2月17日 午前9時」であると仮定します。
```
// Case 1-1. タイムゾーンの指定なし
$nowJst = new DateTime('now');
var_dump($nowJst->format('Y-m-d H:i:s, e, U'));
// => string(43) "2025-02-17 09:00:00, Asia/Tokyo, 1739750400"

// Case 1-2. 第2引数でタイムゾーンを指定する
$nowUtc1 = new DateTime('now', new DateTimeZone('UTC'));
var_dump($nowUtc1->format('Y-m-d H:i:s, e, U'));
// => string(36) "2025-02-17 00:00:00, UTC, 1739750400"

// Case 1-3. タイムゾーンを後から変換する
$nowUtc2 = new DateTime('now');
$nowUtc2->setTimezone(new DateTimeZone('UTC'));
var_dump($nowUtc2->format('Y-m-d H:i:s, e, U'));
// => string(36) "2025-02-17 00:00:00, UTC, 1739750400"
```
## ポイント

* 第2引数が省略された場合、デフォルトのタイムゾーンである Asia/Tokyo が使われる。
* 第2引数が指定された場合、そのタイムゾーンが使われる。
* UNIX時間 （1970年1月1日 0時0分0秒 からの経過秒数）はどのケースでも同じ値になる。
  * now はタイムゾーンの指定に関わらず同じUNIX時間が生成される。

コンストラクタでタイムゾーンを指定したかどうかに関わらず、 UNIX時間は同じ値になります。また setTimeZone() で後からタイムゾーンを変換してもUNIX時間は変化しません。

なお format() で出力される日時は、現在設定されているタイムゾーンを反映したものになります。日本時間では午前9時に、UTCでは午前0時になっているのは、 format() 時点のタイムゾーンの違いによるものです。

# Case 2. 年月日時分秒のみを指定する
次に、年月日時分秒を指定して DateTime を作ったときの実例を見てみましょう。

今回のケースでは、コンストラクタで「2025年2月17日 午前9時」を指定します。
```
// Case 2-1. タイムゾーンの指定なし
$nowJst = new DateTime('2025-02-17 09:00:00');
var_dump($nowJst->format('Y-m-d H:i:s, e, U'));
// => string(43) "2025-02-17 09:00:00, Asia/Tokyo, 1739750400"

// Case 2-2. 第2引数でタイムゾーンを指定する
$nowUtc1 = new DateTime('2025-02-17 09:00:00', new DateTimeZone('UTC'));
var_dump($nowUtc1->format('Y-m-d H:i:s, e, U'));
// => string(36) "2025-02-17 09:00:00, UTC, 1739782800"

// Case 2-3. タイムゾーンを後から変換する
$nowUtc2 = new DateTime('2025-02-17 09:00:00');
$nowUtc2->setTimezone(new DateTimeZone('UTC'));
var_dump($nowUtc2->format('Y-m-d H:i:s, e, U'));
// => string(36) "2025-02-17 00:00:00, UTC, 1739750400"

var_dump($nowUtc1 <=> $nowUtc2);
// => int(1)

var_dump($nowJst <=> $nowUtc2);
// => int(0)
```
## ポイント

+ UNIX時間は「第1引数の日時を、タイムゾーンで解釈した値」から算出される。
  + 明示的にタイムゾーンが指定された場合はその値が使われ、そうでない場合はデフォルトのタイムゾーンが使われる。
+ setTimeZone() を呼び出しても UNIX時間は変化しない。
+ UNIX時間が同じ場合、タイムゾーンを問わず DateTime は等価とみなされる。

Case １ で述べた通り now はタイムゾーンによる解釈が変わらない値でした。

一方で、今回のような「2025年2月17日 午前9時」という値はタイムゾーンによって解釈が変化する値です。算出されるUNIX時間もまた変わります。

この違いにより、 Case 2 では以下のようなUNIX時間の差異が発生しています。

+ 「タイムゾーンを省略した場合（Case 2-1）」と「タイムゾーンをコンストラクタで指定した場合（Case 2-2）」でUNIX時間が異なる。
+ 「タイムゾーンをコンストラクタで指定した場合（Case 2-2）」と「タイムゾーンを後から変更した場合（Case 2-3）」でUNIX時間が異なる。
# Case 3. today を指定する
now と似た値で today という値を使うこともできます。しかし、両者の挙動は大きく異なります。
```
// 現在日時は「日本時間で 2025年2月17日 午前9時」であると仮定します。

// Case 3-1. タイムゾーンの指定なし
$nowJst = new DateTime('today');
var_dump($nowJst->format('Y-m-d H:i:s, e, U'));
// => string(43) "2025-02-17 00:00:00, Asia/Tokyo, 1739718000"

// Case 3-2. 第2引数でタイムゾーンを指定する
$nowUtc = new DateTime('today', new DateTimeZone('UTC'));
var_dump($nowUtc->format('Y-m-d H:i:s, e, U'));
// => string(36) "2025-02-17 00:00:00, UTC, 1739750400"
```
## ポイント

+ today は「その日の日時を0時0分0秒にしたもの」とみなされます。
  + [PHPマニュアル – サポートする日付と時刻の書式](https://www.php.net/manual/ja/datetime.formats.php#datetime.formats.relative)
+ 「0時0分0秒」の解釈はタイムゾーンによって変わります。

today はタイムゾーンによって解釈が変わる値です。その結果、概ね Case 2 と同様の結果になります。

なお yesterday, tomorrow, noon といった値も同様に、タイムゾーンによって変わる値です。

# Case 4. タイムゾーンありの日時を指定する
今までは DateTime の第2引数でタイムゾーンを指定していましたが、実は第1引数でタイムゾーンを指定する方法もあります。

第1引数に以下のような値を渡すことで、タイムゾーンありの日時を指定することができます。

+ now UTC
+ 2025-02-17T09:00:00+00:00

これらの値を使ったとき、生成される DateTime は以下のようになります。
```
// 現在日時は「日本時間で 2025年2月17日 午前9時」であると仮定します。

// Case 3-1. 第1引数のみタイムゾーンを指定し、現在時刻を生成する
$nowCase31 = new DateTime('now UTC');
var_dump($nowCase31->format('Y-m-d H:i:s, e, U'));
// => string(36) "2025-02-17 00:00:00, UTC, 1739750400"

// Case 3-2. 第1引数と第2引数の両方でタイムゾーンを指定し、現在時刻を生成する
$nowCase32 = new DateTime('now UTC', new DateTimeZone('America/Los_Angeles'));
var_dump($nowCase32->format('Y-m-d H:i:s, e, U'));
// => string(36) "2025-02-17 00:00:00, UTC, 1739750400"

// Case 3-3. 第1引数のみタイムゾーンを指定し、指定された年月日を生成する
$nowCase33 = new DateTime('2025-02-17T09:00:00+00:00');
var_dump($nowCase33->format('Y-m-d H:i:s, e, U'));
// => string(39) "2025-02-17 09:00:00, +00:00, 1739782800"

// Case 3-4. 第1引数と第2引数の両方でタイムゾーンを指定し、指定された年月日を生成する
$nowCase34 = new DateTime('2025-02-17T09:00:00+00:00', new DateTimeZone('America/Los_Angeles'));
var_dump($nowCase34->format('Y-m-d H:i:s, e, U'));
// => string(39) "2025-02-17 09:00:00, +00:00, 1739782800"
```
## ポイント

+ 第1引数でタイムゾーンが指定された場合、第2引数やデフォルトのタイムゾーンは無視される。

結果はいたってシンプルで、単に第1引数のタイムゾーンのみが使用されます。この挙動は PHP 公式のマニュアルにも記載されています。

https://www.php.net/manual/ja/datetime.construct.php より

> **注意**: 
>
> _\\$datetime パラメータが UNIX タイムスタンプ (@946684800 など) であったりタイムゾーンつきで指定した場合 (2010-01-28T15:00:00+02:00 など) は、$timezone パラメータや現在のタイムゾーンは無視されます。_

# まとめ
この記事ではPHP の DateTime クラスの仕様について、主に DateTime コンストラクタ を中心に説明しました。

DateTime コンストラクタで使用可能の書式には、タイムゾーンによって解釈が変わる値と、そうでない値があります。now はタイムゾーンによらず一定のUNIX時間が生成されます。一方で、 today や日時を指定した場合はタイムゾーンによってUNIX時間が変わります。

DateTime コンストラクタの第1引数にタイムゾーンが含まれる場合、第2引数は無視されます。

setTimeZone() で後からタイムゾーンを変更してもUNIX時間に変化はありません。

format() で出力される年月日時分秒はタイムゾーンの影響を受けます。

DateTime の大小比較は、UNIX時間を比較した結果と同じになります。設定されているタイムゾーンは関係ありません。