前言

laravel 在启动时,会加载项目的 env 文件,本文将会详细介绍 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_ENVproduction,那么 laravel 将会自动加载 .env.production 文件。

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

laravel 为用户提供了自定义 ENV 文件路径或文件名的函数,

例如,若想要自定义 env 路径,就可以在 bootstrap 文件夹中 app.php 文件:

  1. $app = new Illuminate\Foundation\Application(
  2. realpath(__DIR__.'/../')
  3. );
  4. $app->useEnvironmentPath('/customer/path')

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

  1. $app = new Illuminate\Foundation\Application(
  2. realpath(__DIR__.'/../')
  3. );
  4. $app->loadEnvironmentFrom('customer.env')

ENV 文件变量设置

  • env 文件中,我们可以为变量赋予具体值:
  1. CFOO=bar

值得注意的是,这种具体值不允许赋予多个,例如:

  1. CFOO=bar baz
  • 可以为变量赋予字符串引用
  1. CQUOTES="a value with a # character"

值得注意的是,这种引用不允许字符串中存在符号 \,只能使用转义字符 \\

而且也不允许内嵌符号 "",只能使用转移字符 \",否则取值会意外结束:

  1. CQUOTESWITHQUOTE="a value with a # character & a quote \" character inside quotes" # " this is a comment
  2. $this->assertEquals('a value with a # character & a quote " character inside quotes', getenv('CQUOTESWITHQUOTE'));
  • 可以在 env 文件中添加注释,方法是以 # 开始:
  1. CQUOTES="a value with a # character" # this is a comment
  • 可以使用 export 来为变量赋值:
  1. export EFOO="bar"
  • 可以在 env 文件中使用变量为变量赋值:
  1. NVAR1="Hello"
  2. NVAR2="World!"
  3. NVAR3="{$NVAR1} {$NVAR2}"
  4. NVAR4="${NVAR1} ${NVAR2}"
  5. NVAR5="$NVAR1 {NVAR2}"
  6. $this->assertEquals('{$NVAR1} {$NVAR2}', $_ENV['NVAR3']); // not resolved
  7. $this->assertEquals('Hello World!', $_ENV['NVAR4']);
  8. $this->assertEquals('$NVAR1 {NVAR2}', $_ENV['NVAR5']); // not resolved

ENV 加载源码分析

laravel 加载 ENV

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

  1. public function bootstrap(Application $app)
  2. {
  3. if ($app->configurationIsCached()) {
  4. return;
  5. }
  6. $this->checkForSpecificEnvironmentFile($app);
  7. try {
  8. (new Dotenv($app->environmentPath(), $app->environmentFile()))->load();
  9. } catch (InvalidPathException $e) {
  10. //
  11. }
  12. }

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

  1. protected function checkForSpecificEnvironmentFile($app)
  2. {
  3. if (php_sapi_name() == 'cli' && with($input = new ArgvInput)->hasParameterOption('--env')) {
  4. $this->setEnvironmentFilePath(
  5. $app, $app->environmentFile().'.'.$input->getParameterOption('--env')
  6. );
  7. }
  8. if (! env('APP_ENV')) {
  9. return;
  10. }
  11. $this->setEnvironmentFilePath(
  12. $app, $app->environmentFile().'.'.env('APP_ENV')
  13. );
  14. }
  15. protected function setEnvironmentFilePath($app, $file)
  16. {
  17. if (file_exists($app->environmentPath().'/'.$file)) {
  18. $app->loadEnvironmentFrom($file);
  19. }
  20. }

vlucas/phpdotenv 源码解读

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

  1. class Dotenv
  2. {
  3. public function __construct($path, $file = '.env')
  4. {
  5. $this->filePath = $this->getFilePath($path, $file);
  6. $this->loader = new Loader($this->filePath, true);
  7. }
  8. public function load()
  9. {
  10. return $this->loadData();
  11. }
  12. protected function loadData($overload = false)
  13. {
  14. $this->loader = new Loader($this->filePath, !$overload);
  15. return $this->loader->load();
  16. }
  17. }

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

  1. class Loader
  2. {
  3. public function load()
  4. {
  5. $this->ensureFileIsReadable();
  6. $filePath = $this->filePath;
  7. $lines = $this->readLinesFromFile($filePath);
  8. foreach ($lines as $line) {
  9. if (!$this->isComment($line) && $this->looksLikeSetter($line)) {
  10. $this->setEnvironmentVariable($line);
  11. }
  12. }
  13. return $lines;
  14. }
  15. }

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

  • 判断 env 文件是否可读
  • 读取整个 env 文件,并将文件按行存储
  • 循环读取每一行,略过注释
  • 进行环境变量赋值
  1. protected function ensureFileIsReadable()
  2. {
  3. if (!is_readable($this->filePath) || !is_file($this->filePath)) {
  4. throw new InvalidPathException(sprintf('Unable to read the environment file at %s.', $this->filePath));
  5. }
  6. }
  7. protected function readLinesFromFile($filePath)
  8. {
  9. // Read file into an array of lines with auto-detected line endings
  10. $autodetect = ini_get('auto_detect_line_endings');
  11. ini_set('auto_detect_line_endings', '1');
  12. $lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  13. ini_set('auto_detect_line_endings', $autodetect);
  14. return $lines;
  15. }
  16. protected function isComment($line)
  17. {
  18. return strpos(ltrim($line), '#') === 0;
  19. }
  20. protected function looksLikeSetter($line)
  21. {
  22. return strpos($line, '=') !== false;
  23. }

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

  1. public function setEnvironmentVariable($name, $value = null)
  2. {
  3. list($name, $value) = $this->normaliseEnvironmentVariable($name, $value);
  4. if ($this->immutable && $this->getEnvironmentVariable($name) !== null) {
  5. return;
  6. }
  7. if (function_exists('apache_getenv') && function_exists('apache_setenv') && apache_getenv($name)) {
  8. apache_setenv($name, $value);
  9. }
  10. if (function_exists('putenv')) {
  11. putenv("$name=$value");
  12. }
  13. $_ENV[$name] = $value;
  14. $_SERVER[$name] = $value;
  15. }

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

  1. protected function normaliseEnvironmentVariable($name, $value)
  2. {
  3. list($name, $value) = $this->splitCompoundStringIntoParts($name, $value);
  4. list($name, $value) = $this->sanitiseVariableName($name, $value);
  5. list($name, $value) = $this->sanitiseVariableValue($name, $value);
  6. $value = $this->resolveNestedVariables($value);
  7. return array($name, $value);
  8. }

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

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

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

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

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

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

这段代码是加载 env 文件最复杂的部分,我们详细来说:

  • 若环境变量值是具体值,那么仅仅需要分割注释 # 部分,并判断是否存在空格符即可。

  • 若环境变量值由引用构成,那么就需要进行正则匹配,具体的正则表达式为:

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

这个正则表达式的意思是:

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