【Codeigniter4】.envファイルを環境ごとに読み分けできるようにする

# Codeigniter4のDotEnvは使いにくい

Codeigniter4はDotEnvに対応しているが、LaravelやViteなどのように環境による読み分けができない。
簡単に言えば、以下のような構成が利用できない。

```shell
/
├── app
├── .env           # 共通の環境変数（これだけが適用される） 
├── .env.dev       # 開発環境用
├── .env.stg       # ステージング環境用
└── .env.prod      # 本番環境用
```

デフォルトのまま利用しようとすると、`.env`のみが読み込まれてしまう。

## 複数の.envを読み込むことは（一応）できるが・・・

`public/index.php` の記述を読むと、ここでenvファイルをロードしていることが分かる。
※sparkにも同様に記述がある

``` public/index.php
// Load environment settings from .env files into $_SERVER and $_ENV
require_once SYSTEMPATH . 'Config/DotEnv.php';
(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
```

ここに行を追加して

``` public/index.php
// Load environment settings from .env files into $_SERVER and $_ENV
require_once SYSTEMPATH . 'Config/DotEnv.php';
(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();

// SERVER_ENVIRONMENT は .envに記載しておく
+ (new CodeIgniter\Config\DotEnv(ROOTPATH, '.env.' . getenv('SERVER_ENVIRONMENT')))->load();
```

このようにすれば読み込みは可能だが、 **.envで設定済みの値が上書きできない** というデメリットがある。
なぜならば、`system/Config/DotEnv.php` の記述を見てみると

```system/Config/DotEnv.php
protected function setVariable(string $name, string $value = '')
{
    if (!getenv($name, true)) {
        putenv("{$name}={$value}");
    }

    if (empty($_ENV[$name])) {
        $_ENV[$name] = $value;
    }

    if (empty($_SERVER[$name])) {
        $_SERVER[$name] = $value;
    }
}
```

このように設定済みの値は上書きできない仕様となっているため。
**.env と .env.xxx の値を完全に分離できるのであれば問題はないが、他フレームワークの仕様の頭でいるとトラブル発生の素となる** ので、できれば上書きできるようにしたい。
（特にenv系をgitignoreしている場合 ）

## 継承クラスを作る

### 手順1. `app/Config/DotEnv.php` を作成して既存のクラスの継承クラスを作成する

overwriteメソッドを作成して上書きのロジックを作成する。
コメントに「〇〇の代替」と記載しているが、__construct と parse, setVariable を組み合わせてゴニョゴニョしたもの

```app/Config/DotEnv.php
<?php

namespace App\Config;

/**
 * DotEnvの上書き用クラス
 */
class DotEnv extends \CodeIgniter\Config\DotEnv
{
    /**
     * 設定済みの値を別のファイルで上書きする
     * setVariableだと設定済みの値を上書きできないので、このメソッドを使う
     * 
     * @param string $path .envファイルのパス
     * @param string $file .envファイル名 
     * @return bool 成功したらtrue
     */
    public function overwrite(string $path, string $file = '.env'): bool
    {
        // __constructの代替
        $filepath = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file;

        // parseメソッドの代替
        // $this->pathをfilepathに変更し
        // setVariableを修正したもの

        // We don't want to enforce the presence of a .env file, they should be optional.
        if (!is_file($filepath)) {
            return false;
        }

        // Ensure the file is readable
        if (!is_readable($filepath)) {
            log_message('error', "The .env file is not readable: {$filepath}");
            return false;
        }

        $vars = [];
        $lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

        foreach ($lines as $line) {
            // Is it a comment?
            if (strpos(trim($line), '#') === 0) {
                continue;
            }

            // If there is an equal sign, then we know we are assigning a variable.
            if (strpos($line, '=') !== false) {
                [$name, $value] = $this->normaliseVariable($line);
                $vars[$name]    = $value;

                // 設定済みの値を取得
                $envValue = getenv($name, true);

                // 設定済みの値が存在しない場合は、setVariableで設定する
                if ($envValue === false) {
                    $this->setVariable($name, $value);
                    continue;
                }

                // setVariableの代替
                
                // putenvで上書き
                putenv("{$name}={$value}");

                // 設定済みの値が$envValueと一致する場合は、$_ENV, $_SERVERを更新する
                if ($_ENV[$name] === $envValue) {
                    $_ENV[$name] = $value;
                }
                if ($_SERVER[$name] === $envValue) {
                    $_SERVER[$name] = $value;
                }
            }
        }

        return true;
    }
}
```

