分享一个 JSON 相关小需求的解决过程与思路

今天分享一个小技巧的解决过程。
起因
昨天同事问我,能不能在接口返回中不要将中文转成 Unicode 编码,因为这是 Laravel 框架做的事情,所以我们要实现这个效果无非就是在 json_encode 第二个参数中加入常量 JSON_UNESCAPED_UNICODE 选项即可,但是我们在控制器返回的是对象,或者是数组,这个 encode 动作是框架最后输出前完成的。应该是一个非常小小小的需求了。
啃源码
我花了 5 分钟跟完源代码,发现它在 Illuminate\Http\Response 中有这么一段来完成 JSON 转化的:
vendor/laravel/framework/src/Illuminate/Http/Response.php
if ($this->shouldBeJson($content)) {
$this->header('Content-Type', 'application/json');
$content = $this->morphToJson($content);
}
其中通过 shouldBeJson 这个方法来判断当前的响应内容是否需要转化成 JSON 格式:
vendor/laravel/framework/src/Illuminate/Http/Response.php
protected function shouldBeJson($content)
{
return $content instanceof Arrayable ||
$content instanceof Jsonable ||
$content instanceof ArrayObject ||
$content instanceof JsonSerializable ||
is_array($content);
}
最后通过 morphToJson 完成了转化动作:
vendor/laravel/framework/src/Illuminate/Http/Response.php
protected function morphToJson($content)
{
if ($content instanceof Jsonable) {
return $content->toJson();
} elseif ($content instanceof Arrayable) {
return json_encode($content->toArray());
}
return json_encode($content);
}
所以聪明的你已经发现了,这里的 json_encode 没有传递任何选项,所以我们无法通过简单的方法调用来实现它。
解决方案1
既然最终出口是这么干的,那我立即想到一个简单的处理方式:在 public/index.php 中输出响应值前处理:
public/index.php
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
// 取到内容
$content = $response->original;
// 检查原始内容的类型是否需要转 json
if ($content instanceof Arrayable ||
$content instanceof Jsonable ||
$content instanceof ArrayObject ||
$content instanceof JsonSerializable ||
is_array($content)) {
// 重新设置响应内容
$response->setContent(json_encode($content, JSON_UNESCAPED_UNICODE));
}
$response->send();
就这样轻松的搞定了这个需求。
强迫症犯了
虽然问题解决了,始终觉得这种改入口文件的骚操作不太能接受,总觉得应该有更科学一点的方法,哪怕更科学一丢丢都行。
继续探索
突然想到,我们的接口都是返回的是 Api Resource 模式,也就是说最后返回的都是 Illuminate\Http\Resources\Json\JsonResource 实例或者集合,那可否在这里支持选项定义呢?
答案是可以:
在 Illuminate\Http\Resources\Json\JsonResource 中有一个 toResponse 方法:
vendor/laravel/framework/src/Illuminate/Http/Resources/Json/JsonResource.php
public function toResponse($request)
{
return (new ResourceResponse($this))->toResponse($request);
}
它实例化并调用了 Illuminate\Http\Resources\Json\ResourceResponse 的 toResponse 的方法做为返回值:
vendor/laravel/framework/src/Illuminate/Http/Resources/Json/ResourceResponse.php
public function toResponse($request)
{
return tap(response()->json(
$this->wrap(
$this->resource->resolve($request),
$this->resource->with($request),
$this->resource->additional
),
$this->calculateStatus()
), function ($response) use ($request) {
$response->original = $this->resource->resource;
$this->resource->withResponse($request, $response);
});
}
这个方法最后返回了 Illuminate\Http\JsonResponse,终于,我们发现这个类是支持选项定义的:
vendor/symfony/http-foundation/JsonResponse.php
protected $encodingOptions = self::DEFAULT_ENCODING_OPTIONS;
可以通过它的方法:setEncodingOptions($encodingOptions) 来传递我们想要的 json_encode 选项,所以,我们只需要在我们的 Resource 基类(我们接口返回值都使用了一个 Resource 基类 App\Http\Resources\Resource)中添加如下方法即可:
app/Http/Resources/Resource.php
/**
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function toResponse($request)
{
return parent::toResponse($request)->setEncodingOptions(\JSON_UNESCAPED_UNICODE);
}
可是,我还没来得及高兴,问题又来了,某个接口由于不是标准的模型格式,没有返回 Resource 实例,所以最后觉得这么干还是不行,必须得在 Laravel 输出前统一处理。
终极解决方案
我想到了 Laravel 的 ternimate 中间件特性,然后发现不可行,因为你会发现在 public/index.php 中,ternimate 中间件的最后在响应输出之后:
public/index.php
//...
$response->send();
$kernel->terminate($request, $response);
所以时机不合适。
那么在这三行代码里寻找答案吧:
public/index.php
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
我发现在这个逻辑的最后,在 Illuminate\Foundation\Http\Kernel 中有一个 handle 方法:
vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e));
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new Events\RequestHandled($request, $response)
);
return $response;
}
上面最后部分有一个事件 Illuminate\Foundation\Http\Events\RequestHandled 被触发,所以这里就是突破口了:监听这个事件,修改 $response 的内容。
创建一个事件监听器:
$ ./artisan make:listener SetResponseEncodingOptions --event=Illuminate\Foundation\Http\Events\RequestHandled
代码如下:
app/Listensers/SetResponseEncodingOptions
<?php
namespace App\Listeners;
use ArrayObject;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Foundation\Http\Events\RequestHandled;
class SetResponseEncodingOptions
{
/*...*/
public function handle(RequestHandled $event)
{
$content = $event->response->original;
if ($content instanceof Arrayable ||
$content instanceof Jsonable ||
$content instanceof ArrayObject ||
$content instanceof \JsonSerializable ||
is_array($content)) {
$event->response->setContent(json_encode($content, \JSON_UNESCAPED_UNICODE));
}
}
}
配置监听规则:
app/Providers/EventServiceProvider.php
protected $listen = [
//...
\Illuminate\Foundation\Http\Events\RequestHandled::class => [
\App\Listeners\SetResponseEncodingOptions::class,
],
];
终于,找到了一个看起来合理的做法解决了这个小小小需求。
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
一楼,嘻嘻
沙发
@eightone 没坐成
板凳
大神,威武!!!
之前做的还有个问题,就是Laravel存入数据库的数据,中文还是转成了Unicode 编码,不利于查看。后面我简单在基类Model中用asJson再做了一层转换,也是不优雅的解决方式。
大佬 排查问题 思路很清晰啊 一步步的发掘
这个思路佩服
不确定你具体实现如何,但这样可以输出正常的中文
json@Cryven 你这样肯定没有问题啊,但是我并不想在每一个控制器返回的时候加这些哇 :joy:
@Cryven:比较同意 @overtrue 所说。如果是返回 JsonResponse 。直接返回数组就好了。。
如果返回是 200,加
response()->json($data, 200)什么的,有点多余。。。然后在返回要塞中,做统一处理,就像 @overtrue 定义事件一样。。
非常棒
我们直接弄了一个 jsonSuccess 这样的助手方法,在这里处理一下,感觉更简化
我也有这种表现为“眼不见心不烦”的强迫症 :joy:我是定义了个了Basecontroller类,封装了
success($message, $data = []),fail($message, $data = [])的方法,在这两个方法中传递了JSON_UNESCAPED_UNICODE超哥~ 有没办法获取Redis Cache 某个tags中缓存items的数量的方法~~啃了两天源码,始终没找大突破口,自定义缓存驱动,改写一些方法是可以实现,不过总觉得不够优雅 :see_no_evil: 发现自己在写优(zhuang)雅(bi)代码的路上越走越偏 了:grin:
@ALMAS 没研究过这块呢
曾经也研究过这个问题,最后还是觉得整个BaseController,里面弄个success方法来处理返回,因为一般返回的不仅仅是数据对象,还要有code码及message之类的信息,所以如果要返回一个数组,总归有个方法来处理比较好,不然每个方法里去拼这个数组格式也还是很烦。
用中间件做格式转换可能更好些,可以单独指定需要转格式的路由组
感谢分享,学习了。 :smile:
@Kamicloud 中间件也可以实现,控制好中间件顺序,使用后置中间件即可。
应该还有一种方案,可以在
Handler里面处理最后可能需要再过滤下,Illuminate\Http\Resources\Json\JsonResource响应的respone里面的内容是处理过的,和直接处理respone->original的结果不一样
利用中间件解决, 思路简单而清晰.
了解一下 Laravel 灵活使用 中间件, 自定义全局 API JSON 返回格式
对 我也准备说这种使用后置中间件是不是好些
对 我也准备说这种使用后置中间件是不是好些
为啥评论重复了😂
Laravel 的事件系统真的很灵活 扩展性也更强
与
赞,我都是单独写一个类,用于API返回的
这个思路,强的一批!
相对修改入口文件 这种优雅太多啦 嘻嘻