サーバー、MySQL、PHP、JavaScript、全ての日付が狂ってた話
AIに一発生成させたPHPスクリプトの話
簡易的なPHPスクリプト(単一〜数ファイル程度でクラス定義もそこまでない、フレームワークを使うまでもないもの)は最近AIにパパッと作ってもらっている。本番用途ではなく、ちょっと動かす程度なら結構楽。
さすがに仕事で使う分には機能別に作らせて自動テストも書かせるが、「動きます!」と嘘を言ってるときがあるのでちゃんと見ないといけない。
今回は仕様駆動で一発生成したときの話。
何が起きたか
フォームに入力したデータがおかしい。東京は午前2時と表示される。システム表示の方は……いやなんで未来判定なんだ、今だぞ。
仮にMySQLでTIMESTAMP型で自動で入っていてもUTCだから未来にはならないはず。DATETIME型だったら入れたとおりのはずだし、なんだこれってなった(MySQLの型の話は後述)。
まあでも時刻がずれるって話は、だいたい問題はPHP側、というかphp.iniの話とソースコード両方じゃないかと普通はなる。
PHPのタイムゾーン設定
サーバー設定によるけど、通常はphp.iniにタイムゾーン設定がないと思う。だいたいは宣言がない場合、デフォルトではサーバー自体のタイムゾーンが使われる。じゃあサーバー自体のタイムゾーンが実は日本じゃなかったら?
案の定、サーバーはUTCだったし、php.iniにタイムゾーン設定はなかった。PHPスクリプト側でもタイムゾーン設定していなかった。
AIに任せるとスポーンと抜ける
人間が作っても1回はやらかすミスだから、それ自体はいい。問題はAIに任せたときにタイムゾーンがちゃんと設定されるかという話。
AIは言われたとおりに作ってるだけなので、「ここがアジアの日本だなんて言ってなかったじゃないですか!」と思っている(思ってるかどうかは知らないが)。
日本人だけのチームだと最初にタイムゾーン設定を何にするか仕様で決めていると思う。それに沿って出力時に調整するか、フロントのJSで変えたりする(個人的にはピュアJSで時刻計算は全くおすすめしないが)。
対策:タイムゾーン設定
php.iniに書く場合
めんどくさかったらphp.iniに書いておく。
date.timezone = Asia/Tokyo
設定ファイルの場所は環境によって違うので php --ini で確認。
スクリプトの頭に書く場合
ローカル専用で使い捨てとか一発書きなら、頭にタイムゾーン設定しておけという話。
<?php
setlocale(LC_ALL, 'ja_JP.UTF-8');
date_default_timezone_set('Asia/Tokyo');
または、DateTimeクラスを使う前に直接指定する方法もある。
<?php
$tz = new DateTimeZone('Asia/Tokyo');
$now = new DateTime('now', $tz);
echo $now->format('Y-m-d H:i:s');
DateTimeImmutableのすすめ
個人的には頭で宣言しておくのが無難だと思う。めんどくさいし。
それと、dateやtimeはあまり使わずDateTimeクラスを使っている人が多いと思うけど、イミュータブルな設計の方が好きなので、DateTimeImmutableを使う方が良いと思う。名前がクソ長いからエイリアスしておくのがおすすめ。
<?php
use DateTimeImmutable as DTI;
use DateTimeZone as DTZ;
date_default_timezone_set('Asia/Tokyo');
$now = new DTI('now', new DTZ('Asia/Tokyo'));
echo $now->format('Y-m-d H:i:s');
DateTimeImmutableの何が良いか
イミュータブルの良さは、日付をいじったときに元のオブジェクトに影響がないこと。普通にこっちの方が使い勝手が良い。
<?php
// DateTime(ミュータブル)の場合
$today = new DateTime('2025-12-06');
$tomorrow = $today->modify('+1 day');
echo $today->format('Y-m-d'); // 2025-12-07(変わってしまう!)
echo $tomorrow->format('Y-m-d'); // 2025-12-07
// DateTimeImmutable(イミュータブル)の場合
$today = new DateTimeImmutable('2025-12-06');
$tomorrow = $today->modify('+1 day');
echo $today->format('Y-m-d'); // 2025-12-06(そのまま)
echo $tomorrow->format('Y-m-d'); // 2025-12-07
JavaScriptの日付は信用するな
JavaScriptの日付はブラウザ=クライアント依存。ユーザーの端末設定次第で何が返ってくるかわからないので、そもそも信用できない。
加えてDateオブジェクトの操作がイカれている。月が0始まり(1月が0)とか、タイムゾーン処理が直感的じゃないとか、罠が多すぎる。
// 2025年1月1日を作りたい
const date = new Date(2025, 1, 1);
console.log(date); // 2025-02-01 ← は?
// 月は0始まり(これはクソ)
const correct = new Date(2025, 0, 1);
console.log(correct); // 2025-01-01
// タイムゾーンもクライアント依存(これもクソ)
const now = new Date();
console.log(now.getTimezoneOffset()); // 環境によって違う値が返る
なのでピュアJSで日付計算するのは避けて、day.jsやdate-fnsなどのライブラリを使うか、サーバー側で処理して結果だけ渡す方が無難。
補足:MySQLのカラム型の違い
ちなみにMySQLの日付型はこういう違いがある。
| 型 | 保存形式 | タイムゾーン | 範囲 |
|---|---|---|---|
| TIMESTAMP | UTC変換して保存 | 取得時にセッションTZで変換 | 1970-2038年 |
| DATETIME | 入れたまま保存 | 変換なし | 1000-9999年 |
TIMESTAMPはUTCで保存されて取得時に変換されるので、サーバーやセッションのタイムゾーン設定が効く。DATETIMEは入れたまま出てくるので、アプリ側で統一しないとカオスになる。
(TIMESTAMPは2038年問題があるけど、どうなるんだろうこれ……)
ちなみにTIMESTAMPには自動で日時を入れる機能がある。
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
DEFAULT CURRENT_TIMESTAMPでINSERT時に自動挿入、ON UPDATE CURRENT_TIMESTAMPでUPDATE時に自動更新。便利だけど、やはりTIMESTAMPは2038年問題があるので使わない方がいいと思う。AIはよく提案してくるが。
まとめ
PHPを扱っている人なら常識だと思うけど、AIにやらせているとスポーンと抜けることがある(これでも前よりはちゃんと処理してくれているので助かっているが)。
日付系はちゃんと仕様に書いておこうという話。AIとの対話ムズカシイネー。
ちなみに「午前2時」ってのは適当な時間を挙げただけだけど、思いついたのはこの曲。話にはめっちゃ関係ないけど。
