响应预处理
简介
上一章,我们了解了控制器与方法的运行原理。
由于控制器与方法,都是我们自己自定义的,那么返回的数据类型也会出现不确定的情况。
我总结一下,最常用的两类返回写法:
- 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-Type
为 application/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 生命周期结束的时候。
还有两章了,加油。。。