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

# 套件的授權 Policy 註冊與覆寫

> 解讀 Laravel 13 的 Gate 實作，說明套件的 Policy 註冊、避免依賴使用者模型的設計、解析優先順序，以及在使用端應用程式中的覆寫與測試。

有時你希望為套件提供的模型加入授權，同時又讓使用套件的應用程式保留替換為自有規則的空間。若明確宣告模型與 Policy 的對應，並在執行授權的時點才判定使用者，就能將註冊處理與每個請求的判斷分開。

本頁參考 Laravel 官方文件的預設分支 `13.x`，以及 2026 年 11 月 10 日確認的 Framework 最新版本 `v13.42.0` 的官方實作與測試。授權的基礎請參閱[授權指南](https://kawax.biz/zh-TW/authorization.md)，提供者的載入請參閱[套件自動探索](https://kawax.biz/zh-TW/advanced/package-discovery.md)。

## 不依賴使用端應用程式的 User 類別

以為套件中的 `Acme\Reports\Models\Report` 加入 `update` 授權為例。假設 `acme_reports` 資料表中有儲存擁有者認證識別碼的 `owner_id` 欄位。遷移的提供方式請參閱[套件的遷移](https://kawax.biz/zh-TW/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` 識別碼，再將兩者轉為字串比較，以消除從資料庫取得的整數與字串之間的型別差異。此範例只允許擁有者更新。在處理多種使用者類型或 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，也不會查詢資料庫或判定使用者。當 `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/zh-TW/authorization.md)的呼叫方式。

## 解讀 Policy 解析的優先順序

`Gate::getPolicyFor()` 在目標為物件時會轉換為類別名稱，並依以下順序尋找。這是**選擇 Policy 的順序**，而非整個授權流程中鉤子的執行順序。

| 順序 | 尋找對象 |
| - | - |
| 1 | 針對目標類別本身的 `Gate::policy()` 明確註冊 |
| 2 | 目標類別本身的 `#[UsePolicy]` |
| 3 | 依標準命名規則或 `guessPolicyNamesUsing()` 的推測 |
| 4 | 針對父類別、實作介面的明確註冊 |
| 5 | 從父類別繼承尋找的 `#[UsePolicy]` |

因此，在已有針對同一 `Report` 類別的明確註冊時，僅修改 `#[UsePolicy]` 或命名規則並不會替換。另一方面，若使用端應用程式使用繼承自 `Report` 的模型，透過子類別本身的屬性或推測找到的 Policy 會優先於針對父類別的明確註冊。也請以實際使用的模型類別進行確認。

> [!WARNING]
>
> `guessPolicyNamesUsing()` 並不是加入回呼清單，而是替換 Gate 持有的唯一推測回呼。若獨立的套件只為了自己的模型設定它，也會影響使用端應用程式與其他套件的推測。請以針對套件所擁有模型的 `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，但對應能力的方法不存在時不會被呼叫。若要交由後續方法判斷，請回傳 `null` 而非 `false`。

此外，此範例的 `update()` 要求非 nullable 的 `Authenticatable`，因此訪客不會執行 Policy 方法。若沒有全域鉤子等覆寫結果，就會遭拒絕。若要在公開瀏覽等情境允許訪客，請另外準備 nullable 的使用者參數與明確的判定。

## 分別測試註冊與判定

此處假設使用[套件測試](https://kawax.biz/zh-TW/advanced/package-testing.md)中介紹的 Testbench 基底類別，並在 `getPackageProviders()` 中載入 `ReportsServiceProvider::class`。以下是 `tests/Feature/ReportPolicyTest.php` 的範例。使用者採用 Laravel 的通用認證模型，不需儲存到資料庫即可判定。

```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 測試。僅憑上述測試，無法驗證路由是否已套用授權，以及資料庫中擁有者識別碼的儲存格式。

### 更新時需維持的契約

* 擁有者、非擁有者、訪客、未設定擁有者時的結果是否沒有改變。
* 公開的能力名稱與 Policy 方法的參數是否一致。
* 使用端應用程式對 Policy 的替換是否仍然有效。
* 若支援子模型，子類別實際選擇的 Policy 是否正確。
* 包含應用程式的全域鉤子在內，實際的更新請求是否如預期被允許或拒絕。

刪除既有能力或改變預設允許、拒絕行為的更新會影響使用端應用程式的行為。請搭配[版本相容性管理](https://kawax.biz/zh-TW/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

- [授權（Gate 與 Policy）](https://kawax.biz/zh-TW/authorization.md)
- [進階主題](https://kawax.biz/zh-TW/advanced/index.md)
- [套件的路由註冊與快取](https://kawax.biz/zh-TW/advanced/package-routes.md)
- [套件的郵件傳輸註冊與測試](https://kawax.biz/zh-TW/advanced/package-mail-transports.md)
- [套件的 Artisan 指令註冊與相依性解析](https://kawax.biz/zh-TW/advanced/package-commands.md)


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