ThinkPHP 6 统一 JSON 响应结构需重写 think\response\Json::output() 并通过容器绑定,同时用全局中间件兜底异常响应;业务状态码须集中配置管理。

ThinkPHP 6 的 json() 默认不走统一结构,别改 ResponseBehavior
直接在 app\common\behavior\ResponseBehavior 里试图包装响应体是无效的——它只处理「已经生成好的 Response 对象」,而 return json([]) 这类调用早在进入行为前就完成了序列化。你看到的纯数组输出,是 think\response\Json::output() 直接返回的原始 json_encode($data),没经过任何包裹逻辑。
真正能拦截并重写的点只有两个:think\response\Json 类本身,或全局中间件。前者更干净、可控,且不影响异常响应链路;后者适合兜底补救,但得额外判断响应类型。
- 别碰
__construct或init(),它们负责推导$this->code和$this->message,乱改会导致状态码/提示丢失 - 只重写
output()方法,用$this->code和$this->message做默认值,兼容json($data, 200, [], ['code' => 1])这种显式传参写法 - 若想支持旧项目里已有的
['code'=>1,'msg'=>'xxx','data'=>[]]结构,可在output()中加一层判断:当$data是数组且含code键时跳过包装——但会增加歧义,不建议
替换 think\response\Json 类必须用容器绑定,不是文件覆盖
ThinkPHP 不允许简单地把原 think\response\Json.php 替换成自己的版本——框架通过 Composer 自动加载,硬覆盖会被 vendor 更新冲掉。正确方式是在服务提供者中用容器绑定:
在 app\common\provider\AppServiceProvider::register() 里写:$this->app->bind('think\response\Json', \app\common\response\Json::class);
立即学习“PHP免费学习笔记(深入)”;
基于三引擎设计,从微信文章、新闻和博客网页提取干净内容,支持标题作者日期元数据,多格式和批量处理。
然后新建 app\common\response\Json.php,继承原类,仅重写 output():
namespace app\common\response;
use think\response\Json as BaseJson;
class Json extends BaseJson
{
protected function output($data): string
{
$result = [
'code' => $this->code ?: 0,
'msg' => $this->message ?: 'ok',
'data' => $data,
];
return json_encode($result, JSON_UNESCAPED_UNICODE);
}
}
- 命名空间和类名必须严格匹配,否则容器无法解析
- 不要删掉
use think\response\Json as BaseJson,否则$this->code等属性访问会失败 -
JSON_UNESCAPED_UNICODE必须带上,否则中文字段名(如msg)会被转成 Unicode 编码
异常响应(如 ValidateException)不会自动进新 Json 类,得靠中间件兜底
上面改完后,return json(...) 和 $this->success() 都会走新结构,但 throw new ValidateException('参数错误') 或未捕获的 HttpException 仍返回原生格式:{ "code": 400, "msg": "参数错误", "data": [] } —— 注意,这是 think\exception\Handle 内部用 json() 构建的,但它调用的是原始 think\response\Json,不受你容器绑定影响。
解决方案是在全局中间件(如 app/middleware/ApiJsonFormat.php)中拦截非 Json 响应,并对 HttpException 类做二次包装:
- 检查
$response instanceof \think\response\Json === false且$response->getOriginalContent()是数组 - 提取原内容中的
code、msg、data(若存在),否则设默认值 - 用
json(['code'=>$code,'msg'=>$msg,'data'=>$data])重建响应,避免重复编码 - 注意:中间件顺序很重要,必须放在
AllowCrossDomain之后、CheckRequestCache之前,否则跨域头可能丢失
业务状态码别散落在各处,用 config/status.php 统一管理
硬编码 api_return(404, '用户不存在') 或 $this->error('登录失败', 1001) 很快会让状态码失控。推荐在 config/status.php 里集中定义:
return [
'success' => 0,
'user_not_found' => 1001,
'token_expired' => 1002,
'param_error' => 400,
'server_error' => 500,
];
然后在封装函数或基类方法里引用:return json(['code' => config('status.user_not_found'), 'msg' => '用户不存在', 'data' => []]);
- 配置键名用下划线,语义清晰,避免与 HTTP 状态码(如 400/500)混淆
- 不要把 HTTP 状态码(
$httpStatus)和业务code混为一谈:前者控制浏览器缓存、重定向等行为,后者仅用于前端逻辑分支 - 若需动态组合(如「登录失败,剩余尝试次数:2」),msg 字段应留空,由调用方拼接,配置里只存模板字符串
json() 调用都必须走你重写的 Json 类,而所有异常必须被中间件识别并重包——这两层缺一不可。最容易漏掉的是中间件对 ValidateException 的处理,它不像普通 HttpException 那样有固定 code 字段,得手动从异常对象里取 getMessage() 并映射到业务码。


















