响应预处理

未匹配的标注

简介

上一章,我们了解了控制器与方法的运行原理。

由于控制器与方法,都是我们自己自定义的,那么返回的数据类型也会出现不确定的情况。

我总结一下,最常用的两类返回写法:

  • view 辅助函数返回的数据---一般情况下,web 路由的返回格式
return view('auth.register', [
    'message' => 'good',
]);
  • response 辅助函数返回的 json 字符串---一般情况下,api 路由的返回格式
return response()->json([
    'code' => 200,
    'message' => 'good',
]);

我想上面这两种,大家应该非常熟悉了,但是有没有两种情况的变种,使代码更加简练,更加优雅呢

答案:是有的。

这就涉及到响应预处理的源码执行原理了。

先从上一章过渡一下。

Illuminate\Routing\Router

protected function runRouteWithinStack(Route $route, Request $request)
{
    $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                            $this->container->make('middleware.disable') === true;

    $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

    return (new Pipeline($this->container))
                    ->send($request)
                    ->through($middleware)
                    ->then(function ($request) use ($route) {
                        return $this->prepareResponse(
                            // 这个 run 函数就是我们控制器与方法执行后的 return。
                            $request, $route->run()
                        );
                    });
}

$route->run(),run 函数执行完毕取得的数据,就是我们控制器与方法执行完毕,返回的 第一手数据

prepareResponse:响应预处理

Illuminate\Routing\Router

public function prepareResponse($request, $response)
{
    return static::toResponse($request, $response);
}

toResponse:将我们自行定义返回的烂七八糟的数据类型,统一转换成 Response 或 JsonResponse。

Illuminate\Routing\Router

public static function toResponse($request, $response)
{
    /*
    * 如果我们返回的数据属于 Responsable 接口的实现,那么调用 toResponse 方法。
    * toResponse 方法一定会有的,因为 Responsable 接口规定了必须有 toResponse 方法。
    * 这段作用我想应该是:让我们在返回控制器与方法数据时,统一一个返回数据处理方式,来替代系统默认。
    */
    if ($response instanceof Responsable) {
        $response = $response->toResponse($request);
    }

    // if elseif 结构,只能运行一个哦。
    if ($response instanceof PsrResponseInterface) {
        // 当 $response 属于 PSR 规范的响应接口时,执行此段代码,用的比较少,不做多介绍
        $response = (new HttpFoundationFactory)->createResponse($response);
    } elseif ($response instanceof Model && $response->wasRecentlyCreated) {
        /*
        * 当 $response 是模型示例时,且模型示例 save 时,是新增数据,而不是修改数据时,此段代码。
        * 默认返回的是 jsonResponse,内容包括新增数据的全部内容。
        * 此时 HTTP 状态码为 201,代表资源创建成功。
        */
        $response = new JsonResponse($response, 201);
    } elseif (! $response instanceof SymfonyResponse &&
               ($response instanceof Arrayable ||
                $response instanceof Jsonable ||
                $response instanceof ArrayObject ||
                $response instanceof JsonSerializable ||
                is_array($response))) {
        /*
        * 当 $response 不属于 SymfonyResponse ,且 $response 只要满足以下条件任意一个,就执行此代码
        * 1、$response 属于 Arrayable,此时 $response 必有 toArray 方法;
        * 2、$response 属于 Jsonable,此时 $response 必有 toJson 方法;
        * 3、$response 属于 ArrayObject;
        * 4、$response 属于 JsonSerializable,此时 $response 必有 jsonSerialize 方法;
        * 5、$response 是 PHP 数组。
        * 结果,同 $response 是模型示例时,唯一区别这个的返回码是 200,仅代表请求成功。
        */
        $response = new JsonResponse($response);
    } elseif (! $response instanceof SymfonyResponse) {
        // 最后 $response 上面的条件都不满足;那就仅剩 Response 了,直接实例化。。
        $response = new Response($response);
    }

    // 最后如果 $response 的状态码是 304,就返回的内容和上次一样,直接告诉浏览器从缓存加载。
    if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
        $response->setNotModified();
    }

    // 往 $response 中写入一些必要的响应头信息,返回 Response。。。
    return $response->prepare($request);
}

通过上面源码分析,我们知道了,响应就两种:JsonResponse 和 Response。但是它们两个的最终归宿仅仅只有一种,为什么呢?

因为给前端(无论是浏览器还是安卓)就只有一个响应,没毛病吧,所以这两种响应最终继承自 Symfony\Component\HttpFoundation\Response

JsonResponse:满足 api 接口要求返回 json 字符串的要求,它继承自 Symfony\Component\HttpFoundation\JsonResponse,然而 Symfony\Component\HttpFoundation\JsonResponse 又继承自 Symfony\Component\HttpFoundation\Response

Response:满足浏览器获取网页数据的要求,它继承自 Symfony\Component\HttpFoundation\Response

Response 变种写法

我们熟悉的写法:

return view('auth.register', [
    'message' => 'good',
]);

原理:view 函数最终返回 Illuminate\View\View 类对象。到执行到 toResponse 时,View 对象不满足转换成 JsonResponse 的条件,那么只能转成 Response 。

