Laravelのスコープとは?ローカル・グローバルスコープの使い方を徹底解説

実装・応用テクニック

Laravelのスコープとは

スコープ(Scope)は、Eloquentのクエリ条件に名前を付けてモデル側にまとめておき、何度でも使い回せるようにする仕組みです。where('active', 1) のような条件をコントローラーに散らかす代わりに、User::active() と書けるようになります。

スコープには2種類あります。

  • ローカルスコープ:呼び出したときだけ効く。モデルにメソッドとして定義する
  • グローバルスコープ:そのモデルのすべてのクエリに自動で効く。クラスとして定義する

なお、Laravel 12 でローカルスコープの書き方が変わりました。従来の scopeXxx() という命名規則ではなく、#[Scope] アトリビュートを付ける方式が公式ドキュメントの標準になっています。旧記法も動きますが、新規に書くなら新方式です。この記事では両方を対応表付きで解説します。


TL;DR

ローカルスコープ モデルのメソッドに #[Scope] を付ける(Laravel 12以降)
Laravel 11以前は scopeActive() のように scope プレフィックス
グローバルスコープ php artisan make:scope ActiveScope で生成し、モデルに #[ScopedBy([ActiveScope::class])] を付ける
呼び出し方 User::active()->popular()->get()(チェーン可)
グローバルスコープの解除 User::withoutGlobalScope(ActiveScope::class)->get()
よくある罠 #[Scope] を付けたメソッドを private にすると認識されない
orWhere と組み合わせると条件のグループ化がずれる

ローカルスコープの書き方

Laravel 12以降:#[Scope] アトリビュート

Illuminate\Database\Eloquent\Attributes\Scopeuse して、メソッドに #[Scope] を付けます。第1引数には Builder $query を受け取ります。

<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * 人気ユーザーだけに絞り込む
     */
    #[Scope]
    protected function popular(Builder $query): void
    {
        $query->where('votes', '>', 100);
    }

    /**
     * 有効なユーザーだけに絞り込む
     */
    #[Scope]
    protected function active(Builder $query): void
    {
        $query->where('active', 1);
    }
}

メソッド名がそのままスコープ名になります。popular() と定義したら User::popular() で呼べます。

Laravel 11以前:scope プレフィックス

Laravel 11以前は、メソッド名の頭に scope を付けるという命名規則で表現していました。

class User extends Model
{
    public function scopePopular(Builder $query): void
    {
        $query->where('votes', '>', 100);
    }
}

// 呼び出すときは scope を外して先頭を小文字にする
$users = User::popular()->get();

2つの記法の対応表

#[Scope] アトリビュート scope プレフィックス
導入 Laravel 12で追加 Laravel 11以前の標準(現在も動作)
定義 #[Scope] protected function popular() public function scopePopular()
呼び出し User::popular()(どちらも同じ)
可視性 protected / publicprivateは不可) public
現行バージョンでの動作 動く 動く(後方互換あり)

Laravel 13時点でも、モデル側の解決処理は「scope+メソッド名 が存在するか」と「#[Scope] が付いているか」の両方を見ています。つまり旧記法のコードをいますぐ書き換える必要はありません。混在も動作します。ただし公式ドキュメントのサンプルはすべて #[Scope] に置き換わっているため、新規コードは新方式に寄せておくと後々読みやすくなります。


ローカルスコープの呼び出し方

定義したスコープは、そのままモデルの静的メソッドのように呼び出せます。複数つなげることもできます。

use App\Models\User;

$users = User::popular()->active()->orderBy('created_at')->get();

orで組み合わせるときの注意

or でつなぐ場合は、条件のグループ化がずれないようクロージャで囲む必要があります。

$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

毎回これを書くのは面倒なので、Laravelには「高階 orWhere」という書き方が用意されています。クロージャなしで同じ結果になります。

$users = User::popular()->orWhere->active()->get();

引数を受け取る動的スコープ

スコープに値を渡したいときは、$query後ろに引数を追加します。

<?php

namespace App\Models;

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

class User extends Model
{
    #[Scope]
    protected function ofType(Builder $query, string $type): void
    {
        $query->where('type', $type);
    }
}

呼び出し側では、そのまま引数を渡します。

$users = User::ofType('admin')->get();

グローバルスコープの書き方

グローバルスコープは、そのモデルに対するすべてのクエリに条件を足します。SoftDeletes(論理削除)が「削除済みを自動で除外する」のも、内部的にはグローバルスコープです。

1. クラスを生成する

php artisan make:scope AncientScope

app/Models/ ディレクトリがあるプロジェクトでは app/Models/Scopes/ に、無い場合は app/Scopes/ に生成されます。make:scope はLaravel 10で追加されたコマンドです。

2. applyメソッドに条件を書く

生成されるクラスは Illuminate\Database\Eloquent\Scope インターフェースを実装しており、apply メソッドを1つ持ちます。

<?php

namespace App\Models\Scopes;

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

class AncientScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('created_at', '<', now()->subYears(2000));
    }
}

select句にカラムを足すスコープを書く場合は、select ではなく addSelect を使ってください。select だと元のクエリのselect句を丸ごと上書きしてしまいます。

3. モデルに適用する

一番簡潔なのは #[ScopedBy] アトリビュートです。

<?php

namespace App\Models;

use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([AncientScope::class])]
class User extends Model
{
    //
}

booted メソッドで手動登録することもできます。条件によって登録を切り替えたい場合はこちらです。

class User extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope(new AncientScope);
    }
}

これで User::all() のような単純な取得でも、次のように where が自動で付いたSQLが発行されるようになります。

select * from `users` where `created_at` < ?

クラスを作らない無名グローバルスコープ

