Laravel学习笔记之bootstrap源码解析

2021-11-08 09:21:00 浏览数 (1)

说明:Laravel在把Request通过管道Pipeline送入中间件Middleware和路由Router之前,还做了程序的启动Bootstrap工作,本文主要学习相关源码,看看Laravel启动程序做了哪些具体工作,并将个人的研究心得分享出来,希望对别人有所帮助。Laravel在入口index.php时先加载Composer加载器:Laravel学习笔记之Composer自动加载,然后进行Application的实例化:Laravel学习笔记之IoC Container实例化源码解析,得到实例化后的Application对象再从容器中解析出Kernel服务,然后进行Request实例化(Request实例化下次再聊),然后进行Bootstrap操作启动程序,再通过Pipeline送到Middleware:Laravel学习笔记之Middleware源码解析,然后经过路由映射找到对该请求的操作action(以后再聊),生成Response对象经过Kernel的send()发送给Client。本文主要聊下程序的启动操作,主要做了哪些准备工作。

开发环境:Laravel5.3 PHP7 OS X 10.11

在Laravel学习笔记之Middleware源码解析聊过,Kernel中的sendRequestThroughRouter()处理Request,并把Request交给Pipeline送到Middleware和Router中,看源码:

代码语言:javascript复制
    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 = [
            'IlluminateFoundationBootstrapDetectEnvironment',
            'IlluminateFoundationBootstrapLoadConfiguration',
            'IlluminateFoundationBootstrapConfigureLogging',
            'IlluminateFoundationBootstrapHandleExceptions',
            'IlluminateFoundationBootstrapRegisterFacades',
            'IlluminateFoundationBootstrapRegisterProviders',
            'IlluminateFoundationBootstrapBootProviders',
        ];*/
        $this->bootstrap();

        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

在Request被Pipeline送到Middleware前还有一步操作bootstrap()操作,这步操作就是启动程序,看下IlluminateFoundationHttpKernel中的bootstrap()源码:

代码语言:javascript复制
    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 = [
        'IlluminateFoundationBootstrapDetectEnvironment',
        'IlluminateFoundationBootstrapLoadConfiguration',
        'IlluminateFoundationBootstrapConfigureLogging',
        'IlluminateFoundationBootstrapHandleExceptions',
        'IlluminateFoundationBootstrapRegisterFacades',
        'IlluminateFoundationBootstrapRegisterProviders',
        'IlluminateFoundationBootstrapBootProviders',
    ];

从以上源码可知道,程序将会依次bootstrapWith()数组$bootstrappers中各个bootstrapper,看下容器中的bootstrapWith()源码:

代码语言:javascript复制
    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,看下程序做了哪些启动工作。

1. 环境检测

查看IlluminateFoundationBootstrapDetectEnvironment中的bootstrap()源码:

代码语言:javascript复制
    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全局变量中DotenvDotenv::load()函数实现了这个功能,具体不详述了。可以通过Xdebug调试查看:

2. 配置加载

配置加载就是读取config/文件夹下的所有配置值,然后存入IlluminateConfigRepository对象中,而环境检测是读取.env文件存入$_ENV全局变量中,加载环境配置主要是使用SymfonyComponentFinderFinder这个组件进行文件查找,看下LoadConfiguration::bootstrap()的源码:

代码语言:javascript复制
    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',服务是IlluminateConfigRepository对象
        $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文件,看下源码:

代码语言:javascript复制
    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的返回值是这样的数组:

代码语言:javascript复制
    $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.phpenv的值取出来存入Application对象的$env属性中:

代码语言:javascript复制
    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可以后期添加属性,如:

代码语言:javascript复制
class ClassField
{
    
}

$class_field = new ClassField();
$class_field->name = 'Laravel';
echo $class_field->name . PHP_EOL;

