はじめに
新プロジェクトでのタスクでプロジェクト内のModelを一斉作成していた時に気付いたこと。
初めてModelを1から作成しており、$hidden
や$fillable
, $guarded
などいろんなプロパティがあって便利だなあと思っていました。
その中でも属性の$casts
がちょっと直感的な挙動とは違った挙動だったのでメモ。
前提
-
Windows Home 10
-
PHP 8.0.7
-
Laravel 8.5.0
どんな落とし穴?
具体例で記載。
やりたいこと
Userテーブルにあるemail_verified_at
というカラムの値を「YYYY年MM月DD日」というフォーマットで出力したい
調べたこと
ドキュメントによると
アクセサ、ミューテタ、および属性キャストを使用すると、Eloquentモデルインスタンスで属性値を取得または設定するときに、それらの属性値を変換できます。
さらに
属性キャストは、モデルで追加のメソッドを定義することなく、アクセサやミューテタと同様の機能を提供します。
なので自分は
「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
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
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);
}
}
結果
↓想定値
'2022年04月13日'
↓実際値
'2022-04-13 07:03:31' // 変化なし
あれ。フォーマットされてない。。。
設定ミスったか?と思い、試しにintegerでキャストしてみると
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'integer',
];
結果↓
2022
なんだかintegerへのキャストはうまくいっているらしい。(2022
が表示されたのは2022年だからかな?)
なぜうまくいかないのか原因調査してみた。
結論
しっかりとドキュメント見返すと答えが書かれていた
date
またはdatetime
キャストを定義するときに、日付の形式を指定することもできます。この形式は、モデルが配列またはJSONにシリアル化される場合に使用されます。
なんだ。そういうことか。要は
「$casts
は他の型(integerとかstring)への変換と違って、first()
とかfind()
で取得したタイミングで実行されるわけではなく、toArray()
とかtoJson()
したタイミングで実行される」
ということでした。
なので当初のやりたいこと(YYYY年MM月DD日の出力)に戻ると
...
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime:YYYY年MM月DD日',
];
...
こうして、
$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関係の話ですが)
最後まで読んでいただきありがとうございました!