资讯专栏INFORMATION COLUMN

Laravel核心解读--ENV的加载和读取

Anleb / 2322人阅读

摘要:在启动时会加载项目中的文件。我们来看一下的源码来分析下是怎么加载中的配置的。我们来看看函数的源码它直接通过内建函数读取环境变量。我们看到了在加载配置和读取配置的时候,使用了和两个函数。

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 IlluminateFoundationApplication(
    realpath(__DIR__."/../")
);

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

若想要自定义 env 文件名称,就可以在 bootstrap 文件夹中 app.php 中使用Application实例的loadEnvironmentFrom方法:

$app = new IlluminateFoundationApplication(
    realpath(__DIR__."/../")
);

$app->loadEnvironmentFrom("customer.env")
Laravel 加载ENV配置

Laravel加载ENV的是在框架处理请求之前,bootstrap过程中的LoadEnvironmentVariables阶段中完成的。

我们来看一下IlluminateFoundationBootstrapLoadEnvironmentVariables的源码来分析下Laravel是怎么加载env中的配置的。

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  IlluminateContractsFoundationApplication  $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  IlluminateContractsFoundationApplication  $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 IlluminateFoundation;
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属性设置成了falseLoader设置变量的时候如果通过getEnvironmentVariable方法读取到了变量值,那么就会跳过该环境变量的设置。所以Dotenv默认情况下不会覆盖已经存在的环境变量,这个很关键,比如说在docker的容器编排文件里,我们会给PHP应用容器设置关于Mysql容器的两个环境变量

    environment:
      - "DB_PORT=3306"
      - "DB_HOST=database"

这样在容器里设置好环境变量后,即使env文件里的DB_HOSThomesteadenv函数读取出来的也还是容器里之前设置的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读取环境变量。

我们看到了在加载配置和读取配置的时候,使用了putenvgetenv两个函数。putenv设置的环境变量只在请求期间存活,请求结束后会恢复环境之前的设置。因为如果php.ini中的variables_order配置项成了 GPCS不包含E的话,那么php程序中是无法通过$_ENV读取环境变量的,所以使用putenv动态地设置环境变量让开发人员不用去关注服务器上的配置。而且在服务器上给运行用户配置的环境变量会共享给用户启动的所有进程,这就不能很好的保护比如DB_PASSWORDAPI_KEY这种私密的环境变量,所以这种配置用putenv设置能更好的保护这些配置信息,getenv方法能获取到系统的环境变量和putenv动态设置的环境变量。

本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/29552.html

相关文章

  • Laravel核心解读--完结篇

    摘要:过去一年时间写了多篇文章来探讨了我认为的框架最核心部分的设计思路代码实现。为了大家阅读方便,我把这些源码学习的文章汇总到这里。数据库算法和数据结构这些都是编程的内功,只有内功深厚了才能解决遇到的复杂问题。 过去一年时间写了20多篇文章来探讨了我认为的Larave框架最核心部分的设计思路、代码实现。通过更新文章自己在软件设计、文字表达方面都有所提高,在刚开始决定写Laravel源码分析地...

    laoLiueizo 评论0 收藏0
  • Laravel学习笔记之bootstrap源码解析

    摘要:总结本文主要学习了启动时做的七步准备工作环境检测配置加载日志配置异常处理注册注册启动。 说明:Laravel在把Request通过管道Pipeline送入中间件Middleware和路由Router之前,还做了程序的启动Bootstrap工作,本文主要学习相关源码,看看Laravel启动程序做了哪些具体工作,并将个人的研究心得分享出来,希望对别人有所帮助。Laravel在入口index...

    xiaoxiaozi 评论0 收藏0
  • Laravel核心解读--HTTP内核

    摘要:终止程序终止中间件内核的方法会调用中间件的方法,调用完成后从请求进来到返回响应整个应用程序的生命周期就结束了。 Http Kernel Http Kernel是Laravel中用来串联框架的各个核心组件来网络请求的,简单的说只要是通过public/index.php来启动框架的都会用到Http Kernel,而另外的类似通过artisan命令、计划任务、队列启动框架进行处理的都会用到C...

    chenjiang3 评论0 收藏0
  • Laravel核心解读--服务提供器(ServiceProvider)

    摘要:调用了的可以看出,所有服务提供器都在配置文件文件的数组中。启动的启动由类负责引导应用的属性中记录的所有服务提供器,就是依次调用这些服务提供器的方法,引导完成后就代表应用正式启动了,可以开始处理请求了。 服务提供器是所有 Laravel 应用程序引导中心。你的应用程序自定义的服务、第三方资源包提供的服务以及 Laravel 的所有核心服务都是通过服务提供器进行注册(register)和引...

    Richard_Gao 评论0 收藏0
  • 源码解读Laravel php artisan route:cache

    摘要:然而,本文的讨论重点,还是背后的源码,是怎么做到这一步的。从哪开始看源码位于你还是可以使用编辑器搜,就可以看到源码了。第三步序列化所有路由注册映射关系,还是在的方法中上面的方法位于中的中。所以到这里,的源码解读就完成了。 学 Laravel 和 Vuejs 你真应该来 codecasts.com ! Laravel ​route:cache 可以直接缓存路由文件,这样其实可以在一定程度...

    wangzy2019 评论0 收藏0

发表评论

0条评论

Anleb

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<