Laravel は PHP のフレームワークであり、その中には日時を取り扱う機能もあります。Laravel は Carbon というライブラリを取り込んで日時操作をしやすい様にしています。
Carbon – A simple PHP API extension for DateTime.
Laravel 内で用いられる Carbon のタイムゾーンですが、実行環境のシステム設定、Laravel の設定、実行時の指定で定まります。実行環境のシステム設定とLaravel の設定のずれや実行時の指定の予期せぬ指定によりタイムゾーンのずれが出ます。午前9時に何かが変わる不具合が起きているならば、タイムゾーンのずれから探すと不具合の原因を見つけやすいです。
実行環境のシステム設定は php.ini の date.timezone です。これは PHP 自体の設定です。Carbon を無視したルートを経由して Carbon を用いた場合に予期せぬタイムゾーンになることがあります。
PHP: 実行時設定 – Manual#date.timezone
Laravel 内では /config/app.php の timezone からタイムゾーンを指定できます。PHP のタイムゾーンを設定しても UTC のままならばここの設定漏れが原因の可能性があります。
laravel/app.php at 9.x · laravel/laravel#L61-L72
// /config/app.php
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| ここで、アプリケーションのデフォルトのタイムゾーンを指定します。
| これは、PHP の日付・時刻関数で使用されます。
| この設定は、PHP の日付・時刻関数で使用します。
|
*/
'timezone' => 'UTC',
最後に実行時のタイムゾーン指定です。これはソースコード内で setTimezone とするのみにとどまらず、ユーザー入力などの日時文字列パースでも起きえます。具体的には次です。
<? php
// あらかじめ Asia/Tokyo にタイムゾーンを設定しています。
use Illuminate\Support\Carbon;
dump((new Carbon())->toISOString(true));
// 2022-10-06T17:19:53.624668+09:00
// 期待通り現在日時が日本のタイムゾーンで取得できました。
dump((new Carbon('2022-10-06 17:15:00'))->toISOString(true));
// 2022-10-06T17:15:00.000000+09:00
// 与えた日時を日本のタイムゾーン内で指定された日時として解釈してくれました
dump((new Carbon('2022-10-06T17:15:00+00:00'))->toISOString(true));
// 2022-10-06T17:15:00+00:00
// タイムゾーンのオフセット付きで与えたところ、そのタイムゾーンを保持したままになります。
dump((new Carbon('2022-10-06T17:15:00Z'))->toISOString(true));
// 2022-10-06T17:15:00+00:00
// タイムゾーン指定付きで与えたところ、そのタイムゾーンを保持したままになります。
下の二つについて Carbon に与えたタイミングで自動的にタイムゾーンを切り替えて欲しいのにそうはなりませんでした。全世界に展開するサービスなどの場合はこの挙動の方がよさそうですが、タイムゾーンが一つしかない国の国内限定サービスでこの挙動はかえって不便です。JavaScript でだしぬけにnew Date.toISOString()で得られた文字列を受け取った場合などは特に不具合に繋がりやすいです。
この実行時のタイムゾーン指定を無視して常に同じタイムゾーンで Carbon を扱いたいのであれば、先述の PHP と Laravel の設定に加えて次の様な Carbon インスタンスを生成する度にタイムゾーンをセットする関数を作ると便利です。
/** 与えられた値を元に Laravel で設定されたタイムゾーンの Carbon インスタンスを返す */
function createCarbon($datetime = null){
return (new Carbon($datetime))->setTimezone(config('app.timezone'));
}
dump((createCarbon())->toISOString(true));
dump((createCarbon('2022-10-06 17:15:00'))->toISOString(true));
dump((createCarbon('2022-10-06T17:15:00+00:00'))->toISOString(true));
dump((createCarbon('2022-10-06T17:15:00Z'))->toISOString(true));
dump((createCarbon('2022-10-06T17:15:00+09:00'))->toISOString(true));
これを使用したデモが次です。