输入读完了,本节管回程:控制器把结果交给框架后,响应以什么形态、什么状态码、什么响应头送出。控制器里随手 return $data 的背后是响应对象的自动包装,本节把这层「自动」打开,让你在 JSON、跳转、视图、下载四种形态间做出正确选择,并能完全自控输出的每个细节。
控制器返回什么,框架就尝试包装成什么:数组与对象包成 JSON,字符串包成 HTML,视图结果包成页面,Response 实例原样放行。大多数接口项目里,json() 助手就够了:
// 显式 JSON:状态码、响应头一步到位 public function detail(int $id) { $book = BookService::detail($id); if (! $book) { return json(['code' => 404, 'msg' => '不存在'], 404); } return json(['code' => 0, 'data' => $book]) ->header(['Cache-Control' => 'max-age=60']); }
自动包装省心,但有两个必须自己把关的点:状态码默认永远是 200——业务失败靠 JSON 里的 code 字段表达没问题,但 HTTP 层语义(404、422、429)客户端网关与监控都在看,该给就给;响应头是输出契约的一部分,缓存策略、跨域标记都在这里(跨域中间件是更系统的做法,见 2.4)。

表单提交后的「成功并回到列表」、登录后的「回到来时的页面」,都靠重定向。关键是闪存数据(flash)——把一次性提示挂到会话上,跳转后的页面取一次即失效:
public function save() { BookService::save($this->request->only(['title', 'price'])); // 跳转 + 闪存提示:下一个请求能看到,再下一个就没了 return redirect((string) Route::buildUrl('Book/list')) ->with('notice', '保存成功'); } // 列表页控制器里取出闪存 $notice = session('notice'); session()->forget('notice'); // 用完即清,防止残留
重定向的 URL 用 3.1 的 buildUrl 生成,两个纪律在这里交汇。
视图出口只留一个引子(第 5 章整章展开):
public function index() { return view('book/index', [ 'list' => BookService::pageList(1), 'title' => '校园二手书', ]); }
下载出口值得完整写一遍,因为它是「响应对象完全自控」的最佳示例:
use think\Response; public function export() { // 服务层产出 CSV 内容字符串(业务在先,输出在后) $csv = OrderService::exportCsv($this->request->only(['date_start', 'date_end'])); return Response::create($csv, 'html', 200)->header([ 'Content-Type' => 'text/csv; charset=utf-8', 'Content-Disposition' => 'attachment; filename="orders.csv"', ]); }
状态码、内容类型、处置头全部显式——这就是响应对象的控制力。导出场景另一个常被忽略的点是内存:大表导出应分批取数、流式输出,这根线到第 4 章分页与第 8 章队列(异步导出)还会接续。
背景:练习项目 JSON 返回格式随写随变,前端对字段叫苦。操作:在 BaseController 里加两个受保护方法 ok($data) 与 fail(int $code, string $msg),内部统一 json(['code'=>..,'msg'=>..,'data'=>..]) 并对失败路径给对应 HTTP 状态码;把已有接口全部改走信封。结果示例:前端只需处理一种结构,401、404、422 在 HTTP 层可观测,监控告警可以按状态码统计。解读:信封方法看似只是省几行,实际是把「对外契约」收敛到一处——将来要加 trace_id、要换字段名,改一个类完成。变式:给信封附上响应耗时(结合 2.4 的 RequestLog 中间件已经在加的响应头,体会中间件与信封各自该管什么)。
⚠️ 常见坑:不要在响应里直接吐异常消息与 SQL——调试信息属于日志,不属于客户端。生产环境的异常输出策略在第 8 章调试章节统一配置。
💡 关键直觉:响应是承诺——状态码、结构、字段名一旦发布就是契约。宁可一开始多花十分钟定信封,也别让前端陪你改三轮字段名。
统一信封解决了「结构一致」,工程上还要解决两个配套问题。一是 HTTP 状态码与业务码的两层表达:信封里的业务码表达「业务层怎么看」,HTTP 状态码表达「传输层怎么看」——参数不合规配 422、未登录配 401、越权配 403、业务规则失败(库存不足)配 200 加非零业务码。前端据此分派:4 跳登录页、42 提示表单、业务码弹业务提示。两层混用(一切皆 200,或业务错误全配 500)都会把网关监控与前端处理同时搞瞎。
二是 异常到响应的统一映射:业务代码里随手返回错误信封,会让「错误怎么表达」散落几十处。工程做法是把可预期的业务异常定义成专门的异常类型,在全局异常处理里统一翻译成信封——控制器只管抛,出口只管译,改信封结构时只动一处。这个模式与 3.2 的输入、6.1 的验证天然衔接:验证失败抛验证异常,出口翻译成 422 信封,全链路无人手工拼错误结构。
本章的分拣台到此完工。下一站是全书最重的一站——第 4 章数据站台:模型与 ORM。