【Laravel】日時を正しく取得するには

# 結論

serializeDateメソッドをオーバーライドする
https://laravel.com/docs/11.x/eloquent-serialization#date-serialization

```php:コード例
class HogeModel extends Model {
    protected function serializeDate(DateTimeInterface $date): string {
        return $date->format('Y-m-d H:i:s');
    }
}
```



# 具体例
EloquentモデルのオブジェクトをtoArray()すると **日時が9時間前になってしまう** ことがある

<table>
<caption>Hogeテーブル
<thead>
<tr><th>id<th>..<th>...<th>updated_at
<tbody>
<tr><td>1<td>...<td>...<td>2021-06-24 17:50:31 
<tr><td>2<td>...<td>...<td>2021-06-24 17:50:31 
<tr><td>3<td>...<td>...<td>2021-06-24 17:50:31 
</table>

```php:コード例
$result = HogeModel::first()->toArray();
var_dump($result); // "updated_at" => "2021-06-24 17:50:31"を想定
```

```php:実行結果
array(1) {
  ["updated_at"]=>
  string(27) "2021-06-24T08:50:31.000000Z" // 想定に反してUTCに変換されてしまう
}
```

## 原因
コンテナおよびLaravelのタイムゾーン設定はAsia/Tokyoなのになぜ？
→ [Laravel 7.x以降のtoArrayメソッド](https://laravel.com/docs/7.x/upgrade#date-serialization) が原因

## 結果

serializeDateをオーバーライドすることでtoArray()しても日付情報をJSTのまま取得できる

```php:コード例
class HogeModel extends Model {
    protected function serializeDate(DateTimeInterface $date): string {
        return $date->format('Y-m-d H:i:s');
    }
}
```

```php:実行結果
array(1) {
  ["updated_at"]=>
  string(19) "2021-06-24 17:50:31" // JSTのまま！
}
```

:::note info
プロジェクト初期のうちに継承元モデルにてオーバーライドすることで保守性を担保できる
:::


# 補足1

本事象はEloquentモデル内の _CREATED_AT_ および _UPDATED_AT_ で指定されたカラムにて発生する
- _CREATED_AT_ プロパティの初期値（デフォルトカラム名）は 'created_at'
- _UPDATED_AT_ プロパティの初期値（デフォルトカラム名）は 'updated_at'

:::note info
たとえば作成日時のカラム名を 'reg_date' としている場合
Eloquentモデルにて _CREATED_AT_ = 'reg_date' と設定する必要がある 
:::

# 補足2

- タイムゾーンの自動変換を防止するのみであれば Eloquentモデルによるタイムスタンプ管理 を無効にすることでも対処可能
- Eloquentモデルのプロパティに下記を追加する
    ```php
    public $timestamps = false;
    ```

:::note warn
$timestampsのみでは日付のフォーマットを指定することはできない
:::

# 補足3

- フォーマットを指定するのみであれば Eloquentモデルで$castsプロパティを利用することでも対処可能
  ```php
  protected $casts = [
      'created_at' => 'datetime:Y/m/d H:i:s'
  ];
  ```

:::note warn
- _CREATED_AT_ や _UPDATED_AT_ に指定されているカラムはタイムゾーン変換の対象になってしまう
  - $castsによるフォーマット後にtoArray()が実行されてしまうため
- またserializeDateで指定されたフォーマットのほうが優先度は高い
:::

# 補足4

`use softDeletes`の対象カラムでも同様の現象が見受けられる

これもserializeDateめそｄで解決できる

# 付録

なぜUTCに変換されるのか実際の実装を確認する
serializeDateメソッドをオーバーライドしなかった場合下記処理が実行される

```php:\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();
}
```

```php:\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");
    }
```