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);
}
⑤我们简单分析一下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 协议》,转载必须注明作者和本文链接
推荐文章: