はじめに

Laravelは案件の⼀部で使⽤したり、Laravel案件に少し参画した程度の初学者です。
基本的にはQueryBuilderを使⽤していましたし、Eloquentとどちらが良いかと⾔われると⼀⻑⼀短であると思います。
EloquentORM VS QueryBuilderなんて⼤戦に参加するのも怖いですし。。。
ただ、Eloquentが関連するLaravelの機能はかなりの数があるため、Eloquentで⼀気通貫させるのも良いのかなと思っています。N+1問題もwithで⼀応解決はできると思います。
まぁモデルにjoin使うのだけは中⾝わけわからくてアクティブレコードがお亡くなりになってるので、勘弁とは思っています。

パフォーマンス⾯や学習コスト、冗⻑性など様々な⾯で良し悪しがある両者ですが、今回はどうせEloquetORMを使⽤するなら、ということで下記4つの機能が便利だったので、⾃分もこれから使っていきたいという気持ちも込めた紹介です!
Eloquent最近好きです。

環境

  • バージョン
    • Laravel 10
  • OS
    • Windows

キャスト

属性にアクセスした際に、定義したキャストに変換してくれるものです。
モデルをみたときに処理ではどのような型で必要としているのか知れたり、アクセスしたら想定と違った、なんてことは防げます。

class User extends Model
{
   /**
   *
   * @var array
   */
   protected $casts = [
       'is_flg' => 'boolean',
       'open_date => 'datetime',
       'close_date => 'datetime',
   ];
}

アクセサ

指定した属性にアクセスされたときに、値を変形して取得できます。
値の指定はアクセサ名をアクセスする属性にします。
スネークケースの属性はキャメルケースで指定します。
下記の例では、nameにアクセスして取得する際にHelloという⽂字列を連結させて取得しています。
そのままのユースケースをそのままコードに落とし込むと、ついメソッドを作成してしまいがちですが、アクセサを使⽤することで、毎度値を変形させる処理を記載する必要がなくなります。アロー関数でなくても動作します。

コード

use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Model
{
     protected function name(): Attribute
         {
             return Attribute::make(
                 get: fn ($attributes) => $attributes . 'Hello!'
             );
         }
}

動作確認

