Response
前面兩節我們分別講了Laravel的控制器和Request物件,在講Request物件的那一節我們看了Request物件是如何被建立出來的以及它支援的方法都定義在哪裡,講控制器時我們詳細地描述瞭如何找到Request對應的控制器方法然後執行處理程式的,本節我們就來說剩下的那一部分,控制器方法的執行結果是如何被轉換成響應物件Response然後返回給客戶端的。
建立Response
讓我們回到Laravel執行路由處理程式返回響應的程式碼塊:
namespace Illuminate\Routing;
class Router implements RegistrarContract, BindingRegistrar
{
protected function runRoute(Request $request, Route $route)
{
$request->setRouteResolver(function () use ($route) {
return $route;
});
$this->events->dispatch(new Events\RouteMatched($route, $request));
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
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(
$request, $route->run()
);
});
}
}
複製程式碼
在講控制器的那一節裡我們已經提到過runRouteWithinStack
方法裡是最終執行路由處理程式(控制器方法或者閉包處理程式)的地方,通過上面的程式碼我們也可以看到執行的結果會傳遞給Router
的prepareResponse
方法,當程式流返回到runRoute
裡後又執行了一次prepareResponse
方法得到了要返回給客戶端的Response物件, 下面我們就來詳細看一下prepareResponse
方法。
class Router implements RegistrarContract, BindingRegistrar
{
/**
* 通過給定值建立Response物件
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param mixed $response
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function prepareResponse($request, $response)
{
return static::toResponse($request, $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 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);
}
}
複製程式碼
在上面的程式碼中我們看到有三種Response:
Class Name | Representation |
---|---|
PsrResponseInterface(Psr\Http\Message\ResponseInterface的別名) | Psr規範中對服務端響應的定義 |
Illuminate\Http\JsonResponse (Symfony\Component\HttpFoundation\Response的子類) | Laravel中對服務端JSON響應的定義 |
Illuminate\Http\Response (Symfony\Component\HttpFoundation\Response的子類) | Laravel中對普通的非JSON響應的定義 |
通過prepareResponse
中的邏輯可以看到,無論路由執行結果返回的是什麼值最終都會被Laravel轉換為成一個Response物件,而這些物件都是Symfony\Component\HttpFoundation\Response類或者其子類的物件。從這裡也就能看出來跟Request一樣Laravel的Response也是依賴Symfony框架的HttpFoundation
元件來實現的。
我們來看一下Symfony\Component\HttpFoundation\Response的構造方法:
namespace Symfony\Component\HttpFoundation;
class Response
{
public function __construct($content = '', $status = 200, $headers = array())
{
$this->headers = new ResponseHeaderBag($headers);
$this->setContent($content);
$this->setStatusCode($status);
$this->setProtocolVersion('1.0');
}
//設定響應的Content
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)));
}
$this->content = (string) $content;
return $this;
}
}
複製程式碼
所以路由處理程式的返回值在創業Response物件時會設定到物件的content屬性裡,該屬性的值就是返回給客戶端的響應的響應內容。
設定Response headers
生成Response物件後就要執行物件的prepare
方法了,該方法定義在Symfony\Component\HttpFoundation\Resposne
類中,其主要目的是對Response進行微調使其能夠遵從HTTP/1.1協議(RFC 2616)。
namespace Symfony\Component\HttpFoundation;
class Response
{
//在響應被髮送給客戶端之前對其進行修訂使其能遵從HTTP/1.1協議
public function prepare(Request $request)
{
$headers = $this->headers;
if ($this->isInformational() || $this->isEmpty()) {
$this->setContent(null);
$headers->remove('Content-Type');
$headers->remove('Content-Length');
} else {
// Content-type based on the Request
if (!$headers->has('Content-Type')) {
$format = $request->getRequestFormat();
if (null !== $format && $mimeType = $request->getMimeType($format)) {
$headers->set('Content-Type', $mimeType);
}
}
// Fix Content-Type
$charset = $this->charset ?: 'UTF-8';
if (!$headers->has('Content-Type')) {
$headers->set('Content-Type', 'text/html; charset='.$charset);
} elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) {
// add the charset
$headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset);
}
// Fix Content-Length
if ($headers->has('Transfer-Encoding')) {
$headers->remove('Content-Length');
}
if ($request->isMethod('HEAD')) {
// cf. RFC2616 14.13
$length = $headers->get('Content-Length');
$this->setContent(null);
if ($length) {
$headers->set('Content-Length', $length);
}
}
}
// Fix protocol
if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
$this->setProtocolVersion('1.1');
}
// Check if we need to send extra expire info headers
if ('1.0' == $this->getProtocolVersion() && false !== strpos($this->headers->get('Cache-Control'), 'no-cache')) {
$this->headers->set('pragma', 'no-cache');
$this->headers->set('expires', -1);
}
$this->ensureIEOverSSLCompatibility($request);
return $this;
}
}
複製程式碼
prepare
裡針對各種情況設定了相應的response header
比如Content-Type
、Content-Length
等等這些我們常見的首部欄位。
傳送Response
建立並設定完Response後它會流經路由和框架中介軟體的後置操作,在中介軟體的後置操作裡一般都是對Response進行進一步加工,最後程式流回到Http Kernel那裡, Http Kernel會把Response傳送給客戶端,我們來看一下這部分的程式碼。
//入口檔案public/index.php
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
複製程式碼
namespace Symfony\Component\HttpFoundation;
class Response
{
public function send()
{
$this->sendHeaders();
$this->sendContent();
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif ('cli' !== PHP_SAPI) {
static::closeOutputBuffers(0, true);
}
return $this;
}
//傳送headers到客戶端
public function sendHeaders()
{
// headers have already been sent by the developer
if (headers_sent()) {
return $this;
}
// headers
foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
foreach ($values as $value) {
header($name.': '.$value, false, $this->statusCode);
}
}
// status
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
// cookies
foreach ($this->headers->getCookies() as $cookie) {
if ($cookie->isRaw()) {
setrawcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
} else {
setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
}
}
return $this;
}
//傳送響應內容到客戶端
public function sendContent()
{
echo $this->content;
return $this;
}
}
複製程式碼
send
的邏輯就非常好理解了,把之前設定好的那些headers設定到HTTP響應的首部欄位裡,Content會echo後被設定到HTTP響應的主體實體中。最後PHP會把完整的HTTP響應傳送給客戶端。
send響應後Http Kernel會執行terminate
方法呼叫terminate中介軟體裡的terminate
方法,最後執行應用的termiate
方法來結束整個應用生命週期(從接收請求開始到返回響應結束)。
本文已經收錄在系列文章Laravel核心學習指南裡,歡迎訪問閱讀。