前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >分享一个 JSON 相关小需求的解决过程与思路

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

作者头像
overtrue
发布2019-05-14 15:43:10
8841
发布2019-05-14 15:43:10
举报
文章被收录于专栏:假装我会写代码

今天分享一个小技巧的解决过程。

起因

昨天同事问我,能不能在接口返回中不要将中文转成 Uncode 编码,因为这是 Laravel 框架做的事情,所以我们要实现这个效果无非就是在 json_encode 第二个参数中加入常量 JSON_UNESCAPED_UNICODE 选项即可,但是我们在控制器返回的是对象,或者是数组,这个 encode 动作是框架最后输出前完成的。应该是一个非常小小小的需求了。

啃源码

我花了 5 分钟跟完源代码,发现它在 Illuminate\Http\Response 中有这么一段来完成 JSON 转化的:

vendor/laravel/framework/src/Illuminate/Http/Response.php

代码语言:javascript
复制
if ($this->shouldBeJson($content)) {$this->header('Content-Type', 'application/json');
    $content = $this->morphToJson($content);}

其中通过 shouldBeJson 这个方法来判断当前的响应内容是否需要转化成 JSON 格式:

vendor/laravel/framework/src/Illuminate/Http/Response.php

代码语言:javascript
复制

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

代码语言:javascript
复制

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

代码语言:javascript
复制
$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

代码语言:javascript
复制
public function toResponse($request){    return (new ResourceResponse($this))->toResponse($request);}

它实例化并调用了 Illuminate\Http\Resources\Json\ResourceResponsetoResponse 的方法做为返回值:

vendor/laravel/framework/src/Illuminate/Http/Resources/Json/ResourceResponse.php

代码语言:javascript
复制

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

代码语言:javascript
复制
protected $encodingOptions = self::DEFAULT_ENCODING_OPTIONS;

可以通过它的方法:setEncodingOptions($encodingOptions) 来传递我们想要的 json_encode 选项,所以,我们只需要在我们的 Resource 基类(我们接口返回值都使用了一个 Resource 基类 App\Http\Resources\Resource)中添加如下方法即可:

app/Http/Resources/Resource.php

代码语言:javascript
复制

/**
 * @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

代码语言:javascript
复制
$response = $kernel->handle(    $request = Illuminate\Http\Request::capture());

我发现在这个逻辑的最后,在 Illuminate\Foundation\Http\Kernel 中有一个 handle 方法:

vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php

代码语言:javascript
复制

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 的内容。

创建一个事件监听器:

代码语言:javascript
复制
$ ./artisan make:listener SetResponseEncodingOptions --event=Illuminate\Foundation\Http\Events\RequestHandled

代码如下:

app/Listensers/SetResponseEncodingOptions

代码语言:javascript
复制

<?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

代码语言:javascript
复制
protected $listen = [       //...    \Illuminate\Foundation\Http\Events\RequestHandled::class => [        \App\Listeners\SetResponseEncodingOptions::class,    ],];

终于,找到了一个看起来合理的做法解决了这个小小小需求。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-04-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 假装我会写代码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 起因
  • 啃源码
  • 解决方案1
  • 强迫症犯了
  • 继续探索
    • 终极解决方案
    相关产品与服务
    消息队列 TDMQ
    消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档