深度挖掘 Laravel 生命周期

本文首发于个人博客 深度挖掘 Laravel 生命周期,转载请注明出处。

这篇文章我们来聊聊 「Laravel 生命周期」 这个主题。虽然网络上已经有很多关于这个主题的探讨,但这个主题依然值得我们去研究和学习。

我想说的是当我们在决定使用某项技术的时候,除了需要了解它能「做什么」,其实还应当研究它是「怎么做的」。

Laravel 框架或者说任何一个 Web 项目,我们都需要理解它究竟是如何接收到用户发起的 HTTP 请求的;又是如何响应结果给用户的;在处理请求和响应的过程中都存在哪些处理值得深入学习。

所有这些内容其实都包含在 「Laravel 生命周期」 这个主题里面。

本文较长建议使用合适的 IDE 进行代码查阅;或者通过文中的链接,或是代码注释的 「@see」部分直接在 Github 畅读代码。

目录结构

  • 一 摘要
  • 二 生命周期之始末

    • 2.1 加载项目依赖
    • 2.2 创建 Laravel 应用实例

      • 2.2.1 创建应用实例
      • 2.2.2 内核绑定
      • 2.2.3 注册异常处理
      • 2.2.4 小结
    • 2.3 接收请求并响应

      • 2.3.1 解析内核实例
      • 2.3.2 处理 HTTP 请求

        • 2.3.2.1 创建请求实例
        • 2.3.2.2 处理请求

          • 2.3.2.2.1 启动「引导程序」
          • 2.3.2.2.2 发送请求至路由
    • 2.4 发送响应
    • 2.5 终止程序
  • 三 总结
  • 四 生命周期流程图
  • 参考资料

一 摘要

Laravel 生命周期(或者说请求生命周期)概括起来主要分为 3 个主要阶段:

  • 加载项目依赖
  • 创建 Laravel 应用实例
  • 接收请求并响应

而这 3 个阶段的处理都发生在入口文件 public/index.php 文件内(public/index.php 是一个新安装的 Laravel 项目默认入口文件)。

然而 index.php 文件仅包含极少的代码,但却出色的完成了一个 HTTP 请求从接收到响应的全部过程,逻辑组织的几近完美。

我们来看下入口文件实现的代码:

make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

// 其它
$kernel->terminate($request, $response);

二 生命周期之始末

2.1 加载项目依赖

现代 PHP 依赖于 Composer 包管理器,入口文件通过引入由 Composer 包管理器自动生成的类加载程序,可以轻松注册并加载项目所依赖的第三方组件库。

所有组件的加载工作,仅需一行代码即可完成:

require __DIR__.'/../vendor/autoload.php';

2.2 创建 Laravel 应用实例

创建应用实例(或称服务容器),由位于 bootstrap/app.php 文件里的引导程序完成,创建服务容器的过程即为应用初始化的过程,项目初始化时将完成包括:注册项目基础服务、注册项目服务提供者别名、注册目录路径等在内的一些列注册工作。

下面是 bootstrap/app.php 的代码,包含两个主要部分「创建应用实例」和「绑定内核至 APP 服务容器」:

singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

return $app;

2.2.1 创建应用实例

创建应用实例即实例化 Illuminate\Foundation\Application 这个服务容器,后续我们称其为 APP 容器。在创建 APP 容器主要会完成:注册应用的基础路径并将路径绑定到 APP 容器 、注册基础服务提供者至 APP 容器 、注册核心容器别名至 APP 容器 等基础服务的注册工作。

    /**
     * Create a new Illuminate application instance.
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php#L162:27
     * @param  string|null  $basePath
     * @return void
     */
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }
        $this->registerBaseBindings();
        $this->registerBaseServiceProviders();
        $this->registerCoreContainerAliases();
    }

2.2.2 内核绑定

接着将关注的焦点转移到绑定内核部分。

Laravel 会依据 HTTP 请求的运行环境的不同,将请求发送至相应的内核: HTTP 内核Console 内核。无论 HTTP 内核还是 Console 内核,它们的作用都是是接收一个 HTTP 请求,随后返回一个响应,就是这么简单。

