Laravel ENV/Config-环境变量/配置文件的加载与源码解析

思考并回答以下问题:

ENV

ENV文件的使用

多环境ENV文件的设置

一、在项目写多个ENV文件,例如三个env文件:

  • .env.development
  • .env.staging
  • .env.production

这三个文件中分别针对不同环境为某些变量配置了不同的值。

二、配置APP_ENV环境变量值

配置环境变量的方法有很多,其中一个方法是在nginx的配置文件中写下这句代码:

1
fastcgi_param APP_ENV production;

那么Laravel会通过env(‘APP_ENV’)根据环境变量APP_ENV来判断当前具体的环境,假如环境变量APP_ENV为production,那么Laravel将会自动加载.env.production文件。

自定义ENV文件的路径与文件名

Laravel为用户提供了自定义ENV文件路径或文件名的函数,例如,若想要自定义env路径,就可以在bootstrap文件夹中修改app.php文件:

1
2
3
4
5
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);

$app->useEnvironmentPath('/customer/path')

若想要自定义env文件名称,就可以在bootstrap文件夹中修改app.php文件:

1
2
3
4
5
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);

$app->loadEnvironmentFrom('customer.env')

ENV加载源码分析

Laravel加载ENV

ENV的加载功能由类\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class完成,它的启动函数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function bootstrap(Application $app)
{
if ($app->configurationIsCached())
{
return;
}

$this->checkForSpecificEnvironmentFile($app);

try
{
(new Dotenv($app->environmentPath(), $app->environmentFile()))->load();
}
catch (InvalidPathException $e)
{
//
}
}

如果我们在环境变量中设置了APP_ENV变量,那么就会调用函数checkForSpecificEnvironmentFile来根据环境加载不同的env文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected function checkForSpecificEnvironmentFile($app)
{
if (php_sapi_name() == 'cli' && with($input = new ArgvInput)->hasParameterOption('--env'))
{
$this->setEnvironmentFilePath(
$app,
$app->environmentFile().'.'.$input->getParameterOption('--env')
);
}

if (! env('APP_ENV'))
{
return;
}

$this->setEnvironmentFilePath(
$app, $app->environmentFile().'.'.env('APP_ENV')
);
}

protected function setEnvironmentFilePath($app, $file)
{
if (file_exists($app->environmentPath().'/'.$file))
{
$app->loadEnvironmentFrom($file);
}
}

vlucas/phpdotenv 源码解读

Laravel中对env文件的读取是采用vlucas/phpdotenv的开源项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Dotenv
{
public function __construct($path, $file = '.env')
{
$this->filePath = $this->getFilePath($path, $file);

$this->loader = new Loader($this->filePath, true);
}

public function load()
{
return $this->loadData();
}

protected function loadData($overload = false)
{
$this->loader = new Loader($this->filePath, !$overload);
return $this->loader->load();
}
}

env 文件变量的读取依赖类/Dotenv/Loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Loader
{
public function load()
{
$this->ensureFileIsReadable();

$filePath = $this->filePath;

$lines = $this->readLinesFromFile($filePath);

foreach ($lines as $line)
{
if (!$this->isComment($line) && $this->looksLikeSetter($line))
{
$this->setEnvironmentVariable($line);
}
}
return $lines;
}
}

我们可以看到,env文件的读取的流程:

  • 判断env文件是否可读
  • 读取整个env文件,并将文件按行存储
  • 循环读取每一行,略过注释
  • 进行环境变量赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected function ensureFileIsReadable()
{
if (!is_readable($this->filePath) || !is_file($this->filePath))
{
throw new InvalidPathException(sprintf('Unable to read the environment file at %s.', $this->filePath));
}
}

protected function readLinesFromFile($filePath)
{
// Read file into an array of lines with auto-detected lineendings
$autodetect = ini_get('auto_detect_line_endings');

ini_set('auto_detect_line_endings', '1');

$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

ini_set('auto_detect_line_endings', $autodetect);

return $lines;
}

protected function isComment($line)
{
return strpos(ltrim($line), '#') === 0;
}

protected function looksLikeSetter($line)
{
return strpos($line, '=') !== false;
}

