preventAccessingMissingAttributesの挙動
Laravel の Eloquent を利用してデータを取得する場合、特に列の指定がなければすべての列情報を取得してきます。
$user = User::find(1);
// App\Models\User {
// id: 5,
// name: "kiichiro",
// email: "kiichiro@example.com",
// email_verified_at: "2024-05-28 06:44:34",
// password: "$2y$12$jJuJzdMPZ/gSwVSp3DzNE.1SwACJGqPLsWLWP3HGUZRhP1Q/9zJs2",
// remember_token: "KjmFVc1rUZ",
// created_at: "2024-05-28 06:44:35",
// updated_at: "2024-05-28 06:51:32",
// }
列の指定があれば指定された列情報のみを取得します。
$user = User::select(['id', 'name'])->find(1);
// App\Models\User {
// id: 1,
// name: "kiichiro",
// }
このとき、取得しなかったプロパティへのアクセスが発生すると、null を返します。
$user->email; // null
この挙動故に、アクセスしたプロパティをそもそも取得していないのか、取得した上で null なのかが判断出来ず、バグになってしまうのはよくある話ですね。
これを解決するには AppServiceProvider
に Model::preventAccessingMissingAttributes
を呼び出すと良いらしいです。
public function boot(): void
{
Model::preventAccessingMissingAttributes();
}
取得していないプロパティへのアクセスが発生した場合は MissingAttributeException
を投げてくれます。
$user = User::select(['id', 'name'])->find(1);
$user->email;
// Illuminate\Database\Eloquent\MissingAttributeException The attribute [email] either does not exist or was not retrieved for model [App\Models\User].
ただし、列情報を取得していなくても動的プロパティとしてセットしたり、fill
でプロパティを埋めた場合はこの例外は投げられません。
$user = User::select(['id', 'name'])->find(1);
$user->email = 'kiichiro@example.com';
$user->email;
// 'kiichiro@example.com'
$user = User::select(['id', 'name'])->find(1);
$user->fill(['email' => 'kiichiro@example.com']);
$user->email;
// 'kiichiro@example.com'
プロパティへのアクセスは基本的に attributes
という配列のキーに相当するものがあるかどうかを判別しています。判別の結果、attributes
になければ
を投げ、MissingAttributeException
attributes
にあればその値を返します。よって、動的プロパティとしてセットした場合は attributes
にあるので値がそのまま返ってくるということのようです。
/**
* Dynamically retrieve attributes on the model.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->getAttribute($key);
}
/**
* Get an attribute from the model.
*
* @param string $key
* @return mixed
*/
public function getAttribute($key)
{
if (! $key) {
return;
}
// If the attribute exists in the attribute array or has a "get" mutator we will
// get the attribute's value. Otherwise, we will proceed as if the developers
// are asking for a relationship's value. This covers both types of values.
if ($this->hasAttribute($key)) {
return $this->getAttributeValue($key);
}
// Here we will determine if the model base class itself contains this given key
// since we don't want to treat any of those methods as relationships because
// they are all intended as helper methods and none of these are relations.
if (method_exists(self::class, $key)) {
return $this->throwMissingAttributeExceptionIfApplicable($key);
}
return $this->isRelation($key) || $this->relationLoaded($key)
? $this->getRelationValue($key)
: $this->throwMissingAttributeExceptionIfApplicable($key);
}
/**
* Determine whether an attribute exists on the model.
*
* @param string $key
* @return bool
*/
public function hasAttribute($key)
{
if (! $key) {
return false;
}
return array_key_exists($key, $this->attributes) ||
array_key_exists($key, $this->casts) ||
$this->hasGetMutator($key) ||
$this->hasAttributeMutator($key) ||
$this->isClassCastable($key);
}
おまけ
preventAccessingMissingAttributes
は Laravel 9系で実装された機能なので、Laravel 9 の公式ドキュメントには記載があるのですが、なぜか10以降のドキュメントから削除されていました。
どうやら以下のコミットで削除されたようです。
Eloquent ORM の機能を制限するガードレールとして設定したいという需要がありそうなので、ドキュメントの削除はミスかなと思ったのですが、同じようなことを思った人が既にいたようでプルリクエストが上がっていました。
コメントに削除した理由についての言及がありました。
I don’t really like this feature so I removed it from the docs.
https://github.com/laravel/docs/pull/9260#issuecomment-1892644460
まあ、好みじゃないならしょうがないですよね。。