使Laravel 在不同的环境自动加载不同的环境配置

这篇文章会先说明一下环境配置的加载过程,然后再说明如何使 laravel 在不同的环境自动加载不同的环境配置。

一. 环境配置的加载过程:

运行 laravel 框架主要有两种方式, 一种是作为web服务的运行的laravel,另一种是作为命令行脚本运行的 laravel。web 服务的入口文件是 public/index.php , 命令行脚本的入口文件是 artisan。可以说框架的加载也是从 public/index.phpartisan 开始的。我们拿 public/index.php 举例, 可以看到先引用了 laravel 框架, 然后通过服务提供者 make 出来一个 Kernel , 然后通过调用 Kernelhandle 方法 和 send 方法创建了 $response$request 实例

# public/index.php 和 artisan 中的这一行代码引入了 laravel 框架。
$app = require_once __DIR__.'/bootstrap/app.php';

$kernel = $app->make(Kernel::class);
$response = $kernel->handle(
    $request = Request::capture()
)->send();

/bootstrap/app.php 中通过下面这行代码创建的 laravel 实例。

$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

Illuminate\Foundation\Application 中有一个 bootstrapWith 方法, 这个方法先通过 $this->make() 方法创建了 bootstrapper 的实例, 然后执行 bootstrapperbootstrap 方法。

 public function bootstrapWith(array $bootstrappers)
    {
        $this->hasBeenBootstrapped = true;

        foreach ($bootstrappers as $bootstrapper) {
            $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);

            $this->make($bootstrapper)->bootstrap($this);

            $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
        }
    }

内核 Kernelbootstrap 方法调用了 Application 中的 bootstrapWith 方法,这里还是拿 Illuminate\Foundation\Http\Kernel 举例子, 可以看到 $bootstrappers 中的第一个bootstrapper就是 \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables, 这个bootstrapper就是用来加载环境配置的bootstrapper

protected $bootstrappers = [
        \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
        \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
        \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
        \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
        \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
        \Illuminate\Foundation\Bootstrap\BootProviders::class,
    ];

...

public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
  }
}

我们接着来看下 \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariablesbootstrap 方法,$app->configurationIsCached()true 表示有配置的缓存文件,如果有的话就不会重复加载。然后 $this->checkForSpecificEnvironmentFile($app); 检查特定环境配置文件。 这个方法检查会修改 $app 中环境配置文件的路径, 所以是今天的重点。

public function bootstrap(Application $app)
{
        if ($app->configurationIsCached()) {
            return;
        }

        $this->checkForSpecificEnvironmentFile($app);

        try {
            $this->createDotenv($app)->safeLoad();
        } catch (InvalidFileException $e) {
            $this->writeErrorAndDie($e);
        }
}

首先判断是不是命令行环境,如果是的话, 会获取 --env 的值,并将这个值和 $app->environmentFile(). 做字符串拼接, 然后调用 setEnvironmentFilePath 修改环境配置文件的文件名, 比如说 --env=local 那最后加载的环境配置文件就是 .env.local。 这里如果不是命令行环境的话, 后面会通过 Env::get('APP_ENV'); 获取当前环境, 然后同样做一个字符串拼接, 然后调用 setEnvironmentFilePath 修改。

protected function checkForSpecificEnvironmentFile($app)
{
        if ($app->runningInConsole() &&
            ($input = new ArgvInput)->hasParameterOption('--env') &&
            $this->setEnvironmentFilePath($app, $app->environmentFile().'.'.$input->getParameterOption('--env'))) {
            return;
        }

        $environment = Env::get('APP_ENV');

        if (! $environment) {
            return;
        }

        $this->setEnvironmentFilePath(
            $app, $app->environmentFile().'.'.$environment
        );
}

protected function setEnvironmentFilePath($app, $file)
{
        if (is_file($app->environmentPath().'/'.$file)) {
            $app->loadEnvironmentFrom($file);
            return true;
        }
        return false;
}

如果在 checkForSpecificEnvironmentFile 方法中没有修改环境配置文件的文件名, 则会加载默认的环境配置文件 。 这个默认的文件名是通过 \Illuminate\Foundation\Application$environmentFile 属性定义的, 前面的 setEnvironmentFilePath 修改的也是 environmentFile 的值。

protected $environmentFile = '.env';

public function loadEnvironmentFrom($file)
{
    $this->environmentFile = $file;
    return $this;
}

这里再说明一下, Illuminate\Foundation\Http\Kernelbootstrap 方法是在 sendRequestThroughRouter 方法中调用的, 而 sendRequestThroughRouter 是在 handle 方法中调用的。

