PHP で実装するコンフィグツール

ソースコードには含めたくない設定内容を外部ファイルから読み込みたい、というケースはあらゆるアプリケーションで発生します。
今までも、各種 PHP スクリプトで活用できる Config ツールを書いてきており、その内容は単一の config.json ファイルを読み込み、すべての設定をスタティックで保持し、値もスタティック関数で読み込む、という単純なものでした。

それらは今までのアプリケーションによく適応できましたが、アプリケーション規模が大きくなるにつれ、単一の設定ファイルだけのサポートでは見通しが悪くなってきました。

そこで、以下の様なコンフィグツールを設計してみます。

  • 複数の設定ファイルは名前で区別して連想配列に保持
    • 指定されたディレクトリに存在する指定された拡張子のコンフィグをロードする。
    • 設定ファイルを区別する名前はファイル名の拡張子を除いたファイル名とする。
    • サブディレクトリ下の設定ファイル読み込みはサポートしない。
  • 名前単位でコンフィグインスタンスを取得することが可能
  • インスタンスではプロパティのみで値を取得
  • DI などは必要とせず、クラス自体をサービスロケーターとして利用する。

<?php
namespace Wws\Config;

use Symfony\Component\Yaml\Yaml;
use Yosymfony\Toml\Toml;

class Config
{
    private static $instances = [];

    public static function loadDir(string $dir): void
    {
        $files = scandir($dir);
        foreach ($files as $file) {
            if (in_array(['.', '..'], $file)) continue;
            $ns   = pathinfo($file, PATHINFO_FILENAME);
            $path = "{$dir}/{$file}";
            Config::load($ns, $path);
        }
    }

    public static function load(string $ns, string $path): void
    {
        $instance = new static();
        $instance->loadFile($path);
        Config::setInstance($ns, $instance);
    }

    public static function setInstance(string $ns, Config $instance): void
    {
        self::$instances[$ns] = $instance;
    }

    public static function getInstance(string $ns, Config $instance): Config
    {
        return self::$instances[$ns];
    }

    const INI  = 'ini';
    const YML  = 'yml';
    const YAML = 'yaml';
    const JSON = 'json';
    const TOML = 'toml';
    
    private $_data = null;
    
    public function loadFile(string $path): void
    {
        if (!file_exists($path)) {
            throw new \InvalidArgumentException($path . ' not found. Please specify existing file.');
        }
        if (!$file = file_get_contents($path)) {
            throw new \InvalidArgumentException($path . ' could not read. Please confirm access rights.');
        };
        if (self::YAML === substr($path, -1 * strlen(self::YAML)) || $type == self::YAML) {
            $this->_data = Yaml::parse($file);
        } elseif (self::YML === substr($path, -1 * strlen(self::YML)) || $type == self::YML) {
            $this->_data = Yaml::parse($file);
        } elseif (self::JSON === substr($path,-1 * strlen(self::JSON)) || $type == self::JSON) {
            $this->_data = json_decode($file, true);
        } elseif (self::INI === substr($path,-1 * strlen(self::INI)) || $type == self::INI) {
            $this->_data = Toml::Parse($file);
        } elseif (self::TOML === substr($path,-1 * strlen(self::TOML)) || $type == self::TOML) {
            $this->_data = Toml::Parse($file);
        } else {
            throw new \InvalidArgumentException('Please specify valid config file type. .yaml .json .toml');
        }
    }

    public function setValue(string $path, $obj, string $delimiter='.')
    {
        $nodes = explode($delimiter, $path);
        $value = &$this->_data;
        for ($i=0; $i < count($nodes); $i++) {
            $node = $nodes[$i];
            if ($i == count($nodes) - 1) {
                if (preg_match('/(?P<prop>\w+)\[(?P<idx>\d*)\]/', $node, $matches)) {
                    $prop = $matches['prop'];
                    if (!array_key_exists($prop, $value))
                        $value[$prop] = [];
                    if ('' === $matches['idx']) {
                        $value[$prop][] = $obj;
                    } else {
                        $idx = (int)$matches['idx'];
                        $value[$prop][$idx] = $obj;
                    }
                } else {
                    $value[$node] = $obj;
                }
            } else {
                if (preg_match('/(?P<prop>\w+)\[(?P<idx>\d+)\]/', $node, $matches)) {
                    $prop = $matches['prop'];
                    if (!array_key_exists($prop, $value))
                        $value[$prop] = [];
                    if ('' === $matches['idx']) {
                        $len = count($value[$prop]);
                        $value[$prop][] = [];
                        $value = &$value[$prop][$len];
                    } else {
                        $idx = (int)$matches['idx'];
                        $value = &$value[$prop][$idx];
                    }
                } else {
                    $value = &$value[$node];
                }
            }
        }
    }

    public function hasValue(?string $path=null, string $delimiter='.')
    {
        if (is_null($path)) return true;
        try {
            $this->getValue($path, $delimiter);
            return true;
        } catch (\OutOfRangeException $e) {
            return false;
        }
    }

    public function getValue(?string $path=null, string $delimiter='.')
    {
        if (is_null($path)) return $this->_data;

        $nodes = explode($delimiter, $path);
        $value = $this->_data;

        foreach ($nodes as $node) {
            if ($node === '') continue;
            if (preg_match('/(?P<prop>\w+)\[(?P<idx>\d*)\]/', $node, $matches)) {
                $prop = $matches['prop'];
                $idx  = (int)$matches['idx'];
                if (!array_key_exists($prop, $value) || !array_key_exists($idx, $value[$prop])) {
                    throw new \OutOfRangeException('The config has no value for '.$path);
                }
                $value = $value[$prop][$idx];
            } else {
                if (!array_key_exists($node, $value)) {
                    throw new \OutOfRangeException('The config has no value for '.$path);
                }
                $value = $value[$node];
            }
        }
        return $value;
    }
}

  • devops/php/config_util.txt
  • 最終更新: 2021/10/18 19:49
  • by admin