Laravel 底层分析:生命周期——响应(第四部分)
这个指南介绍了Laravel框架5.6版的源代码.
小结
在 第一部分, 我们学习了在 index.php
加载完成之后立马发生了什么, 并且这些事件是在Laravel 框架的核心服务 (core services) 加载之前发生的.
在 第二部分, 我们学习了框架是如何加载它的结构与配置的 (configuration), 如何设置好错误处理, 如何注册所有的 service providers, 如何解析所有的 facades.
在 第三部分, 我们学习了 Laravel 如何处理你你发出的 url 指令到具体的 route 上, 如何加载 controller 来处理具体的某一个 route.
现在, 让我们来学习 controller 是如何自动生成适用的回应 (response) 然后显示在浏览器上.
响应前的准备
在第三部分, 我们看到 toResponse
方法转变了所有你传过来的数据为一个响应对象 (Response object). 我们还发现变量 $response
是这个控制器的直接输出
public static function toResponse($request, $response)
{
if ($response instanceof Responsable) {
$response = $response->toResponse($request);
}
if ($response instanceof PsrResponseInterface) {
$response = (new HttpFoundationFactory)->createResponse($response);
} elseif ($response instanceof Model && $response->wasRecentlyCreated) {
$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 = new JsonResponse($response);
} elseif (! $response instanceof SymfonyResponse) {
$response = new Response($response);
}
if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
$response->setNotModified();
}
return $response->prepare($request);
}
我们发现这里就是Laravel 神奇的转变 数组, 字符串和Eloquent 模型到 JSON的地方 (因为它们在这实施了可序列化的接口, 里面包含了toJson
方法).
如果我们观察一下 Illuminate\Http
命名空间里的 Response class,
我们可以发现这个 class 继承了Symfony 的HttpFoundation 组件, 并且名字相同.
并且我们发现这个class 并没有很多代码. 因为大多数的配置是Symfony在后台完成的. 在我们查看响应方法 (preparation method) 之前, 我们要先看看他的构造方法里都做了什么在此查看 .
新的实例
我们立即就看到了从空的 $headers
数组创建了一个 header 包,因为我们在实例化( new Response($response)
)的过程中没有发送初始头。
public function __construct($content = '', int $status = 200, array $headers = array())
{
$this->headers = new ResponseHeaderBag($headers);
$this->setContent($content);
$this->setStatusCode($status);
$this->setProtocolVersion('1.0');
}
如果你在储存了头的内容之后检查 Response 对象的内容,你会发现它是一个空的对象 -- 仅仅设置了日期和缓存控制头。接下来,我们来设置一些初始属性,比如 HTTP 版本、响应内容、状态码和最后前往的准备。如果在设置初始属性之后我们使用 dd
来查看这个类,我们将看到下面的内容:(请记住,这里加载的 welcome 视图是刚刚安装好的 Laravel ):
我们当前应该注意几件事儿:original
属性是一个视图对象, statusText
属性是根据HTTP状态码来设置相应的文本内容(404 - Not Found,200 - OK等), content
包含了将要呈现给客户端的文字输出(HTML就是一个例子)。我有点好奇这个视图对象...有趣的是,setContent
方法实际上是从 Laravel 的 Response类中调用的。我们把注释删除之后,会发现这个方法非常简单干净。
public function setContent($content)
{
$this->original = $content;
if ($this->shouldBeJson($content)) {
$this->header('Content-Type', 'application/json');
$content = $this->morphToJson($content);
} elseif ($content instanceof Renderable) {
$content = $content->render();
}
parent::setContent($content);
return $this;
}
首选我们要做的是将初始化内容(对象、视图等等)保存到 $content
属性中,以便我们之后使用。接下来,我们需要修改 $content
变量。应该有一个作为JSON(字符串、模型、集合、JSON 相应等)返回的对象?只需要将 header 头中的 Content-Type
设置为 application/json
并且将输出 编码 为JSON。返回视图?或者任意实现 Renderable
接口的实现?可以在那些对象上调用 render
方法,并且调用 setContent
方法来修改它们的父级内容。在我们的例子中, render
方法使用 Blade 编译视图并创建原始HTML。此刻,我们可以打印 $content
,它将展示最终的HTML内容,但我们并不是在这里完成的。
头的准备
没错,现在我们在 Router 中创建了一个 Response 类的新实例,我们已经准备好调用 prepare
方法了。 我们可以看到 这个方法中包含了很多代码,所以我不会粘贴在这里,以免破坏帖子的排版结构。首先,我们先要搞清楚响应是有信息的还是空的。这里我们要知道,如果响应状态码是 100 至 200 之间,则说明响应是有信息的 ,如果是空的 ,那么响应状态吗应该是 204 或 304。如果他是有信息的或空的,请删除所有的内容和与内容有关的头。否则需要根据请求来设置内容类型和内容长度,同时也要设置正确的字符集。注意,默认的HTTP版本是 v1.1。 最后,检查 是否应该为了IE的SSL加密下载删除 Cache-Control。
JSON 和重定向响应
JSON 和重定向响应只是一些稍微改动的 Response 对象。JSON 响应由 Laravel 中的 JsonResponse
来进行处理。而这个类继承了 Symfony 类并进行了一些 JSON 验证,编码,设置 JSON content-type 响应头部等内容。重定向响应做了它应当做的事情,同样也是继承了 Symfony 类。这个类做的唯一一件事情就是设置 Location
头部为目标 URL。
重定向
重定向 是一个比较特殊的类,它包含了一些重定向响应背后的常用函数:重定向后退,重定向主页,重定向到指定路由等等。所以当我们调用重定向的 facade/helper 时,实际上是启动了 Redirector 对象。他还使用 URLGenerator
类来生成指定路由的URL和来自header/session 先前的URL等等。所以就如 redirect()->back()
实际上调用了这段代码:
public function back($status = 302, $headers = [], $fallback = false)
{
return $this->createRedirect($this->generator->previous($fallback), $status, $headers);
}
如果你看过 createRedirect
方法,你就会知道这个方法事实上只是创建了一个 RedirectResponse
类的新实例,并且在 Response 中设置了 session 和请求。
响应助手
同样值得注意的是,如果您调用 Laravel 的惊人的 response()
助手,那么 工厂类 ResponseFactory
实际上就是从容器中解析出来的。这是一个类,它只是响应对象的包装器,并提供用于创建公共响应类型(JSON、下载、重定向等)的委托:
// Illuminate/Foundation/helpers.php
function response($content = '', $status = 200, array $headers = [])
{
$factory = app(ResponseFactory::class);
if (func_num_args() === 0) {
return $factory;
}
return $factory->make($content, $status, $headers);
}
完成生命周期
我们现在已经完成了 Laravel 应用生命周期的每一步,并且通过栈回到了那个大名鼎鼎的 index.php
文件。这里唯一要做的就是向客户端展现(发送)响应并调用任意 terminable 中间件。
$response->send();
$kernel->terminate($request, $response);
发送响应
我们准备好了头部信息, 编译好了 view 至 HTML 格式, 完成了所有控制器和模型里的商业逻辑, 现在唯一剩下的就是发送头部信息然后展示内容了.
这就是 Symfony 的 Response class 里 send
方法所做的.
public function send()
{
$this->sendHeaders();
$this->sendContent();
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (!\in_array(PHP_SAPI, array('cli', 'phpdbg'), true)) {
static::closeOutputBuffers(0, true);
}
return $this;
}
这个方法还为使用 PHP-FPM 服务器的用户多做了一些数据的检查和规整, 其实这完全没有必要.
如果你是使用 Laravel Valet 和 brew 安装的 PHP, 那么这个方法基本上一定会被使用到. 如果你使用 output control 的话其实还有一个方法会关闭输出缓存 并且输出内容, 但是我们不会再这个指南中提到. 如果你有兴趣的话可以 在此 查看.
发送头部数据
让我们稍微关注一下 sendHeaders
方法,可以看到在这里使用了两个 PHP 的自带函数: headers_sent
和 header
。
public function sendHeaders()
{
if (headers_sent()) {
return $this;
}
foreach ($this->headers->allPreserveCase() as $name => $values) {
foreach ($values as $value) {
header($name.': '.$value, false, $this->statusCode);
}
}
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
return $this;
}
如果你把你的 CodeIgniter 应用弄的乱七八糟,那你一定不要忘记之前发生过的错误 -- 已经发送了头部数据?
好在, headers_sent
的条件限制了这一点。 如果我们没有发送过头部数据,那我们将会在现在发送它!在遍历 headers
属性时每次循环的过程中调用 PHP 的 header
方法 。同时,我们也需要一个额外的头部调用来设置响应状态码和 HTTP 协议的版本。这一步是在循环外完成的,因为所有其他头都是以 「 Name : Header 」 的形式填入的(例如 header('Content-Type: application/json');
),虽然头的状态看起来像是 「 HTTP/version code text 」,例如 header("HTTP/1.0 404 Not Found");
。
发送内容
在发送完响应头部(headers)之后,我们需要将内容发送给用户。还记得我们说过我们可以直接将内容返回获得 HTML 响应么?这就是sendContent
方法所做的。它仅仅是调用了echo
方法:
public function sendContent()
{
echo $this->content;
return $this;
}
此时,响应已经发送并且浏览器已经开始显示内容。或者如果你使用的是API,那么返回的是 JSON 格式的内容。
终止应用程序
在发送响应之后,生命周期中唯一需要完成的步骤就是调用任何额外的可终止中间件。我们看到 index.php
中内核上的 terminate
方法被称为--$kernel->terminate($request, $response);
,该方法在应用程序实例上委托 terminate
方法,但在调用任何其他中间件之前不会。
public function terminate($request, $response)
{
$this->terminateMiddleware($request, $response);
$this->app->terminate();
}
现在我们已经了解了中间件的工作原理,我们只需快速查看 terminateMiddleware
方法,并了解它在做什么:
protected function terminateMiddleware($request, $response)
{
$middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge(
$this->gatherRouteMiddleware($request),
$this->middleware
);
foreach ($middlewares as $middleware) {
if (! is_string($middleware)) {
continue;
}
list($name) = $this->parseMiddleware($middleware);
$instance = $this->app->make($name);
if (method_exists($instance, 'terminate')) {
$instance->terminate($request, $response);
}
}
}
在获得中间件列表之后,循环遍历它们并在内核上调用 terminate
方法——但仅当存在一个方法时才调用。然后,委托给应用程序对象上的 terminate
方法,该方法将激发终止事件并调用可能为该事件创建的任何其他侦听器。最后,完成整个应用程序的生命周期!
总结
总结:我们查看了在您提出请求时加载的第一个文件,它如何注册核心Laravel 组件,它如何创建一个HTTP内核来注册和启动所有服务提供者,从环境变量创建全局请求对象,运行路由器,以及路由器如何执行您在您的公司中提供的代码。最后,将响应内容回复给发出请求的客户机。
再见
这将是 laravel幕后 中的生命周期系列。我希望你喜欢它,并且学到了一些新的东西!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。