这篇文章会先说明一下环境配置的加载过程,然后再说明如何使 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
加载不同的环境配置文件。
本来是要写别的, 结果发现一台电脑只能保留一篇草稿,被迫补完作业。