LaravelではModelやCollectionやRequestなどのクラスにおいて動的プロパティがコードの短縮形としてよく使われます。しかし、同じクラスでメソッドとして定義したものがいきなりプロパティとして使われるので、私は昔よく混乱したものです。今回はまずEloquent編として代表的な動的プロパティの活用を混乱しないように説明します。
Eloquentのアクセッサー
まず、動的プロパティの例として、DBには対応する項目は存在しないのに、あたかも存在する項目のように振る舞うEloquentのアクセッサーです(ミューテーターも同じですが)。
DBのテーブルにfirst_name(名前)とlast_name(苗字)が存在するとして、姓名(full_name)を返すアクセッサーメソッドを定義します。
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
...
/**
* 姓名を返すアクセッサー
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function fullName(): Attribute
{
return Attribute::make(
get: fn ($value, $attribute) => $attribute['last_name'].' '.$attribute['first_name'],
);
}
}
これを使用するときは、動的プロパティとして以下のように使います。
> User::find(1);
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
= App\Models\User {#5393
id: 1,
created_at: "2024-04-16 03:42:31",
updated_at: "2024-04-16 03:42:31",
first_name: "直子",
last_name: "山田",
}
> User::find(1)->full_name;
= "山田 直子"
先ほどのアクセッサーのメソッドfullName()をコールして姓名full_nameを動的プロパティとして返します。
ちなみに、fullName()のメソッドはprotectedで宣言されているので直接はコールできません。
> User::find(1)->fullName(); BadMethodCallException Call to undefined method App\Models\User::fullName().
EloquentのRelationship
今度は、EloquentのRelationshipの動的プロパティです
典型的なブログを例として、ユーザーとブログの投稿を使います。それぞれのDBテーブルを代表してUserとPostがModelのクラスとなります。そして、Userのクラスでは、それらの1対多の関係を以下のように定義します。
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* ユーザー1に対して複数のブログの投稿
*/
public function posts()
{
return $this->hasMany(Post::class);
}
}
この関係を利用して、特定のユーザーのブログのレコードを取得するのは通常以下のようになります。
$user = User::find(1); $posts = $user->posts()->get(); // Collectionを返す
しかし、動的プロパティを使うと、
$posts = $user->posts;
と短縮形で書くことができます。先の非短縮形と同じ結果となります。便利ですね。
しかし、この短縮形、非短縮形でできることがなんでもできる訳ではありません
例えば、非短縮形のメソッド方式では、
$user->posts()->orderBy('created_at')->get();
とレコードをソートできますが、短縮形の動的プロパティの使用ではエラーとなります。
$user->posts->orderBy('created_at');
BadMethodCallException Method Illuminate\Database\Eloquent\Collection::orderBy does not exist.
これは、$user->posts()がIlluminate\Database\Eloquent\Relations\HasManyを返すのに、$user->postではIlluminate\Database\Eloquent\Collectionを返すからです。Collectionには、orderBy()というメソッドはないのです。しかし、前者にはあります。
ちなみに後者では、CollectionのメソッドsortBy()を使用すると同じ結果が得られます。
$user->posts->sortBy('created_at');
もう1つ、より混乱しそうな例を挙げてみましょう。
まずは非短縮形から、
$user->posts()->where('title', '=', 'ブログタイトル')->get();
タイトルに特定の条件で絞ったクエリーです。ちなみに、実行されたSQLは、以下となります。
select * from `post` where `user`.`id` = 1 and `user`.`id` is not null and `title` = 'ブログタイトル';
さて、これを短縮形の動的プロパティで書くと、
$user->posts->where('title', '=', 'ブログタイトル');
->get()がないだけの違いですが、これはまったく正規です。返す結果も前者と同じです。where()はCollectionのメソッドにもあるのですね。
実行されたSQLも見てみましょう。
select * from `post` where `user`.`id` = 1 and `user`.`id` is not null;
違いわかりますか?タイトルを制限するwhereの条件文がSQL文にありません。つまり、ユーザーが投稿したレコードをいったん全部取得して、Collectionのwhere()のメソッドでフィルターしているのです。
両者ともに実行結果は同じですが、どこまでがデータベースにより実行されるか、どこからがCollectionのメソッドで実行するかにおいての違いです。この違いでパフォーマンスが違ってきます。一般的には前者のようにDBにたくさん仕事をしてもらうのが効率的です。注意が必要ですね。
参照
混乱してはいけないLaravelの動的プロパティ – Collection編
混乱してはいけないLaravelの動的プロパティ – 裏側編
混乱してはいけないLaravelの動的プロパティ – PHP8.2で廃止となった編
