【Laravel】Eloquentのスコープ(scope)の使い方|ローカルスコープ・グローバルスコープ完全解説

はじめに

Laravelで「アクティブなユーザーだけ取得する」「論理削除されていないレコードだけ取得する」といった条件を、毎回 where で書いていませんか?

Eloquentのスコープ(scope)を使うと、よく使うクエリ条件をメソッドとして定義し、可読性が高くシンプルなコードで再利用できます。

この記事では、ローカルスコープ・グローバルスコープ・動的スコープの3種類を、実務で使えるコード例とともに解説します。

スコープの種類

種類適用タイミング主な用途
ローカルスコープ明示的に呼び出したときだけ再利用したい任意の条件
グローバルスコープ常に自動適用論理削除・テナント分離など
動的スコープ引数付きで呼び出すときだけ条件が可変なフィルタ

ローカルスコープ

モデルに scope プレフィックスのメソッドを定義します。使うときはプレフィックスなしで呼び出します。

定義

<?php

namespace App\\Models;

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

class User extends Model
{
    // アクティブなユーザーだけ取得
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('is_active', true);
    }

    // 管理者だけ取得
    public function scopeAdmin(Builder $query): Builder
    {
        return $query->where('role', 'admin');
    }
}

使い方

// アクティブなユーザーを取得
$users = User::active()->get();

// メソッドチェーンで複数スコープを組み合わせる
$admins = User::active()->admin()->orderBy('name')->get();

発行されるSQLはこうなります。

SELECT * FROM users WHERE is_active = 1 AND role = 'admin' ORDER BY name ASC;

ローカルスコープを使うメリット

条件をメソッドとして名前をつけることで、コードの意図が伝わりやすくなります。

// スコープなし:何を意図しているか読みにくい
$users = User::where('is_active', true)->where('role', 'admin')->get();

// スコープあり:意図が明確
$users = User::active()->admin()->get();

動的スコープ(引数付きスコープ)

スコープのメソッドに引数を追加すると、条件を動的に変えられます。

定義

public function scopeOfRole(Builder $query, string $role): Builder
{
    return $query->where('role', $role);
}

public function scopeCreatedAfter(Builder $query, string $date): Builder
{
    return $query->where('created_at', '>=', $date);
}

使い方

// 役割を引数で指定
$editors = User::ofRole('editor')->get();
$viewers = User::ofRole('viewer')->get();

// 日付を引数で指定
$recentUsers = User::createdAfter('2025-01-01')->get();

// 組み合わせも可能
$recentEditors = User::active()->ofRole('editor')->createdAfter('2025-01-01')->get();

グローバルスコープ

グローバルスコープはモデルのすべてのクエリに自動的に適用されます。論理削除(SoftDeletes)もグローバルスコープとして実装されています。

クラスで定義する方法(推奨)

Scope インターフェースを実装したクラスを作成します。

<?php

namespace App\\Models\\Scopes;

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

class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('is_active', true);
    }
}

モデルの booted() メソッドで登録します。

<?php

namespace App\\Models;

use App\\Models\\Scopes\\ActiveScope;
use Illuminate\\Database\\Eloquent\\Model;

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

これでモデルのすべてのクエリに WHERE is_active = 1 が自動的に追加されます。

User::all();
// SELECT * FROM users WHERE is_active = 1;

User::where('role', 'admin')->get();
// SELECT * FROM users WHERE role = 'admin' AND is_active = 1;

クロージャで定義する方法

シンプルな条件であれば、クロージャで直接定義することもできます。

protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('is_active', true);
    });
}

グローバルスコープを一時的に無効化する

グローバルスコープをすべて取得したい場合は withoutGlobalScope または withoutGlobalScopes を使います。

// 特定のスコープだけ無効化
User::withoutGlobalScope(ActiveScope::class)->get();
User::withoutGlobalScope('active')->get(); // クロージャで登録した場合

// すべてのグローバルスコープを無効化
User::withoutGlobalScopes()->get();

// 複数を指定して無効化
User::withoutGlobalScopes([ActiveScope::class, AnotherScope::class])->get();

SoftDeletes(論理削除)との関係

SoftDeletes トレイトは内部的にグローバルスコープを使って deleted_at IS NULL を自動付与しています。削除済みレコードを取得したい場合は withTrashed()onlyTrashed() を使いますが、これもグローバルスコープを解除する仕組みです。

// 削除済みも含めて取得
User::withTrashed()->get();

// 削除済みのみ取得
User::onlyTrashed()->get();

SoftDeletes の内部実装を知っておくと、自前のグローバルスコープの設計に役立ちます。

実務でよく使うパターン

マルチテナント(テナント分離)

SaaS系アプリで、ログイン中のテナントのデータだけ取得するグローバルスコープを定義する例です。

<?php

namespace App\\Models\\Scopes;

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

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if (auth()->check()) {
            $builder->where('tenant_id', auth()->user()->tenant_id);
        }
    }
}

これをベースモデルに適用することで、全モデルでテナント分離が保証されます。

ローカルスコープをチェーンしてフィルタを構築

$query = Post::query();

if ($request->filled('status')) {
    $query->where('status', $request->status);
}

if ($request->filled('category')) {
    $query->ofCategory($request->category);
}

$posts = $query->latest()->paginate(20);

スコープをメソッドとして定義しておくと、コントローラでのクエリ組み立てがシンプルになります。

スコープをテストする

スコープはメソッドとして分離されているため、単体テストが書きやすいです。

public function test_active_scope_returns_only_active_users(): void
{
    User::factory()->create(['is_active' => true]);
    User::factory()->create(['is_active' => false]);

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

    $this->assertCount(1, $users);
    $this->assertTrue($users->first()->is_active);
}

スコープを使うべき場面・避けるべき場面

使うべき場面

  • 同じ where 条件を複数箇所で使い回しているとき
  • 条件の意図を名前で表現したいとき
  • すべてのクエリに自動で条件を付けたいとき(グローバルスコープ)

避けるべき場面

  • 1箇所でしか使わない単純な条件(where で十分)
  • スコープの中が複雑すぎてテストしにくくなっているとき(責務が大きすぎるサイン)
  • グローバルスコープが予期しないクエリに影響していることに気づかずバグになるとき

グローバルスコープは便利ですが、「なぜか取得できない」というバグの原因になることもあります。withoutGlobalScope の使い方を理解しておくことが重要です。

まとめ

種類定義方法呼び出し方主な用途
ローカルスコープscopeXxx() メソッドModel::xxx()再利用したい任意の条件
動的スコープscopeXxx($query, $arg) メソッドModel::xxx($value)引数で条件を変えたい場合
グローバルスコープScope クラスまたはクロージャ自動適用全クエリに共通の条件

スコープはEloquentのクエリをより読みやすく・再利用しやすくするための強力な機能です。where を何度も書いているならスコープに切り出すタイミングです。

LaravelLaravel

Posted by 千原 耕司