本文首发于我的博客 深度挖掘 Laravel 生命周期,转载请注明出处。
这篇文章咱们来聊聊 「Laravel 生命周期」 这个主题。虽然网络上已经有不少关于这个主题的探讨,但这个主题依然值得咱们去研究和学习。php
我想说的是当咱们在决定使用某项技术的时候,除了须要了解它能「作什么」,其实还应当研究它是「怎么作的」。html
Laravel 框架或者说任何一个 Web 项目,咱们都须要理解它到底是如何接收到用户发起的 HTTP 请求的;又是如何响应结果给用户的;在处理请求和响应的过程当中都存在哪些处理值得深刻学习。laravel
全部这些内容其实都包含在 「Laravel 生命周期」 这个主题里面。git
本文较长建议使用合适的 IDE 进行代码查阅;或者经过文中的连接,或是代码注释的 「@see」部分直接在 Github 畅读代码。github
二 生命周期之始末web
2.2 建立 Laravel 应用实例bootstrap
2.3 接收请求并响应segmentfault
2.3.2 处理 HTTP 请求数组
2.3.2.2 处理请求缓存
Laravel 生命周期(或者说请求生命周期)归纳起来主要分为 3 个主要阶段:
而这 3 个阶段的处理都发生在入口文件 public/index.php 文件内(public/index.php 是一个新安装的 Laravel 项目默认入口文件)。
然而 index.php 文件仅包含极少的代码,但却出色的完成了一个 HTTP 请求从接收到响应的所有过程,逻辑组织的几近完美。
咱们来看下入口文件实现的代码:
<?php // 阶段一 require __DIR__.'/../vendor/autoload.php'; // 阶段二 $app = require_once __DIR__.'/../bootstrap/app.php'; // 阶段三 $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); $response->send(); // 其它 $kernel->terminate($request, $response);
现代 PHP 依赖于 Composer 包管理器,入口文件经过引入由 Composer 包管理器自动生成的类加载程序,能够轻松注册并加载项目所依赖的第三方组件库。
全部组件的加载工做,仅需一行代码便可完成:
require __DIR__.'/../vendor/autoload.php';
建立应用实例(或称服务容器),由位于 bootstrap/app.php 文件里的引导程序完成,建立服务容器的过程即为应用初始化的过程,项目初始化时将完成包括:注册项目基础服务、注册项目服务提供者别名、注册目录路径等在内的一些列注册工做。
下面是 bootstrap/app.php 的代码,包含两个主要部分「建立应用实例」和「绑定内核至 APP 服务容器」:
<?php // 第一部分: 建立应用实例 $app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../') ); // 第二部分: 完成内核绑定 $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;
建立应用实例即实例化 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(); }
接着将关注的焦点转移到绑定内核部分。
Laravel 会依据 HTTP 请求的运行环境的不一样,将请求发送至相应的内核: HTTP 内核 或 Console 内核。不管 HTTP 内核仍是 Console 内核,它们的做用都是是接收一个 HTTP 请求,随后返回一个响应,就是这么简单。
这篇文章主要研究 HTTP 内核,HTTP 内核继承自 Illuminate\Foundation\Http\Kernel 类.
在 「HTTP 内核」 内它定义了 中间件 相关数组;在 「Illuminate\Foundation\Http\Kernel」 类内部定义了属性名为 「bootstrappers」 的 引导程序 数组。
至于 「中间件」 和 「引导程序」如何被使用的,会在后面的章节讲解。
项目的异常处理由 App\Exceptions\Handler::class 类完成,这边也不作深刻的讲解。
经过上面的分析咱们能够发如今「建立 Laravel 应用实例」这个阶段它作了不少的基础工做,包括但不限于:建立 APP 容器、注册应用路径、注册基础服务提供者、配置中间件和引导程序等。
在完成建立 APP 容器后即进入了第三个阶段 「接收请求并响应」。
「接收请求并响应」有关代码以下:
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); $response->send();
咱们须要逐行分析上面的代码,才能窥探其中的原貌。
在第二阶段咱们已经将 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; } ...
以前的全部处理,基本都是围绕在配置变量、注册服务等运行环境的构建上,构建完成后才是真刀真枪的来处理一个「HTTP 请求」。
处理请求实际包含两个阶段:
// 处理请求 $response = $kernel->handle( // 建立请求实例 $request = Illuminate\Http\Request::capture() );
请求实例 Illuminate\Http\Request 的 capture() 方法内部经过 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; }
请求处理发生在 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()); }
将发现这段代码没有一行废话,它完成了大量的逻辑处理:
记得咱们在以前「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\Application 的 bootstrapWith() 方法究竟如何来启动这些引导程序的。
/** * 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() 辅助函数获取配置数据。
<?php class LoadConfiguration { /** * Bootstrap the given application. * * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php * @param \Illuminate\Contracts\Foundation\Application $app * @return void */ public function bootstrap(Application $app) { $items = []; if (file_exists($cached = $app->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); } } ... }
全部 「引导程序」列表功能以下:
完成「引导程序」启动操做后,随机进入到请求处理阶段。
/** * 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 请求,在它的内部会完成以下处理:
<?php ... // @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Routing/Router.php class Router implements RegistrarContract, BindingRegistrar { /** * Dispatch the request to the application. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function dispatch(Request $request) { $this->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 请求的请求处理。
通过一系列漫长的操做,HTTP 请求进入的最终章 - 发送响应值客户端 $response->send()。
<?php // @see https://github.com/laravel/laravel/blob/master/public/index.php // 阶段一 require __DIR__.'/../vendor/autoload.php'; // 阶段二 $app = require_once __DIR__.'/../bootstrap/app.php'; // 阶段三 $kernel = $app->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; }
程序终止,完成终止中间件的调用
// @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 研究资料: