说明: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。下次有好的技术再分享,到时见。