Eloquentモデルを配列に変換して渡す必要がある際にたまに使うtoArray()ですが。先日何気なく使用していて思わぬエラーに遭遇、原因を調べてみると実はLaravel 7.xから変更されていた仕様でした。Upgrade Guideには目を通していたつもりでしたが、どうしてなかなか行き当たりばったりなキャッチアップになってしまう、Lazy Loadingな私です。

$user->toArray()

まず、toArray()を使用していて遭遇したエラーについて説明します。
Laravelインストール後にデフォルトで存在するUserモデルをtoArray()で変換してみましょう。

// factory でダミーレコード生成
>>> $user = User::factory()->create();
=> App\Models\User {#3575
     name: "Mr. Buck Schinner MD",
     email: "waters.montana@example.com",
     email_verified_at: "2022-06-05 04:34:30",
     #password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
     #remember_token: "wp5fKxXY7s",
     updated_at: "2022-06-05 04:34:30",
     created_at: "2022-06-05 04:34:30",
     id: 1,
   }

>>> $user->toArray();
=> [
     "name" => "Mr. Buck Schinner MD",
     "email" => "waters.montana@example.com",
     "email_verified_at" => "2022-06-05T04:34:30.000000Z",
     "updated_at" => "2022-06-05T04:34:30.000000Z",
     "created_at" => "2022-06-05T04:34:30.000000Z",
     "id" => 1,
   ]

お気づきでしょうか?
email_verified_at, updated_at, created_at の日付形式がY-m-d H:i:sからISO-8601に変換されています。

なぜでしょうか?

日付のフォーマット

これはLaravel 7から内部的にCarbonのtoJson()を使用するようになったためのようです。

https://laravel.com/docs/7.x/upgrade#date-serialization

確かに、CarbonでtoJson()を使用するとISO-8601形式で日付が出力されますね。

Carbon::today()->toJson();
=> "2022-06-05T00:00:00.000000Z"

とは言え、全てのdatetimeやtimestamp型の項目がフォーマットされるわけではなく、デフォルトではcreated_atupdated_atのみです。
その他の項目についてはemail_verified_atの様に、$castsにてdatetimeを指定すると同様にISO-8601形式でフォーマットされます。

デフォルトのフォーマットを変更する

今回私が遭遇したケースではcreated_atupdated_atY-m-d H:i:sの形式のまま配列に変換する必要がありました。
toArray()で変換する際に日付のフォーマットを指定するにはどうすれば良いでしょう?

$casts

1つはemail_verified_atの様に$castsにて個別に指定する方法です。以下の様に、datetime:に続いて出力したいフォーマットを指定することができます。

...
    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'created_at'        => 'datetime:Y-m-d H:i:s',    // ←日付の形式を指定
        'updated_at'        => 'datetime:Y-m-d H:i:s',    // ←日付の形式を指定
    ];
...

tinkerで出力を確認してみましょう。

>>> $user->toArray();
=> [
     "id" => 1,
     "name" => "Dr. Wanda Gottlieb",
     "email" => "brenda39@example.com",
     "email_verified_at" => "2022-03-09T13:55:28.000000Z",
     "created_at" => "2022-03-09 13:55:28",
     "updated_at" => "2022-03-09 13:55:28",
   ]

正しく反映されていますね。

serializeDate

もう1つはserializeDate()をオーバーライドすることです。このメソッドはModelクラスがuseしているtraitのHasAttributesに定義されており、toArray()実行時に内部的に呼び出されています。このメソッドをオーバーライドすることでcreated_atupdated_atを含めdatetimeにキャストしている項目toArray(), toJson()時のデフォルトのフォーマットを指定できます。フォーマットは以下の様にformat()で指定します。

...
use DateTimeInterface;
...
    /**
     * Prepare a date for array / JSON serialization.
     *
     * @param  \DateTimeInterface  $date
     * @return string
     */
    protected function serializeDate(DateTimeInterface $date)
    {
        return $date->format('Y-m-d H:i:s');
    }
...

因みに、serializeDate()で指定しているのはデフォルトのフォーマットなので、$castsでのフォーマット指定と併用した場合は$casts側の指定が優先されます。以下は併用した例です。

...
    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime:Y-m-d',    // 日付のみに指定してみる
    ];
...

toArray()すると、created_at, updated_atはY-m-d H:i:sですが、email_verified_atはY-m-dのみの出力となります。

>>> $user->toArray();
=> [
     "id" => 1,
     "name" => "Dr. Wanda Gottlieb",
     "email" => "brenda39@example.com",
     "email_verified_at" => "2022-03-09",    // Y-m-d のみ
     "created_at" => "2022-03-09 13:55:28",
     "updated_at" => "2022-03-09 13:55:28",
   ]

By hikaru