Laravel
在啓動時會加載項目中的.env
文件。對於應用程序運行的環境來講,不一樣的環境有不一樣的配置一般是頗有用的。 例如,你可能但願在本地使用測試的Mysql
數據庫而在上線後但願項目可以自動切換到生產Mysql
數據庫。本文將會詳細介紹 env
文件的使用與源碼的分析。php
項目中env
文件的數量每每是跟項目的環境數量相同,假如一個項目有開發、測試、生產三套環境那麼在項目中應該有三個.env.dev
、.env.test
、.env.prod
三個環境配置文件與環境相對應。三個文件中的配置項應該徹底同樣,而具體配置的值應該根據每一個環境的須要來設置。mysql
接下來就是讓項目可以根據環境加載不一樣的env
文件了。具體有三種方法,能夠按照使用習慣來選擇使用:nginx
APP_ENV
環境變量fastcgi_param APP_ENV dev;
www
用戶的/home/www/.bashrc
中添加export APP_ENV dev
cp .env.dev .env
針對前兩種方法,Laravel
會根據env('APP_ENV')
加載到的變量值去加載對應的文件.env.dev
、.env.test
這些。 具體在後面源碼裏會說,第三種比較好理解就是在部署項目時將環境的配置文件覆蓋到.env
文件裏這樣就不須要在環境的系統和nginx
裏作額外的設置了。laravel
env
文件默認放在項目的根目錄中,laravel
爲用戶提供了自定義 ENV
文件路徑或文件名的函數,git
例如,若想要自定義 env
路徑,能夠在 bootstrap
文件夾中 app.php
中使用Application
實例的useEnvironmentPath
方法:github
$app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../') ); $app->useEnvironmentPath('/customer/path')
若想要自定義 env
文件名稱,就能夠在 bootstrap
文件夾中 app.php
中使用Application
實例的loadEnvironmentFrom
方法:sql
$app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../') ); $app->loadEnvironmentFrom('customer.env')
Laravel
加載ENV
的是在框架處理請求以前,bootstrap過程當中的LoadEnvironmentVariables
階段中完成的。docker
咱們來看一下\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables
的源碼來分析下Laravel
是怎麼加載env
中的配置的。數據庫
<?php namespace Illuminate\Foundation\Bootstrap; use Dotenv\Dotenv; use Dotenv\Exception\InvalidPathException; use Symfony\Component\Console\Input\ArgvInput; use Illuminate\Contracts\Foundation\Application; class LoadEnvironmentVariables { /** * Bootstrap the given application. * * @param \Illuminate\Contracts\Foundation\Application $app * @return void */ public function bootstrap(Application $app) { if ($app->configurationIsCached()) { return; } $this->checkForSpecificEnvironmentFile($app); try { (new Dotenv($app->environmentPath(), $app->environmentFile()))->load(); } catch (InvalidPathException $e) { // } } /** * Detect if a custom environment file matching the APP_ENV exists. * * @param \Illuminate\Contracts\Foundation\Application $app * @return void */ protected function checkForSpecificEnvironmentFile($app) { if ($app->runningInConsole() && ($input = new ArgvInput)->hasParameterOption('--env')) { if ($this->setEnvironmentFilePath( $app, $app->environmentFile().'.'.$input->getParameterOption('--env') )) { return; } } if (! env('APP_ENV')) { return; } $this->setEnvironmentFilePath( $app, $app->environmentFile().'.'.env('APP_ENV') ); } /** * Load a custom environment file. * * @param \Illuminate\Contracts\Foundation\Application $app * @param string $file * @return bool */ protected function setEnvironmentFilePath($app, $file) { if (file_exists($app->environmentPath().'/'.$file)) { $app->loadEnvironmentFrom($file); return true; } return false; } }
在他的啓動方法bootstrap
中,Laravel
會檢查配置是否緩存過以及判斷應該應用那個env
文件,針對上面說的根據環境加載配置文件的三種方法中的頭兩種,由於系統或者nginx環境變量中設置了APP_ENV
,因此Laravel會在checkForSpecificEnvironmentFile
方法里根據 APP_ENV
的值設置正確的配置文件的具體路徑, 好比.env.dev
或者.env.test
,而針對第三中狀況則是默認的.env
, 具體能夠參看下面的checkForSpecificEnvironmentFile
還有相關的Application裏的兩個方法的源碼:apache
protected function checkForSpecificEnvironmentFile($app) { if ($app->runningInConsole() && ($input = new ArgvInput)->hasParameterOption('--env')) { if ($this->setEnvironmentFilePath( $app, $app->environmentFile().'.'.$input->getParameterOption('--env') )) { return; } } if (! env('APP_ENV')) { return; } $this->setEnvironmentFilePath( $app, $app->environmentFile().'.'.env('APP_ENV') ); } namespace Illuminate\Foundation; class Application .... { public function environmentPath() { return $this->environmentPath ?: $this->basePath; } public function environmentFile() { return $this->environmentFile ?: '.env'; } }
判斷好後要讀取的配置文件的路徑後,接下來就是加載env
裏的配置了。
(new Dotenv($app->environmentPath(), $app->environmentFile()))->load();
Laravel
使用的是Dotenv
的PHP版本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(); } }
它依賴/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; } }
Loader
讀取配置時readLinesFromFile
函數會用file
函數將配置從文件中一行行地讀取到數組中去,而後排除以#
開頭的註釋,針對內容中包含=
的行去調用setEnvironmentVariable
方法去把文件行中的環境變量配置到項目中去:
namespace Dotenv; class Loader { public function setEnvironmentVariable($name, $value = null) { list($name, $value) = $this->normaliseEnvironmentVariable($name, $value); $this->variableNames[] = $name; // Don't overwrite existing environment variables if we're immutable // Ruby's dotenv does this with `ENV[key] ||= value`. if ($this->immutable && $this->getEnvironmentVariable($name) !== null) { return; } // If PHP is running as an Apache module and an existing // Apache environment variable exists, overwrite it 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; } public function getEnvironmentVariable($name) { switch (true) { case array_key_exists($name, $_ENV): return $_ENV[$name]; case array_key_exists($name, $_SERVER): return $_SERVER[$name]; default: $value = getenv($name); return $value === false ? null : $value; // switch getenv default to null } } }
Dotenv
實例化Loader
的時候把Loader
對象的$immutable
屬性設置成了false
,Loader
設置變量的時候若是經過getEnvironmentVariable
方法讀取到了變量值,那麼就會跳過該環境變量的設置。因此Dotenv
默認狀況下不會覆蓋已經存在的環境變量,這個很關鍵,好比說在docker
的容器編排文件裏,咱們會給PHP
應用容器設置關於Mysql
容器的兩個環境變量
environment: - "DB_PORT=3306" - "DB_HOST=database"
這樣在容器裏設置好環境變量後,即便env
文件裏的DB_HOST
爲homestead
用env
函數讀取出來的也仍是容器裏以前設置的DB_HOST
環境變量的值database
(docker中容器連接默認使用服務名稱,在編排文件中我把mysql容器的服務名稱設置成了database, 因此php容器要經過database這個host來鏈接mysql容器)。由於用咱們在持續集成中作自動化測試的時候一般都是在容器裏進行測試,因此Dotenv
不會覆蓋已存在環境變量這個行爲就至關重要這樣我就能夠只設置容器裏環境變量的值完成測試而不用更改項目裏的env
文件,等到測試完成後直接去將項目部署到環境上就能夠了。
若是檢查環境變量不存在那麼接着Dotenv就會把環境變量經過PHP內建函數putenv
設置到環境中去,同時也會存儲到$_ENV
和$_SERVER
這兩個全局變量中。
在Laravel應用程序中可使用env()
函數去讀取環境變量的值,好比獲取數據庫的HOST:
env('DB_HOST`, 'localhost');
傳遞給 env
函數的第二個值是「默認值」。若是給定的鍵不存在環境變量,則會使用該值。
咱們來看看env
函數的源碼:
function env($key, $default = null) { $value = getenv($key); if ($value === false) { return value($default); } 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 (strlen($value) > 1 && Str::startsWith($value, '"') && Str::endsWith($value, '"')) { return substr($value, 1, -1); } return $value; }
它直接經過PHP
內建函數getenv
讀取環境變量。
咱們看到了在加載配置和讀取配置的時候,使用了putenv
和getenv
兩個函數。putenv
設置的環境變量只在請求期間存活,請求結束後會恢復環境以前的設置。由於若是php.ini中的variables_order
配置項成了 GPCS
不包含E
的話,那麼php程序中是沒法經過$_ENV
讀取環境變量的,因此使用putenv
動態地設置環境變量讓開發人員不用去關注服務器上的配置。並且在服務器上給運行用戶配置的環境變量會共享給用戶啓動的全部進程,這就不能很好的保護好比DB_PASSWORD
、API_KEY
這種私密的環境變量,因此這種配置用putenv
設置能更好的保護這些配置信息,getenv
方法能獲取到系統的環境變量和putenv
動態設置的環境變量。
本文已經收錄在系列文章Laravel源碼學習裏,歡迎訪問閱讀。