🎻

[Symfony] 配列や連想配列などの複雑な形式の値を環境変数として設定する

2022/01/20に公開約6,500字

やりたかったこと

  • 単純な文字列だけでなく、配列や連想配列のような複雑な形式の値を環境変数として設定したかった
  • .env でやろうとすると、設定値をJSON文字列として書く必要があり、エスケープとかが大変だしコメントも書けない
  • YAMLかPHPで設定を書きたい

1. .env でやる方法(しんどい)

まずは .env でやる方法です。

Symfonyの設定ファイルにおいて環境変数の値を取り扱う方法については

Environment Variable Processors (Symfony Docs)

こちらの公式ドキュメントにすべてが載っています。

目を通してみると分かりますが、今回の用途に使えそうな値の型はJSONぐらいしかありません。なので、.env でやるなら、JSON形式の文字列を値に書く必要があります。

例えば以下のような感じになるでしょう。

# .env

FOO={"a":1,"b":"c","d":[1,2,3]}

その上で、config/services.yaml 等でこの値を利用する際には、

'%env(json:FOO)%'

で読み込むようにすれば、json_decode() した値を取得してくれます。

ちなみに、.env の値に改行を含めたい場合には、

ENV="a
b
c"

のように "" で囲った上で普通に改行文字を含めてしまえばOKです。

参考

今回の例なら

FOO="{
  \"a\": 1,
  \"b\": \"c\",
  \"d\": [
    1,
    2,
    3
  ]
}"

こんな感じで書けます。" のエスケープがちょっと面倒ですね。

あとこれはかなり特殊な例ですが、僕が最近作っていたアプリでは環境変数として preg_replace() に渡す正規表現文字列をキーに持つ連想配列を設定したいという要件がありました。 PHPの連想配列で言うと以下のようなものです。

[
    '/regex pattern1/' => 'replacement1',
    '/regex pattern2/' => 'replacement2',
    '/regex pattern3/' => 'replacement3',
]

これをJSON文字列にして .env に改行入りの値として書こうと思うと、正規表現のレイヤーとJSONのレイヤーの両方でエスケープが必要になり、とてもじゃないけどまともな神経では書けないなと思いました。

2. PHPファイルでやる方法

そこで、.env で頑張るのではなく素直にPHPファイルを設定ファイルとして利用することを考えました。

YAMLが使えればベストだったのですが、Environment Variable Processors (Symfony Docs) を見るとYAMLをパースする機能がないようだったので諦めました。

PHPファイルから環境変数を読み込むには、env(require:FOO) を使います。

<?php
// config/env/FOO.php
return [
    // JSONと違ってコメントも書ける!
    'a' => 1,

    // JSONと違ってコメントも書ける!
    'b' => 'c',

    // JSONと違ってコメントも書ける!
    'd' => [
        1,
        2,
        3,
    ],
];

例えばこんな感じでPHPファイルを作っておいて、

parameters:
  env(FOO_FILE): '%kernel.project_dir%/config/env/FOO.php'
  foo: '%env(require:FOO_FILE)%'

services:
  some_service:
    arguments:
      $foo: '%foo%'

こんな感じで読み込んだ配列をそのまま利用することができます。

さて、これでほぼ解決に思えますが、僕が実際に作っていたアプリだとこんな感じの複雑な設定項目がいくつかあったので、1つのPHPファイルの中で複数の環境変数を設定できるようにしたいと思いました。

<?php
// config/env.php
return [
    'FOO' => [/* 略 */],
    'BAR' => [/* 略 */],
    'BAZ' => [/* 略 */],
];

こんな感じの設定ファイルを作っておいて、env(key:FOO:BAR) を使って指定のキーの値を取り出して利用します。

parameters:
  env(ENV_FILE): '%kernel.project_dir%/config/env.php'
  foo: '%env(key:FOO:require:ENV_FILE)%'
  bar: '%env(key:BAR:require:ENV_FILE)%'
  baz: '%env(key:BAZ:require:ENV_FILE)%'

services:
  some_service:
    arguments:
      $foo: '%foo%'
      $bar: '%bar%'
      $baz: '%baz%'

これで特に問題ないですが、

  foo: '%env(key:FOO:require:ENV_FILE)%'
  bar: '%env(key:BAR:require:ENV_FILE)%'
  baz: '%env(key:BAZ:require:ENV_FILE)%'

この部分で require を重複して実行しているのが少し気になるので、require:ENV_FILE の結果を一旦変数化しておきたいと思いました。

しかしそれをやる場合は少し注意が必要で、require:ENV_FILE の結果をそのあと env() 内で使うためには、パラメータではなく環境変数として変数化しておく必要があります。

そして、環境変数には文字列しか持たせられないので、PHPファイルの中身は一旦JSON文字列として取り出しておいて、使う直前に json: によってパースする、という手順を踏む必要があります。

