[Symfony] 配列や連想配列などの複雑な形式の値を環境変数として設定する
やりたかったこと
- 単純な文字列だけでなく、配列や連想配列のような複雑な形式の値を環境変数として設定したかった
-
.env
でやろうとすると、設定値をJSON文字列として書く必要があり、エスケープとかが大変だしコメントも書けない - YAMLかPHPで設定を書きたい
.env
でやる方法(しんどい)
1. まずは .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
を使う
という動作にしたくなりました。
.env
を読む方法
3. PHPファイルがあればPHPファイルを、なければ というわけで最後にその方法です。
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
を読む」という動作を実現する方法を解説しました。
どなたかのお役に立てば幸いです!
Discussion