那么我们看一下 Response 类做了什么,先看构造函数

Symfony\Component\HttpFoundation\Response

public function __construct($content = '', int $status = 200, array $headers = array())
{
    // 初始化响应头信息
    $this->headers = new ResponseHeaderBag($headers);
    // 设置响应体内容
    $this->setContent($content);
    // 设置 HTTP 状态码
    $this->setStatusCode($status);
    // 设置 HTTP 协议版本
    $this->setProtocolVersion('1.0');
}

我们重点看 setContent 方法:先子类后父类

Illuminate\Http\Response

public function setContent($content)
{
    $this->original = $content;

    if ($this->shouldBeJson($content)) {
        $this->header('Content-Type', 'application/json');

        $content = $this->morphToJson($content);
    }

    // 这一行比较有意思,因为 Laravel-admin 框架就是运用这里
    elseif ($content instanceof Renderable) {
        $content = $content->render();
    }

    // View 不满足以上条件,到父类找
    parent::setContent($content);

    return $this;
}

Symfony\Component\HttpFoundation\Response

public function setContent($content)
{
    if (null !== $content && !\is_string($content) && !is_numeric($content) && !\is_callable(array($content, '__toString'))) {
        throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', \gettype($content)));
    }

    // View 一定在这里执行了。我们知道一个对象转换成字符串,那么对象中一定有 __toString() 魔术方法。
    $this->content = (string) $content;

    return $this;
}

View 中的 __toString 魔术方法

Illuminate\View\View

public function __toString()
{
    return $this->render();
}

那么我们来看 render 方法,字面意思:渲染。

Illuminate\View\View

public function render(callable $callback = null)
{
    try {

        // renderContents 会调用 blade 模板引擎,加载响应数据,获取原始 html 代码字符串。
        $contents = $this->renderContents();

        $response = isset($callback) ? call_user_func($callback, $this, $contents) : null;

        $this->factory->flushStateIfDoneRendering();

        // 最终返回 html 字符串。。。
        return ! is_null($response) ? $response : $contents;
    } catch (Exception $e) {
        $this->factory->flushState();

        throw $e;
    } catch (Throwable $e) {
        $this->factory->flushState();

        throw $e;
    }
}

关于 blade 模板引擎如何运行,这里不展开了,篇幅太长。。。我们仅知道 render 方法返回是 html 字符串就行了,浏览器会对这些字符进行编译和渲染,然后我们就看到了页面。

讲了半天,也没说怎么变种去写,,,汗!!!源码太复杂,写着写着就跑调啦。

变种一:函数实例化 Response

return response(view('auth.register', [
    'message' => 'good',
]));

变种二:直接实例化 Response

return new Response(view('auth.register', [
    'message' => 'good',
]));

需要 use Illuminate\Http\Response

变种三:直接 render

return view('auth.register', [
    'message' => 'good',
])->render();

变种四:View 门面

return View::make('auth.register', [
    'message' => 'good',
])

当然后两种与前两种随便组合。。。

最后,推荐原始写法,那种最简便。。。

JsonResponse 变种写法

return response()->json([
    'code' => 200,
    'message' => 'good',
]);

原理: response() 没有参数故返回 response 工厂类对象,当有参数时,直接返回 Response 对象

我们来看 json 方法

Illuminate\Routing\ResponseFactory

public function json($data = [], $status = 200, array $headers = [], $options = 0)
{
    return new JsonResponse($data, $status, $headers, $options);
}

哦,原来是直接实例化 JsonResponse 类。

关于 JsonResponse 内部如何运行的,就不展开了。我简单讲一下:首先在构造函数中调用 setData 方法,setData 方法将数组转换成 json 字符串,然后调用 setJson,将 json 字符串赋值给 data 属性,最后调用 update 方法,设置 Content-Typeapplication/json,最后调用 setContent,将 json 字符串设置到 Response 的响应体中。

变种一:当通过 api 接口新增数据时,我们可以直接返回 新增的模型

public function store(Request $request) :Model
{
    // 开启数据库事物是个好习惯。
    DB::beginTransaction();
    try {
        $article = new Article;
        $article->title = $request->input('title', '');
        $article->content = $request->input('content', '');
        // 尝试保存
        $article->save();
        // 没异常,提交事物
        DB::commit();
        // 返回模型对象
        return $article;
    } catch (Exception $e) {
        // 有异常,回滚数据库操作
        DB::rollBack();
        // 向上抛出异常
        throw $e;
    }
}

变种二:直接返回集合

return collect([
    'code' => 200,
    'message' => 'good',
]);

理由:看下面

class Collection implements ArrayAccess, Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable
{
    //...
}

实现了 Arrayable 和 Jsonable 的接口。

变种三:直接返回数组(推荐方法,简单。。。)

return [
    'code' => 200,
    'message' => 'good',
];

最后讲一下

下一章。我们把 Response 的 prepare 方法看一下。
最后一章。就是 Laravel 生命周期结束的时候。

还有两章了,加油。。。

本篇如有错误、不当或者需补充的内容,请各位同僚多提宝贵意见。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
发起讨论 只看当前版本


暂无话题~