> $user = new App\Models\User();
= App\Models\User {#3788}

> $user -> name = 'テスト';
= "テスト"

> $user -> name
= "テストHello!"

属性にアクセスする際に、下記のようにgetAttributes()を使うことで、アクセサを無効化できます。

$user -> getAttributes()['name']

ミューテータ

指定した属性を設定する際にその値を変形させます。
先ほどのアクセサとは逆のものです。
下記では、nameを設定する際に「さん」という⽂字列を連結させています。
こちらもアクセサ同様に設定毎に値を変形させる処理記載の⼿間が省けます。
こちらもアロー関数でなくても動作します。

ソース

use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Model
{
    protected function name(): Attribute
        {
            return Attribute::make(
                set: fn (string $attributes) => $attributes . 'さん'
            );
        }
{

動作確認

> $user -> name = 'テスト';
= "テスト"

> $user -> name
= "テストさん"

ミューテータに関しては推奨されるような無効化⼿段は⽤意されていそうにないです。
そのため、ミューテータに関しては個⼈開発ではない場合、慎重に扱う必要がありそうです。

アクセッサ&ミューテータ混同

次は両者をひとまとめにして記載する⽅法です。
単体のアクセサとミューテータにも通ずるところですが、
「use Illuminate\Database\Eloquent\Casts\Attribute; 」を忘れると、動作しません。
下記のコードは先ほどのアクセサとミューテータをひとまとめに記載し、
nameが設定されたら「さん」を連結し、nameへアクセスして取得すると「Hello!」が連結されて返ってきます。
かなり⾒た⽬がスッキリしました。

コード

use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Model
{
    public function name(): Attribute
       {
           return new Attribute(
               // アクセッサ
               get: fn ($attribute) => $attribute . 'Hello!',
               // ミューテータ
               set: fn ($attribute) => $attribute . 'さん',
           );
       }
}

動作確認

> $user = new App\Models\User();
= App\Models\User {#3788}
> $user -> name = 'テスト';
= "テスト"
> $user -> name
= "テストさんHello!"

スコープ

Laravelで⾔うスコープとはwhere句のようなものです。
全然order_byも使えるので半分うそです。
主に活躍するのは、⼤爆発しちゃいがち検索クエリだと思います。
スコープには下記の2種類があります。

  • グローバルスコープ
  • ローカルスコープ

グローバルスコープ

グローバルスコープは設定したモデルへの全てのクエリに対して、適⽤される条件です。
下記のコードはまず、Scopesディレクトリ配下にTestScopeというファイルを作成しています。

php artisan make:scope TestScope

この中Scopeは「is_public = 1」として、公開済みの記事のみを取得するwhere句を指定しています。

コード

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class TestScope implements Scope
{
     public function apply(Builder $builder, Model $model)
     {
         $builder->where('is_public', '=', 1);
     }
}

このTestScopeをArticleモデルへ適⽤させることで、Articleモデルへの全てのクエリにScopeで定義した条件を適⽤させることができます。
bootedメソッドのオーバーライドとaddGlobalScopeメソッドの指定が必要で、addGlobalScopeメソッドはスコープのインスタンスのみを引数とします。

use App\Models\Scopes\TestScope;

class Article extends Model
{
     protected static function booted()
    {
        static::addGlobalScope(new TestScope);
    }
{

下記の記述でグローバルスコープの解除も実現できます。

Article::withoutGlobalScope(TestScope::class)->get();

ローカルスコープ

ローカルスコープはグローバルスコープと違って、全てのクエリに適⽤されるものではなく、必要性があるときのみ、使⽤することができます。
基本的にはこちらの⽅が多く使うかなと個⼈的には感じてます。
まず、モデルにscopeを作成します。
このとき、モデルメソッドの最初に「scope」と付けてください

class User extends Model
{
    // 20代のユーザーを取得する
    public function scopeTwenties(Builder $query)
    {
        $query->whereBetween('age', [20, 29]);
    }

    // 性別が1のユーザーを取得する
    public function scopeMale(Builder $query)
    {
         $query->where('gender', '=', 1);
    }
{

呼び出すときは下記のようにスコープ定義で付けた先頭の「scope」は外します。
これで、同じクエリを書く⼿間を省き、Serviceファイルへの記載などが減ります。

$users = User::twenties()->get();
$users = User::male()->get();

さらに、このローカルスコープの良いところはチェーンすることができます。
そのため、個⼈的にはかなり⼤きめの検索のユースケースの際にはいくつかに切り分けて、チェーンしてあげることで、メソッドの肥⼤化や可読性の低下を対策できるかなと思います。

// 20代で性別が1のユーザーを取得する
$users = User::twenties()->male()->get();

さいごに

Eloquentの機能は書き⽅にそれなりのがっちりとしたタイプヒントなどの秩序を与えてくれているので、これはありがたポイントです。

⾃分的にはかなりEloquentの便利さに最近ハマっているのですが、これらをみたときに気づいた⼈も多いと思います。ほとんど、Modelへの記載になっています。

MVCモデルと⾔われるLaravelはFatController問題によってユースケースによってUseCaseに分けたり、Serviceに分けたり(ここの切り分け⽅の賛否両論は置いといて)しています。

書いといて元も⼦もないことを⾔うとEloquentの機能によってそこの処理は確かにスマートになりました
が、結局はFatModel問題になっていく気もしなくはありません。
責務を分けるという点では、いいんですかね?

プロジェクトの規模にもよるとは思いますが、ここもよくわかっていないので勉強していきたいです。

参考

Laravel10 ReaDouble