跳转到主要内容
有时你希望扩展包的用户能从多个控制器返回相同格式的错误 JSON。公开响应 Macro 后,无需要求控制器继承基类,就能添加 response()->acmeFailure(...) 这样的生成 API。 本页参考 Laravel 官方文档的默认分支 13.x,以及 2026 年 11 月 8 日确认的 Framework 最新版本 v13.40.0 的官方实现。在 响应 和 Macroable trait 的基础上,讲解将其作为分发扩展包的公开 API 维护时需要注意的边界。

注册目标是 Factory 而不是响应

Response 这个名称既指请求生成响应的门面,也指生成后的 HTTP 响应类。官方文档中的 Response::macro() 扩展的是门面解析到的 ResponseFactory。 不带参数的 response() 返回 Factory。而 response('正文') 或 response(null) 返回由 make() 生成的响应,因此调用目标并不相同。
即使注册到 Illuminate\Http\Response::macro(),也不会成为 response()->acmeFailure() 的 Macro。请不要在 use 语句中混淆门面和同名的响应类。

公开错误 JSON 的生成 API

在 Service Provider 中注册

在 src/HttpServiceProvider.php 的 boot() 中注册带扩展包前缀的 Macro。关于服务提供者的加载,请参阅 扩展包自动发现。 本例的公开契约是包含 error.code、error.message 的 JSON,以及 400 到 599 的 HTTP 状态码。这个状态码限制并非 Laravel Macro 机制的约束,而是扩展包作为错误专用 API 所规定的。
使用 Macro 之前,这个服务提供者必须已经启动。不要把注册放在加载 HTTP 路由的文件中,也不要放在 runningInConsole() 的否定条件里。HTTP 功能测试同样从 CLI 运行。 只负责这项注册的服务提供者,也不需要做成等待服务解析的延迟提供者。延迟注册的机制请参阅 延迟服务提供者。

从控制器调用

在业务逻辑判定出冲突后,传入事先确定的错误码和面向用户的消息。
HTTP 状态码为 409,JSON 如下所示。与普通的 json() 一样,Content-Type 为 application/json。
通过门面也可以调用同一个 Factory 的 Macro。
Macro 只生成调用了该方法的响应。它不会自动把验证异常或认证错误转换为这种格式。也没有添加根据 Accept 头切换 HTML 与 JSON 的处理。

Closure 的 $this 与调用路径

Factory 使用了 Macroable。调用实例上未定义的方法时,__call() 会查找已注册的 Macro,并把普通 Closure 重新绑定到被调用的实例上。 即使注册代码位于 Service Provider 的方法中,运行时的 $this 也是 Factory,而不是 Service Provider。因此示例中的 $this->json() 调用的是 Factory 的普通方法。
  • 使用 $this 的 Macro 要用普通的 function (...) { ... } 定义。static function 没有实例的 $this。
  • 注册 invokable 对象时,不会像 Closure 那样重新绑定。所需的依赖应由该对象自己通过构造函数等方式接收。
  • Macro 的参数是调用方传入的参数。即使声明了 Request 等类型,Macroable 也不会从容器自动注入。
门面的 Response::acmeFailure() 看起来是静态调用,但内部会转发到 Factory 实例。而具体类的 Illuminate\Routing\ResponseFactory::acmeFailure() 是真正的静态调用,__callStatic() 会在没有实例的情况下绑定 Closure。像本例这样使用 $this->json() 的 Macro,不支持对具体类的静态调用。

将名称冲突视为兼容性问题

macro() 只是向以名称为键的静态数组赋值。注册已存在的名称也不会发出警告。 与其使用 failure 这样的通用名称,不如选择 acmeFailure 这样能识别扩展包的名称。升级 Laravel 时,也要用 method_exists(ResponseFactory::class, 'acmeFailure') 确认是否与实际方法冲突。 如果看到 hasMacro() 就无条件跳过注册,可能会把其他扩展包的实现当作自己的 API 来使用。只有在把”应用可以通过预先注册来覆盖”作为规范时,才公开其优先顺序以及所需的参数和返回值。上面的示例采用的方针是每次服务提供者启动时都注册扩展包自己的定义。

区分注册状态与响应的生命周期

Macro 注册表是 Factory 的静态属性。仅仅重新创建 Factory 或清除门面已解析的实例,并不会清除这张表。在同一个 PHP 进程中重新创建应用的测试里,注册状态也可能残留。 另一方面,示例中的 Macro 每次调用时都由 json() 生成新的 JsonResponse。共享的是生成逻辑,而不是响应的正文或头本身。 在长驻 worker 中应遵守的边界如下。
  1. 注册时不要用 use (...) 捕获当前的 Request、认证用户或租户。这些值应在调用时作为参数传入。
  2. 不要重用注册时生成的响应。每次都返回新的响应。
  3. 如果在注册时把配置作为值捕获,要在规范中说明该值会在进程内保留。
  4. 更新扩展包后,重启常驻进程以使新的注册代码生效。
扩展包服务的生命周期设计 更详细地讨论了容器的共享状态和请求级别的状态。
Response::flushMacros() 会删除注册到 Factory 的所有 Macro。其他扩展包的注册也会丢失,所以不要在生产环境的请求处理中,或作为共享测试环境的草率清理步骤使用它。冲突和重置的验证应在隔离的测试进程中进行。

用实际返回值测试 HTTP 契约

在能够通过 Composer 加载扩展包的 Laravel 13 验证用应用中,放置以下 tests/Feature/ResponseMacroTest.php。关于 Tests\TestCase 和 HTTP 测试的基础,请参阅 HTTP 测试;关于扩展包单独的环境搭建,请参阅 扩展包测试。
如果像 Response::shouldReceive('acmeFailure') 这样模拟生成 API 本身,Macro 的注册、Closure 的绑定和 JSON 的生成都不会执行。请与调用次数的验证分开,像上面那样验证实际的 HTTP 响应。 此外,要把用户收到的正文、状态码和头视为兼容性的对象。
  • 保持 error.code 稳定以供机器判定,并将其与显示消息的变更区分开。
  • 参数名会被用作命名参数,因此要像默认值和顺序一样确认变更的影响。
  • 204 这样没有正文的响应,不要作为错误 JSON 返回,而应使用 noContent() 等其他 API。
  • 即使允许通过自定义头修改 Content-Type 的扩展,也要使其与正文格式一致。
  • 如果目的是数据序列化或分页,不要只用 Macro 重新实现,也可以考虑 Eloquent API 资源。

长期维护检查清单

  • 在 Laravel 13 支持的组合中,服务提供者能够启动,并且可以从辅助函数和门面两种方式调用。
  • Macro 名称不与其他注册或 Factory 的实际方法冲突。
  • 测试覆盖了 JSON 键、HTTP 状态码、头、默认值和命名参数的兼容性。
  • 连续调用时不共享响应,也不在注册时保存 Request 或用户信息。
  • 重置注册的测试与其他扩展包隔离。
  • 更新流程中包含常驻进程的重启。

参考的一手资料

最后修改于 2026年11月8日