😲

Laravel で .env ファイルの環境変数が読み込まれなくて困った件

2025/02/27に公開

はじめに

ある時、 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 の環境変数読み込みの仕組みを調査したところ、以下のような優先順位で環境変数が処理されることが分かりました

  1. サーバー環境変数($_SERVER
  2. PHP環境変数($_ENV
  3. .envファイルの変数

docker-compose.yml の environment: で設定した環境変数は、コンテナの OS レベルで設定されるため、.env ファイルよりも常に優先されてしまうのです。

解決方法

なので私は、docker-compose.yml から環境変数を削除しても問題の無い構成であることを確認して、環境変数を削除することにしました。

services:
  app:
  # environment を削除

これにより、.env.testing の設定が正しく反映されるようになりました!

Laravel の環境変数設定の仕組み

これより以下は、今回調査した Laravel の環境変数の読み込みについてまとめています📝

  1. LoadEnvironmentVariables::bootstrap() の呼び出し
  2. Dotenv インスタンスの作成
  3. 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() ですが、これは Loaderload() を呼び出します。
引数の $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() を実行することで、 ImmutableWriterAdapterRepository に渡すことが可能になります。

    // 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