Laravel Policyの使い方完全ガイド|作成・登録・認可の実装パターン

実装・応用テクニック
  • カテゴリ: 認可(Authorization)
  • 掲載バージョン: Laravel 13(Laravel 11以降ほぼ共通の挙動)・PHP 8.4
  • 関連クラス / コマンド: php artisan make:policy / Illuminate\Support\Facades\Gate / Illuminate\Auth\Access\Response
  • 関連: Laravel Gate / Laravelのアクセス制御 / abort関数
  • 変更履歴: Laravel 11でAuthServiceProviderが既定のスケルトンから削除され、命名規則によるPolicyの自動検出が標準に。Laravel 12で#[UsePolicy]属性が、Laravel 13でGate::guessPolicyNamesUsingなど検出ロジックのカスタマイズがそれぞれ利用可能。

要点(TL;DR)

  • PolicyはEloquentモデルなど特定のリソースに対する認可ロジックをまとめるクラス。生成は php artisan make:policy PostPolicy --model=Post
  • Laravel 11以降は命名規則が合っていれば登録不要App\Models\PostApp\Policies\PostPolicyを自動検出)。手動登録が必要ならAppServiceProviderboot()Gate::policy(Post::class, PostPolicy::class)
  • 呼び出しは $request->user()->can('update', $post) / コントローラの $this->authorize('update', $post) / Bladeの @can('update', $post) / ルートミドルウェアの ->middleware('can:update,post') のいずれか
  • よくある罠:
    • 新規プロジェクトにAuthServiceProviderが存在しない(Laravel 11以降)。古い記事の手順どおりに探すと迷子になる
    • Policyクラス名・配置ディレクトリが規約とズレていると自動検出されず、権限チェックが常にfalseになる
    • 未ログイン(ゲスト)ユーザーはメソッド引数を?User $userにしないと呼び出し前に自動で拒否される

概要:LaravelにおけるPolicyとは

Policyは、特定のEloquentモデルやリソースに関連付けて認可ロジックを整理するためのクラスです。Laravelの認可機能にはGateとPolicyの2種類があり、公式ドキュメントでは「Gateはルート、Policyはコントローラのようなもの」と例えられています。管理画面の表示可否のようにモデルに紐づかないアクションはGate、投稿の編集・削除のように特定のモデルインスタンスに対する権限判定はPolicyという使い分けが基本です。

1つのPolicyクラスにviewAnyviewcreateupdatedeleterestoreforceDeleteといったCRUD相当のメソッドをまとめて定義できるため、コントローラやBladeテンプレートに散らばりがちなif文の権限判定を1か所に集約でき、テストもしやすくなります。

Policyの作成

make:policy Artisanコマンドで生成します。存在しなければapp/Policiesディレクトリも自動作成されます。

# 空のPolicyクラスを生成
php artisan make:policy PostPolicy

# viewAny/view/create/update/delete/restore/forceDeleteの雛形付きで生成
php artisan make:policy PostPolicy --model=Post

--modelオプションを付けると、各アクションに対応するメソッドの雛形(引数の型ヒント込み)が自動で入るため、CRUD一式を実装する場合はこちらが効率的です。

Policyの登録:自動検出と手動登録

自動検出(既定・推奨)

Laravel 11以降、新規プロジェクトのスケルトンからAuthServiceProvider自体が削除されています。命名規則(モデルと同じ階層以上にあるPoliciesディレクトリに、モデル名+Policyというクラス名)に従っていれば、登録コードを書かなくてもLaravelが自動でPolicyを見つけます。

モデル期待されるPolicyクラス
App\Models\PostApp\Policies\PostPolicy
App\Models\UserApp\Policies\UserPolicy

検出ロジック自体を変更したい場合は、AppServiceProviderboot()Gate::guessPolicyNamesUsing()にコールバックを渡してカスタマイズできます。

手動登録(Gate::policy)

命名規則から外れる場合や明示的に登録したい場合は、AppServiceProviderboot()メソッドでGate::policy()を呼び出します(旧来の記事に出てくるAuthServiceProviderではない点に注意)。

// app/Providers/AppServiceProvider.php
use App\Models\Order;
use App\Policies\OrderPolicy;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::policy(Order::class, OrderPolicy::class);
}