条件が単純でクラスを切るほどでもない場合は、クロージャで登録できます。第1引数にスコープ名(任意の文字列)を渡します。

class User extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope('ancient', function (Builder $builder) {
            $builder->where('created_at', '<', now()->subYears(2000));
        });
    }
}

グローバルスコープを一時的に外す

グローバルスコープは常に効いてしまうため、「管理画面では全件見たい」といった場面では明示的に外します。

// クラスで定義したスコープを外す
User::withoutGlobalScope(AncientScope::class)->get();

// クロージャで定義したスコープは、登録時の名前を渡す
User::withoutGlobalScope('ancient')->get();

// すべて外す
User::withoutGlobalScopes()->get();

// 指定したものだけ残して外す
User::withoutGlobalScopesExcept([AncientScope::class])->get();

「レコードがあるはずなのに取得できない」というときは、まず withoutGlobalScopes() を付けて結果が変わるか試すと、グローバルスコープが原因かどうかを切り分けられます。


withAttributesでスコープの条件を新規作成にも反映する

スコープで絞り込んだ条件を、そのスコープ経由で作るモデルの初期値にもしたい、というケースがあります。withAttributes を使うと、where条件の追加とデフォルト属性の設定を同時に行えます。

<?php

namespace App\Models;

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

class Post extends Model
{
    #[Scope]
    protected function draft(Builder $query): void
    {
        $query->withAttributes([
            'hidden' => true,
        ]);
    }
}
$draft = Post::draft()->create(['title' => 'In Progress']);

$draft->hidden; // true

where条件は付けず、新規作成時の属性だけを設定したい場合は asConditions: false を渡します。

$query->withAttributes([
    'hidden' => true,
], asConditions: false);

ローカルとグローバル、どちらを使うか

やりたいこと 選ぶもの
「公開中の記事だけ」など、呼びたいときだけ絞りたい ローカルスコープ
同じ絞り込みを複数の画面で使い回したい ローカルスコープ
論理削除済みを常に除外したい グローバルスコープ
マルチテナントで、常に自社のデータだけに限定したい グローバルスコープ
絞り込み条件に引数を渡したい ローカルスコープ(動的スコープ)

判断に迷ったらローカルスコープを選ぶのが無難です。グローバルスコープは「書いていない条件が勝手に付く」状態を作るため、後からコードを読む人が原因を追いにくくなります。「付け忘れると事故になる条件」だけをグローバルにするのが目安です。


つまずきやすいポイント

  • #[Scope] を付けたメソッドを private にすると認識されない。Laravelはリフレクションでアトリビュートを探しますが、private は除外されます。protectedpublic にしてください
  • スコープの戻り値は「同じクエリビルダ」か void に統一する。別のインスタンスを返すとチェーンが途切れます
  • scope プレフィックス方式では、呼び出し時に scope を外すscopePopularUser::scopePopular() ではなく User::popular() です
  • グローバルスコープのselect句上書き。カラムを足すときは addSelect を使います
  • orWhere との組み合わせ。素直につなぐと ANDOR のグループ化が意図とずれます。クロージャか高階 orWhere を使ってください

よくある質問

Laravel 11以前のプロジェクトで #[Scope] は使えますか?

使えません。Illuminate\Database\Eloquent\Attributes\Scope というクラス自体がLaravel 12で追加されたものです。Laravel 11以前では scope プレフィックス方式で書いてください。

既存の scopeXxx は書き換えないといけませんか?

必要ありません。Laravel 13時点でも scope プレフィックス方式は動作します。1つのモデル内で両方の記法を混在させることもできます。

クエリスコープとモデルスコープは違うものですか?

どちらもこの記事で扱っているEloquentのスコープを指す呼び方で、実体は同じです。公式ドキュメントでは「Query Scopes」という章名でまとめられています。

スコープとクエリビルダの where はどちらが速いですか?

生成されるSQLは同じなので、速度差はありません。スコープは可読性と再利用性のための仕組みで、パフォーマンス最適化の手段ではありません。

グローバルスコープが効いているか確認するには?

toSql() で発行されるSQLを確認するか、withoutGlobalScopes() を付けて結果が変わるかを比べます。


まとめ

  • スコープはクエリ条件に名前を付けて再利用する仕組みで、ローカルグローバルの2種類がある
  • ローカルスコープはLaravel 12から #[Scope] アトリビュート方式が標準。旧 scope プレフィックスも引き続き動作する
  • グローバルスコープは make:scope で生成し、#[ScopedBy] でモデルに適用する
  • グローバルスコープは withoutGlobalScope 系で外せる。想定外の絞り込みを疑ったらまずここを確認する
  • 迷ったらローカルスコープ。グローバルは「付け忘れが事故になる条件」に限定する

関連記事:Laravel Eloquentとは?使い方の基本からリレーション・クエリビルダとの違いまで徹底解説make:scope — スコープを作成するLaravelでの論理削除を理解する:実装方法とメリットを徹底解説

レン (Wren)

こんにちは。レンです。

Laravelのコードの森に住んでいる、小さな案内役です。
ルーティングの枝やクラスの影を歩きながら、コードの流れや仕組みを眺めています。

このサイトでは、Laravelの基本から実装のコツまで、開発で役立つポイントを静かに整理しています。
難しいことを増やすのではなく、コードの見通しが少し良くなるヒントを届けるのが役目です。

「この処理はどこに書くのがいいのか」
「Laravelではどう考えると整理できるのか」

そんな疑問に、小さなメモを残すような気持ちで記事を書いています。

コードを書いている途中で迷ったとき、
このサイトが少し立ち止まって整理できる場所になればうれしいです。

レン (Wren)をフォローする

コメント