生命周期 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;
    }

由于之前我们分析过,$contentRenderable的实例,因此,执行下面这段:

    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\EncryptCookiesencrypt方法中.

    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 协议》,转载必须注明作者和本文链接
日拱一卒
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 2

源码最多深入3层就看不下去了怎么办啊

6年前 评论

@L伟 可能你还不习惯吧,多看看,习惯就好了,几十层都可以的

6年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
92
粉丝
87
喜欢
152
收藏
121
排名:72
访问:11.3 万
私信
所有博文
社区赞助商