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\Scope を use して、メソッドに #[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 / public(privateは不可) |
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は除外されます。protectedかpublicにしてください- スコープの戻り値は「同じクエリビルダ」か
voidに統一する。別のインスタンスを返すとチェーンが途切れます scopeプレフィックス方式では、呼び出し時にscopeを外す。scopePopularはUser::scopePopular()ではなくUser::popular()です- グローバルスコープのselect句上書き。カラムを足すときは
addSelectを使います orWhereとの組み合わせ。素直につなぐとAND/ORのグループ化が意図とずれます。クロージャか高階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での論理削除を理解する:実装方法とメリットを徹底解説

コメント