深度挖掘 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 請求從接收到響應的全部過程,邏輯組織的幾近完美。
我們來看下入口文件實現的代碼:
<?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);二 生命周期之始末
2.1 加載項目依賴
現代 PHP 依賴于 Composer 包管理器,入口文件通過引入由 Composer 包管理器自動生成的類加載程序,可以輕松注冊并加載項目所依賴的第三方組件庫。
所有組件的加載工作,僅需一行代碼即可完成:
require __DIR__.'/../vendor/autoload.php';2.2 創建 Laravel 應用實例
創建應用實例(或稱服務容器),由位于 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;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 請求」。
處理請求實際包含兩個階段:
- 創建請求實例
- 處理請求
2.3.2.1 創建請求實例
請求實例 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;}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\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);}}... }所有 「引導程序」列表功能如下:
- IlluminateFoundationBootstrapLoadEnvironmentVariables : 環境檢測,通過 DOTENV 組件將 .env 配置文件載入到 $_ENV 變量中;
- IlluminateFoundationBootstrapLoadConfiguration : 加載配置文件,這個我們剛剛分析過;
- IlluminateFoundationBootstrapHandleExceptions : 異常處理;
- IlluminateFoundationBootstrapRegisterFacades : 注冊 Facades,注冊完成后可以以別名的方式訪問具體的類;
- IlluminateFoundationBootstrapRegisterProviders : 注冊服務提供者,我們在 「2.2.1 創建應用實例」已經將基礎服務提供者注冊到 APP 容器。在這里我們會將配置在 app.php 文件夾下 providers 節點的服務器提供者注冊到 APP 容器,供請求處理階段使用;
- IlluminateFoundationBootstrapBootProviders : 啟動服務
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 請求,在它的內部會完成如下處理:
執行 $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()。
<?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;}2.5 終止程序
程序終止,完成終止中間件的調用 // @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Http/Kernel.phppublic 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 研究資料:
- http://blog.mallow-tech.com/2...
- http://laravel-recipes.com/re...
- http://www.cnblogs.com/sweng/...
- https://www.dyike.com/2017/04...
- http://www.cnblogs.com/wxw16/...
- http://www.php.cn/php-weiziji...
- https://segmentfault.com/a/11...
- https://segmentfault.com/a/11...
- https://blog.csdn.net/cDonDon...
- https://segmentfault.com/a/11...
總結
以上是生活随笔為你收集整理的深度挖掘 Laravel 生命周期的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: commons-lang3:DateUt
- 下一篇: Hyper-V数据文件丢失解决方案(有图