/* output:
Laravel

3. 日志配置

Laravel主要利用Monolog日志库来做日志处理,IlluminateLogWriter相当于Monolog Bridge,把Monolog库接入到Laravel中。看下ConfigureLogging::bootstrap()源码:

代码语言:javascript复制
    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()源码:

代码语言:javascript复制
    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()的源码,其余三个也类似:

代码语言:javascript复制
    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是IlluminateSupportFacadesLog:

代码语言:javascript复制
class Log extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'log';
    }
}

而'log'服务在上文中bootstrap()源码第一步registerLogger()就注册了。当然,至于使用Facade来从容器中获取服务也聊过,也不复杂,看下IlluminateSupportFacadesFacade的resolveFacadeInstance()源码就知道了:

代码语言:javascript复制
    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']来获取服务
    }

4. 异常处理

异常处理是十分重要的,Laravel中异常处理类AppExceptionHandler中有一个方法report(),该方法可以用来向第三方服务(如Sentry)发送程序异常堆栈(以后在一起聊聊这个Sentry,效率神器),如Production Code线上环境报出个异常,可以很清楚整个堆栈,出错在哪一行:

OK,看下异常设置的启动源代码,HandleExceptions::bootstrap()的源码:

代码语言:javascript复制
    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()的源码:

代码语言:javascript复制
    public function handleException($e)
    {
        if (! $e instanceof Exception) {
            $e = new FatalThrowableError($e);
        }
        // (new AppExceptionsHandler($container))->report($e)
        $this->getExceptionHandler()->report($e);

        if ($this->app->runningInConsole()) {
            $this->renderForConsole($e);
        } else {
            $this->renderHttpResponse($e);
        }
    }
    
    protected function getExceptionHandler()
    {
        // 解析出AppExceptionsHandler对象
        // 在boostrap/app.php中做过singleton()绑定
        return $this->app->make('IlluminateContractsDebugExceptionHandler');
    }
    
    protected function renderHttpResponse(Exception $e)
    {
        // 使用(new AppExceptionsHandler($container))->render(Request $request, $e)
        $this->getExceptionHandler()->render($this->app['request'], $e)->send();
    }

从源码中知道,重点是使用AppExceptionsHandler的report()方法报告异常情况,如向Sentry报告异常堆栈和其他有用信息;AppExceptionsHandler的render()方法通过Request发送到浏览器。关于使用第三方服务Sentry来做异常报告以后详聊,我司每天都在用这样的效率神器,很好用,值得推荐下。

5. 注册Facades

在路由文件中经常会出现Route::get()这样的写法,但实际上并没有Route类,Route只是IlluminateSupportFacadesRoute::class外观类的别名,这样取个别名只是为了简化作用,使用的是PHP内置函数class_alias(string $class, string $alias)来给类设置别名。看下RegisterFacades::bootstrap()的源码:

代码语言:javascript复制
    public function bootstrap(Application $app)
    {
        Facade::clearResolvedInstances();

        Facade::setFacadeApplication($app);

        AliasLoader::getInstance($app->make('config')->get('app.aliases', []))->register();
    }
    
    // IlluminateSupportFacadesFacade
    public static function clearResolvedInstances()
    {
        static::$resolvedInstance = [];
    }
    
    // IlluminateSupportFacadesFacade
    public static function setFacadeApplication($app)
    {
        static::$app = $app;
    }

$app->make('config')->get('app.aliases', [])是从config/app.php中读取'aliases'的值,然后注册外观类的别名,注册的外观类有:

代码语言:javascript复制
    'aliases' => [

        'App' => IlluminateSupportFacadesApp::class,
        'Artisan' => IlluminateSupportFacadesArtisan::class,
        'Auth' => IlluminateSupportFacadesAuth::class,
        'Blade' => IlluminateSupportFacadesBlade::class,
        'Cache' => IlluminateSupportFacadesCache::class,
        'Config' => IlluminateSupportFacadesConfig::class,
        'Cookie' => IlluminateSupportFacadesCookie::class,
        'Crypt' => IlluminateSupportFacadesCrypt::class,
        'DB' => IlluminateSupportFacadesDB::class,
        'Eloquent' => IlluminateDatabaseEloquentModel::class,
        'Event' => IlluminateSupportFacadesEvent::class,
        'File' => IlluminateSupportFacadesFile::class,
        'Gate' => IlluminateSupportFacadesGate::class,
        'Hash' => IlluminateSupportFacadesHash::class,
        'Lang' => IlluminateSupportFacadesLang::class,
        'Log' => IlluminateSupportFacadesLog::class,
        'Mail' => IlluminateSupportFacadesMail::class,
        'Notification' => IlluminateSupportFacadesNotification::class,
        'Password' => IlluminateSupportFacadesPassword::class,
        'Queue' => IlluminateSupportFacadesQueue::class,
        'Redirect' => IlluminateSupportFacadesRedirect::class,
        'Redis' => IlluminateSupportFacadesRedis::class,
        'Request' => IlluminateSupportFacadesRequest::class,
        'Response' => IlluminateSupportFacadesResponse::class,
        'Route' => IlluminateSupportFacadesRoute::class,
        'Schema' => IlluminateSupportFacadesSchema::class,
        'Session' => IlluminateSupportFacadesSession::class,
        'Storage' => IlluminateSupportFacadesStorage::class,
        'URL' => IlluminateSupportFacadesURL::class,
        'Validator' => IlluminateSupportFacadesValidator::class,
        'View' => IlluminateSupportFacadesView::class,

    ],

从以上外观别名数组中知道RouteIlluminateSupportFacadesRoute::class的别名,所以Route::get()实际上就是IlluminateSupportFacadesRoute::get(),看下AliasLoader类的getInstance()和register()方法源码:

代码语言:javascript复制
    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()函数的源码:

代码语言:javascript复制
    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()函数查找到RouteIlluminateSupportFacadesRoute的别名,那就调用IlluminateSupportFacadesRoute::get(),当然这里IlluminateSupportFacadesRoute没有get()静态方法,那就调用父类Facade__callStatic()来找到名为router的服务,名为'router'的服务那就是早就注册到容器中的IlluminateRoutingRouter对象,所以最终就是调用IlluminateRoutingRouter::get()方法。这个过程主要使用了两个技术:一个是外观类的别名;一个是PHP的重载,可看这篇:Laravel学习笔记之PHP重载(overloading)。

6. 注册Providers

外观注册是注册config/app.php中的$aliases[ ]得值,Providers注册就是注册$providers[ ]的值。看下RegisterProviders::bootstrap()的源码:

代码语言:javascript复制
    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()的源码:

代码语言:javascript复制
    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()方法的源码:

代码语言:javascript复制
    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等到需要里面绑定的服务时再执行绑定。

7. 启动Providers

最后一步,就是启动程序了,看下BootProviders::bootstrap()源码:

代码语言:javascript复制
    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()方法时里面有个检测程序是否已经启动的代码:

代码语言:javascript复制
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。下次有好的技术再分享,到时见。

0 人点赞