本文首發於我的博客 深度挖掘 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 研究資料: