Laravel
在啟動時會載入專案中的.env
檔案。對於應用程式執行的環境來說,不同的環境有不同的配置通常是很有用的。 例如,你可能希望在本地使用測試的Mysql
資料庫而在上線後希望專案能夠自動切換到生產Mysql
資料庫。本文將會詳細介紹 env
檔案的使用與原始碼的分析。
Env檔案的使用
多環境env的設定
專案中env
檔案的數量往往是跟專案的環境數量相同,假如一個專案有開發、測試、生產三套環境那麼在專案中應該有三個.env.dev
、.env.test
、.env.prod
三個環境配置檔案與環境相對應。三個檔案中的配置項應該完全一樣,而具體配置的值應該根據每個環境的需要來設定。
接下來就是讓專案能夠根據環境載入不同的env
檔案了。具體有三種方法,可以按照使用習慣來選擇使用:
-
在環境的nginx配置檔案裡設定
APP_ENV
環境變數fastcgi_param APP_ENV dev;
-
設定伺服器上執行PHP的使用者的環境變數,比如在
www
使用者的/home/www/.bashrc
中新增export APP_ENV dev
-
在部署專案的持續整合任務或者部署指令碼里執行
cp .env.dev .env
針對前兩種方法,Laravel
會根據env('APP_ENV')
載入到的變數值去載入對應的檔案.env.dev
、.env.test
這些。 具體在後面原始碼裡會說,第三種比較好理解就是在部署專案時將環境的配置檔案覆蓋到.env
檔案裡這樣就不需要在環境的系統和nginx
裡做額外的設定了。
自定義env檔案的路徑與檔名
env
檔案預設放在專案的根目錄中,laravel
為使用者提供了自定義 ENV
檔案路徑或檔名的函式,
例如,若想要自定義 env
路徑,可以在 bootstrap
資料夾中 app.php
中使用Application
例項的useEnvironmentPath
方法:
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
$app->useEnvironmentPath('/customer/path')
若想要自定義 env
檔名稱,就可以在 bootstrap
資料夾中 app.php
中使用Application
例項的loadEnvironmentFrom
方法:
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
$app->loadEnvironmentFrom('customer.env')
Laravel 載入ENV配置
Laravel
載入ENV
的是在框架處理請求之前,bootstrap過程中的LoadEnvironmentVariables
階段中完成的。
我們來看一下\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裡的兩個方法的原始碼:
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
這兩個全域性變數中。
在專案中讀取env配置
在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核心程式碼學習中,歡迎訪問閱讀,多多交流。
本作品採用《CC 協議》,轉載必須註明作者和本文連結