Laravel $bootstrappers数组加载源码分析(一)

基于laravel10分析
这篇分析先前三个,不然太长了
我们知道不论是http请求还是console命令,执行的时候都会加载$bootstrappers数组,只是console命令的$bootstrappers会多一个设置Request实例,
我们以http请求分析
http请求会经过public/index.php,在index.php中会有这么一段代码

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Kernel::class);

$response = $kernel->handle(
    $request = Request::capture()
)->send();

会执行App\Http\Kernel类的handle方法,这个方法在这个类的父类Illuminate\Foundation\Http\Kernel中,在handle方法中调用了sendRequestThroughRouter方法,在这个方法中调用了bootstrap方法

我们来看一下这个bootstrap方法做了什么处理

public function bootstrap()
 {
         //这里会先判断是否已经加载了
        if (! $this->app->hasBeenBootstrapped()) {
            //bootstrappers方法中就是返回bootstrappers数组
            $this->app->bootstrapWith($this->bootstrappers());
        }
 }

protected function bootstrappers()
{
    return $this->bootstrappers;
}

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,
];

看一下app容器中的bootstrapWith方法中做了什么处理

public function bootstrapWith(array $bootstrappers)
{
        //这里会标记已加载
        $this->hasBeenBootstrapped = true;

        foreach ($bootstrappers as $bootstrapper) {
            $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);
            //主要是看这里,这里是会初始化类,调用bootstrap方法
            $this->make($bootstrapper)->bootstrap($this);

            $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
        }
 }

先看第一个\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class

这个是加载环境变量和读取.env文件
我们看一下这个类的bootstrap方法做了什么处理

public function bootstrap(Application $app)
{
        //判断是否配置緩存,緩存文件位於bootstrap/cache/config.php
        if ($app->configurationIsCached()) {
            return;
        }
        //检测是否存在与APP_ENV匹配的自定义环境文件
        ①$this->checkForSpecificEnvironmentFile($app);

        ...
}

①看一下checkForSpecificEnvironmentFile方法做了什么处理

protected function checkForSpecificEnvironmentFile($app)
    {
        //这里是检测是否是命令行下运行且存在--env参数 拼接.env后面
        if ($app->runningInConsole() &&
            ($input = new ArgvInput)->hasParameterOption('--env') &&
            $this->setEnvironmentFilePath($app, $app->environmentFile().'.'.$input->getParameterOption('--env'))) {
            return;
        }
        //这里是去环境变量里面获取值
        $environment = Env::get('APP_ENV');

        if (! $environment) {
            return;
        }
        //如果有值则设置拼接到后面
        $this->setEnvironmentFilePath(
            $app, $app->environmentFile().'.'.$environment
        );
    }

再回到bootstrap方法,后续做了什么处理

public function bootstrap(Application $app)
{
       ...

        try {
            //这里是创建Dotenv实例并读取和加载环境文件,如果无法读取任何文件,则以静默方式失败
            ②$this->createDotenv($app)->safeLoad();
        } catch (InvalidFileException $e) {
            $this->writeErrorAndDie($e);
        }
}

②看一下createDotenv方法做了什么处理

protected function createDotenv($app)
{
        return Dotenv::create(
            //获取环境存储库实例
            ③Env::getRepository(),
            //获取环境路径
            $app->environmentPath(),
            //获取环境文件名
            $app->environmentFile()
        );
}

③看一下Env类的getRepository方法做了什么处理

public static function getRepository()
{
        //这里做了一个单例,不会重复加载
        if (static::$repository === null) {
            //这里是创建默认适配器
            $builder = RepositoryBuilder::createWithDefaultAdapters();

            if (static::$putenv) {
                //PutenvAdapter 这里是对应getenv putenv函数
                $builder = $builder->addAdapter(PutenvAdapter::class);
            }
           //这里是设置环境变量不可以被修改,返回新的适配器存储库实例
           //$builder 是 `Dotenv\Repository\RepositoryBuilder`类的实例static::$repository = $builder->immutable()->make();
        }

        return static::$repository;
}

public static function createWithDefaultAdapters()
{    
    //将迭代器转成数组
    $adapters = \iterator_to_array(self::defaultAdapters());

    return new self($adapters, $adapters);
}

private static function defaultAdapters()
{
    foreach (self::DEFAULT_ADAPTERS as $adapter) {
        //这里是用 PhpOption 包装一下适配器
        $instance = $adapter::create();
        if ($instance->isDefined()) {
            //这里返回适配器
            yield $instance->get();
        }
    }
}

private const DEFAULT_ADAPTERS = [
    //对应$_SERVER
    ServerConstAdapter::class,
    //对应$_ENV
    EnvConstAdapter::class,
];

④看一下Dotenv\Repository\RepositoryBuilder类的immutable方法做了什么处理

public function immutable()
 {
         //这里只是设置immutable属性为true
        return new self($this->readers, $this->writers, true, $this->allowList);
 }

再来看一下Dotenv\Repository\RepositoryBuilder类的make方法做了什么处理

public function make()
 {    
         //这里是创建一个多读实例
        $reader = new MultiReader($this->readers);
        //这里是创建一个多写实例
        $writer = new MultiWriter($this->writers);
        //如果设置了不可修改
        if ($this->immutable) {
            //这里返回一个不可修改多写实例
            $writer = new ImmutableWriter($writer, $reader);
        }

        if ($this->allowList !== null) {
            //这里是允许哪些环境变量可以被写
            $writer = new GuardedWriter($writer, $this->allowList);
        }
        //返回一个新的适配存储器
        return new AdapterRepository($reader, $writer);
 }

所以Env::getRepository()这个就是返回一个Dotenv\Repository\AdapterRepository实例
回到createDotenv方法,Dotenv类的create方法参数就介绍完了
我们再来看一下Dotenv类的create方法里面做了什么处理

public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null)
{    
        //这里是创建一个`Dotenv\Store\StoreBuilder`类实例
        $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames();

        foreach ((array) $paths as $path) {
            $builder = $builder->addPath($path);
        }

        foreach ((array) $names as $name) {
            $builder = $builder->addName($name);
        }
        //这里是判断是否短路
        if ($shortCircuit) {
            $builder = $builder->shortCircuit();
        }

        return new self(
        //使用指定的文件编码创建存储生成器 make是创建一个`Dotenv\Store\FileStore`实例
        $builder->fileEncoding($fileEncoding)->make(), 
        //解析器 用于解析.env文件
        new Parser(), 
        //加载器 用于读取环境变量中的值
        new Loader(), 
        //这里就是`Dotenv\Repository\AdapterRepository`实例
        $repository);
}

创建完Dotenv实例后,调用了safeLoad方法,看一下这个方法做了什么处理

public function safeLoad()
 {
        try {
            //这里调用了load方法
            return $this->load();
        } catch (InvalidPathException $e) {
            // suppressing exception
            return [];
        }
}

public function load()
{
    //这里是去读取.env文件 然后通过parse解析.env文件 变成一个个的`Dotenv\Parser\Entry`实例(如下图)
    //就是切割换行符转成数组,然后判断是否是变量(${XXX})这种,获取每个$下标
    $entries = $this->parser->parse($this->store->read());
    //这里就是去加载
    return ⑤$this->loader->load($this->repository, $entries);
}

Laravel $bootstrappers数组加载源码分析

⑤我们简单分析一下Dotenv\Loader\Loader类的load方法

public function load(RepositoryInterface $repository, array $entries)
 {
        return \array_reduce($entries, static function (array $vars, Entry $entry) use ($repository) {
            $name = $entry->getName();
            //这里就是获取每个key的值
            $value = $entry->getValue()->map(static function (Value $value) use ($repository) {
                //这里是解析${XXX}变量 所以XXX需要在这个key解析前先加载
                return Resolver::resolve($repository, $value);
            });

            if ($value->isDefined()) {
                //获取值
                $inner = $value->get();
                //往$_SERVER $_ENV putenv里面去设置值 所以我们才能从env函数里面去拿到.env文件中的值
                if ($repository->set($name, $inner)) {
                    return \array_merge($vars, [$name => $inner]);
                }
            } else {
                //如果是null 就清空环境变量中的值
                if ($repository->clear($name)) {
                    return \array_merge($vars, [$name => null]);
                }
            }

            return $vars;
        }, []);
 }

有一个需要注意的点,如果使用配置缓存的时候,用env函数是读取不到.env文件中的值的,因为.env文件是需要调用Dotenv类的load方法才会加载到环境变量里面,但是使用配置缓存的时候跳过了这一步

再看第二个\Illuminate\Foundation\Bootstrap\LoadConfiguration::class

这个是加载配置文件
我们看一下这个类的bootstrap方法做了什么处理

public function bootstrap(Application $app)
{
        $items = [];
        //这里是判断缓存文件是否存在 缓存文件里面就是return 了一个数组
        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容器
        $app->detectEnvironment(fn () => $config->get('app.env', 'production'));
        //设置默认时区
        date_default_timezone_set($config->get('app.timezone', 'UTC'));
        //设置编码
        mb_internal_encoding('UTF-8');
}

①看一下loadConfigurationFiles这个方法做了什么处理

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) {
            //把文件中的返回值设置到config单例里面
            $repository->set($key, require $path);
        }
 }

protected function getConfigurationFiles(Application $app)
{
    $files = [];
    //获取config路径
    $configPath = realpath($app->configPath());
    //查找这个目录下的所有php文件,递归查找
    foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file) {
        //获取配置文件嵌套路径
        $directory = $this->getNestedDirectory($file, $configPath);
        //这里就是拿到嵌套路径和文件名称当成key 路径为值
        $files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath();
    }

    ksort($files, SORT_NATURAL);

    return $files;
}

再看第三个\Illuminate\Foundation\Bootstrap\HandleExceptions::class

这个是异常处理
我们看一下这个类的bootstrap方法做了什么处理

public function bootstrap(Application $app)
{
        //这里是保留内容 让错误能够正常展示
        self::$reservedMemory = str_repeat('x', 32768);

        static::$app = $app;
        //-1是显示所有错误
        error_reporting(-1);
        //设置用户自定义的错误处理函数
        set_error_handler(①$this->forwardsTo('handleError'));
        //设置用户自定义的异常处理函数
        set_exception_handler(②$this->forwardsTo('handleException'));
        //设置脚本执行完成或者 exit() 后调用函数
        register_shutdown_function(③$this->forwardsTo('handleShutdown'));
        //如果不是测试环境 关闭直接输出错误到浏览器
        if (! $app->environment('testing')) {
            ini_set('display_errors', 'Off');
        }
}

protected function forwardsTo($method)
{
    return fn (...$arguments) => static::$app
        ? $this->{$method}(...$arguments)
        : false;
}

①看一下handleError方法做了什么处理

public function handleError($level, $message, $file = '', $line = 0)
 {
         //判断是否是废弃错误
        if ($this->isDeprecation($level)) {
            $this->handleDeprecationError($message, $file, $line, $level);
        } 
        //如果显示错误
        elseif (error_reporting() & $level) {
            //抛出异常 触发用户自定义的异常处理函数
            throw new ErrorException($message, 0, $level, $file, $line);
        }
}

public function handleDeprecationError($message, $file, $line, $level = E_DEPRECATED)
{
    //是否应该忽略弃用错误    
    if ($this->shouldIgnoreDeprecationErrors()) {
        return;
    }

    try {
        //实例化日志
        $logger = static::$app->make(LogManager::class);
    } catch (Exception) {
        return;
    }
    //确保已配置弃用通道 如果没有 则那null通道的
    $this->ensureDeprecationLoggerIsConfigured();
    //
    $options = static::$app['config']->get('logging.deprecations') ?? [];

    with($logger->channel('deprecations'), function ($log) use ($message, $file, $line, $level, $options) {
        //判断是否记录追踪
        if ($options['trace'] ?? false) {
            $log->warning((string) new ErrorException($message, 0, $level, $file, $line));
        } else {
            $log->warning(sprintf('%s in %s on line %s',
                $message, $file, $line
            ));
        }
    });
}

protected function shouldIgnoreDeprecationErrors()
{
    //如果没有日志类 或者Bootstrapped还没加载 或者 是测试用例
    return ! class_exists(LogManager::class)
        || ! static::$app->hasBeenBootstrapped()
        || static::$app->runningUnitTests();
}

②看一下handleException方法做了什么处理

public function handleException(Throwable $e)
{
        //释放内存
        self::$reservedMemory = null;

        try {
            //getExceptionHandler这里是从容器中拿到异常处理类
            //report 记录日志
            $this->getExceptionHandler()->report($e);
        } catch (Exception) {
            //捕捉到错误 设置为true 如果直接抛出异常就死循环了
            $exceptionHandlerFailed = true;
        }
        //判断是否是命令行运行
        if (static::$app->runningInConsole()) {
            //熏染到控制台
            $this->renderForConsole($e);
            //这里的目的是让其他命令监听这个命令的返回码时判断是否执行正常
            if ($exceptionHandlerFailed ?? false) {
                exit(1);
            }
        } else {
            //渲染http响应
            $this->renderHttpResponse($e);
        }
}

③看一下handleShutdown方法做了什么处理

public function handleShutdown()
{
        //释放内存
        self::$reservedMemory = null;
        //如果能够获取到最后的错误并且是致命错误(因为致命错误自定义异常函数捕捉不到)
        if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
            $this->handleException($this->fatalErrorFromPhpError($error, 0));
        }
 }

protected function isFatal($type)
{
    return in_array($type, [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]);
}

以上就是$bootstrappers数组前三个的执行流程了,如果有写的有误的地方,请大佬们指正

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 1年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!