这篇文章主要研究 HTTP 内核,HTTP 内核继承自 Illuminate\Foundation\Http\Kernel 类.

在 「HTTP 内核」 内它定义了 中间件 相关数组;在 「Illuminate\Foundation\Http\Kernel」 类内部定义了属性名为 「bootstrappers」 的 引导程序 数组。

  • 中间件 提供了一种方便的机制来过滤进入应用的 HTTP 请求。
  • 「引导程序」 包括完成环境检测、配置加载、异常处理、Facades 注册、服务提供者注册、启动服务这六个引导程序。

至于 「中间件」 和 「引导程序」如何被使用的,会在后面的章节讲解。

2.2.3 注册异常处理

项目的异常处理由 App\Exceptions\Handler::class 类完成,这边也不做深入的讲解。

2.2.4 本节小结

通过上面的分析我们可以发现在「创建 Laravel 应用实例」这个阶段它做了很多的基础工作,包括但不限于:创建 APP 容器、注册应用路径、注册基础服务提供者、配置中间件和引导程序等。

2.3 接收请求并响应

在完成创建 APP 容器后即进入了第三个阶段 「接收请求并响应」。

「接收请求并响应」有关代码如下:

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

我们需要逐行分析上面的代码,才能窥探其中的原貌。

2.3.1 解析内核实例

在第二阶段我们已经将 HTTP 内核Console 内核 绑定到了 APP 容器,使用时通过 APP 容器make() 方法将内核解析出来,解析的过程就是内核实例化的过程。

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

内核实例化时它的内部究竟又做了哪些操作呢?

进一步挖掘 Illuminate\Foundation\Http\Kernel 内核的 __construct(IlluminateContractsFoundationApplication $app, \Illuminate\Routing\Router $router) 构造方法,它接收 APP 容器路由器 两个参数。

在实例化内核时,构造函数内将在 HTTP 内核定义的「中间件组」注册到 路由器,注册完后就可以在实际处理 HTTP 请求前调用这些「中间件」实现 过滤 请求的目的。

...
    /**
     * Create a new HTTP kernel instance. 创建 HTTP 内核实例
     * 
     * @class Illuminate\Foundation\Http\Kernel
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @param  \Illuminate\Routing\Router  $router
     * @return void
     */
    public function __construct(Application $app, Router $router)
    {
        $this->app = $app;
        $this->router = $router;

        $router->middlewarePriority = $this->middlewarePriority;

        foreach ($this->middlewareGroups as $key => $middleware) {
            $router->middlewareGroup($key, $middleware);
        }

        foreach ($this->routeMiddleware as $key => $middleware) {
            $router->aliasMiddleware($key, $middleware);
        }
    }
...
...
    /**
     * Register a group of middleware. 注册中间件组
     *
     * @class \Illuminate\Routing\Router
     * @param  string  $name
     * @param  array  $middleware
     * @return $this
     */
    public function middlewareGroup($name, array $middleware)
    {
        $this->middlewareGroups[$name] = $middleware;

        return $this;
    }

    /**
     * Register a short-hand name for a middleware. 注册中间件别名
     *
     * @class \Illuminate\Routing\Router
     * @param  string  $name
     * @param  string  $class
     * @return $this
     */
    public function aliasMiddleware($name, $class)
    {
        $this->middleware[$name] = $class;

        return $this;
    }
...

2.3.2 处理 HTTP 请求

之前的所有处理,基本都是围绕在配置变量、注册服务等运行环境的构建上,构建完成后才是真刀真枪的来处理一个「HTTP 请求」。

处理请求实际包含两个阶段:

  • 创建请求实例
  • 处理请求
// 处理请求
$response = $kernel->handle(
    // 创建请求实例
    $request = Illuminate\Http\Request::capture()
);
2.3.2.1 创建请求实例