具体的には、PHPファイルを以下のように json_encode() してJSON文字列として返すように変更します。

<?php
// config/env.php
return json_encode([
    'FOO' => [/* 略 */],
    'BAR' => [/* 略 */],
    'BAZ' => [/* 略 */],
]);

その上で、

parameters:
  env(ENV_FILE): '%kernel.project_dir%/config/env.php'
  env(ENV_JSON): '%env(require:ENV_FILE)%'
  foo: '%env(key:FOO:json:ENV_JSON)%'
  bar: '%env(key:BAR:json:ENV_JSON)%'
  baz: '%env(key:BAZ:json:ENV_JSON)%'

services:
  some_service:
    arguments:
      $foo: '%foo%'
      $bar: '%bar%'
      $baz: '%baz%'

これで require が1回しか走らないようにできました。

さて、長くなってきましたが、実はここでもう一つ問題が残りました。

僕が実際に作っていたアプリだとこれらの設定項目はすべて デフォルトでは省略できる もので、省略しないまでも 場合によっては簡素なJSON文字列を書くだけで済む こともありそうだったので、

  • 複雑な設定を書きたい場合はPHPファイルを使う
  • デフォルトのままでよかったり、ちょっとしか設定を書かない場合はPHPファイルは作らず .env を使う

という動作にしたくなりました。

3. PHPファイルがあればPHPファイルを、なければ .env を読む方法

というわけで最後にその方法です。

env(default:fallback_param:BAR) を使って、PHPファイルがある場合とない場合で読み込む値を切り替えるようにします。

まず、PHPファイルのほうを、return する配列全体だけでなく、各設定項目の値も json_encode()するようにしておきます。ここで返される値を一旦JSON文字列として(パラメータではなく)環境変数に入れておかないと、後から default: に渡すことができないためです。

<?php
// config/env/app.php
return json_encode([
    'FOO' => json_encode([/* 略 */]),
    'BAR' => json_encode([/* 略 */]),
    'BAZ' => json_encode([/* 略 */]),
]);

このファイルは存在したりしなかったりすることを想定します。

一方、.env にも以下のように設定の初期値(JSON文字列)を書いておきます。このファイルはPHPファイルが存在しない場合にのみ使用されます。

FOO={}
BAR={}
BAZ={}

では、config/services.yaml の内容を見てみましょう。こんな感じで書けば意図したとおりの動作が実現できます。

parameters:
  # .env の設定値
  dotenv.FOO_JSON: '%env(FOO)%'
  dotenv.BAR_JSON: '%env(BAR)%'
  dotenv.BAZ_JSON: '%env(BAZ)%'

  # PHPファイルの設定値
  env(ENV_FILE): '%kernel.project_dir%/config/env.php'
  env(ENV_JSON): '%env(require:ENV_FILE)%'
  env(PHP_FOO_JSON): '%env(key:FOO:json:ENV_JSON)%' # この結果がJSON文字列でないと環境変数に入れられないのでPHPファイル側で各項目をjson_encode()する必要があった
  env(PHP_BAR_JSON): '%env(key:BAR:json:ENV_JSON)%' # この結果がJSON文字列でないと環境変数に入れられないのでPHPファイル側で各項目をjson_encode()する必要があった
  env(PHP_BAZ_JSON): '%env(key:BAZ:json:ENV_JSON)%' # この結果がJSON文字列でないと環境変数に入れられないのでPHPファイル側で各項目をjson_encode()する必要があった

  # 最終的に使用される設定値
  foo: '%env(json:default:dotenv.FOO_JSON:PHP_FOO_JSON)%' # PHP_FOO_JSON があればそれを、なければ dotenv.FOO_JSON を採用し、最後に json: でパース
  bar: '%env(json:default:dotenv.BAR_JSON:PHP_BAR_JSON)%' # PHP_BAR_JSON があればそれを、なければ dotenv.BAR_JSON を採用し、最後に json: でパース
  baz: '%env(json:default:dotenv.BAZ_JSON:PHP_BAZ_JSON)%' # PHP_BAZ_JSON があればそれを、なければ dotenv.BAZ_JSON を採用し、最後に json: でパース

services:
  some_service:
    arguments:
      $foo: '%foo%'
      $bar: '%bar%'
      $baz: '%baz%'

一見複雑ですが、よくよく読めば十分に理解できる内容だと思います。

おわりに

というわけで、配列や連想配列のような複雑な形式の値を環境変数として設定したい場合に、「PHPの設定ファイルがあればそちらを読み、なければ .env を読む」という動作を実現する方法を解説しました。

どなたかのお役に立てば幸いです!

GitHubで編集を提案

Discussion

ログインするとコメントできます