昨天同事在他的项目中遇到了一个技术难题让我协助解决
问题出在env
helper 函数上,在Middleware中读取环境变量时取到的竟然是null
dump(env('APP_ENV') === null); // true
据他反映,之前本地开发时功能都是正常的,上线之后才开始出现的问题
查看env
的代码
// Illuminate/Foundation/helpers.php
function env($key, $default = null)
{
$value = getenv($key);
if ($value === false) {
return value($default);
}
// 此处省略部分代码
return $value;
}
可以发现,实际上调用的就是php自带的getenv
函数,如果取不到值,那必定是某段执行putenv
或者操作$_ENV
全局变量的代码没有执行,而且这些环境变量设置操作一定是在初始化的时候进行的
如果对Laravel的Kernel执行顺序比较了解的话,应该知道,在Kernel启动阶段会首先执行一组bootstrappers
//Illuminate/Foundation/Http/Kernel.php
class Kernel implements KernelContract
{
/**
* The bootstrap classes for the application.
*
* @var array
*/
protected $bootstrappers = [
'Illuminate\Foundation\Bootstrap\DetectEnvironment',
'Illuminate\Foundation\Bootstrap\LoadConfiguration',
'Illuminate\Foundation\Bootstrap\ConfigureLogging',
'Illuminate\Foundation\Bootstrap\HandleExceptions',
'Illuminate\Foundation\Bootstrap\RegisterFacades',
'Illuminate\Foundation\Bootstrap\RegisterProviders',
'Illuminate\Foundation\Bootstrap\BootProviders',
];
其中一个bootstrapper名为DetectEnvironment
,而它的嫌疑是最大的,检查这个类
//Illuminate/Foundation/Bootstrap/DetectEnvironment.php
class DetectEnvironment
{
/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
if (! $app->configurationIsCached()) {
$this->checkForSpecificEnvironmentFile($app);
try {
(new Dotenv($app->environmentPath(), $app->environmentFile()))->load();
} catch (InvalidPathException $e) {
//
}
}
}
// ……
}
在代码中,我们并没有发现任何执行putenv
或者操作$_ENV
全局变量的地方,实际上在整个Laravel的框架代码里也没有发现蛛丝马迹,那么这些环境变量是如何保存进来的呢?
如果Laravel框架里没有执行这些代码,我们应该可以推测是框架使用的第三方组件做了这些事情
而事实也确实如此,可以看到在DetectEnvironment
类的bootstrap
方法里实例化了一个Dotenv
类的对象,而Dotenv\Loader
正是执行了读取.env
文件,执行putenv
的操作。
vlucas/phpdotenv Laravel那一套.env文件的玩法就是使用的这个组件,有兴趣的同学可以研究一下
好了,暂时总结一下分析的结果
- 环境变量的读取函数env是在helpers.php里定义的,执行的是php的
getenv
- 环境变量的存储经过确认是第三方的 vlucas/phpdotenv 来完成的
那么env
获取不到值只能说明Dotenv
的实例化并没有执行
再仔细看一下DetectEnvironment类,在实例化Dotenv
前,先进行了一个 if 判断
if (! $app->configurationIsCached())
而这个方法的作用就是判断 bootstrap/cache/config.php
缓存文件是否存在,如果文件存在则不会执行Dotenv
的实例化操作,也就不会往$_ENV
里存数据,自然env
函数是取不到值的
bootstrap/cache/config.php
是执行下面的命令生成的
php artisan config:cache
命令说明如下
Create a cache file for faster configuration loading
这是对config目录下的所有配置文件做的缓存
那么,真相大白了,之前在开发时并没有出现问题,是因为没有执行 config:cache
命令,而项目上线后为了优化访问速度,生成了缓存文件,导致env
取值失败
可以疑问还是存在,既然Laravel提供了env
函数,为什么还有这个坑呢?是bug吗?如果env
不能使用,那该使用什么方式呢?
本着一探究竟的态度,仔细阅读了文档,在 Upgrade Guide 里找到了答案
Caching And Env
If you are using the config:cache command during deployment, you must make sure that you are only calling the env function from within your configuration files, and not from anywhere else in your application.
如果使用了config:cache
,env函数只能在config目录下的配置文件的php里使用,不可以在其他地方使用。
一个非常简单的办法就是将
env('APP_ENV')
改为
config('app.env')
config
函数会优先读取 bootstrap/cache/config.php
中缓存的配置,如果没有缓存文件,则会直接读取 config 目录下的所有配置文件
想研究这块代码实现的可以看下另一个bootstrapper:
LoadConfiguration
,它是用来初始化config service
的,而config
函数使用的就是这个service