勉強日記

チラ裏

【Laravel】configをテストするなどした


背景

  • /config/hoge.phpの中身を\Config::get('hoge.path.to.value')って感じに読み取れるアレ
  • /config/配下のPHPはarrayを返しさえすれば何でも書ける
    • Facadeとかは動かないけど
  • 業務で、configの中にif-then-elseのロジックを書くことになった
  • ロジックを書くならテストも書きたくなるのが人情というもの

サンプル

<?php

$ret = [];

if (env('APP_ENV') === 'local') {
    $ret['some_key'] = 'some value for local';
} elseif (env('APP_ENV') === 'staging') {
    ...
}
...

return $ret;
  • これだけなら「.env.localとか.env.stagingとか作れや」という話で終わり
  • 今回は「envをいっぱい作りたくない・envにいっぱい書きたくない」という要望につきこうなった
    • envの中で環境により変えるのは最低限APP_ENVだけで済ます

テスト

<?php

...

class ConfigConditionalTest extends TestCase
{
    // ----------------------------------------
    // fixtures
    // ----------------------------------------

    /** 
     * phpunit.xmlのAPP_ENV設定値を退避するやつ
     * @var string 
     */
    private static $appEnv;

    /** 
     * phpunit.xmlのAPP_ENV設定値を退避する
     */
    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass();
        self::$appEnv = env('APP_ENV');
    }
    
    /**
     * APP_ENVを復元する
     */
    public function tearDown(): void
    {
        $this->setAppEnv(self::$appEnv);
    }
    
    /**
     * APP_ENVを設定するヘルパ
     * @var string $appEnv
     */
    private function setAppEnv(string $appEnv): void
    {
        putenv("APP_ENV=${appEnv}");
        
        // 環境変数の変更をconfigに反映する
        $this->refreshApplication();
    }
    
    // ----------------------------------------
    // cases
    // ----------------------------------------
    
    /**
     * @test
     */
    public function some_keyはlocal環境にて所定の値である(): void
    {
        $this->setAppEnv('local');
        $this->assertSame(
            'some value for local',
            \Config::get('const.some_key')
        );
    }
}
  • 思い出しつつ改変しながら書いた。動作未確認
  • 下記3点がミソ
  • これで安心してconfigにロジックを書ける

そもそもアンチパターンなのでは?

某氏による指摘。ありがとうございます:

configであればそもそも環境変数を参照するようにしておいて.envを変えれば済むわけで。
envによる分岐をするならサービスプロバイダの中がいいかと

そもそもconfigにsetする必要があるのかな?

特定のconfigの値が決まったときにもう一つのconfigの値が自動的に決まるような制約がある場合であればFactoryで整合性を保証するという手もあるけど。
configにsetし直すというのであれば少々まどろっこしいな

  • 今回のconfigを振り返ってみる
<?php

$ret = [];

if (env('APP_ENV') === 'local') {
    $ret['some_key'] = 'some value for local';
} elseif (env('APP_ENV') === 'staging') {
    ...
}
...

return $ret;
<?php

abstract class ConstRepository
{
    abstract public function getSomeValue(): string;
    abstract public function getAnotherValue(): string;
    ...
}

class LocalConstRepository extends ConstRepository
{
    public function getSomeValue(): string
    {
        return 'some value for local'
    }
    
    public function getAnotherValue(): string
    {
        return 'another value for local'
    }
    ...
}

class StagingConstRepository extends ConstRepository
{
...
  • で、いつもどおりサービスプロバイダの中でbindする
  • その際にAPP_ENVにより分岐する
    • 同じ分岐を何度も書かなくて済む
<?php

...

    if (env('APP_ENV') === 'local') {
        app()->bind(ConstRepository::class, LocalConstRepository::class);
    } elseif (env('APP_ENV') === 'staging') {
        app()->bind(ConstRepository::class, StagingConstRepository::class);
    } ...
  • \Config::get('some_key')を読んでいたクライアントコードはConstRepository@getSomeValue()を呼び出すようにすればOK