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テーブルを代表してUserPostが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文にありません。つまり、ユーザーが投稿したレコードをいったん全部取得して、Collectionwhere()のメソッドでフィルターしているのです。

両者ともに実行結果は同じですが、どこまでがデータベースにより実行されるか、どこからがCollectionのメソッドで実行するかにおいての違いです。この違いでパフォーマンスが違ってきます。一般的には前者のようにDBにたくさん仕事をしてもらうのが効率的です。注意が必要ですね。

By khino