在laravel框架中对环境配置文件的加载过程步骤浅析

2023-06-01 00:00:00 配置文件 浅析 中对

在laravel中是支持配置加载不同环境配置文件的,下面来看看laravel框架在不同的环境自动加载不同的环境配置过程。


一.环境配置的加载过程:

运行 laravel 框架主要有两种方式, 

一种是作为 web服务的运行的 laravel,

另一种是作为命令行脚本运行的 laravel。


web 服务的入口文件是 public/index.php , 命令行脚本的入口文件是 artisan。

可以说框架的加载也是从 public/index.php 或 artisan 开始的。

我们拿 public/index.php 举例, 可以看到先引用了 laravel 框架, 

然后通过服务提供者 make 出来一个 Kernel , 

然后通过调用 Kernel 的 handle 方法 和 send 方法创建了 $response 和 $request 实例。

# public/index.php 和 artisan 中的这一行代码引入了 laravel 框架。
$app = require_once __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Kernel::class);
$response = $kernel->handle(
    $request = Request::capture()
)->send();

在 /bootstrap/app.php 中通过下面这行代码创建的 laravel 实例。

$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

在 Illuminate\Foundation\Application 中有一个 bootstrapWith 方法, 

这个方法先通过 $this->make() 方法创建了 bootstrapper 的实例, 

然后执行 bootstrapper 的 bootstrap 方法。

 public function bootstrapWith(array $bootstrappers)
    {
        $this->hasBeenBootstrapped = true;
        foreach ($bootstrappers as $bootstrapper) {
            $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);
            $this->make($bootstrapper)->bootstrap($this);
            $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
        }
    }

内核 Kernel 中 bootstrap 方法调用了 Application 中的 bootstrapWith 方法,

这里还是拿 Illuminate\Foundation\Http\Kernel 举例子, 

可以看到 $bootstrappers 中的第一个 bootstrapper 就是 

\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables, 

这个 bootstrapper 就是用来加载环境配置的 bootstrapper。

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,
    ];
 ...
public function bootstrap()

{

    if (! $this->app->hasBeenBootstrapped()) {

        $this->app->bootstrapWith($this->bootstrappers());

   }
}  

 

我们接着来看下 

\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables 的 bootstrap 方法,

$app->configurationIsCached() 为 true 表示有配置的缓存文件,如果有的话就不会重复加载。

然后 $this->checkForSpecificEnvironmentFile($app); 检查特定环境配置文件。 

这个方法检查会修改 $app 中环境配置文件的路径, 所以是今天的重点。

public function bootstrap(Application $app)
{
        if ($app->configurationIsCached()) {
            return;
        }
        $this->checkForSpecificEnvironmentFile($app);
        try {
            $this->createDotenv($app)->safeLoad();
        } catch (InvalidFileException $e) {
            $this->writeErrorAndDie($e);
        }
}

首先判断是不是命令行环境,如果是的话, 会获取 --env 的值,

并将这个值和 $app->environmentFile() 和 . 做字符串拼接, 然后调用 setEnvironmentFilePath 修改环境配置文件的文件名, 比如说 --env=local 那最后加载的环境配置文件就是 .env.local。 

这里如果不是命令行环境的话, 后面会通过 Env::get('APP_ENV'); 

获取当前环境, 然后同样做一个字符串拼接, 然后调用 setEnvironmentFilePath 修改。

protected function checkForSpecificEnvironmentFile($app)
{
        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
        );
}
protected function setEnvironmentFilePath($app, $file)
{
        if (is_file($app->environmentPath().'/'.$file)) {
            $app->loadEnvironmentFrom($file);
            return true;
        }
        return false;
}

如果在 checkForSpecificEnvironmentFile 方法中没有修改环境配置文件的文件名, 则会加载默认的环境配置文件 。

 这个默认的文件名是通过 \Illuminate\Foundation\Application 的 $environmentFile 属性定义的, 

前面的 setEnvironmentFilePath 修改的也是 environmentFile 的值。

protected $environmentFile = '.env';
public function loadEnvironmentFrom($file)
{
    $this->environmentFile = $file;
    return $this;
}

这里再说明一下, Illuminate\Foundation\Http\Kernel的bootstrap方法是在sendRequestThroughRouter方法中调用的, 而 sendRequestThroughRouter 是在 handle 方法中调用的。

