[Laravel]$casts => datetime(date):~の落とし穴

# はじめに

新プロジェクトでのタスクでプロジェクト内のModelを一斉作成していた時に気付いたこと。

初めてModelを1から作成しており、`$hidden`や`$fillable`, `$guarded`などいろんなプロパティがあって便利だなあと思っていました。

その中でも属性の`$casts`がちょっと直感的な挙動とは違った挙動だったのでメモ。

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

## 前提

- Windows Home 10

- PHP 8.0.7

- Laravel 8.5.0

# どんな落とし穴？

具体例で記載。

## やりたいこと

Userテーブルにある`email_verified_at`というカラムの値を「YYYY年MM月DD日」というフォーマットで出力したい

## 調べたこと

ドキュメントによると

> アクセサ、ミューテタ、および属性キャストを使用すると、Eloquentモデルインスタンスで属性値を取得または設定するときに、それらの属性値を変換できます。

https://readouble.com/laravel/8.x/ja/eloquent-mutators.html#introduction

さらに

> 属性キャストは、モデルで追加のメソッドを定義することなく、アクセサやミューテタと同様の機能を提供します。

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

なので自分は

> 「first()とかでdatetime型のカラム（今回ならemail_verified_at）を取得した時点でキャストにより、型変換及びフォーマット変更されるんだな」

と理解したので、意気揚々と実装を進めた。

## テーブル構成

- Userテーブル

| カラム名 | 型 | 制御 | 説明 |
| --- | --- | --- | --- |
| id | int | PK | ユーザーID |
| name | string | not null | 名前 |
| ... | ... | ... | ... |
| email_verified_at | datetime |  | アプリからの確認メールをユーザーが確認した日時
「YYYY年MM月DD日」としたい |

今回のキャストするターゲットの`email_verified_at`が登録されている

### ソースコード

### Route

適当にアクセスできるように実装しています。(URL決めるのが手間＆記事の文字数増えるので割愛)

### Model

```php
class User extends Model
{
    use HasFactory

    /**
     * related table.
     *
     * @var string
     */
    protected $table = 'users';

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime:YYYY年MM月DD日',
    ];

		...
}
```

今回の目玉の`$casts`。これで想定通りのフォーマットで取得できる（と思っていた）

### Controller

一先ず、簡単にddしてみる

```php
<?php

namespace App\Http\Controllers\Mypage;

use App\Http\Controllers\Controller;
use App\Models\User;

class IndexController extends Controller
{
    /**
     * Index interface.
     *
     */
    public function index()
    {
        $user = User::first(); // 一番最初のユーザー取得
		dd($user->email_verified_at);
    }
}
```

## 結果

↓想定値

```php
'2022年04月13日'
```

↓実際値

```php
'2022-04-13 07:03:31' // 変化なし
```

あれ。フォーマットされてない。。。

設定ミスったか？と思い、試しにintegerでキャストしてみると

```php
	/**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'integer',
    ];
```

結果↓

```php
2022
```

なんだかintegerへのキャストはうまくいっているらしい。（`2022`が表示されたのは2022年だからかな？）


なぜうまくいかないのか原因調査してみた。

## 結論

しっかりとドキュメント見返すと答えが書かれていた

> `date`または`datetime`キャストを定義するときに、日付の形式を指定することもできます。この形式は、[モデルが配列またはJSONにシリアル化される](https://readouble.com/laravel/8.x/ja/eloquent-serialization.html)場合に使用されます。

https://readouble.com/laravel/8.x/ja/eloquent-mutators.html#date-casting

なんだ。そういうことか。要は

「`$casts`は他の型（integerとかstring）への変換と違って、`first()`とか`find()`で取得したタイミングで実行されるわけではなく、`toArray()`とか`toJson()`したタイミングで実行される」

ということでした。

なので当初のやりたいこと（YYYY年MM月DD日の出力）に戻ると

```php
    ...

	/**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime:YYYY年MM月DD日',
    ];

		...
```

こうして、

```php
$user = User::first();

dump($user->email_verified_at); // ここではDBの値のまま
>>> 2022-04-13 07:03:31

dump($user->toArray());
>>>
array:5 [▼
  "id" => 1
	...
  "email_verified_at" => "2022年04月13日" // cast＆フォーマットされる！
	...
]
```

こんな感じで`toArray`や`toJson`などシリアライズするタイミングで変換される、ということでした

## おわりに

- なぜ`date`, `datetime`だけ他の型に比べて、$castsが実行されるタイミングが違う仕様になっているのか、までは追及できませんでした。
    - **妥協的仕様**（Carbonが返ってくるから仕方ない的な）なのか、**利便性重視的仕様**（この仕様のほうが多くのユースケースにおいて便利、的な）なのかくらいは知りたかった
    - 個人的には実際にハマっているので前者であってほしいと思うばかり...
- とはいえ、Eloquentモデルインスタンスの取得時のキャストやフォーマット変更が普段の開発の意識外で可能であることが分かり、今後の想定外の挙動に対応する手助けになるはずなのでドキュメントを深読みする機会が得れてよかった。
- $castsの中でサポートしているキャストタイプの中でも `immutable_date`と `immutable_datetime`という見慣れないモノも発見したのでこれは別途記事でまとめます。（Carbon関係の話ですが）

最後まで読んでいただきありがとうございました！