请求实例 Illuminate\Http\Requestcapture() 方法内部通过 Symfony 实例创建一个 Laravel 请求实例。这样我们就可以获取到用户请求报文的相关信息了。

    /**
     * Create a new Illuminate HTTP request from server variables.
     * 
     * @class Illuminate\Http\Request
     * @return static
     */
    public static function capture()
    {
        static::enableHttpMethodParameterOverride();
        return static::createFromBase(SymfonyRequest::createFromGlobals());
    }

    /**
     * Create an Illuminate request from a Symfony instance.
     *
     * @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/Request.php
     * @param  \Symfony\Component\HttpFoundation\Request  $request
     * @return \Illuminate\Http\Request
     */
    public static function createFromBase(SymfonyRequest $request)
    {
        if ($request instanceof static) {
            return $request;
        }

        $content = $request->content;

        $request = (new static)->duplicate(
            $request->query->all(), $request->request->all(), $request->attributes->all(),
            $request->cookies->all(), $request->files->all(), $request->server->all()
        );

        $request->content = $content;

        $request->request = $request->getInputSource();

        return $request;
    }
2.3.2.2 处理请求

请求处理发生在 HTTP 内核handle() 方法内。

    /**
     * Handle an incoming HTTP request.
     *
     * @class Illuminate\Foundation\Http\Kernel
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function handle($request)
    {
        try {
            $request->enableHttpMethodParameterOverride();

            $response = $this->sendRequestThroughRouter($request);
        } catch (Exception $e) {
            $this->reportException($e);

            $response = $this->renderException($request, $e);
        } catch (Throwable $e) {
            $this->reportException($e = new FatalThrowableError($e));

            $response = $this->renderException($request, $e);
        }

        $this->app['events']->dispatch(
            new Events\RequestHandled($request, $response)
        );

        return $response;
    }

handle() 方法接收一个 HTTP 请求,并最终生成一个 HTTP 响应。

继续深入到处理 HTTP 请求的方法 $this->sendRequestThroughRouter($request) 内部。

    /**
     * Send the given request through the middleware / router.
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Http/Kernel.php
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendRequestThroughRouter($request)
    {
        $this->app->instance('request', $request);

        Facade::clearResolvedInstance('request');

        $this->bootstrap();

        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

将发现这段代码没有一行废话,它完成了大量的逻辑处理:

  • 首先,将 $request 实例注册到 APP 容器 供后续使用;
  • 之后,清除之前 $request 实例缓存;
  • 然后,启动「引导程序」;
  • 最后,发送请求至路由。
2.3.2.2.1 启动「引导程序」

记得我们在之前「2.2.2 内核绑定」章节,有介绍在「HTTP 内核」中有把「引导程序(bootstrappers)」绑定到了 APP 容器,以及这些引导程序的具体功能。

但是没有聊如何调用这些「引导程序」。

    /**
     * Send the given request through the middleware / router.
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Http/Kernel.php
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendRequestThroughRouter($request)
    {
        ...

        // 启动 「引导程序」
        $this->bootstrap();

        ...
    }

上面的代码块说明在 $this->bootstrap(); 方法内部有实际调用「引导程序」,而 bootstrap() 实际调用的是 APP 容器的 bootstrapWith(),来看看

... 
    /**
     * The bootstrap classes for the application. 应用的引导程序
     *
     * @var array
     */
    protected $bootstrappers = [
        \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
        \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
        \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
        \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
        \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
        \Illuminate\Foundation\Bootstrap\BootProviders::class,
    ];

    /**
     * Bootstrap the application for HTTP requests.
     * 
     * @class Illuminate\Foundation\Http\Kernel
     * @return void
     */
    public function bootstrap()
    {
        if (! $this->app->hasBeenBootstrapped()) {
            $this->app->bootstrapWith($this->bootstrappers());
        }
    }

    protected function bootstrappers()
    {
        return $this->bootstrappers;
    }
...

最终还是要看 Illuminate\Foundation\ApplicationbootstrapWith() 方法究竟如何来启动这些引导程序的。

    /**
     * Run the given array of bootstrap classes.
     * 
     * @class  Illuminate\Foundation\Application APP 容器
     * @param  array  $bootstrappers
     * @return void
     */
    public function bootstrapWith(array $bootstrappers)
    {
        $this->hasBeenBootstrapped = true;

        foreach ($bootstrappers as $bootstrapper) {
            $this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);

            $this->make($bootstrapper)->bootstrap($this);

            $this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
        }
    }