环境变量赋值是env文件加载的核心,主要由setEnvironmentVariable函
数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function setEnvironmentVariable($name, $value = null)
{
list($name, $value) = $this->normaliseEnvironmentVariable($name, $value);

if ($this->immutable && $this->getEnvironmentVariable($name)
!== null)
{
return;
}

if (function_exists('apache_getenv') && function_exists('apa
che_setenv') && apache_getenv($name))
{
apache_setenv($name, $value);
}

if (function_exists('putenv'))
{
putenv("$name=$value");
}

$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}

normaliseEnvironmentVariable 函数用来加载各种类型的环境变量:

1
2
3
4
5
6
7
8
9
10
11
12
protected function normaliseEnvironmentVariable($name, $value)
{
list($name, $value) = $this->splitCompoundStringIntoParts($name, $value);

list($name, $value) = $this->sanitiseVariableName($name, $value);

list($name, $value) = $this->sanitiseVariableValue($name, $value);

$value = $this->resolveNestedVariables($value);

return array($name, $value);
}

splitCompoundStringIntoParts用于将赋值语句转化为环境变量名name和环境变量值value。

1
2
3
4
5
6
7
8
protected function splitCompoundStringIntoParts($name, $value)
{
if (strpos($name, '=') !== false)
{
list($name, $value) = array_map('trim', explode('=', $name, 2));
}
return array($name, $value);
}

sanitiseVariableName用于格式化环境变量名:

1
2
3
4
5
6
protected function sanitiseVariableName($name, $value)
{
$name = trim(str_replace(array('export ', '\'', '"'), '', $name));

return array($name, $value);
}

sanitiseVariableValue用于格式化环境变量值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected function sanitiseVariableValue($name, $value)
{
$value = trim($value);
if (!$value) {
return array($name, $value);
}
if ($this->beginsWithAQuote($value)) { // value starts with
a quote
$quote = $value[0];
$regexPattern = sprintf(
'/^
%1$s # match a quote at the start of the value
( # capturing sub-pattern used
(?: # we do not need to capture this
|\\\\\\\\ # or two backslashes together
|\\\\%1$s # or an escaped quote e.g \"
)* # as many characters that match the previous rules
) # end of the capturing sub-pattern
%1$s # and the closing quote
.*$ # and discard any string after the closing quote/mx',
$quote
);
$value = preg_replace($regexPattern, '$1', $value);
$value = str_replace("\\$quote", $quote, $value);
$value = str_replace('\\\\', '\\', $value);
} else {
$parts = explode(' #', $value, 2);
$value = trim($parts[0]);
// Unquoted values cannot contain whitespace
if (preg_match('/\s+/', $value) > 0) {
throw new InvalidFileException('Dotenv values contai
ning spaces must be surrounded by quotes.');
}
}
return array($name, trim($value));
}

这段代码是加载env文件最复杂的部分,我们详细来说:
若环境变量值是具体值,那么仅仅需要分割注 #部分,并判断是否存在空格符即可。
若环境变量值由引用构成,那么就需要进行正则匹配,具体的正则表达式为:

1
/^"((?:[^"\\]|\\\\|\\"))".*$/mx

这个正则表达式的意思是:
提取 “” 双引号内部的字符串,抛弃双引号之后的字符串
若双引号内部还有双引号,那么以最前面的双引号为提取内容,例如”dfd(“dfd”)fdf”,我们只能提取出来最前面的部分 “dfd(“
对于内嵌的引用可以使用 \” ,例如 “dfd\”dfd\”fdf”,我们就可以提取出来”dfd\”dfd\”fdf”。
不允许引用中含有 \ ,但可以使用转义字符 \\

Config

config配置文件的加载

config配置文件由类\Illuminate\Foundation\Bootstrap\LoadConfiguration::class完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

namespace Illuminate\Foundation\Bootstrap;

use Exception;
use Illuminate\Config\Repository;
use Illuminate\Contracts\Config\Repository as RepositoryContract;
use Illuminate\Contracts\Foundation\Application;
use SplFileInfo;
use Symfony\Component\Finder\Finder;

class LoadConfiguration
{
/**
* 引导给定的应用程序
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
$items = [];

// 首先,我们将查看是否有缓存配置文件。如果有,我们将从该文件中加载配置项,以使其非常快。
// 否则,我们将需要遍历每个配置文件并全部加载它们。
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);
}

// 最后,我们将基于已加载的配置值设置应用程序的环境。
// 我们将传递一个回调,该回调将用于在不存在“--env”开关的Web上下文中获取环境。
$app->detectEnvironment(function () use ($config) {
return $config->get('app.env', 'production');
});

date_default_timezone_set($config->get('app.timezone', 'UTC'));

mb_internal_encoding('UTF-8');
}
}

可以看到,配置文件的加载步骤:

  • 加载缓存
  • 若缓存不存在,则利用函数loadConfigurationFiles加载配置文件
  • 加载环境变量、时间区、编码方式

函数loadConfigurationFiles用于加载配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 从所有文件加载配置项
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Contracts\Config\Repository $repository
* @return void
*
* @throws \Exception
*/
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)
{
$repository->set($key, require $path);
}
}

加载配置文件有两部分:搜索配置文件、加载配置文件的数组变量值

搜索配置文件

getConfigurationFiles可以根据配置文件目录搜索所有的php为后缀的文件,并将其转化为files数组,其key为目录名以字符“.”为连接的字符串,value为文件真实路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 获取该应用程序的所有配置文件
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return array
*/
protected function getConfigurationFiles(Application $app)
{
$files = [];

$configPath = realpath($app->configPath());

foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file)
{
$directory = $this->getNestedDirectory($file, $configPath);

$files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath();
}

ksort($files, SORT_NATURAL);

return $files;
}

/**
* 获取配置文件的嵌套路径
*
* @param \SplFileInfo $file
* @param string $configPath
* @return string
*/
protected function getNestedDirectory(SplFileInfo $file, $configPath)
{
$directory = $file->getPath();

if ($nested = trim(str_replace($configPath, '', $directory), DIRECTORY_SEPARATOR))
{
$nested = str_replace(DIRECTORY_SEPARATOR, '.', $nested).'.';
}

return $nested;
}

加载配置文件数组

加载配置文件由类Illuminate\Config\Repository\LoadConfiguration完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace Illuminate\Config;

use ArrayAccess;
use Illuminate\Contracts\Config\Repository as ConfigContract;
use Illuminate\Support\Arr;

class Repository implements ArrayAccess, ConfigContract
{
/**
* Set a given configuration value.
*
* @param array|string $key
* @param mixed $value
* @return void
*/
public function set($key, $value = null)
{
$keys = is_array($key) ? $key : [$key => $value];

foreach ($keys as $key => $value)
{
Arr::set($this->items, $key, $value);
}
}
}

加载配置文件时间上就是将所有配置文件的数值放入一个巨大的多维数组中,这一部分由类Illuminate\Support\Arr完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Arr
{
/**
* Set an array item to a given value using "dot" notation.
*
* If no key is given to the method, the entire array will be replaced.
*
* @param array $array
* @param string|null $key
* @param mixed $value
* @return array
*/
public static function set(&$array, $key, $value)
{
if (is_null($key))
{
return $array = $value;
}

$keys = explode('.', $key);

while (count($keys) > 1)
{
$key = array_shift($keys);

// If the key doesn't exist at this depth, we will just create an empty array
// to hold the next value, allowing us to create the arrays to hold final
// values at the correct depth. Then we'll keep digging into the array.
if (! isset($array[$key]) || ! is_array($array[$key]))
{
$array[$key] = [];
}

$array = &$array[$key];
}

$array[array_shift($keys)] = $value;

return $array;
}
}

例如dir1.dir2.app,配置文件会生成$array[dir1][dir2][app]这样的数组。

配置文件数值的获取

当我们利用全局函数config来获取配置值的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (! function_exists('config')) 
{
/**
* Get / set the specified configuration value.
*
* If an array is passed as the key, we will assume you want to set an array of values.
*
* @param array|string|null $key
* @param mixed $default
* @return mixed|\Illuminate\Config\Repository
*/
function config($key = null, $default = null)
{
if (is_null($key))
{
return app('config');
}

if (is_array($key))
{
return app('config')->set($key);
}

return app('config')->get($key, $default);
}
}

配置文件的获取和加载类似,都是将字符串转为多维数组,然后获取具体数组值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* Get an item from an array using "dot" notation.
*
* @param \ArrayAccess|array $array
* @param string|int|null $key
* @param mixed $default
* @return mixed
*/
public static function get($array, $key, $default = null)
{
if (! static::accessible($array))
{
return value($default);
}

if (is_null($key))
{
return $array;
}

if (static::exists($array, $key))
{
return $array[$key];
}

if (strpos($key, '.') === false)
{
return $array[$key] ?? value($default);
}

foreach (explode('.', $key) as $segment)
{
if (static::accessible($array) && static::exists($array, $segment))
{
$array = $array[$segment];
}
else
{
return value($default);
}
}

return $array;
}

0%