Laravel启动时会加载项目中的.env文件。为应用程序运行的不同环境设置不同的配置通常很有用。比如你可能想在本地使用测试Mysql数据库,希望项目上线后能自动切换到生产Mysql数据库。本文将详细介绍env文件的使用和源码分析。项目中env文件的数量通常与项目中环境的数量相同。如果一个项目有开发、测试、生产三个环境,那么项目中应该有三个.env.dev、.env.test和.env.prod这三个环境配置文件,分别对应环境。三个文件中的配置项应该是完全一样的,具体的配置值要根据各个环境的需要来设置。接下来就是让项目根据环境加载不同的env文件。具体有3种方法,大家可以根据自己的使用习惯选择使用:在环境的nginx配置文件中设置APP_ENV环境变量fastcgi_paramAPP_ENVdev;设置服务器上运行PHP的用户的环境变量,如/home/www/。在bashrc中添加exportAPP_ENVdev,在部署项目的持续集成任务或部署脚本中执行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=newIlluminate\Foundation\Application(realpath(__DIR__.'/../'));$app->useEnvironmentPath('/customer/path')if要自定义env文件的名称,可以使用bootstrap文件夹下app.php中Application实例的loadEnvironmentFrom方法:$app=newIlluminate\Foundation\Application(realpath(__DIR__.'/../'));$app->loadEnvironmentFrom('customer.env')Laravel加载ENV配置Laravel在框架处理请求之前的引导过程的LoadEnvironmentVariables阶段加载ENV。下面我们通过\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables的源码来分析Laravel是如何加载env中的配置的。configurationIsCached()){return;}$this->checkForSpecificEnvironmentFile($app);尝试{(newDotenv($app->environmentPath(),$app->environmentFile()))->load();}catch(InvalidPathException$e){//}}/***检测是否存在与APP_ENV匹配的自定义环境文件。**@param\Illuminate\Contracts\Foundation\Application$app*@returnvoid*/保护函数checkForSpecificEnvironmentFile($app){if($app->runningInConsole()&&($input=newArgvInput)->hasParameterOption('--env')){if($this->setEnvironmentFilePath($app,$app->environmentFile().'.'.$input->getParameterOption('--env'))){返回;}}if(!env('APP_ENV')){返回;}$this->setEnvironmentFilePath($app,$app->environmentFile().'.'.env('APP_ENV'));}/***加载自定义环境文件。**@param\Illuminate\Contracts\Foundation\Application$app*@paramstring$file*@returnbool*/protectedfunctionsetEnvironmentFilePath($app,$file){if(file_exists($app->environmentPath().'/'.$file)){$app->loadEnvironmentFrom($file);返回真;}返回假;}}在它的启动方法bootstrap中,Laravel会检查配置是否已经被缓存,并决定应用哪个env文件对于上面提到的三种根据环境加载配置文件的方法中的前两种,由于在系统或nginx环境变量中设置了APP_ENV,Laravel会在checkForSpecificEnvironmentFile方法中根据APP_ENV的值设置正确的配置文件。路径,例如.env.dev或.env.test,对于第三种情况,它是默认的.env。具体可以参考下面的checkForSpecificEnvironmentFile和相关Application中两个方法的源码:('--env')){if($this->setEnvironmentFilePath($app,$app->environmentFile().'.'.$input->getParameterOption('--env'))){返回;}}if(!env('APP_ENV')){返回;}$this->setEnvironmentFilePath($app,$app->environmentFile().'.'.env('APP_ENV'));}namespaceIlluminate\Foundation;classApplication....{publicfunctionenvironmentPath(){返回$this->环境路径?:$this->basePath;}publicfunctionenvironmentFile(){return$this->environmentFile?:'.env';}}判断好要读取的配置文件的路径后,接下来就是加载env中的配置(newDotenv($app->environmentPath(),$app->environmentFile()))->load();Laravel使用的是Dotenv的PHP版本vlucas/phpdotenvclassDotenv{publicfunction__construct($path,$file='.env'){$this->filePath=$this->getFilePath($path,$file);$this->loader=newLoader($this->filePath,true);}publicfunctionload(){return$this->loadData();}protectedfunctionloadData($overload=false){$this->loader=newLoader($this->filePath,!$overload);返回$this->loader->load();}}它依赖/Dotenv/Loader来加载数据:classLoader{publicfunctionload(){$this->ensureFileIsReadable();$filePath=$this->filePath;$lines=$this->readLinesFromFile($filePath);foreach($linesas$line){if(!$this->isComment($line)&&$this->looksLikeSetter($line)){$this->setEnvironmentVariable($line);}}}返回$行;}}Loader读取配置时,readLinesFromFile函数会使用file函数从文件中逐行读取配置到数组中,然后排除#开头的注释,到内容中包含=的行调用setEnvironmentVariable方法配置文件行中的环境变量到项目中:namespaceDotenv;classLoader{publicfunctionsetEnvironmentVariable($name,$value=null){list($name,$value)=$this->normaliseEnvironmentVariable($名称,$值);$this->variableNames[]=$name;//如果我们是不可变的,不要覆盖现有的环境变量//Ruby的dotenv使用`ENV[key]||=value`.if($this->immutable&&$this->getEnvironmentVariable($name)!==空){返回;}//如果PHP作为Apache模块运行并且存在现有的//Apache环境变量,则覆盖它,$值);}如果(function_exists('putenv')){putenv("$name=$value");}$_ENV[$名称]=$值;$_SERVER[$名称]=$值;}publicfunctiongetEnvironmentVariable($name){switch(true){casearray_key_exists($name,$_ENV):return$_ENV[$name];casearray_key_exists($name,$_SERVER):返回$_SERVER[$name];默认值:$value=getenv($name);返回$value===false?空值:$值;//switchgetenvdefaulttonull}}}当Dotenv实例化Loader时,它将Loader对象的$immutable属性设置为false。getEnvironmentVariable方法读取变量值,然后跳过环境变量的设置所以Dotenv默认不会覆盖已有的环境变量。这个非常重要。比如在docker的容器配置文件中,我们会为PHP应用容器环境设置两个关于Mysql容器的环境变量:-"DB_PORT=3306"-"DB_HOST=database"在容器中设置好环境变量后,即使如果env文件中的DB_HOST是homestead用env函数读取的,还是之前在容器数据库中设置的DB_HOST环境变量的值(docker中的容器链接默认使用服务名。在编排文件中,我把mysql容器的服务名设置为database,所以php容器需要通过数据库主机连接到mysql容器)。因为我们在持续集成中做自动化测试的时候,一般都是在容器中进行测试,所以Dotenv不会覆盖已有的环境变量是很重要的,这样我只能在容器中设置环境变量的值来完成测试。不需要更改项目中的env文件,测试完成后直接将项目部署到环境中即可。如果检查到环境变量不存在,那么Dotenv会通过PHP内置函数putenv将环境变量设置为environment,同时也会存储在$_ENV和$_SERVER这两个全局变量中。读取项目中的env配置在Laravel应用中,可以使用env()函数读取环境变量的值,比如获取数据库的HOST:env('DB_HOST`,'localhost');传递给第二个env函数的值是“默认值”。如果给定键不存在环境变量,则将使用该值。我们看一下env函数的源码:functionenv($key,$default=null){$value=getenv($key);如果($value===false){返回值($default);}switch(strtolower($value)){case'true':case'(true)':returntrue;case'false':case'(false)':返回false;case'empty':case'(empty)':return'';case'null':case'(null)':返回;}if(strlen($value)>1&&Str::startsWith($value,'"')&&Str::endsWith($value,'"')){returnsubstr($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源码学习系列文章中,欢迎访问阅读。
