Laravel ENV—— 環境變數的載入與原始碼解析

leoyang發表於2017-08-12

前言

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/laravel-source-analysis

laravel 在啟動時,會載入專案的 env 檔案,本文將會詳細介紹 env 檔案的使用與原始碼的分析。

 

ENV 檔案的使用

多環境 ENV 檔案的設定

laravel 支援在不同的環境下載入不同的 env 檔案,若想要實現多環境 env 檔案,需要做兩件事:

一、在專案寫多個 ENV 檔案,例如三個 env 檔案:

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

這三個檔案中分別針對不同環境為某些變數配置了不同的值,

二、配置 APP_ENV 環境變數值

配置環境變數的方法有很多,其中一個方法是在 nginx 的配置檔案中寫下這句程式碼:

fastcgi_param  APP_ENV  production;

那麼 laravel 會通過 env('APP_ENV') 根據環境變數 APP_ENV 來判斷當前具體的環境,假如環境變數 APP_ENVproduction,那麼 laravel 將會自動載入 .env.production 檔案。

自定義 ENV 檔案的路徑與檔名

laravel 為使用者提供了自定義 ENV 檔案路徑或檔名的函式,

例如,若想要自定義 env 路徑,就可以在 bootstrap 資料夾中 app.php 檔案:

$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

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

若想要自定義 env 檔名稱,就可以在 bootstrap 資料夾中 app.php 檔案:

$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

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

ENV 檔案變數設定

  • env 檔案中,我們可以為變數賦予具體值:
CFOO=bar

值得注意的是,這種具體值不允許賦予多個,例如:

CFOO=bar baz
  • 可以為變數賦予字串引用
CQUOTES="a value with a # character"

值得注意的是,這種引用不允許字串中存在符號 \,只能使用轉義字元 \\

而且也不允許內嵌符號 "",只能使用轉移字元 \",否則取值會意外結束:

CQUOTESWITHQUOTE="a value with a # character & a quote \" character inside quotes" # " this is a comment

$this->assertEquals('a value with a # character & a quote " character inside quotes', getenv('CQUOTESWITHQUOTE'));
  • 可以在 env 檔案中新增註釋,方法是以 # 開始:
CQUOTES="a value with a # character" # this is a comment
  • 可以使用 export 來為變數賦值:
export EFOO="bar"
  • 可以在 env 檔案中使用變數為變數賦值:
NVAR1="Hello"
NVAR2="World!"
NVAR3="{$NVAR1} {$NVAR2}"
NVAR4="${NVAR1} ${NVAR2}"
NVAR5="$NVAR1 {NVAR2}"

$this->assertEquals('{$NVAR1} {$NVAR2}', $_ENV['NVAR3']); // not resolved
$this->assertEquals('Hello World!', $_ENV['NVAR4']);
$this->assertEquals('$NVAR1 {NVAR2}', $_ENV['NVAR5']); // not resolved

 

ENV 載入原始碼分析

laravel 載入 ENV

ENV 的載入功能由類 \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class 完成,它的啟動函式為:

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 檔案:

 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 的開源專案:

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:

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 檔案,並將檔案按行儲存
  • 迴圈讀取每一行,略過註釋
  • 進行環境變數賦值
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 line endings
    $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 函式:

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('apache_setenv') && apache_getenv($name)) {
        apache_setenv($name, $value);
    }

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

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

normaliseEnvironmentVariable 函式用來載入各種型別的環境變數:

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

protected function splitCompoundStringIntoParts($name, $value)
{
    if (strpos($name, '=') !== false) {
        list($name, $value) = array_map('trim', explode('=', $name, 2));
    }

    return array($name, $value);
}

sanitiseVariableName 用於格式化環境變數名:

 protected function sanitiseVariableName($name, $value)
{
    $name = trim(str_replace(array('export ', '\'', '"'), '', $name));

    return array($name, $value);
}

sanitiseVariableValue 用於格式化環境變數值:

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
              [^%1$s\\\\] # any character other than a quote or backslash
              |\\\\\\\\   # 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 containing spaces must be surrounded by quotes.');
        }
    }

    return array($name, trim($value));
}

這段程式碼是載入 env 檔案最複雜的部分,我們詳細來說:

  • 若環境變數值是具體值,那麼僅僅需要分割註釋 # 部分,並判斷是否存在空格符即可。

  • 若環境變數值由引用構成,那麼就需要進行正則匹配,具體的正規表示式為:
/^"((?:[^"\\]|\\\\|\\"))".*$/mx

這個正規表示式的意思是:

  • 提取 “” 雙引號內部的字串,拋棄雙引號之後的字串
  • 若雙引號內部還有雙引號,那麼以最前面的雙引號為提取內容,例如 "dfd("dfd")fdf",我們只能提取出來最前面的部分 "dfd("
  • 對於內嵌的引用可以使用 \" ,例如 "dfd\"dfd\"fdf",我們就可以提取出來 "dfd\"dfd\"fdf"。
  • 不允許引用中含有 \,但可以使用轉義字元 \\

相關文章