👻

Laravelで日付を属性キャストするときはUSTになるから気をつけろ!

2022/06/29に公開

経緯

画面に日付を表示するときに、y-m-d形式(ex. 2022-06-22)で表示したかった時のことです。
データベースでは、その日付はtimestamp型で2022-06-22 00:37:37のように格納されています。

Laravelではアクセサ(データベースから取得する時の処理)が定義できるので、フロント側で変換処理をしなくても、簡単にできるんじゃないかと調べてみたら、
属性キャストを見つけたので使ってみました。

※何か間違い等あればご指摘いただけると幸いです・・・!

環境

  • PHP8.0.x
  • Laravel9

属性キャストとは?

属性キャストは、モデルで追加のメソッドを定義することなく、アクセサやミューテタと同様の機能を提供します。定義する代わりに、モデルの$castsプロパティにより属性を一般的なデータ型に変換する便利な方法を提供します。

https://readouble.com/laravel/9.x/ja/eloquent-mutators.html#attribute-casting

これでできるやん!
ということで以下のように指定してみました。

やった!できたぞ!!
と思いきや・・・?

/**
 * キャストする属性
 *
 * @var array
 */
protected $casts = [
    'created_at' => 'datetime:Y-m-d',
];

日付を超えた時のテストでエラー

上記のまま特に問題なく実装を続けていると、
夜の12時をまわった時のテストが失敗しました。

ファクトリーでデータを登録した後に、データを取得するメソッドを実行して、同じデータが取れてるよね?
というテストです。

エラー内容を見てみると、、、

// 1日ずれている
 -        'created_at' => '2022-06-29'
 +        'created_at' => '2022-06-28'

デバック実行してみるとUSTになっているようでした。
なんだ、、データベースをUST設定にしちまってたか・・・?
と思い調べてみると、JST(日本標準時)でした。

あれ、おかしいな・・・?

日付の属性キャストは固定でUSTになる!

改めてドキュメントを見直すと、以下のような記載がありました。

dateとdatetimeのキャストはデフォルトで、アプリケーションのtimezone設定オプションで指定されているタイムゾーンに関わらず、日付をUTC ISO-8601の日付文字列(1986-05-28T21:05:54.000000Z)にシリアライズします。アプリケーションのtimezone設定オプションをデフォルトのUTCから変更せずに、常にこのシリアライズ形式を使用し、アプリケーションの日付をUTCタイムゾーンで保存することを強く推奨します。

なんと、、、そもそもtimezoneの設定はデフォルトをUTCにすることを推奨されているようでした。
ただ、私はJSTに設定してしまっていたので、日付の属性キャストは使えませんでした。

じゃあどうすればいいか?

serializeDateメソッドのカスタマイズかアクセサを使う

serializeDataの場合

該当モデルの全ての日付のフォーマットに影響してしまいますが、
簡単にキャストを行うにはserializeDateメソッドをオーバーライドしてカスタマイズすることです。

こちらもドキュメントに記載がありました。

https://readouble.com/laravel/9.x/ja/eloquent-serialization.html#date-serialization

/**
* 配列/JSONシリアル化の日付を準備
*
* @param  \DateTimeInterface  $date
* @return string
*/
protected function serializeDate(\DateTimeInterface $date)
{
return $date->format('Y-m-d');
}

この場合は、アプリケーションで設定したタイムゾーン(JST)が適用されていました。
ちなみにこの方法は、DB保存には影響しません。

アクセサの場合

特定の属性値だけ変えたい場合はアクセサを使うことができます。
https://readouble.com/laravel/9.x/ja/eloquent-mutators.html#accessors-and-mutators

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Carbon;

// Laravel9のアクセサの書き方
public function createdAt(): Attribute
{
    return new Attribute(
        // アクセサ
        get: fn ($value) => Carbon::parse($value)->timezone('Asia/Tokyo')->format('Y-m-d')
    );
}

これでJSTで日付を取得でき、テストも成功しました!

GitHubで編集を提案

Discussion