Laravelのグローバルスコープとモデル結合ルートについて

Laravelのグローバルスコープとモデル結合ルートについて

どうもこんにちは塚本です。

Laravelを使っていると、グローバルスコープを用いる機会があると思います。
また、モデル結合ルートを用いると、Controllerの記述がシンプルになり大変便利です。

例えば、ブログサイトを構築するとして、以下の2つの画面があると思います。

  • 読者に見てもらうフロント画面
  • 筆者がアクセスする管理画面

この運用の場合、非表示設定した記事については以下のようなシチュエーションがあると思います。

  • フロント画面では自動的に非表示になって欲しい
  • 管理画面では常に表示されていてほしい

それについて、ベストプラクティスが分からなかったのですが、
現時点で僕がやっている方法をメモしておきます。

まずは、グローバルスコープとモデル結合ルートの簡単な説明をしたいと思います。

グローバルスコープ

投稿記事一覧を取得したい場合に、
非表示設定しているものは取得したくない場合がありますよね。

そういった場合において、
単発で使うなら、投稿記事をPostモデルとして、以下のようにすると思います。

$posts = Post::where('is_hidden', false)->get();

ここで、グローバルスコープを用いると、
is_hiddenfalseのものを取得しないようにデフォルトで設定することが可能です。

グローバルスコープの設定例

以下のようにするとグローバルスコープが追加されます。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    // ここを追加する            
    protected static function boot(): void
    {
        parent::boot();

        static::addGlobalScope('hidden', static function(Builder $builder){
            $builder->where('is_hidden', '=', false);
        });
    }
    
    // 省略
}

グローバルスコープの設定を外したい場合

非公開設定のものも含め、投稿を全て取得したいような場合は以下のコードでグローバルスコープの設定を外すことが可能です。

$posts = Post::withOutGlobalScope('hidden')->get();

モデル結合ルート(暗黙の結合)

モデル結合ルートを用いると、
ルーティングの中でEloquentモデルを簡単に取得することが可能です。

例えば、ブログの管理画面で、ID1の記事を編集したい場合を考えてみます。
以下のソースコードを見ると分かりやすいかと思います。

route.php

 // admin.post.
Route::group(['prefix' => 'admin', 'as' => 'admin.'], static function (): void {
        Route::group(['prefix' => 'post', 'as' => 'post.'], static function (): void {
            Route::get('/edit/{post}', [PostController::class, 'edit'])->name('edit');
        });
    });
});

View

<a href="{{ route('admin.post.edit', ['post' => 1]) }}">
    記事を編集する
</a>

PostController.php

上記のようなルーティング設定を行うことで、
id1の記事が自動的に注入されています。

public function edit(Post $post)
{
    // Postモデルが自動的に注入されている
    $title = $post->title;
}

ちなみに、モデル結合ルートを使わない場合は以下のような形になると思います。

public function edit(int $post_id)
{
    $post = Post::find($post_id);

    $title = $post->title;
}

優先度合いについて

ここからが本題です。
グローバルスコープとモデル結合ルートという便利な2つの機能を簡単に紹介しました。

この2つを同時に使用すると果たしてどうなるでしょうか?
答えとしては、グローバルスコープが優先されます。
非表示設定になっているものにアクセスすると404になります。

resolveRouteBinding

フロント画面ではこの動作でも良いのですが
管理画面では、モデル結合ルートを用いると、一回非表示した記事は編集できないということになってしまいます。

なので、resolveRouteBindingを用いて管理画面でのみ
グローバルスコープを無効化するコードをモデルに追加しました。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected static function boot(): void
    {
        static::addGlobalScope('hidden', static function(Builder $builder){
            $builder->where('is_hidden', '=', false);
        });
    }
    
    // ここから追加
    public function resolveRouteBinding($value, $field = null): Model
    {
        if (Auth::guard('admins')->check()) {
            /** @var Builder<static> $query */
            $query = $this->resolveRouteBindingQuery($this, $value, $field);

            return $query
                ->withoutGlobalScope('is_hidden')
                ->first();
        }

        return parent::resolveRouteBinding($value, $field);
    }
}

まとめ

そもそも管理画面でモデル結合ルートを使わないというのも解決策にはなります。
ただ、withOutGlobalScope()の付け忘れ等もあったりするので
今回のような方法で対応していくのが良いのではないかと思っています。

ただ、これがベストな方法なのか?と言われるとそうではないとも思います…。

参考記事