我们看到在 APP 容器内,会先解析对应的「引导程序」(即实例化),随后调用「引导程序」的 bootstrap() 完成的「引导程序」的启动操作。

作为示例我们随便挑一个「引导程序」来看看其内部的启动原理。

这边我们选 Illuminate\Foundation\Bootstrap\LoadConfiguration::class,它的功能是加载配置文件。

还记得我们讲解「2.2 创建 Laravel 应用实例」章节的时候有「注册应用的基础路径并将路径绑定到 APP 容器」。此时,LoadConfiguration 类就是将 config 目录下的所有配置文件读取到一个集合中,这样我们就可以项目里通过 config() 辅助函数获取配置数据。

getCachedConfigPath())) {
            $items = require $cached;
            $loadedFromCache = true;
        }

        $app->instance('config', $config = new Repository($items));

        if (! isset($loadedFromCache)) {
            $this->loadConfigurationFiles($app, $config);
        }

        $app->detectEnvironment(function () use ($config) {
            return $config->get('app.env', 'production');
        });

        date_default_timezone_set($config->get('app.timezone', 'UTC'));
        mb_internal_encoding('UTF-8');
    }

    /**
     * Load the configuration items from all of the files.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @param  \Illuminate\Contracts\Config\Repository  $repository
     * @return void
     * @throws \Exception
     */
    protected function loadConfigurationFiles(Application $app, RepositoryContract $repository)
    {
        $files = $this->getConfigurationFiles($app);

        if (! isset($files['app'])) {
            throw new Exception('Unable to load the "app" configuration file.');
        }

        foreach ($files as $key => $path) {
            $repository->set($key, require $path);
        }
    }

    ...
}

所有 「引导程序」列表功能如下:

2.3.2.2.2 发送请求至路由

完成「引导程序」启动操作后,随机进入到请求处理阶段。

    /**
     * Send the given request through the middleware / router.
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Http/Kernel.php
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendRequestThroughRouter($request)
    {
        ...

        // 发送请求至路由
        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

在 「发送请求至路由」这行代码中,完成了:管道(pipeline)创建、将 $request 传入管道、对 $request 执行「中间件」处理和实际的请求处理四个不同的操作。

在开始前我们需要知道在 Laravel 中有个「中间件」 的概念,即使你还不知道,也没关系,仅需知道它的功能是在处理请求操作之前,对请求进行过滤处理即可,仅当请求符合「中间件」的验证规则时才会继续执行后续处理。

有关 「管道」的相关知识不在本文讲解范围内。

那么,究竟一个请求是如何被处理的呢?

我们来看看 $this->dispatchToRouter() 这句代码,它的方法声明如下:


    /**
     * Get the route dispatcher callback. 获取一个路由分发器匿名函数
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Http/Kernel.php
     * @return \Closure
     */
    protected function dispatchToRouter()
    {
        return function ($request) {
            $this->app->instance('request', $request);

            return $this->router->dispatch($request);
        };
    }

回顾下「2.3.1 解析内核实例」章节,可知我们已经将 Illuminate\Routing\Router 对象赋值给 $this->router 属性。

通过 router 实例的 disptach() 方法去执行 HTTP 请求,在它的内部会完成如下处理:

  1. 查找对应的路由实例
  2. 通过一个实例栈运行给定的路由
  3. 运行在 routes/web.php 配置的匹配到的控制器或匿名函数
  4. 返回响应结果