属性で登録(#[UsePolicy])

モデルクラス自体に属性を付けて、対応するPolicyを明示することもできます。

use App\Policies\OrderPolicy;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;

#[UsePolicy(OrderPolicy::class)]
class Order extends Model
{
    //
}

Policyメソッドの書き方

Policyクラスはサービスコンテナ経由で解決されるため、コンストラクタで依存を型ヒントすれば自動注入されます。

メソッド対応アクション引数の例
viewAny一覧表示User $user
view個別詳細表示User $user, Post $post
create新規作成User $user(モデルなし)
update更新User $user, Post $post
delete削除User $user, Post $post
restoreソフトデリートの復元User $user, Post $post
forceDelete完全削除User $user, Post $post
namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    // createはモデルインスタンスを受け取らない
    public function create(User $user): bool
    {
        return $user->role === 'writer';
    }
}

理由付きで拒否する(Response)

単純なboolの代わりにIlluminate\Auth\Access\Responseを返すと、拒否理由をエラーメッセージとしてそのまま画面や例外に伝播できます。ステータスコードを変えたい場合はdenyWithStatus()、404として隠したい場合はdenyAsNotFound()が使えます。

use Illuminate\Auth\Access\Response;

public function update(User $user, Post $post): Response
{
    return $user->id === $post->user_id
        ? Response::allow()
        : Response::deny('この投稿の所有者ではありません。');
}

未ログイン(ゲスト)ユーザーを許可する

既定では未認証リクエストのPolicy呼び出しは自動的にfalseを返します。ゲストにもチェックを通したい場合は、引数をnullable(?User $user)にします。

public function view(?User $user, Post $post): bool
{
    // 未ログインでも公開投稿なら閲覧可、それ以外は所有者のみ
    return $post->is_public || $user?->id === $post->user_id;
}

before()で管理者に全権限を与える

Policyクラスにbefore()メソッドを定義すると、他のどのメソッドよりも先に評価されます。true/falseを返すとその結果が最終判定になり、nullを返すと通常のメソッドの判定にフォールバックします。管理者への一括許可に多用されます。

public function before(User $user, string $ability): ?bool
{
    if ($user->isAdministrator()) {
        return true;
    }

    return null;
}

Policyを使う4つの方法

1. コントローラ(can / cannot / authorize)

use App\Models\Post;
use Illuminate\Http\Request;

public function update(Request $request, Post $post)
{
    // 方法A: Userモデルのcan/cannot
    if ($request->user()->cannot('update', $post)) {
        abort(403);
    }

    // 方法B: コントローラのauthorizeヘルパ(未許可なら自動で403例外)
    $this->authorize('update', $post);

    // 更新処理...
}

public function store(Request $request)
{
    // createのようにモデルを持たないアクションはクラス名を渡す
    $this->authorize('create', Post::class);

    // 作成処理...
}

2. Bladeテンプレート(@can / @cannot / @canany)

@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">編集</a>
@elsecan('create', App\Models\Post::class)
    <a href="{{ route('posts.create') }}">新規作成</a>
@endcan

@canany(['update', 'delete'], $post)
    <!-- 更新・削除いずれかが可能な場合 -->
@endcanany

3. ルートミドルウェア(can:)

use App\Models\Post;

// 文字列指定
Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('can:update,post');

// メソッドチェーン版(可読性が高い)
Route::put('/posts/{post}', [PostController::class, 'update'])
    ->can('update', 'post');

4. 追加の引数を渡す

第2引数を配列にすると、先頭要素でPolicyを特定し、残りの要素がメソッドの追加引数として渡されます。

public function update(User $user, Post $post, int $categoryId): bool
{
    return $user->id === $post->user_id
        && $user->canUpdateCategory($categoryId);
}

// 呼び出し側
$this->authorize('update', [$post, $request->category_id]);

テスト例(Pest)

use App\Models\Post;
use App\Models\User;

it('allows the owner to update their own post', function () {
    $user = User::factory()->create();
    $post = Post::factory()->for($user)->create();

    expect($user->can('update', $post))->toBeTrue();
});

it('denies other users from updating a post', function () {
    $owner = User::factory()->create();
    $other = User::factory()->create();
    $post = Post::factory()->for($owner)->create();

    expect($other->can('update', $post))->toBeFalse();
});

