Laravel で `php artisan config:cache` すると `.env` と `.env.[APP_ENV]` の2つから環境変数が読み込みされる事がある

# 概要

Laravel はパフォーマンスチューニングの一環として `php artisan config:cache` (`php artisan optimize`) で設定キャッシュを生成するかと思います。

このとき、環境変数 `APP_ENV` が設定されていなければ `.env` から環境変数を読み込んでから設定キャッシュが生成される動作を期待すると思います。

しかし、私は `.env` で `APP_ENV` を設定していたら、`.env.[APP_ENV]` も読み込まれて設定キャッシュが生成される経験をしました。

## 背景

あるプロジェクトで動作中の Laravel v7 アプリケーションの動作確認用の環境（仮想サーバークローン）を用意して
データベースなどの接続先も動作確認用の環境にセットアップすることになりました。

本番環境からコピーされた `.env` を元に動作確認用の `.env` を作ることになりましたが、
`diff` で設定変更箇所を簡単に確認したいという考えで `.env` をコピーして `.env.production` を作成しました。

その後 `php artisan config:cache` してアプリケーションにアクセスすると、データベース接続エラーが発生。

ログを見ると何故か本番環境のデータベースにアクセスしようとしていました（ネットワークが異なるので接続エラーで失敗していた）。

## 切り分け

設定キャッシュを確認すると、書き換えたはずのデータベース接続情報が本番環境のものになっていることを確認しました。

その設定は `.env.production` にしか存在しないはずなので、設定キャッシュの上書きに失敗したのかと思い
キャッシュ削除、キャッシュ生成などを繰り返すと `.env.production` が読み込まれたり、読み込まれたりすることを発見。

Laravel v7 はバージョンが古いから、切り分けのために Laravel v12 のプロジェクトを一から作り出して `.env` と `.env.production` をコピーして設定をキャッシュすると、
やはり `.env.production` も読み込みされていました。

Laravel の動作がおかしいのではと思いソースコードを調査した結果、 Laravel の中では環境変数と設定データの生存期間が異なることがわかりました。

* `.env` などから読み込んだ環境変数は `Illuminate\Support\Env` で `static` 宣言されている変数で管理されている（ほぼグローバル変数）
* 設定データは Laravel アプリケーション `Illuminate\Foundation\Application` （以降 `Application` 表記します）オブジェクトごとに管理されている

`php artisan config:cache` が実行されると `Illuminate\Foundation\Console\ConfigCacheCommand` （以降 `ConfigCacheCommand` のみ表記します）が実行されるのですが、
`ConfigCacheCommand` を実行する前に `Application` が生成されています（以降「コマンド実行 `Application`」とします）。
しかし、 `ConfigCacheCommand` を実行する時にはもう一つ `Application` を生成しており（以降「設定キャッシュ `Application`」とします）、
こちらの `Application` が管理している設定データがキャッシュされていました。

:::note info
「コマンド実行 `Application`」と「設定キャッシュ `Application`」の動作は設定キャッシュ有無によって変化しますが、
ここで開設する動作は設定キャッシュが無いときの動作です。
:::

「コマンド実行 `Application`」は生成されると環境変数を `.env` から読み込み、設定データも構築します。

読み込まれた環境変数は前述の通り、事実上のグローバル変数に格納されることになります。
この時点の環境変数を元に生成された設定データは「コマンド実行 `Application`」だけが参照します。

`ConfigCacheCommand` は設定データのキャッシュを生成するのですが、
既に生成されている「コマンド実行 `Application`」の設定データはキャッシュに使いません。

「設定キャッシュ `Application`」を新たに生成し、このオブジェクトの設定データをキャッシュします: https://github.com/laravel/framework/blob/v12.16.0/src/Illuminate/Foundation/Console/ConfigCacheCommand.php#L78-L92

問題は「設定キャッシュ `Application`」が生成された時点で `.env` に設定されていた `APP_ENV` 環境変数の値を参照できる状態にあるということです。

設定キャッシュが無いので「設定キャッシュ `Application`」は設定データを一から構築しようとしますが、
`APP_ENV` 環境変数がセットされているので `.env.[APP_ENV]` があれば
そちらから設定ファイルの読み込みを行います。

`.env.[APP_ENV]` から読み込まれた環境変数は事実上のグローバル変数に上書きされる形になるので
「設定キャッシュ `Application`」が参照する設定データは `.env` と `.env.[APP_ENV]` の2つの環境設定ファイルを元に構築されてしまいます。

そして、読み込み順序の都合上 `.env.[APP_ENV]` の値が優先されるので、私が遭遇したような問題が発生することがわかりました。

## Laravel への報告

2つの `.env` ファイルが読み込みされる原因が分かったので、 Laravel に Issue として報告しました。

* [Configuration cache is generated with \`\.env\` and \`\.env\.\[APP\_ENV\]\` · Issue \#55895 · laravel/framework](https://github.com/laravel/framework/issues/55895)

少なくとも Laravel v7 から存在している動作なので、ある程度のやり取りが発生するかと思っていましたが
Issue にも記載した[次のドキュメント](https://github.com/laravel/docs/blob/8c05067e5f899d150e0c91e69dea289b7ea1407b/configuration.md#additional-environment-files)で明確に説明されているということでクローズされました。

> Laravel determines if an **APP_ENV environment variable has been externally provided** or if **the --env CLI argument has been specified**

クローズはされましたが「Laravel が `APP_ENV` 環境変数が外部から提供されたとき」と記載されているので
`.env` に記載の環境変数を Laravel がロードしているのだから、これは該当しないのでは無いかと思い
返信をくださった方にメンションしましたが数日返信も無かったので
Laravel フレームワークでこの動作が変更される事は無さそうです。

## 対策

Laravel フレームワークで動作が変更される事は無いようなので、
「Laravel を使用する際は迂闊に `.env.[APP_ENV]` と名前が一致するファイルを生成しない」というルールを周知するしかなさそうです。