currentRequest = $request;

        return $this->dispatchToRoute($request);
    }

    /**
     * Dispatch the request to a route and return the response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    public function dispatchToRoute(Request $request)
    {
        return $this->runRoute($request, $this->findRoute($request));
    }

    /**
     * Find the route matching a given request. 1. 查找对应的路由实例
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Routing\Route
     */
    protected function findRoute($request)
    {
        $this->current = $route = $this->routes->match($request);

        $this->container->instance(Route::class, $route);

        return $route;
    }

    /**
     * Return the response for the given route. 2. 通过一个实例栈运行给定的路由
     *
     * @param  Route  $route
     * @param  Request  $request
     * @return mixed
     */
    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)
        );
    }

    /**
     * Run the given route within a Stack "onion" instance. 通过一个实例栈运行给定的路由
     *
     * @param  \Illuminate\Routing\Route  $route
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    protected function runRouteWithinStack(Route $route, Request $request)
    {
        $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                                $this->container->make('middleware.disable') === true;

        $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

        // 4. 返回响应结果
        return (new Pipeline($this->container))
                        ->send($request)
                        ->through($middleware)
                        ->then(function ($request) use ($route) {
                            return $this->prepareResponse(
                                // 3. 运行在 routes/web.php 配置的匹配到的控制器或匿名函数
                                $request, $route->run()
                            );
                        });
    }

执行 $route->run() 的方法定义在 Illuminate\Routing\Route 类中,最终执行「在 routes/web.php 配置的匹配到的控制器或匿名函数」:

    /**
     * Run the route action and return the response.
     * 
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Routing/Route.php
     * @return mixed
     */
    public function run()
    {
        $this->container = $this->container ?: new Container;

        try {
            if ($this->isControllerAction()) {
                return $this->runController();
            }

            return $this->runCallable();
        } catch (HttpResponseException $e) {
            return $e->getResponse();
        }
    }

这部分如果路由的实现是一个控制器,会完成控制器实例化并执行指定方法;如果是一个匿名函数则直接调用这个匿名函数。

其执行结果会通过 Illuminate\Routing\Router::prepareResponse($request, $response) 生一个响应实例并返回。

至此,Laravel 就完成了一个 HTTP 请求的请求处理。

2.4 发送响应

经过一系列漫长的操作,HTTP 请求进入的最终章 - 发送响应值客户端 $response->send()

make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

// 发送响应
$response->send();

// 其它
$kernel->terminate($request, $response);

发送响应由 Illuminate\Http\Response 父类 Symfony\Component\HttpFoundation\Response 中的 send() 方法完成。

    /**
     * Sends HTTP headers and content.
     * 
     * @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/Response.php
     * @return $this
     */
    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;
    }

2.5 终止程序

程序终止,完成终止中间件的调用
// @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Http/Kernel.php

public function terminate($request, $response)
{
    $this->terminateMiddleware($request, $response);
    $this->app->terminate();
}

// 终止中间件
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, $parameters) = $this->parseMiddleware($middleware);
        $instance = $this->app->make($name);
        if (method_exists($instance, 'terminate')) {
            $instance->terminate($request, $response);
        }
    }
}

以上便是 Laravel 的请求生命周期的始末。

三 总结

在 「创建 Laravel 应用实例」时不仅会注册项目基础服务、注册项目服务提供者别名、注册目录路径等在内的一系列注册工作;还会绑定 HTTP 内核及 Console 内核到 APP 容器, 同时在 HTTP 内核里配置中间件和引导程序。

进入 「接收请求并响应」里,会依据运行环境从 APP 容器 解析出 HTTP 内核或 Console 内核。如果是 HTTP 内核,还将把「中间件」及「引导程序」注册到 APP 容器

所有初始化工作完成后便进入「处理 HTTP 请求」阶段。

一个 Http 请求实例会被注册到 APP 容器,通过启动「引导程序」来设置环境变量、加载配置文件等等系统环境配置;

随后请求被分发到匹配的路由,在路由中执行「中间件」以过滤不满足校验规则的请求,只有通过「中间件」处理的请求才最终处理实际的控制器或匿名函数生成响应结果。

最后发送响应给用户,清理项目中的中间件,完成一个 「请求」 - 「响应」 的生命周期,之后我们的 Web 服务器将等待下一轮用户请求。

参考资料

感谢下列优秀的 Laravel 研究资料:

你可能感兴趣的:(laravel,php)