よくある落とし穴・注意

  • AuthServiceProviderが見つからない:Laravel 11以降の新規プロジェクトには存在しません。手動登録が必要な場合はAppServiceProviderboot()Gate::policy()を書きます。
  • 自動検出されない:モデルとPolicyのディレクトリ・命名規則(App\Models\XxxApp\Policies\XxxPolicy)がずれていると検出に失敗し、Gate側のフォールバックにも一致しなければ常に拒否されます。
  • ゲストが弾かれる:未ログイン状態を許可したいメソッドは引数をnullable(?User $user)にしないと、Policy本体が呼ばれる前に自動でfalse扱いになります。
  • before()が呼ばれないケース:チェック対象のアビリティ名に一致するメソッドがPolicyクラスに存在しない場合、before()自体がスキップされます。
  • createの引数ミスcreateのようにモデルインスタンスがまだ存在しないアクションは、can()authorize()クラス名(文字列)を渡す必要があります。インスタンスを渡すコードをそのまま流用すると型エラーになります。

Gate と Policy の使い分け

観点GatePolicy
定義場所クロージャ(AppServiceProvider専用クラス(app/Policies
向いている用途モデルに紐づかない単発の判定(管理画面表示など)特定モデルのCRUD権限をまとめて管理
登録Gate::define()自動検出 or Gate::policy() / #[UsePolicy]
呼び出し方法Gate::allows() などcan() / authorize() / @can(内部的にGateへ委譲)

実務ではどちらか一方だけを使うのではなく、モデル単位の権限はPolicy、それ以外の横断的な判定はGateという組み合わせが一般的です。

トラブルシュート(エラー別)

症状/エラー原因対処
This action is unauthorized.(403)PolicyメソッドがfalseまたはResponse::deny()を返した該当メソッドの条件を確認。理由文言を出したい場合はResponse::deny('メッセージ')を使う
Policyのメソッドが呼ばれず常に拒否されるクラス名・配置ディレクトリが命名規則から外れ自動検出に失敗Gate::policy(Model::class, Policy::class)で明示登録、または#[UsePolicy]属性を付与
Target class [App\Providers\AuthServiceProvider] does not existLaravel 11以降のスケルトンに存在しないクラスを古い手順どおり参照AppServiceProviderboot()Gate::policy()を書く、または自動検出に任せる
ゲストアクセス時に想定外の403Policyメソッドの第1引数がUser型(非nullable)引数を?User $userにし、$user?->idのようにnullセーフに判定
create系だけ動かない呼び出し側でモデルインスタンスを渡しているcan('create', Post::class)のようにクラス名を渡す

よくある質問(FAQ)

Q. LaravelのPolicyとは何ですか?
特定のEloquentモデルやリソースに対する認可(そのユーザーがそのアクションを行ってよいか)のロジックを1つのクラスにまとめる仕組みです。投稿の編集・削除権限のように、モデルインスタンスごとに判定条件が変わる場合に向いています。

Q. PolicyとGateはどちらを使えばいいですか?
特定のモデル・リソースに紐づく判定(投稿の更新可否など)はPolicy、モデルに紐づかない単発の判定(管理ダッシュボードの表示可否など)はGateが基本方針です。両方を併用しても問題ありません。

Q. Policyを登録しなくても動くことがあるのはなぜですか?
Laravel 11以降は、モデルとPolicyの命名規則(App\Models\Xxxに対してApp\Policies\XxxPolicy)が一致していれば自動検出されるためです。命名や配置がずれている場合、あるいは検出ロジックを独自にしたい場合のみGate::policy()#[UsePolicy]属性での明示登録が必要になります。

Q. 未ログインユーザーにもPolicyのチェックを通したい場合は?
Policyメソッドの第1引数をnullableな?User $userにします。既定では未認証リクエストは呼び出し前に自動でfalse判定されるため、ゲスト向けのロジックを書きたい場合は必須の対応です。

Q. 管理者だけ全操作を許可したい場合は?
Policyクラスにbefore(User $user, string $ability): ?boolを定義し、管理者ならtrue、それ以外はnullを返します。nullを返すと通常の各メソッドの判定に処理が続きます。

関連記事

参考リンク

レン (Wren)

こんにちは。レンです。

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

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

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

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

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

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

コメント