跳轉到主要內容
有時你希望為套件提供的模型加入授權,同時又讓使用套件的應用程式保留替換為自有規則的空間。若明確宣告模型與 Policy 的對應,並在執行授權的時點才判定使用者,就能將註冊處理與每個請求的判斷分開。 本頁參考 Laravel 官方文件的預設分支 13.x,以及 2026 年 11 月 10 日確認的 Framework 最新版本 v13.42.0 的官方實作與測試。授權的基礎請參閱授權指南,提供者的載入請參閱套件自動探索。

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

以為套件中的 Acme\Reports\Models\Report 加入 update 授權為例。假設 acme_reports 資料表中有儲存擁有者認證識別碼的 owner_id 欄位。遷移的提供方式請參閱套件的遷移。 src/Models/Report.php 是一般的 Eloquent 模型。
若套件端的 Policy 將型別指定為 App\Models\User,就會依賴使用端應用程式的命名空間與模型結構。如果只需要使用者的認證識別碼,請使用 Laravel 的 Authenticatable 契約。 在 src/Policies/ReportPolicy.php 放入以下程式碼。
先排除 null 識別碼,再將兩者轉為字串比較,以消除從資料庫取得的整數與字串之間的型別差異。此範例只允許擁有者更新。在處理多種使用者類型或 Guard 的應用程式中,也要確認僅憑識別碼能否唯一確定主體,必要時替換為包含使用者類型或租戶邊界的自有 Policy。
Authenticatable 是關於認證識別碼等的契約,不提供 isAdministrator() 或取得角色的方法。若需要套件特有的權限資訊,請定義另一個契約,並設計成由使用端應用程式注入實作。

在服務提供者中註冊對應

在 src/ReportsServiceProvider.php 的 boot() 中,只註冊套件所擁有的模型。
Gate::policy() 以模型的類別名稱為鍵,儲存 Policy 的類別名稱。這項註冊本身不會產生 Policy,也不會查詢資料庫或判定使用者。當 Gate::getPolicyFor() 等需要 Policy 時,才透過容器的 make() 產生。因此可以在 Policy 的建構子中注入相依服務。 Report 與 ReportPolicy 的放置位置也符合標準的自動探索,但這裡為了明確套件所提供的對應而採用明確註冊。僅註冊並不會對 HTTP 端點套用授權。請在更新處理之前,於控制器等處呼叫以下程式碼。
遭拒絕時會拋出 AuthorizationException,在一般的 HTTP 處理中會成為 403 回應。傳入模型的 update 與傳入模型類別名稱的 create 等,其 Policy 方法的參數不同,請依照授權指南的呼叫方式。

解讀 Policy 解析的優先順序

Gate::getPolicyFor() 在目標為物件時會轉換為類別名稱,並依以下順序尋找。這是選擇 Policy 的順序,而非整個授權流程中鉤子的執行順序。 因此,在已有針對同一 Report 類別的明確註冊時,僅修改 #[UsePolicy] 或命名規則並不會替換。另一方面,若使用端應用程式使用繼承自 Report 的模型,透過子類別本身的屬性或推測找到的 Policy 會優先於針對父類別的明確註冊。也請以實際使用的模型類別進行確認。
guessPolicyNamesUsing() 並不是加入回呼清單,而是替換 Gate 持有的唯一推測回呼。若獨立的套件只為了自己的模型設定它,也會影響使用端應用程式與其他套件的推測。請以針對套件所擁有模型的 policy() 註冊為基本做法。

在使用端應用程式中明確覆寫

對同一模型再次呼叫 policy() 時,對應會被後來的註冊取代。只是放在其他提供者的 boot() 中會依賴載入順序,因此可以從使用端應用程式的 AppServiceProvider::boot() 註冊應用程式的啟動完成回呼。
ApplicationReportPolicy 是使用端應用程式實作的 Policy。若套件端在一般的 boot() 中註冊,此回呼會在其後執行。但這並不是無條件最優先的機制,其他啟動完成回呼中的重新註冊或之後載入的提供者都不在保證範圍內。在此範例中,套件的註冊提供者不設為延遲提供者。 另一種方法是不變更對應表,而是在容器中替換 ReportPolicy::class 的解析目標。resolvePolicy() 每次都會請容器解析,但實際是否成為新的實例取決於該 Policy 的綁定。請避免在建構子中取得使用者或請求資訊並保存在 singleton Policy 中的設計。

區分影響整個授權的鉤子

在一般的 Gate 檢查中,全域的 Gate::before() 會先執行。若它回傳 null 以外的值,就不會執行 Policy 的判定。 此圖節錄找到 Policy 且可呼叫對應方法的路徑。之後的全域 after 鉤子,以及 Policy 沒有目標方法時回退到 Gate 定義的情況皆已省略。 請勿為了在套件內實作管理員處理而註冊允許所有能力的全域 Gate::before()。這會連其他套件與使用端應用程式的授權都一併允許。Policy 內的 before() 只作用於該 Policy,但對應能力的方法不存在時不會被呼叫。若要交由後續方法判斷,請回傳 null 而非 false。 此外,此範例的 update() 要求非 nullable 的 Authenticatable,因此訪客不會執行 Policy 方法。若沒有全域鉤子等覆寫結果,就會遭拒絕。若要在公開瀏覽等情境允許訪客,請另外準備 nullable 的使用者參數與明確的判定。

分別測試註冊與判定

此處假設使用套件測試中介紹的 Testbench 基底類別,並在 getPackageProviders() 中載入 ReportsServiceProvider::class。以下是 tests/Feature/ReportPolicyTest.php 的範例。使用者採用 Laravel 的通用認證模型,不需儲存到資料庫即可判定。
forUser() 並不會改寫共用 Gate 的使用者,而是傳入當時的對應表、能力與回呼來產生另一個 Gate。因此,已產生的使用者別 Gate 不會反映之後對共用 Gate 的 Policy 註冊變更。請在啟動時完成註冊,並避免將使用者別 Gate 保存在長生命週期的服務中。 在使用端應用程式中,還需要以實際的 Guard、使用者模型進行判定,以及針對更新端點的 HTTP 測試。僅憑上述測試,無法驗證路由是否已套用授權,以及資料庫中擁有者識別碼的儲存格式。

更新時需維持的契約

  • 擁有者、非擁有者、訪客、未設定擁有者時的結果是否沒有改變。
  • 公開的能力名稱與 Policy 方法的參數是否一致。
  • 使用端應用程式對 Policy 的替換是否仍然有效。
  • 若支援子模型,子類別實際選擇的 Policy 是否正確。
  • 包含應用程式的全域鉤子在內,實際的更新請求是否如預期被允許或拒絕。
刪除既有能力或改變預設允許、拒絕行為的更新會影響使用端應用程式的行為。請搭配版本相容性管理,將授權的變更也視為公開契約。

參考的一手資料

最後修改於 2026年11月10日