Laravel で .env ファイルの環境変数が読み込まれなくて困った件
はじめに
ある時、 Laravel + Docker 環境でテストコードを実行していた際に、.env.testing
で定義した環境変数が正しく反映されず、原因究明に時間を取られてしまいました。
この記事では、その原因と解決方法、そして Laravel の環境変数読み込みの仕組みについて記載します。
概要
発生した問題
docker compose exec app php artisan test
docker を利用しているので上記のようにテストコードを実行したところ、 .env.testing
で以下のように設定していたにもかかわらず
HOGEHOGE=hogehoge
実際には docker-compose.yml で定義した環境変数が使用されて期待した挙動をしてくれないという事態に遭遇しました。
(hogehoge を参照したいのに fugafuga を参照してしまった)
services:
app:
environment:
HOGEHOGE: fugafuga
原因
Laravel の環境変数読み込みの仕組みを調査したところ、以下のような優先順位で環境変数が処理されることが分かりました
- サーバー環境変数(
$_SERVER
) - PHP環境変数(
$_ENV
) -
.env
ファイルの変数
docker-compose.yml の environment:
で設定した環境変数は、コンテナの OS レベルで設定されるため、.env
ファイルよりも常に優先されてしまうのです。
解決方法
なので私は、docker-compose.yml から環境変数を削除しても問題の無い構成であることを確認して、環境変数を削除することにしました。
services:
app:
# environment を削除
これにより、.env.testing
の設定が正しく反映されるようになりました!
Laravel の環境変数設定の仕組み
これより以下は、今回調査した Laravel の環境変数の読み込みについてまとめています📝
-
LoadEnvironmentVariables::bootstrap()
の呼び出し -
Dotenv
インスタンスの作成 -
ImmutableWriter
について
LoadEnvironmentVariables::bootstrap() の呼び出し
環境変数の読み込みは \Dotenv\Dotenv
インスタンスを利用する形になっています
// vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php
public function bootstrap(Application $app)
{
if ($app->configurationIsCached()) {
return;
}
$this->checkForSpecificEnvironmentFile($app);
try {
$this->createDotenv($app)->safeLoad();
} catch (InvalidFileException $e) {
$this->writeErrorAndDie($e);
}
}
実際の処理の順序的には最後に実行される safeLoad()
ですが、これは Loader
の load()
を呼び出します。
引数の $entries
には、 .env
ファイルで定義した環境変数が代入されて来ます。
そしてこの処理の中で、 $repository->set($name, $inner)
で環境変数を読み込むかどうか判断する感じになってますね👀
// vendor/vlucas/phpdotenv/src/Loader/Loader.php
public function load(RepositoryInterface $repository, array $entries)
{
return \array_reduce($entries, static function (array $vars, Entry $entry) use ($repository) {
$name = $entry->getName();
$value = $entry->getValue()->map(static function (Value $value) use ($repository) {
return Resolver::resolve($repository, $value);
});
if ($value->isDefined()) {
$inner = $value->get();
if ($repository->set($name, $inner)) {
return \array_merge($vars, [$name => $inner]);
}
} else {
if ($repository->clear($name)) {
return \array_merge($vars, [$name => null]);
}
}
return $vars;
}, []);
}
Dotenv インスタンスの作成
LoadEnvironmentVariables::createDotenv()
は、 \Dotenv\Dotenv
インスタンスを作成しますが、その際に環境変数リポジトリを第一引数に渡すことで環境変数を読み込めるようになっています
// vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php
protected function createDotenv($app)
{
return Dotenv::create(
Env::getRepository(),
$app->environmentPath(),
$app->environmentFile()
);
}
この、環境変数リポジトリ作成のメソッドの中でおこなわれている $builder->immutable()->make();
が今回の記事のポイントです。
// vendor/laravel/framework/src/Illuminate/Support/Env.php
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;
}
ImmutableWriter について
RepositoryBuilder::make()
の前に immutable()
を実行することで、 ImmutableWriter
を AdapterRepository
に渡すことが可能になります。
// vendor/vlucas/phpdotenv/src/Repository/RepositoryBuilder.php
public function make()
{
$reader = new MultiReader($this->readers);
$writer = new MultiWriter($this->writers);
if ($this->immutable) {
$writer = new ImmutableWriter($writer, $reader);
}
if ($this->allowList !== null) {
$writer = new GuardedWriter($writer, $this->allowList);
}
return new AdapterRepository($reader, $writer);
}
この ImmutableWriter
では以下のように上書きが出来ないような仕組みになっているのですが、
// vendor/vlucas/phpdotenv/src/Repository/Adapter/ImmutableWriter.php
public function write(string $name, string $value)
{
// 筆者注: `$_SERVER` や `$_ENV` で既に定義済みなのかチェックしている
if ($this->isExternallyDefined($name)) {
return false;
}
// 以下省略
}
この write()
は上記に記載した Loader::load()
の中の $repository->set($name, $inner)
で利用されているのです!!
// vendor/vlucas/phpdotenv/src/Repository/AdapterRepository.php
public function set(string $name, string $value)
{
if ('' === $name) {
throw new InvalidArgumentException('Expected name to be a non-empty string.');
}
return $this->writer->write($name, $value);
}
よって、どんなに .env
ファイルを捏ね繰り回しても意味がないということですね💧
まとめ
- Docker 環境では、コンテナレベルの環境変数が
.env
ファイルよりも優先される - この優先順位は Laravel の環境変数読み込みの仕組みによるもの
- 環境変数の管理は慎重に行い、定期的に設定を確認することを推奨
Discussion