說明:Laravel在把Request經過管道Pipeline
送入中間件Middleware和路由Router以前,還作了程序的啓動Bootstrap工做,本文主要學習相關源碼,看看Laravel啓動程序作了哪些具體工做,並將我的的研究心得分享出來,但願對別人有所幫助。Laravel在入口index.php時先加載Composer加載器:Laravel5.2之Composer自動加載,而後進行Application的實例化:Laravel5.3之IoC Container實例化源碼解析,獲得實例化後的Application對象再從容器中解析出Kernel服務,而後進行Request實例化(Request實例化下次再聊),而後進行Bootstrap操做啓動程序
,再經過Pipeline送到Middleware:Laravel5.3之Middleware源碼解析,而後通過路由映射找到對該請求的操做action(之後再聊),生成Response對象通過Kernel的send()發送給Client。本文主要聊下程序的啓動操做,主要作了哪些準備工做。php
開發環境:Laravel5.3 + PHP7 + OS X 10.11
html
在Laravel5.3之Middleware源碼解析聊過,Kernel中的sendRequestThroughRouter()處理Request,並把Request交給Pipeline送到Middleware和Router中,看源碼:laravel
protected function sendRequestThroughRouter($request) { $this->app->instance('request', $request); Facade::clearResolvedInstance('request'); /* 依次執行$bootstrappers中每個bootstrapper的bootstrap()函數,作了幾件準備事情: 1. 環境檢測 DetectEnvironment 2. 配置加載 LoadConfiguration 3. 日誌配置 ConfigureLogging 4. 異常處理 HandleException 5. 註冊Facades RegisterFacades 6. 註冊Providers RegisterProviders 7. 啓動Providers BootProviders protected $bootstrappers = [ 'Illuminate\Foundation\Bootstrap\DetectEnvironment', 'Illuminate\Foundation\Bootstrap\LoadConfiguration', 'Illuminate\Foundation\Bootstrap\ConfigureLogging', 'Illuminate\Foundation\Bootstrap\HandleExceptions', 'Illuminate\Foundation\Bootstrap\RegisterFacades', 'Illuminate\Foundation\Bootstrap\RegisterProviders', 'Illuminate\Foundation\Bootstrap\BootProviders', ];*/ $this->bootstrap(); return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); }
在Request被Pipeline送到Middleware前還有一步操做bootstrap()操做,這步操做就是啓動程序
,看下\Illuminate\Foundation\Http\Kernel中的bootstrap()源碼:bootstrap
protected $hasBeenBootstrapped = false; ... /** * Bootstrap the application for HTTP requests. * * @return void */ public function bootstrap() { // 檢查程序是否已經啓動 if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } } public function hasBeenBootstrapped() { return $this->hasBeenBootstrapped; } protected function bootstrappers() { return $this->bootstrappers; } protected $bootstrappers = [ 'Illuminate\Foundation\Bootstrap\DetectEnvironment', 'Illuminate\Foundation\Bootstrap\LoadConfiguration', 'Illuminate\Foundation\Bootstrap\ConfigureLogging', 'Illuminate\Foundation\Bootstrap\HandleExceptions', 'Illuminate\Foundation\Bootstrap\RegisterFacades', 'Illuminate\Foundation\Bootstrap\RegisterProviders', 'Illuminate\Foundation\Bootstrap\BootProviders', ];
從以上源碼可知道,程序將會依次bootstrapWith()數組$bootstrappers中各個bootstrapper,看下容器中的bootstrapWith()源碼:數組
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]); } }
首先觸發'bootstrapping: '.$bootstrapper事件,告知將要啓動該bootstrapper,而後從容器中make($bootstrapper)出該$bootstrapper,並執行該$bootstrapper中的bootstrap()方法,最後在觸發事件:'bootstrapped: '.$bootstrapper,告知該$bootstrapper已經啓動OK了。啓動的bootstrappers就是數組$bootstrappers中的7個bootstrapper,看下程序作了哪些啓動工做。瀏覽器
查看Illuminate\Foundation\Bootstrap\DetectEnvironment中的bootstrap()源碼:緩存
public function bootstrap(Application $app) { // 查看bootstrap/cache/config.php緩存文件是否存在 // php artisan config:cache來生成配置緩存文件,就是把config/下的全部文件放在一個緩存文件內,提升性能 // 這裏假設沒有緩存配置文件 if (! $app->configurationIsCached()) { $this->checkForSpecificEnvironmentFile($app); try { $env = $_ENV; // 調試添加的,此時爲空 // 這裏把.env文件值取出存入$_ENV內 (new Dotenv($app->environmentPath(), $app->environmentFile()))->load(); // 這裏$_ENV數組有值了 $env = $_ENV; } catch (InvalidPathException $e) { // } } } protected function checkForSpecificEnvironmentFile($app) { // 讀取$_ENV全局變量中'APP_ENV'值,此時是空 if (! env('APP_ENV')) { return; } $file = $app->environmentFile().'.'.env('APP_ENV'); // .env.local if (file_exists($app->environmentPath().'/'.$file)) { $app->loadEnvironmentFrom($file); } }
環境監測核心就是把.env文件內值存入到$_ENV全局變量中\Dotenv\Dotenv::load()函數實現了這個功能,具體不詳述了。能夠經過Xdebug調試查看:
session
配置加載就是讀取config/文件夾下的全部配置值,而後存入\Illuminate\Config\Repository對象中,而環境檢測是讀取.env文件存入$_ENV全局變量中,加載環境配置主要是使用\Symfony\Component\Finder\Finder這個組件進行文件查找,看下LoadConfiguration::bootstrap()的源碼:app
public function bootstrap(Application $app) { $items = []; // 查看config有沒有緩存文件,緩存文件是在bootstrap/cache/config.php // 經過php artisan config:cache命令來生成緩存文件,把config/下的全部配置文件打包成一個文件,提升程序執行速度 // 這裏假設沒有緩存文件 if (file_exists($cached = $app->getCachedConfigPath())) { $items = require $cached; $loadedFromCache = true; } // 綁定服務'config',服務是\Illuminate\Config\Repository對象 $app->instance('config', $config = new Repository($items)); if (! isset($loadedFromCache)) { // 加載config/*.php全部配置文件,把全部配置存入Repository對象中 $this->loadConfigurationFiles($app, $config); } // 檢查'APP_ENV'環境設置,通常也就是'dev','stg','prd'三個環境,即'development', 'staging', 'production' $app->detectEnvironment(function () use ($config) { return $config->get('app.env', 'production'); }); // 設置時區,$config['app.timezone']就是調用Repository::get('app.timezone'),由於Repository實現了ArrayAccess Interface, // '.'語法讀取是Arr::get()實現的,很好用的一個方法 date_default_timezone_set($config['app.timezone']); mb_internal_encoding('UTF-8'); }
加載配置文件,就是讀取/config/*.php文件,看下源碼:ide
protected function loadConfigurationFiles(Application $app, RepositoryContract $repository) { foreach ($this->getConfigurationFiles($app) as $key => $path) { // 存入到Repository對象中,以'key => value'存入到$items[]屬性中 $repository->set($key, require $path); } } protected function getConfigurationFiles(Application $app) { $files = []; // 就是'config/'這個路徑 $configPath = realpath($app->configPath()); // Finder鏈式接口讀取config/*.php全部文件,獲取全部文件名稱,而後依次遍歷 foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file) { $nesting = $this->getConfigurationNesting($file, $configPath); $files[$nesting.basename($file->getRealPath(), '.php')] = $file->getRealPath(); } return $files; }
能夠經過Xdebug調試知道$files的返回值是這樣的數組:
$files = [ 'app' => '/vagrant/config/app.php', //文件的絕對路徑 'auth' => 'vagrant/config/auth.php', 'broadcasting' => '/vagrant/config/broadcasting.php', 'cache' => '/vagrant/config/cache.php', 'compile' => 'vagrant/config/compile.php', 'database' => '/vagrant/config/databse.php', 'filesystems' => '/vagrant/config/filesystems.php', 'mail' => '/vagrant/config/mail.php', 'queue' => '/vagrant/config/queue.php', 'services' => '/vagrant/config/services.php', 'session' => '/vagrant/config/session.php', 'view' => '/vagrant/config/view.php', ];
而後經過Application的detectEnvironment()方法把app.env
的值即app.php
中env
的值取出來存入Application對象的$env屬性中:
public function detectEnvironment(Closure $callback) { $args = isset($_SERVER['argv']) ? $_SERVER['argv'] : null; return $this['env'] = (new EnvironmentDetector())->detect($callback, $args); } public function detect(Closure $callback, $consoleArgs = null) { if ($consoleArgs) { return $this->detectConsoleEnvironment($callback, $consoleArgs); } return $this->detectWebEnvironment($callback); } protected function detectWebEnvironment(Closure $callback) { return call_user_func($callback); }
因此屬性檢查的時候就存到了$env屬性的值了,開發代碼中就能夠App::environment()獲得這個$env屬性而後進行一些操做,能夠看下environment()的源碼,該方法有兩個feature:若是不傳入值則讀取$env值;若是傳入值則判斷該值是否與$env同樣。這裏若是對Application沒有$env成員屬性定義有疑惑,是由於PHP能夠後期添加屬性,如:
class ClassField { } $class_field = new ClassField(); $class_field->name = 'Laravel'; echo $class_field->name . PHP_EOL; /* output: Laravel
Laravel主要利用Monolog日誌庫來作日誌處理,\Illuminate\Log\Writer至關於Monolog Bridge,把Monolog庫接入到Laravel中。看下ConfigureLogging::bootstrap()源碼:
public function bootstrap(Application $app) { // 註冊'log'服務 $log = $this->registerLogger($app); // 檢查是否已經註冊了Monolog // 這裏假設開始沒有註冊 if ($app->hasMonologConfigurator()) { call_user_func( $app->getMonologConfigurator(), $log->getMonolog() ); } else { // $this->configureHandlers($app, $log); } } protected function registerLogger(Application $app) { // 向容器中綁定'log'服務,即Writer對象 $app->instance('log', $log = new Writer( new Monolog($app->environment()), $app['events']) ); return $log; }
Laravel的Log模塊中已經內置了幾個類型的LogHandler:Single,Daily,Syslog,Errorlog.根據config/app.php文件中'log'的配置選擇其中一個handler,看下configureHandlers()源碼:
protected function configureHandlers(Application $app, Writer $log) { $method = 'configure'.ucfirst($app['config']['app.log']).'Handler'; $this->{$method}($app, $log); }
configureHandlers()這方法也是一個技巧,找到方法名而後調用,這在Laravel中常常這麼用,如Filesystem那一模塊中有'create'.ucfirst(xxx).'Driver'這樣的源碼,是個不錯的設計。這裏看下configureDailyHandler()的源碼,其他三個也相似:
protected function configureDailyHandler(Application $app, Writer $log) { // 解析'config'服務 $config = $app->make('config'); // 默認沒有設置,就爲null $maxFiles = $config->get('app.log_max_files'); $log->useDailyFiles( $app->storagePath().'/logs/laravel.log', // storage/log/laravel.log is_null($maxFiles) ? 5 : $maxFiles, // 5 $config->get('app.log_level', 'debug') ); } // Writer.php public function useDailyFiles($path, $days = 0, $level = 'debug') { $this->monolog->pushHandler( $handler = new RotatingFileHandler($path, $days, $this->parseLevel($level)) ); $handler->setFormatter($this->getDefaultFormatter()); }
利用Mnolog的RotatingFileHandler()來往laravel.log裏打印log值,固然在應用程序中常常\Log::info(),\Log::warning(),\Log::debug()來打印變量值,即Writer類中定義的的方法。Log的facade是\Illuminate\Support\Facades\Log:
class Log extends Facade { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return 'log'; } }
而'log'服務在上文中bootstrap()源碼第一步registerLogger()就註冊了。固然,至於使用Facade來從容器中獲取服務也聊過,也不復雜,看下\Illuminate\Support\Facades\Facade的resolveFacadeInstance()源碼就知道了:
protected static function resolveFacadeInstance($name) { if (is_object($name)) { return $name; } if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } return static::$resolvedInstance[$name] = static::$app[$name]; // 實際上就是使用$app['log']來獲取服務 }
異常處理是十分重要的,Laravel中異常處理類\App\Exception\Handler中有一個方法report(),該方法能夠用來向第三方服務(如Sentry)發送程序異常堆棧(之後在一塊兒聊聊這個Sentry,效率神器),如Production Code線上環境報出個異常,能夠很清楚整個堆棧,出錯在哪一行:
OK,看下異常設置的啓動源代碼,HandleExceptions::bootstrap()的源碼:
public function bootstrap(Application $app) { $this->app = $app; error_reporting(-1); // 出現錯誤,拋出throw new ErrorException set_error_handler([$this, 'handleError']); // 處理異常,使用report()方法來報告,可集成第三方服務Sentry來做爲異常報告處理器ExceptionReportHandler set_exception_handler([$this, 'handleException']); register_shutdown_function([$this, 'handleShutdown']); if (! $app->environment('testing')) { ini_set('display_errors', 'Off'); } }
這裏重點看下handleException()的源碼:
public function handleException($e) { if (! $e instanceof Exception) { $e = new FatalThrowableError($e); } // (new App\Exceptions\Handler($container))->report($e) $this->getExceptionHandler()->report($e); if ($this->app->runningInConsole()) { $this->renderForConsole($e); } else { $this->renderHttpResponse($e); } } protected function getExceptionHandler() { // 解析出App\Exceptions\Handler對象 // 在boostrap/app.php中作過singleton()綁定 return $this->app->make('Illuminate\Contracts\Debug\ExceptionHandler'); } protected function renderHttpResponse(Exception $e) { // 使用(new App\Exceptions\Handler($container))->render(Request $request, $e) $this->getExceptionHandler()->render($this->app['request'], $e)->send(); }
從源碼中知道,重點是使用App\Exceptions\Handler的report()方法報告異常狀況,如向Sentry報告異常堆棧和其餘有用信息;App\Exceptions\Handler的render()方法經過Request發送到瀏覽器。關於使用第三方服務Sentry來作異常報告之後詳聊,我司天天都在用這樣的效率神器,很好用,值得推薦下。
在路由文件中常常會出現Route::get()這樣的寫法,但實際上並無Route類,Route只是\Illuminate\Support\Facades\Route::class外觀類的別名,這樣取個別名只是爲了簡化做用,使用的是PHP內置函數class_alias(string $class, string $alias)來給類設置別名。看下RegisterFacades::bootstrap()的源碼:
public function bootstrap(Application $app) { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); AliasLoader::getInstance($app->make('config')->get('app.aliases', []))->register(); } // \Illuminate\Support\Facades\Facade public static function clearResolvedInstances() { static::$resolvedInstance = []; } // \Illuminate\Support\Facades\Facade public static function setFacadeApplication($app) { static::$app = $app; }
$app->make('config')->get('app.aliases', [])
是從config/app.php中讀取'aliases'的值,而後註冊外觀類的別名,註冊的外觀類有:
'aliases' => [ 'App' => Illuminate\Support\Facades\App::class, 'Artisan' => Illuminate\Support\Facades\Artisan::class, 'Auth' => Illuminate\Support\Facades\Auth::class, 'Blade' => Illuminate\Support\Facades\Blade::class, 'Cache' => Illuminate\Support\Facades\Cache::class, 'Config' => Illuminate\Support\Facades\Config::class, 'Cookie' => Illuminate\Support\Facades\Cookie::class, 'Crypt' => Illuminate\Support\Facades\Crypt::class, 'DB' => Illuminate\Support\Facades\DB::class, 'Eloquent' => Illuminate\Database\Eloquent\Model::class, 'Event' => Illuminate\Support\Facades\Event::class, 'File' => Illuminate\Support\Facades\File::class, 'Gate' => Illuminate\Support\Facades\Gate::class, 'Hash' => Illuminate\Support\Facades\Hash::class, 'Lang' => Illuminate\Support\Facades\Lang::class, 'Log' => Illuminate\Support\Facades\Log::class, 'Mail' => Illuminate\Support\Facades\Mail::class, 'Notification' => Illuminate\Support\Facades\Notification::class, 'Password' => Illuminate\Support\Facades\Password::class, 'Queue' => Illuminate\Support\Facades\Queue::class, 'Redirect' => Illuminate\Support\Facades\Redirect::class, 'Redis' => Illuminate\Support\Facades\Redis::class, 'Request' => Illuminate\Support\Facades\Request::class, 'Response' => Illuminate\Support\Facades\Response::class, 'Route' => Illuminate\Support\Facades\Route::class, 'Schema' => Illuminate\Support\Facades\Schema::class, 'Session' => Illuminate\Support\Facades\Session::class, 'Storage' => Illuminate\Support\Facades\Storage::class, 'URL' => Illuminate\Support\Facades\URL::class, 'Validator' => Illuminate\Support\Facades\Validator::class, 'View' => Illuminate\Support\Facades\View::class, ],
從以上外觀別名數組中知道Route
是IlluminateSupportFacadesRoute::class
的別名,因此Route::get()
實際上就是IlluminateSupportFacadesRoute::get()
,看下AliasLoader類的getInstance()和register()方法源碼:
public static function getInstance(array $aliases = []) { if (is_null(static::$instance)) { // 這裏$aliases就是上面傳進來的$aliases[],即config/app.php中'aliases'值 return static::$instance = new static($aliases); } $aliases = array_merge(static::$instance->getAliases(), $aliases); static::$instance->setAliases($aliases); return static::$instance; } public function register() { if (! $this->registered) { $this->prependToLoaderStack(); $this->registered = true; } } protected function prependToLoaderStack() { // 把AliasLoader::load()放入自動加載函數堆棧中,堆棧首的位置 spl_autoload_register([$this, 'load'], true, true); }
而loader()函數的源碼:
public function load($alias) { if (isset($this->aliases[$alias])) { // @link http://php.net/manual/en/function.class-alias.php return class_alias($this->aliases[$alias], $alias); } }
就是經過class_alias()給外觀類設置一個別名。因此Route::get()
的調用過程就是,首先發現沒有Route
類,就去自動加載函數堆棧中經過AliasLoader::load()
函數查找到Route
是IlluminateSupportFacadesRoute
的別名,那就調用IlluminateSupportFacadesRoute::get()
,固然這裏IlluminateSupportFacadesRoute
沒有get()
靜態方法,那就調用父類Facade
的__callStatic()
來找到名爲router
的服務,名爲'router'的服務那就是早就註冊到容器中的IlluminateRoutingRouter
對象,因此最終就是調用IlluminateRoutingRouter::get()
方法。這個過程主要使用了兩個技術:一個是外觀類的別名;一個是PHP的重載,可看這篇:Laravel5.2之PHP重載(overloading)。
外觀註冊是註冊config/app.php中的$aliases[ ]
得值,Providers註冊就是註冊$providers[ ]
的值。看下RegisterProviders::bootstrap()的源碼:
public function bootstrap(Application $app) { $app->registerConfiguredProviders(); } // Application.php public function registerConfiguredProviders() { // 查找bootstrap/cache/services.php有沒有這個緩存文件 // services.php這個緩存文件存儲的是service providers的數組值: // return [ // 'providers' => [], // 'eager' => [], // 'deferred' => [], // 'when' => [] // ]; $manifestPath = $this->getCachedServicesPath(); // 經過load()方法加載config/app.php中'$providers[ ]'數組值 (new ProviderRepository($this, new Filesystem, $manifestPath)) ->load($this->config['app.providers']); }
看下load()的源碼:
public function load(array $providers) { // 查看bootstrap/cache/services.php有沒有這個緩存文件 // 第一次啓動時是沒有的 $manifest = $this->loadManifest(); // 開始沒有這個緩存文件,那就把$providers[ ]裏的值 if ($this->shouldRecompile($manifest, $providers)) { // 而後根據$providers[ ]編譯出services.php這個緩存文件 $manifest = $this->compileManifest($providers); } foreach ($manifest['when'] as $provider => $events) { // 註冊包含有事件監聽的service provider // 包含有事件監聽的service provider都要有when()函數返回 $this->registerLoadEvents($provider, $events); } foreach ($manifest['eager'] as $provider) { // 把'eager'字段中service provider註冊進容器中, // 即遍歷每個service provider,調用其中的register()方法 // 向容器中註冊具體的服務 $this->app->register($this->createProvider($provider)); } // 註冊延遲的service provider, // deferred的service provider, 一是要設置$defer = true,二是要提供provides()方法返回綁定到容器中服務的名稱 $this->app->addDeferredServices($manifest['deferred']); }
看下編譯緩存文件compileManifest()方法的源碼:
protected function compileManifest($providers) { $manifest = $this->freshManifest($providers); foreach ($providers as $provider) { $instance = $this->createProvider($provider); // 根據每個service provider的defer屬性看是不是延遲加載的service provider if ($instance->isDeferred()) { // 延遲加載的,根據provides()方法提供的服務名稱,寫入到'deferred'字段裏 // 因此延遲加載的service provider都要提供provides()方法 foreach ($instance->provides() as $service) { $manifest['deferred'][$service] = $provider; } // 使用when()函數提供的值註冊下含有事件的service provider, $manifest['when'][$provider] = $instance->when(); } else { // 不是延遲加載的,就放在'eager'字段裏,用$this->app->register()來註冊延遲加載的service provider $manifest['eager'][] = $provider; } } // 最後寫入到services.php緩存文件中 return $this->writeManifest($manifest); } protected function freshManifest(array $providers) { return ['providers' => $providers, 'eager' => [], 'deferred' => []]; }
總之,註冊providers就是把config/app.php中$providers[ ]定義的全部service provider中,把不是defer的service provider中綁定的服務啓動起來,是defer的service provider等到須要裏面綁定的服務時再執行綁定。
最後一步,就是啓動程序了,看下BootProviders::bootstrap()源碼:
public function bootstrap(Application $app) { $app->boot(); } public function boot() { // 若是程序已啓動則返回,顯然還沒啓動,還在booting狀態中 if ($this->booted) { return; } // 執行以前Application實例化的時候在$bootingCallbacks[]註冊的回調 $this->fireAppCallbacks($this->bootingCallbacks); // 以前凡是用Application::register()方法的service provider都寫入到了$serviceProviders[]中 // 這裏依次執行每個service provider裏的boot()方法,若是存在的話 array_walk($this->serviceProviders, function ($p) { $this->bootProvider($p); }); $this->booted = true; // 執行以前Application實例化的時候在$bootedCallbacks[]註冊的回調 $this->fireAppCallbacks($this->bootedCallbacks); } protected function bootProvider(ServiceProvider $provider) { if (method_exists($provider, 'boot')) { return $this->call([$provider, 'boot']); } }
從以上源碼中知道,第(7)步和第(6)步相似:第(6)是依次執行每個不是defer的service provider的register()方法;第(7)步是依次執行每個不是defer的service provider的boot()方法,若是存在的話。因此官網上service provider章節說了這麼一句The Boot Method:
This method is called after all other service providers have been registered, meaning you have access to all other services that have been registered by the framework
這裏就明白了爲啥這句話的含義了。
以前聊過Application::register()方法時裏面有個檢測程序是否已經啓動的代碼:
public function register($provider, $options = [], $force = false) { ... if ($this->booted) { $this->bootProvider($provider); } return $provider; }
剛剛開始實例化Application的時候尚未啓動,在執行全部非defer的service provider boot()方法後程序就啓動了:$this->booted = true;
。
OK, 程序啓動所作的準備工做就聊完了,過程不復雜,只需一步步拆解就能基本清楚Laravel啓動時作了哪些具體工做。
總結:本文主要學習了Laravel啓動時作的七步準備工做:1. 環境檢測 DetectEnvironment; 2. 配置加載 LoadConfiguratio; 3. 日誌配置 ConfigureLogging; 4. 異常處理 HandleException;5. 註冊Facades RegisterFacades;6. 註冊Providers RegisterProviders;7. 啓動Providers BootProviders。下次有好的技術再分享,到時見。