### 手順2. index.php, spark の書き換え

双方の該当部分を差し替える。
sparkの方を忘れがちなので注意

```php
// Load environment settings from .env files into $_SERVER and $_ENV
require_once SYSTEMPATH . 'Config/DotEnv.php';
- (new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
+ ($dotEnv = new App\Config\DotEnv(ROOTPATH))->load();
+ $dotEnv->overwrite(ROOTPATH, '.env.' . getenv('SERVER_ENVIRONMENT'));
```


### テスト

```php
// .env     app.name=hoge
// .env.dev app.name=fuga
echo getenv('app.name');  // fuga

// .env     app.name=hoge
// .env.dev 記述なし
echo getenv('app.name');  // hoge

// .env     記述なし
// .env.dev app.name=fuga
echo getenv('app.name');  // fuga

// .env     app.name=hoge
// .env.dev ファイルなし
echo getenv('app.name');  // hoge

// .env     ファイルなし
// .env.dev app.name=fuga
echo getenv('app.name');  // 500エラー（標準の仕様）
```


## あとがき

正直、カスタマイズしないと利用できないのはちょっと・・・とは思う。
**ここまでカスタマイズするなら、envじゃなくてconstants.phpでやってしまった方がお手軽** だし、私も今までそうしていたが、
Codeigniter4とフロントエンドフレームワーク（特にVite）との併用が多くなってきたので同じ仕様でやりたいと思った次第。

また、**本来は.env.xxx を先に読み込んだ後に .env を読み込むという仕様で、上書き出来ないのは正しい挙動なのではないか** とも思う。
なぜなら、Viteでは `vite --mode dev` と、**起動時にモードが指定されているため先に環境別ファイルを読み込むことが可能だから（※）** だ。
※検証はしていない

ちなみに、そういった（予めモードを指定した）使い方であれば **標準のまま（上書きプログラムは不要）で利用可能** なので、一応コードを記載しておく。

### 予めモードを指定して利用する方法

1. .htaccess（またはnginxのconf）に`SERVER_ENVERONMENT`を設定しておく  
（.envに設定したSERVER_ENVIRONMENTの代わり）
2. index.php, sparkの書き換え  
```php
// (new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); の直前に追記する
// SERVER_ENVERONMENT（サーバー環境の確認）
if (!isset($_SERVER['SERVER_ENVERONMENT'])) {
    // CLIの場合、引数をチェック
    // 引数の最後に --production 等を付与すると対象の環境として扱われる
    if (isset($_SERVER['argv']) && isset($_SERVER['argc']) && strpos($_SERVER['argv'][$_SERVER['argc'] - 1], '--') === 0) {
        $_SERVER['argc']--;
        $_SERVER['SERVER_ENVERONMENT'] = substr($_SERVER['argv'][$_SERVER['argc']], 2);
        unset($_SERVER['argv'][$_SERVER['argc']]);
    } else {
        echo 'SET `SERVER_ENVERONMENT` to $_SERVER';
        exit();
    }
}
// ファイルが読み込み可能であれば設定をロードする
if(is_file(ROOTPATH . '.env.' . $_SERVER['SERVER_ENVERONMENT']) && is_readable(ROOTPATH . '.env.' . $_SERVER['SERVER_ENVERONMENT'])) {
    (new CodeIgniter\Config\DotEnv(ROOTPATH, '.env.' . $_SERVER['SERVER_ENVERONMENT']))->load();
}
// .envのロード（既存コード）
(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
```

正直こちらのほうがシンプルだが、サーバー設定をいじれない場合、.htaccess が gitignore対象（ないしはデプロイ対象外）になってしまうので、そこだけ注意が必要。
また、この方法の場合、cliやsparkの実行に `php public/index.php controller method --dev` にように引数の最後にモードを付与する必要があることにも注意。