public function handle($request)
    {
        $this->requestStartedAt = Carbon::now();
        try {
            $request->enableHttpMethodParameterOverride();
            $response = $this->sendRequestThroughRouter($request);
        } catch (Throwable $e) {
            $this->reportException($e);
            $response = $this->renderException($request, $e);
        }
        $this->app['events']->dispatch(
            new RequestHandled($request, $response)
        );
        return $response;
    }
    /**
     * Send the given request through the middleware / router.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendRequestThroughRouter($request)
    {
        $this->app->instance('request', $request);
        Facade::clearResolvedInstance('request');
        $this->bootstrap();
        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

我们来整理一下。 

整个环境配置加载过程就是 先有了 $app, 

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

再然后 $kernel->handle() 在 handle 方法中调用了 $kernel->bootstrap() 

最后在 bootstrap 中通过 LoadEnvironmentVariables 加载了环境配置.


二.如何使 laravel 在不同的环境自动加载不同的环境配置。


现在 env 的加载过程基本上已经了解了, 

我们接着再详细说明下 checkForSpecificEnvironmentFile 中 Env::get('APP_ENV') 的具体逻辑,

以及如何使 laravel 在不同的环境下自动加载不同环境配置的具体操作。

get 方法没什么好说就是 static::getRepository() 然后尝试获取配置项的值, 

关键是它有内些仓库。

public static function get($key, $default = null)
{
    return Option::fromValue(static::getRepository()->get($key))
        ->map(function ($value) {
            switch (strtolower($value)) {
                case 'true':
                case '(true)':
                    return true;
                case 'false':
                case '(false)':
                    return false;
                case 'empty':
                case '(empty)':
                    return '';
                case 'null':
                case '(null)':
                    return;
  }
            if (preg_match('/\A([\'"])(.*)\1\z/', $value, $matches)) {
                return $matches[2];
  }
            return $value;
  })
        ->getOrCall(fn () => value($default));
}

其实就是 EnvConstAdapter ServerConstAdapter, PutenvAdapter 它们三个。

    public static function getRepository()
    {
        if (static::$repository === null) {
            $builder = RepositoryBuilder::createWithDefaultAdapters();
            if (static::$putenv) {
                $builder = $builder->addAdapter(PutenvAdapter::class);
            }
            static::$repository = $builder->immutable()->make();
        }
        return static::$repository;
    }
final class RepositoryBuilder
{
    /**
     * The set of default adapters.
     */
    private const DEFAULT_ADAPTERS = [
        ServerConstAdapter::class,
        EnvConstAdapter::class,
    ];

Dotenv\Repository\Adapter\ServerConstAdapter 的 read 方法其实就是从 $_SERVER 中读。

    public function read(string $name)
    {
        /** @var \PhpOption\Option<string> */
        return Option::fromArraysValue($_SERVER, $name)
            ->map(static function ($value) {
                if ($value === false) {
                    return 'false';
                }
                if ($value === true) {
                    return 'true';
                }
                return $value;
            })->filter(static function ($value) {
                return \is_string($value);
            });
    }

Dotenv\Repository\Adapter\EnvConstAdapter 的 read 方法是从 $_ENV 中读。

    public function read(string $name)
    {
        /** @var \PhpOption\Option<string> */
        return Option::fromArraysValue($_ENV, $name)
            ->map(static function ($value) {
                if ($value === false) {
                    return 'false';
                }
                if ($value === true) {
                    return 'true';
                }
                return $value;
            })->filter(static function ($value) {
                return \is_string($value);
            });
    }

Dotenv\Repository\Adapter\PutenvAdapter 是从 \getenv() 中读,

\getenv 返回的其实就是 $_SERVER 和 $_ENV 中的值。

public function read(string $name)
    {
        /** @var \PhpOption\Option<string> */
        return Option::fromValue(\getenv($name), false)->filter(static function ($value) {
            return \is_string($value);
        });
    }


总结

使 Laravel 在不同的环境自动加载不同的环境配置其实很简单, 在命令行环境时, 

我们只要在后面加选项 --env=* 就可以使 laravel 加载不同的环境配置文件。

比如

php artisan horizon --env=local

在 web 环境下我们只要修改 $_ENV 或者 $_SERVER 中 APP_ENV 的值就可以使 laravel 加载不同的环境配置文件。

相关文章