生命周期 10--如何准备待发送 response 对象
前言
上一节已经分析了《如何执行路由对应的方法并返回待处理的结果》,本文分析一下「如何准备待发送response对象」。至此,输入request对象,经过kernel这个大黑盒子进行处理,最后返回response对象,已经来到最后一步了。
选择执行的uri
由于不同的uri,执行的过程、返回的结果都不同,因此只是列举一种情况:
/users/7
: 返回id=7的用户信息界面
其他情况遇到时再具体分析,毕竟目前的重点是理清生命周期的流程。
代码起点位置
\Illuminate\Routing\Router::runRouteWithinStack
的
return $this->prepareResponse(
$request, $route->run()
);
上一节已经通过执行$route->run()
返回了一个视图对象,这一节,我们就要准备response对象了。
具体分析
准备工作
为了分析得更清楚,创建视图对象这里,再补充分析一下。
先回到控制器方法\App\Http\Controllers\UsersController::show
中:
public function show(User $user)
{
return view('users.show', compact("user"));
}
使用view方法来获得视图内容。
function view($view = null, $data = [], $mergeData = [])
{
//获得一个`IlluminateViewFactory`对象
$factory = app(ViewFactory::class);
if (func_num_args() === 0) {
return $factory;
}
return $factory->make($view, $data, $mergeData);
}
也就是使用IlluminateViewFactory
的make方法来获得视图内容。
public function make($view, $data = [], $mergeData = [])
{
//使用`IlluminateViewFactory`中的finder对象来找到视图的绝对路径
$path = $this->finder->find(
$view = $this->normalizeName($view)
);
// Next, we will create the view instance and call the view creator for the view
// which can set any data, etc. Then we will return the view instance back to
// the caller for rendering or performing other view manipulations on this.
//整合数据
$data = array_merge($mergeData, $this->parseData($data));
//
return tap($this->viewInstance($view, $path, $data), function ($view) {
$this->callCreator($view);
});
}
创建一个视图实例:
protected function viewInstance($view, $path, $data)
{
return new View($this, $this->getEngineFromPath($path), $view, $path, $data);
}
然后使用tap方法,调用回调函数,回调函数中执行$this->callCreator($view)
方法,然后再返回视图实例。
这一返回就层层返回到了
return $this->prepareResponse(
$request, $route->run()
);
开始prepareResponse
\Illuminate\Routing\Router::prepareResponse
public function prepareResponse($request, $response)
{
return static::toResponse($request, $response);
}
\Illuminate\Routing\Router::toResponse
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);
}
由于$response
是\Illuminate\View\View
对象:
查看\Illuminate\View\View
类文件:
...
use ArrayAccess;
...
use Illuminate\Contracts\View\View as ViewContract;
class View implements ArrayAccess, ViewContract
再查看\Illuminate\Contracts\View\View
接口文件:
use Illuminate\Contracts\Support\Renderable;
interface View extends Renderable
由此可知,$response
- 不是
\Illuminate\Contracts\Support\Responsable
的实例 - 不是
\Psr\Http\Message\ResponseInterface
的实例 - 不是
\Illuminate\Database\Eloquent\Model
的实例 - 不是
\Illuminate\Contracts\Support\Arrayable
的实例 - 不是
\Illuminate\Contracts\Support\Jsonable
的实例 - 不是
\Illuminate\Contracts\Support\Arrayable
的实例 - 不是
\ArrayObject
的实例 - 不是
\JsonSerializable
的实例 - 也不是一个数组
- 也不是
\Symfony\Component\HttpFoundation\Response
的实例
因此,就只能到这段代码中了。
} elseif (! $response instanceof SymfonyResponse) {
$response = new Response($response);
}
这里是新建一个\Illuminate\Http\Response
对象,由于它本身没有construct方法,执行的是父类的construct方法:
public function __construct($content = '', int $status = 200, array $headers = array())
{
//创建一个\Symfony\Component\HttpFoundation\ResponseHeaderBag对象
//并给`\Illuminate\Http\Response`对象的headers属性赋值。
$this->headers = new ResponseHeaderBag($headers);
//对`\Illuminate\Http\Response`对象设置content.
$this->setContent($content);
$this->setStatusCode($status);
$this->setProtocolVersion('1.0');
}
$this->setContent($content);
public function setContent($content)
{
$this->original = $content;
// If the content is "JSONable" we will set the appropriate header and convert
// the content to JSON. This is useful when returning something like models
// from routes that will be automatically transformed to their JSON form.
if ($this->shouldBeJson($content)) {
$this->header('Content-Type', 'application/json');
$content = $this->morphToJson($content);
}
// If this content implements the "Renderable" interface then we will call the
// render method on the object so we will avoid any "__toString" exceptions
// that might be thrown and have their errors obscured by PHP's handling.
elseif ($content instanceof Renderable) {
$content = $content->render();
}
parent::setContent($content);
return $this;
}
由于之前我们分析过,$content
是Renderable
的实例,因此,执行下面这段:
elseif ($content instanceof Renderable) {
$content = $content->render();
}
/**
* Get the string contents of the view.
*
* @param callable|null $callback
* @return string
*
* @throws \Throwable
*/
public function render(callable $callback = null)
{
try {
//从\Illuminate\View\View获取内容
$contents = $this->renderContents();
$response = isset($callback) ? call_user_func($callback, $this, $contents) : null;
// Once we have the contents of the view, we will flush the sections if we are
// done rendering all views so that there is nothing left hanging over when
// another view gets rendered in the future by the application developer.
$this->factory->flushStateIfDoneRendering();
return ! is_null($response) ? $response : $contents;
} catch (Exception $e) {
$this->factory->flushState();
throw $e;
} catch (Throwable $e) {
$this->factory->flushState();
throw $e;
}
}
/**
* Get the contents of the view instance.
*
* @return string
*/
protected function renderContents()
{
// We will keep track of the amount of views being rendered so we can flush
// the section after the complete rendering operation is done. This will
// clear out the sections for any separate views that may be rendered.
$this->factory->incrementRender();
$this->factory->callComposer($this);
//获取内容
$contents = $this->getContents();
// Once we've finished rendering the view, we'll decrement the render count
// so that each sections get flushed out next time a view is created and
// no old sections are staying around in the memory of an environment.
$this->factory->decrementRender();
return $contents;
}
protected function getContents()
{
return $this->engine->get($this->path, $this->gatherData());
}
根据传入的视图绝对完整路径和数据获得计算后的内容
/**
* Get the evaluated contents of the view.
*
* @param string $path
* @param array $data
* @return string
*/
public function get($path, array $data = [])
{
$this->lastCompiled[] = $path;
// If this given view has expired, which means it has simply been edited since
// it was last compiled, we will re-compile the views so we can evaluate a
// fresh copy of the view. We'll pass the compiler the path of the view.
//如果当前缓存视图已失效,也就是说视图模板已经被修改
//,就就需要重新编译视图
if ($this->compiler->isExpired($path)) {
$this->compiler->compile($path);
}
//获得最新编译后的视图
$compiled = $this->compiler->getCompiledPath($path);
// Once we have the path to the compiled file, we will evaluate the paths with
// typical PHP just like any other templates. We also keep a stack of views
// which have been rendered for right exception messages to be generated.
//传入前面获得的数组到编译后的视图,返回视图的html结果.
$results = $this->evaluatePath($compiled, $data);
array_pop($this->lastCompiled);
return $results;
}
$results = $this->evaluatePath($compiled, $data);
/**
* Get the evaluated contents of the view at the given path.
*
* @param string $__path
* @param array $__data
* @return string
*/
protected function evaluatePath($__path, $__data)
{
$obLevel = ob_get_level();
//打开输出缓冲
ob_start();
//将传入的数据全部释放到变量中
extract($__data, EXTR_SKIP);
// We'll evaluate the contents of the view inside a try/catch block so we can
// flush out any stray output that might get out before an error occurs or
// an exception is thrown. This prevents any partial views from leaking.
try {
//由于include的特性,在被包含文件中,当前有效的变量仍然有效
//这就是我们在编写模板时,可以在里面写变量的原因
include $__path;
} catch (Exception $e) {
$this->handleViewException($e, $obLevel);
} catch (Throwable $e) {
$this->handleViewException(new FatalThrowableError($e), $obLevel);
}
return ltrim(ob_get_clean());
}
include $__path
<?php $__env->startSection('title', $user->name . ' 的个人中心'); ?>
<?php $__env->startSection('content'); ?>
<div class="row">
<div class="col-lg-3 col-md-3 hidden-sm hidden-xs user-info">
<div class="card">
<div class="card-body">
<img class="thumbnail img-fluid " src="<?php echo e($user->avatar); ?>" width="300px" height="300px">
<hr>
<h4><strong>个人简介</strong></h4>
<p><?php echo e($user->introduction); ?></p>
<hr>
<h4><strong>注册于</strong></h4>
<p><?php echo e($user->created_at->diffForHumans()); ?></p>
<h4><strong>最后活跃</strong></h4>
<p title="<?php echo e($user->last_actived_at); ?>"><?php echo e($user->last_actived_at->diffForHumans()); ?></p>
</div>
</div>
</div>
<div class="col-lg-9 col-md-9 col-sm-12 col-xs-12">
<div class="card">
<div class="card-body">
<span>
<h1 class="card-title float-left" style="font-size:30px;"><?php echo e($user->name); ?> <small><?php echo e($user->email); ?></small></h1>
</span>
</div>
</div>
<hr>
<div class="card">
<div class="card-body">
<ul class="nav nav-tabs">
<li class="<?php echo e(active_class(if_query('tab', null))); ?> nav-item"><a class="nav-link" href="<?php echo e(route('users.show', $user->id)); ?>">Ta 的话题</a></li>
<li class="<?php echo e(active_class(if_query('tab', 'replies'))); ?> nav-item"><a class="nav-link" href="<?php echo e(route('users.show', [$user->id, 'tab' => 'replies'])); ?>">Ta 的回复</a></li>
</ul>
<?php if(if_query('tab', 'replies')): ?>
<?php echo $__env->make('users._replies', ['replies' => $user->replies()->with('topic')->recent()->paginate(5)], \Illuminate\Support\Arr::except(get_defined_vars(), array('__data', '__path')))->render(); ?>
<?php else: ?>
<?php echo $__env->make('users._topics', ['topics' => $user->topics()->recent()->paginate(5)], \Illuminate\Support\Arr::except(get_defined_vars(), array('__data', '__path')))->render(); ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php $__env->stopSection(); ?>
<?php
//dd(\DB::getQueryLog());
//?>
<?php echo $__env->make('layouts.app', \Illuminate\Support\Arr::except(get_defined_vars(), array('__data', '__path')))->render(); ?>
可以看到,上面就是原生的PHP混编结构了,最开始写PHP就是这样的了,可见框架的模板系统有多么的方便,站的点有多高。
return ltrim(ob_get_clean());
执行缓存模板文件完成后,将所有内存中暂存的内容一次性输出并清空暂存区。
然后,将返回的内容(生成的HTML)返回到\Illuminate\Http\Response::setContent
方法的$content = $content->render();
。
parent::setContent($content);
然后,设置\Illuminate\Http\Response对象的content属性,返回到\Symfony\Component\HttpFoundation\Response::__construct
$this->setStatusCode($status);
设置\Illuminate\Http\Response对象的statusText属性。
$this->setProtocolVersion('1.0');
设置\Illuminate\Http\Response对象的version属性。
然后,返回\Illuminate\Routing\Router::toResponse
,执行到了
判断是否要设置304状态
if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
$response->setNotModified();
}
$response->prepare($request);
使用\Illuminate\Http\Response
对象的prepare方法来准备Response。
/**
* Prepares the Response before it is sent to the client.
*
* This method tweaks the Response to ensure that it is
* compliant with RFC 2616. Most of the changes are based on
* the Request that is "associated" with this Response.
*
* @return $this
*/
public function prepare(Request $request)
{
$headers = $this->headers;
//如果\Illuminate\Http\Response的statusCode>=100 & <200,
//或者\Illuminate\Http\Response的statusCode是204或304
if ($this->isInformational() || $this->isEmpty()) {
//设置\Illuminate\Http\Response的content属性为null
$this->setContent(null);
//移除\Symfony\Component\HttpFoundation\ResponseHeaderBag对象的headerNames属性中的Content-Type
$headers->remove('Content-Type');
//移除\Symfony\Component\HttpFoundation\ResponseHeaderBag对象的headerNames属性中的Content-Length
$headers->remove('Content-Length');
} else {
// Content-type based on the Request
//根据request来设置Content-Type
if (!$headers->has('Content-Type')) {
$format = $request->getRequestFormat();
if (null !== $format && $mimeType = $request->getMimeType($format)) {
$headers->set('Content-Type', $mimeType);
}
}
// Fix Content-Type
//如果没有设置charset,那么就是UTF-8
$charset = $this->charset ?: 'UTF-8';
//如果还没有Content-Type,那么就直接设置为text/html,并设置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
//如果Content-Type为text/开头且没有设置charset,就添加charset=UTF-8
$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
//如果不是HTTP1.0,那么就设置为HTTP1.1.
if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
$this->setProtocolVersion('1.1');
}
// Check if we need to send extra expire info headers
//如果是HTTP1.0,且Cache-Control为no-cache,就要添加额外的一些信息
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);
}
//IE<9的兼容性处理,不管了。
$this->ensureIEOverSSLCompatibility($request);
return $this;
}
接下来,返回到\Illuminate\Pipeline\Pipeline::carry
的下面这段:
return $response instanceof Responsable
? $response->toResponse($this->container->make(Request::class))
: $response;
此时,$response
是\Illuminate\Http\Response
对象,进入此类中
use Symfony\Component\HttpFoundation\Response as BaseResponse;
class Response extends BaseResponse
再进入其父类中:
class Response
{
均没有发现实现了\Illuminate\Contracts\Support\Responsable
接口。
因此,这里就不再处理,直接return $response
。
而结合我们在《request对象是如何通过路由中间件的》分析的,目前应用程序已经处理完了请求,还需要将$response
通过层层中间件返回到客户端,那么此时需要执行的就是所谓的后置中间件中的处理代码!
最近一层中间件为\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken
。
public function handle($request, Closure $next)
{
if (
$this->isReading($request) ||
$this->runningUnitTests() ||
$this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return tap($next($request), function ($response) use ($request) {
if ($this->shouldAddXsrfTokenCookie()) {
$this->addCookieToResponse($request, $response);
}
});
}
throw new TokenMismatchException;
}
可以看到,我们目前就从$next($request)
出来了,然后执行tap函数,调用回调函数处理(添加Xsrf-Token到响应的headers的Cookie)后,然后返回\Illuminate\Http\Response
对象到下一层洋葱的carry()方法:
return $response instanceof Responsable
? $response->toResponse($this->container->make(Request::class))
: $response;
又到了\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::handle
中:
public function handle($request, Closure $next)
{
$response = $next($request);
foreach ($this->cookies->getQueuedCookies() as $cookie) {
$response->headers->setCookie($cookie);
}
return $response;
}
执行$next($request)
下面的代码。然后返回下一层洋葱的carry方法,再返回下一层中间件,如果是后置中间件,就执行处理代码再返回下一层洋葱的carry方法,如果是前置中间件,就直接返回下一层洋葱的carry方法。
然后,返回到最后一个路由中间件\Illuminate\Cookie\Middleware\EncryptCookies
的encrypt
方法中.
protected function encrypt(Response $response)
{
foreach ($response->headers->getCookies() as $cookie) {
if ($this->isDisabled($cookie->getName())) {
continue;
}
$response->headers->setCookie($this->duplicate(
$cookie, $this->encrypter->encrypt($cookie->getValue(), static::serialized($cookie->getName()))
));
}
return $response;
}
对其中的cookies进行加密后,返回\Illuminate\Http\Response
到\Illuminate\Routing\Router
的prepareResponse方法中。
public function prepareResponse($request, $response)
{
return static::toResponse($request, $response);
}
这是要开始通过全局中间件返回了。
进入\Illuminate\Routing\Router::toResponse
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);
}
由于前面已经new Response($response)
,此时的$reponse
已经是\Illuminate\Http\Response
对象,也就是\Symfony\Component\HttpFoundation\Response
对象了。所以,这次,就直接到了$response->prepare($request)
.
然后就开始通过全局中间件返回,由于全局中间件都是前置中间件,没有代码再需要处理了。因此就一路返回到了\App\Http\Kernel的handle方法中。
//触发\Illuminate\Foundation\Http\Events\RequestHandled事件,
//如果之前有定义此事件的listener,那就会去执行了。
$this->app['events']->dispatch(
new Events\RequestHandled($request, $response)
);
return $response;
终于,返回到了public/index.php
的$response->send();
。
好了,下一步就是将laravel框架精心准备的response
对象发送给客户端了。就快胜利了。。
小结
- 程序运行结果出来后,要经过两个洋葱,至少十几个中间件,方能准备好
$response
对象。 - 至此,以前看到的一摩尔的堆栈,看着他们就想快进的日子,终于结束了。知其然,更要知其所以然。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: