概要
EloquentモデルのオブジェクトをtoArray()すると 日時が9時間前になってしまう
id | .. | ... | updated_at |
---|---|---|---|
1 | ... | ... | 2021-06-24 17:50:31 |
2 | ... | ... | 2021-06-24 17:50:31 |
3 | ... | ... | 2021-06-24 17:50:31 |
コード例
$result = HogeModel::first()->toArray();
var_dump($result); // "updated_at" => "2021-06-24 17:50:31"を想定
実行結果
array(1) {
["updated_at"]=>
string(27) "2021-06-24T08:50:31.000000Z" // 想定に反してUTCに変換されてしまう
}
原因
コンテナおよびLaravelのタイムゾーン設定はAsia/Tokyoなのになぜ?
→ Laravel 7.x以降のtoArrayメソッド が原因
解決
コード例
class HogeModel extends Model {
protected function serializeDate(DateTimeInterface $date): string {
return $date->format('Y-m-d H:i:s');
}
}
実行結果
array(1) {
["updated_at"]=>
string(19) "2021-06-24 17:50:31" // JSTのまま!
}
serializeDateメソッドを各モデルに記載するのは面倒
プロジェクト初期段階のうちにあらかじめ継承元モデルにてオーバーライドするよう合意を取っておくと吉
補足1
- 本事象はEloquentモデル内の CREATED_AT および UPDATED_AT で指定されたカラムで発生する
- CREATED_AT の初期値は'created_at'
- UPDATED_AT の初期値は'updated_at'
テーブル側の'reg_date'などの表記ぶれや
Eloquentモデル側の CREATED_AT の指定もれ などに注意
補足2
- 本事象は Eloquentモデルによるタイムスタンプ管理 を無効にすることでも対処可能
Eloquentモデルのプロパティに下記を追加するpublic $timestamps = false;
$timestampsはタイムゾーン変換を防ぐことができるが日付のフォーマットは指定できない
要は継承元モデルでserializeDateをオーバーライドするのが楽
補足3
- Eloquentモデルで$castsプロパティを指定することでフォーマットが可能
- しかし CREATED_AT などに指定されているカラムはUTC変換の対象になってしまう
- 下記フォーマット後にtoArray()が実行されてしまうため
protected $casts = [ 'created_at' => 'datetime:Y/m/d H:i:s' ];
serializeDateで指定されたフォーマットのほうが優先度高い
やはりserializeDateを使うほうが手っ取り早い
付録
- なぜUTCに変換されるのか、実際の実装を見てみる
- serializeDateメソッドをオーバーライドしなかった場合下記処理が実行される
\Illuminate\Database\Eloquent\Concerns\HasAttributes
/**
* Prepare a date for array / JSON serialization.
*
* @param \DateTimeInterface $date
* @return string
*/
protected function serializeDate(DateTimeInterface $date)
{
return Carbon::instance($date)->toJSON();
}
\Carbon\Traits\Converter.php
/**
* Return the ISO-8601 string (ex: 1977-04-22T06:00:00Z) with UTC timezone.
*
* @example
* ```
* echo Carbon::now('America/Toronto')->toJSON();
* ```
*
* @return null|string
*/
public function toJSON()
{
return $this->toISOString();
}
/**
* Return the ISO-8601 string (ex: 1977-04-22T06:00:00Z, if $keepOffset truthy, offset will be kept:
* 1977-04-22T01:00:00-05:00).
*
* @example
* ```
* echo Carbon::now('America/Toronto')->toISOString() . "\n";
* echo Carbon::now('America/Toronto')->toISOString(true) . "\n";
* ```
*
* @param bool $keepOffset Pass true to keep the date offset. Else forced to UTC.
*
* @return null|string
*/
public function toISOString($keepOffset = false)
{
if (!$this->isValid()) {
return null;
}
$yearFormat = $this->year < 0 || $this->year > 9999 ? 'YYYYYY' : 'YYYY';
$tzFormat = $keepOffset ? 'Z' : '[Z]';
$date = $keepOffset ? $this : $this->avoidMutation()->utc();
return $date->isoFormat("$yearFormat-MM-DD[T]HH:mm:ss.SSSSSS$tzFormat");
}