跳转到主要内容
有时你希望为包提供的模型添加授权,同时又给使用该包的应用留出替换为自有规则的余地。如果明确声明模型与 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日