> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# パッケージの認可ポリシー登録と上書き

> Laravel 13のGate実装を読み解き、パッケージのPolicy登録、ユーザーモデルへの依存を避ける設計、解決の優先順位、利用アプリでの上書きとテストを解説します。

パッケージが提供するモデルに認可を追加しつつ、利用アプリケーションには独自のルールへ差し替える余地を残したいことがあります。モデルとPolicyの対応を明示し、ユーザーの判定は認可を実行する時点で行う構造にすると、登録処理とリクエストごとの判断を分離できます。

このページは、Laravel公式ドキュメントのデフォルトブランチ `13.x` と、2026年11月10日に確認したFrameworkの最新リリース `v13.42.0` の公式実装・テストを参照しています。認可の基本は[認可ガイド](https://kawax.biz/jp/authorization.md)、プロバイダーの読み込みは[パッケージ自動検出](https://kawax.biz/jp/advanced/package-discovery.md)を参照してください。

## 利用アプリのUserクラスに依存しない

例として、パッケージの `Acme\Reports\Models\Report` に `update` の認可を追加します。`acme_reports` テーブルには、所有者の認証識別子を保存する `owner_id` カラムがある前提です。マイグレーションの提供方法は[パッケージのマイグレーション](https://kawax.biz/jp/advanced/package-migrations.md)で扱っています。

`src/Models/Report.php` は通常のEloquentモデルです。

```php theme={null}
<?php

namespace Acme\Reports\Models;

use Illuminate\Database\Eloquent\Model;

class Report extends Model
{
    protected $table = 'acme_reports';
}
```

パッケージ側のPolicyが `App\Models\User` を型指定すると、利用アプリの名前空間やモデル構成に依存します。必要なのがユーザーの認証識別子だけなら、Laravelの `Authenticatable` 契約を使います。

`src/Policies/ReportPolicy.php` に次を置きます。

```php theme={null}
<?php

namespace Acme\Reports\Policies;

use Acme\Reports\Models\Report;
use Illuminate\Contracts\Auth\Authenticatable;

class ReportPolicy
{
    public function update(Authenticatable $user, Report $report): bool
    {
        $userId = $user->getAuthIdentifier();
        $ownerId = $report->getAttribute('owner_id');

        return $userId !== null
            && $ownerId !== null
            && (string) $userId === (string) $ownerId;
    }
}
```

識別子の `null` を先に除外し、DBから取得した整数と文字列の型差は文字列化して比較します。この例では所有者だけに更新を許可します。複数のユーザー種別やGuardを扱うアプリでは、識別子だけで主体を一意に特定できるかも確認し、必要ならユーザー種別やテナントの境界を含む独自Policyへ差し替えます。

> [!NOTE]
>
> `Authenticatable` は認証識別子などの契約であり、`isAdministrator()` やロール取得メソッドは提供しません。パッケージ独自の権限情報が必要なら、別の契約を定義して利用アプリから実装を注入する設計にします。

## サービスプロバイダーで対応を登録する

`src/ReportsServiceProvider.php` の `boot()` で、パッケージが所有するモデルだけを登録します。

```php theme={null}
<?php

namespace Acme\Reports;

use Acme\Reports\Models\Report;
use Acme\Reports\Policies\ReportPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class ReportsServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gate::policy(Report::class, ReportPolicy::class);
    }
}
```

`Gate::policy()` は、モデルのクラス名をキーにPolicyのクラス名を保存します。この登録自体ではPolicyを生成せず、DB照会もユーザー判定も行いません。`Gate::getPolicyFor()` などでPolicyが必要になったとき、コンテナの `make()` で生成します。そのため、Policyのコンストラクターには依存サービスを注入できます。

`Report` と `ReportPolicy` の配置は標準の自動検出にも合いますが、ここではパッケージが提供する対応を明確にするため明示登録しています。登録だけではHTTPエンドポイントに認可は適用されません。更新処理の前に、コントローラーなどで次を呼び出します。

```php theme={null}
use Acme\Reports\Models\Report;
use Illuminate\Support\Facades\Gate;

$report = Report::query()->findOrFail($id);

Gate::authorize('update', $report);
```

拒否時には `AuthorizationException` が発生し、通常のHTTP処理では403レスポンスになります。モデルを渡す `update` と、モデルのクラス名を渡す `create` などではPolicyメソッドの引数が異なるため、[認可ガイド](https://kawax.biz/jp/authorization.md)の呼び出し方に合わせます。

## Policy解決の優先順位を読む

`Gate::getPolicyFor()` は、対象がオブジェクトならクラス名へ変換し、次の順で探します。これは **Policyを選ぶ順序** であり、認可全体のフック実行順ではありません。

| 順序 | 探す対象 |
| - | - |
| 1 | 対象クラスそのものへの `Gate::policy()` の明示登録 |
| 2 | 対象クラス自身の `#[UsePolicy]` |
| 3 | 標準命名規則、または `guessPolicyNamesUsing()` による推測 |
| 4 | 親クラス・実装インターフェースに対する明示登録 |
| 5 | 親クラスから継承して探す `#[UsePolicy]` |

したがって、同じ `Report` クラスへの明示登録がある状態では、`#[UsePolicy]` や命名規則を変えるだけでは差し替わりません。一方、利用アプリが `Report` を継承したモデルを使う場合、子クラス自身の属性や推測で見つかるPolicyは、親への明示登録より先です。実際に利用するモデルのクラスでも確認します。

> [!WARNING]
>
> `guessPolicyNamesUsing()` は、追加コールバックのリストではなく、Gateが持つ1つの推測コールバックを置き換えます。独立したパッケージが自分のモデルのためだけに設定すると、利用アプリや他パッケージの推測にも影響します。パッケージが所有するモデルへの `policy()` 登録を基本にしてください。

## 利用アプリで明示的に上書きする

同じモデルへ再び `policy()` を呼ぶと、対応は後の登録で置き換わります。単に別プロバイダーの `boot()` に置くだけでは読み込み順に依存するので、利用アプリの `AppServiceProvider::boot()` からアプリの起動完了コールバックを登録する方法があります。

```php theme={null}
use Acme\Reports\Models\Report;
use App\Policies\ApplicationReportPolicy;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    $this->app->booted(static function (): void {
        Gate::policy(Report::class, ApplicationReportPolicy::class);
    });
}
```

`ApplicationReportPolicy` は利用アプリが実装するPolicyです。パッケージ側が通常の `boot()` で登録する構成なら、このコールバックはその後に実行されます。別の起動完了コールバックによる再登録や、後から読み込まれるプロバイダーまで含めて無条件に最優先になる仕組みではありません。パッケージの登録プロバイダーは、この例では遅延プロバイダーにしません。

別の方法として、対応表を変えずに、コンテナで `ReportPolicy::class` の解決先を差し替えることもできます。`resolvePolicy()` は毎回コンテナへ解決を依頼しますが、実際に新しいインスタンスになるかは、そのPolicyのバインディング次第です。ユーザーやリクエストの情報をコンストラクターで取得し、singletonのPolicyへ保持する設計は避けます。

## 認可全体に影響するフックを分ける

通常のGateチェックでは、グローバルな `Gate::before()` が先に実行されます。そこで `null` 以外が返ると、Policyによる判定は実行されません。

```mermaid theme={null}
flowchart TD
    A["Gate::authorize / allows"] --> B["グローバルGate::before"]
    B --> C{"戻り値はnull?"}
    C -->|No| D["その結果を使う"]
    C -->|Yes| E["対象のPolicyと<br>呼び出せるメソッドを探す"]
    E --> F["Policyのbeforeがあれば呼ぶ"]
    F --> G["beforeがnullなら<br>Policyメソッドで判定"]
```

この図はPolicyが見つかり、対応するメソッドを呼び出せる経路を抜粋しています。その後のグローバルな `after` フックや、Policyの対象メソッドがない場合のGate定義へのフォールバックは省略しています。

パッケージ内の管理者扱いを実装するために、すべての能力を許可するグローバルな `Gate::before()` を登録しないでください。他のパッケージや利用アプリの認可まで許可してしまいます。Policy内の `before()` ならそのPolicyの範囲ですが、対応する能力のメソッドが存在しない場合には呼ばれません。続くメソッドに判断を任せる場合は `false` ではなく `null` を返します。

また、この例の `update()` はnullableではない `Authenticatable` を要求するため、ゲストではPolicyメソッドを実行しません。グローバルフックなどによる結果の上書きがなければ拒否されます。公開閲覧などでゲストを許容する設計は、nullableなユーザー引数と明示的な判定を別途用意します。

## 登録と判定を別々にテストする

[パッケージテスト](https://kawax.biz/jp/advanced/package-testing.md)で紹介しているTestbenchの基底クラスを使い、`getPackageProviders()` で `ReportsServiceProvider::class` を読み込む前提です。次は `tests/Feature/ReportPolicyTest.php` の例です。ユーザーにはLaravelの汎用認証モデルを使い、DBに保存せず判定できます。

```php theme={null}
<?php

namespace Acme\Reports\Tests\Feature;

use Acme\Reports\Models\Report;
use Acme\Reports\Policies\ReportPolicy;
use Acme\Reports\Tests\TestCase;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\User;

class ReportPolicyTest extends TestCase
{
    public function test_provider_registers_the_policy(): void
    {
        $gate = $this->app->make(GateContract::class);

        $this->assertInstanceOf(
            ReportPolicy::class,
            $gate->getPolicyFor(Report::class),
        );
    }

    public function test_only_the_owner_can_update(): void
    {
        $gate = $this->app->make(GateContract::class);
        $report = (new Report)->forceFill(['owner_id' => 7]);
        $owner = (new User)->forceFill(['id' => 7]);
        $other = (new User)->forceFill(['id' => 8]);

        $this->assertTrue($gate->forUser($owner)->allows('update', $report));
        $this->assertFalse($gate->forUser($other)->allows('update', $report));
        $this->assertFalse($gate->forUser(null)->allows('update', $report));
        $this->assertFalse($gate->forUser($owner)->allows('update', new Report));
    }

    public function test_an_override_does_not_change_an_existing_user_gate(): void
    {
        $gate = $this->app->make(GateContract::class);
        $owner = (new User)->forceFill(['id' => 7]);
        $report = (new Report)->forceFill(['owner_id' => 7]);
        $existingUserGate = $gate->forUser($owner);

        $gate->policy(Report::class, DenyReportPolicy::class);

        $this->assertInstanceOf(
            DenyReportPolicy::class,
            $gate->getPolicyFor(Report::class),
        );
        $this->assertFalse($gate->forUser($owner)->allows('update', $report));
        $this->assertTrue($existingUserGate->allows('update', $report));
    }
}

class DenyReportPolicy
{
    public function update(Authenticatable $user, Report $report): bool
    {
        return false;
    }
}
```

`forUser()` は共有Gateのユーザーを書き換えるのではなく、その時点の対応表・能力・コールバックを渡して別のGateを生成します。したがって、生成済みのユーザー別Gateには、その後の共有GateへのPolicy登録変更が反映されません。起動時に登録を済ませ、ユーザー別Gateを長寿命サービスへ保存しないようにします。

利用アプリ側では、実際のGuard・ユーザーモデルでの判定と、更新エンドポイントに対するHTTPテストも必要です。上のテストだけでは、ルートに認可が適用されていることや、DBの所有者識別子の保存形式までは検証できません。

### 更新時に維持する契約

* 所有者・所有者以外・ゲスト・所有者未設定の結果が変わっていないか。
* 公開している能力名とPolicyメソッドの引数が一致しているか。
* 利用アプリによるPolicyの差し替えが引き続き有効か。
* 子モデルをサポートする場合、子クラスで実際に選択されるPolicyが正しいか。
* アプリのグローバルフックも含めて、実際の更新リクエストが期待どおり許可・拒否されるか。

既存の能力を削除したり、既定の許可・拒否を変えたりする更新は利用アプリの振る舞いに影響します。[バージョン互換性管理](https://kawax.biz/jp/advanced/package-versioning.md)と合わせ、認可の変更も公開契約として扱います。

## 参照した一次情報

* [Laravel公式ドキュメント: 認可](https://github.com/laravel/docs/blob/13.x/authorization.md)
* [Laravel公式ドキュメント: パッケージ開発](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Framework v13.42.0: Gateの登録・解決・forUser](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Auth/Access/Gate.php)
* [Framework v13.42.0: Gateをsingleton登録するAuthServiceProvider](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Auth/AuthServiceProvider.php)
* [Framework v13.42.0: Applicationのboot・booted](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Foundation/Application.php)
* [Framework v13.42.0: 認可Gateの公式テスト](https://github.com/laravel/framework/blob/v13.42.0/tests/Auth/AuthAccessGateTest.php)


## Related topics

- [認可(ゲートとポリシー)](https://kawax.biz/jp/authorization.md)
- [応用トピック](https://kawax.biz/jp/advanced/index.md)
- [パッケージのミドルウェア登録と適用範囲](https://kawax.biz/jp/advanced/package-middleware.md)
- [パッケージビューの上書きと更新](https://kawax.biz/jp/advanced/package-views.md)
- [パッケージのサービス解決後フック](https://kawax.biz/jp/advanced/package-service-resolution.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.