13.x,以及 2026 年 11 月 10 日确认的 Framework 最新版本 v13.42.0 的官方实现与测试。授权的基础请参阅授权指南,提供者的加载请参阅包自动发现。
不依赖使用方应用的 User 类
以为包中的Acme\Reports\Models\Report 添加 update 授权为例。假设 acme_reports 表中有一个保存所有者认证标识符的 owner_id 列。迁移的提供方式请参阅包的迁移。
src/Models/Report.php 是普通的 Eloquent 模型。
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 优先于针对父类的显式注册。请同时用实际使用的模型类进行确认。
在使用方应用中显式覆盖
对同一模型再次调用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 是否正确。
- 包括应用的全局钩子在内,实际的更新请求是否按预期被允许或拒绝。