public function handle($request)
    {
        $this->requestStartedAt = Carbon::now();

        try {
            $request->enableHttpMethodParameterOverride();

            $response = $this->sendRequestThroughRouter($request);
        } catch (Throwable $e) {
            $this->reportException($e);

            $response = $this->renderException($request, $e);
        }

        $this->app['events']->dispatch(
            new RequestHandled($request, $response)
        );

        return $response;
    }

    /**
     * Send the given request through the middleware / router.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendRequestThroughRouter($request)
    {
        $this->app->instance('request', $request);

        Facade::clearResolvedInstance('request');

        $this->bootstrap();

        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

我们来整理一下。 整个环境配置加载过程就是 先有了 $app, 然后 $kernel = $app->make(Kernel::class) 再然后 $kernel->handle()handle 方法中调用了 $kernel->bootstrap() 最后在 bootstrap 中通过 LoadEnvironmentVariables加载了 环境配置.

二。如何使 laravel 在不同的环境自动加载不同的环境配置。

现在 env 的加载过程基本上已经了解了, 我们接着再详细说明下 checkForSpecificEnvironmentFileEnv::get('APP_ENV') 的具体逻辑,以及如何使 laravel 在不同的环境下自动加载不同环境配置的具体操作。

get 方法没什么好说就是 static::getRepository() 然后尝试获取配置项的值, 关键是它有内些仓库。

public static function get($key, $default = null)
{
    return Option::fromValue(static::getRepository()->get($key))
        ->map(function ($value) {
            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 (preg_match('/\A([\'"])(.*)\1\z/', $value, $matches)) {
                return $matches[2];
  }

            return $value;
  })
        ->getOrCall(fn () => value($default));
}

其实就是 EnvConstAdapter ServerConstAdapter, PutenvAdapter 它们三个。

    public static function getRepository()
    {
        if (static::$repository === null) {
            $builder = RepositoryBuilder::createWithDefaultAdapters();

            if (static::$putenv) {
                $builder = $builder->addAdapter(PutenvAdapter::class);
            }

            static::$repository = $builder->immutable()->make();
        }

        return static::$repository;
    }
final class RepositoryBuilder
{
    /**
     * The set of default adapters.
     */
    private const DEFAULT_ADAPTERS = [
        ServerConstAdapter::class,
        EnvConstAdapter::class,
    ];

Dotenv\Repository\Adapter\ServerConstAdapterread 方法其实就是从 $_SERVER 中读。

    public function read(string $name)
    {
        /** @var \PhpOption\Option<string> */
        return Option::fromArraysValue($_SERVER, $name)
            ->map(static function ($value) {
                if ($value === false) {
                    return 'false';
                }

                if ($value === true) {
                    return 'true';
                }

                return $value;
            })->filter(static function ($value) {
                return \is_string($value);
            });
    }

Dotenv\Repository\Adapter\EnvConstAdapterread 方法是从 $_ENV 中读。

    public function read(string $name)
    {
        /** @var \PhpOption\Option<string> */
        return Option::fromArraysValue($_ENV, $name)
            ->map(static function ($value) {
                if ($value === false) {
                    return 'false';
                }

                if ($value === true) {
                    return 'true';
                }

                return $value;
            })->filter(static function ($value) {
                return \is_string($value);
            });
    }

Dotenv\Repository\Adapter\PutenvAdapter 是从 \getenv() 中读, \getenv 返回的其实就是 $_SERVER$_ENV 中的值。

public function read(string $name)
    {
        /** @var \PhpOption\Option<string> */
        return Option::fromValue(\getenv($name), false)->filter(static function ($value) {
            return \is_string($value);
        });
    }

最后总结

使Laravel 在不同的环境自动加载不同的环境配置其实很简单, 在命令行环境时, 我们只要在后面加选项 --env=* 就可以使 laravel 加载不同的环境配置文件。比如

php artisan horizon --env=local

在web环境下我们只要修改 $_ENV 或者 $_SERVERAPP_ENV 的值就可以使 laravel 加载不同的环境配置文件。

使Laravel 在不同的环境自动加载不同的环境配置

使Laravel 在不同的环境自动加载不同的环境配置

本来是要写别的, 结果发现一台电脑只能保留一篇草稿,被迫补完作业。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 5
陈先生

关于 SetEnv 其实有很多方案

  1. 如果使用 Docker,可以直接使用 docker-compose.yml 文件来完成声明定义
  2. 如果是宿主机直装,可以使用 env 命令来完成
  3. 如果你看过 Sail 的代码,也是一个新的方案。
10个月前 评论
徵羽宫 (楼主) 10个月前
徵羽宫 (楼主) 10个月前
陈先生 (作者) 10个月前
徵羽宫 (楼主) 10个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!