caseyamcl/configula

简单但通用的PHP配置加载器

v4.3.0 2024-02-03 14:05 UTC

README

Configula 是一个适用于 PHP 7.3+ 的配置库。

Latest Version on Packagist Software License Github Build Code coverage PHPStan Level 8 Total Downloads

当您需要从文件系统、环境和其他来源加载配置时,请使用此库。它在 PHP 中实现了不可变对象作为配置值。它是一个框架无关的工具,可以轻松地用于任何 PHP 应用程序。

特性

  • 从多种来源加载配置
    • .php.ini.json.yml 配置文件类型中加载值
    • 使用 DotEnv 库(vlucasSymfony)从环境变量和 .env 文件中加载值
    • 轻松编写自己的加载器以支持其他文件类型和来源
  • 从多个来源(例如数组、文件、环境等)级联/深度合并值
  • 可选地与 Symfony 配置组件 结合使用以验证配置值并/或将它们缓存
  • 创建不可变对象以访问应用程序中的配置值
    • 数组访问(只读)
    • get(val)has(val)hasValue(val) 方法
    • 魔术方法(__get(val)__isset(val)__invoke(val)
    • 实现 TraversableCountable 接口
  • 提供简单的点访问以访问嵌套值(例如 $config->get('application.sitename.prefix');
  • 代码质量标准:PSR-12,近完善的单元测试覆盖率

安装

composer require caseyamcl/configula

需要 PHP v7.1、7.2 或 Symfony v3 兼容性吗?

Configula v4.x 与 PHP v7.3+ 或 v8.0+ 兼容。如果您需要 PHP 7.1、7.2 兼容性,请指导 Composer 使用此库的 3.x 版本而不是当前版本

composer require caseyamcl/configula:^3.1

需要 PHP v5.* 兼容性吗?

Configula 版本 2.x 与 PHP v5.3+ 兼容。如果您需要 PHP 5.x 兼容性,请指导 Composer 使用此库的 2.x 版本而不是当前版本

composer require caseyamcl/configula:^2.4

升级?

有关从版本 2.x、3.x 升级到 v4 的说明,请参阅 UPGRADE.md

加载配置

您可以使用 Configula\ConfigFactory 从文件、环境或其他来源加载配置

use Configula\ConfigFactory as Config;

// Load all .yml, .php, .json, and .ini files from directory (recursive)
// Supports '.local' and '.dist' modifiers to load config in correct order
$config = Config::loadPath('/path/to/config/files', ['optional' => 'defaults', ...]);

// Load all .yml, .php, .json, and .in files from directory (non-recursive)
// Supports '.local' and '.dist' modifiers to load config in correct order
$config = Config::loadSingleDirectory('/path/to/config/files', ['optional' => 'defaults', ...]);

// Load from array
$config = Config::fromArray(['some' => 'values']);

// Chain loaders -- performs deep merge
$config = Config::fromArray(['some' => 'values'])
    ->merge(Config::loadPath('/some/path'))
    ->merge(Config::loadEnv('MY_APP'));

或者,如果您正在加载数组,您可以直接实例化 Configula\ConfigValues

$config = new Configula\ConfigValues(['array' => 'values']);

或者,您可以手动调用 Configula\Loader 命名空间中的任何加载器

$config = (new Configula\Loader\FileListLoader(['file-1.yml', 'file-2.json']))->load();

访问值

Configula\ConfigValues 对象提供了几种访问配置值的方式

// get method - throws exception if value does not exist
$config->get('some_value');

// get method with default - returns default if value does not exist
$config->get('some_value', 'default');

// find method - returns NULL if value does not exist
$config->find('some_value');
 
// has method - returns TRUE or FALSE
$config->has('some_value');
  
// hasValue method - returns TRUE if value exists and is not empty (NULL, [], "")
$config->hasValue('some_value');   

使用点表示法访问值

Configula 支持通过点表示法访问值(例如 some.nested.var

// Here is a nested array:
$values = [
    'debug' => true,
    'db' => [
        'platform' => 'mysql',
        'credentials' => [
            'username' => 'some',
            'password' => 'thing'
        ]
    ],
];

// Load it into Configula
$config = new \Configula\ConfigValues($values);

// Access top-level item
$values->get('debug'); // bool; TRUE

// Access nested item
$values->get('db.platform'); // string; 'mysql'

// Access deeply nested item
$values->get('db.credentials.username'); // string: 'some'

// Get item as array
$values->get('db'); // array ['platform' => 'mysql', 'credentials' => ...]

// has/hasValue work too
$values->has('db.credentials.key'); // false
$values->hasValue('db.credentials.key'); // false

通过 __get()__isset() 访问配置设置的属性

// Access configuration values
$config = Config::loadPath('/path/to/config/files');

// Throws exception if value does not exist
$some_value = $config->some_key;

// Returns TRUE or FALSE
isset($config->some_key);

迭代器和计数访问您的配置设置

// Basic iteration
foreach ($config as $item => $value) {
    echo "<li>{$item} is {$value}</li>";
}

// Count
count($config); /* or */ $config->count();

通过 __invoke() 访问配置设置的调用

// Throws exception if value does not exist
$value = $config('some_value'); 

// Returns default value if value does not exist
$value = $config('some_value', 'default');

数组访问您的配置设置

// Throws exception if value does not exist
$some_value = $config['some_key'];    

// Returns TRUE or FALSE
$exists = isset($config['some_key']); 

// Not allowed; always throws exception (config is immutable)
$config['some_key'] = 'foobar'; // Configula\Exception\ConfigLogicException
unset($config['some_key']);     // Configula\Exception\ConfigLogicException

合并配置

由于 Configula\ConfigValues 是一个不可变对象,因此一旦设置,您就不能修改配置。但是,您可以使用 mergemergeValues 方法合并值并获取对象的新副本

use Configula\ConfigValues;

$config = new ConfigValues(['foo' => 'bar', 'baz' => 'biz']);

// Merge configuration using merge()
$newConfig = $config->merge(new ConfigValues(['baz' => 'buzz', 'cad' => 'cuzz']));

// For convenience, you can pass in an array using mergeValues()
$newConfig = $config->mergeValues(['baz' => 'buzz', 'cad' => ['some' => 'thing']]);

Configula 执行深度合并。遍历嵌套数组,最后一个值始终占优。

请注意,Configula 不对嵌套对象进行深度合并,只对数组进行合并。

迭代器和计数

内置的 ConfigValues::getIterator()ConfigValues::count() 方法在迭代或计数时会展开嵌套值

// Here is a nested array
$config = new Configula\ConfigValues([
    'debug' => true,
    'db' => [
        'platform' => 'mysql',
        'credentials' => [
            'username' => 'some',
            'password' => 'thing'
        ]
    ],
]);

// ---------------------

foreach ($config as $path => $value) {
    echo "\n" . $path . ": " . $value;
}

// Output:
//
// debug: 1
// db.platform: mysql
// db.credentials.username: some
// db.credentials.password: thing
// 

echo count($config);

// Output: 4

如果您只想迭代配置中的顶层项目,可以使用 getArrayCopy() 方法

foreach ($config->getArrayCopy() as $path => $value) {
    echo "\n" . $path . ": " . $value;
}

// Output:
//
// debug: 1
// db: Array
//

使用文件夹加载器 - Config文件夹布局

Configula中的文件夹加载器将加载以下扩展名的文件(您可以添加自己的自定义加载器;请参见下文)

  • php - Configula 将在该文件中查找名为 $config 的数组。
  • json - 使用内置的 PHP json_decode() 函数
  • yamlyml - 使用 Symfony YAML 解析器
  • ini - 使用内置的 PHP parse_ini_file() 函数

ConfigFactory::loadPath($path) 方法将递归地遍历配置路径中的目录。

ConfigFactory::loadSingleDirectory($path) 方法将以非递归方式加载单个目录中的配置。

本地配置文件

在某些情况下,您可能希望拥有本地配置文件来覆盖默认配置文件。有两种方法可以做到这一点

  1. 将默认配置文件扩展名前缀为 .dist(例如 config.dist.yml),并将本地配置文件命名为正常名称:config.yml
  2. 将默认配置文件命名为正常名称(例如 config.yml)并将 .local 前缀添加到本地配置文件的扩展名:config.local.yml

两种方法都可以工作,您甚至可以将方法结合使用。文件迭代器将始终按以下顺序合并文件

  • FILENAME.dist.EXT
  • FILENAME.EXT
  • FILENAME.local.EXT

如果您想将本地配置文件排除在版本控制之外,这很有用。选择一种范式,只需将以下内容添加到您的 .gitignore

# If keeping .dist files...
[CONFIGDIR]/*
[!CONFIGDIR]/*.dist.*

# or, if ignoring .local files...
[CONFIGDIR]/*.local.*

示例

考虑以下目录布局...

/my/app/config
 ├config.php
 ├config.dist.php
 └/subfolder
  ├database.yml
  └database.dist.yml	

如果您使用 ConfigFactory::loadPath('/my/app/config'),文件将根据其扩展名进行解析,值将按以下顺序合并(列表中较晚的文件中的值将覆盖较早的值)

- /config.dist.php
- /subfolder/database.dist.yml
- /config.php
- /subfolder/database.yml

从环境变量加载

配置通常以以下两种方式存储在环境中

  1. 作为多个环境变量(可能由 phpDotEnv 或 Symfony dotEnv 加载,或由 Heroku/Kubernetes 等.公开)。
  2. 作为一个具有 JSON 编码值的单个环境变量,它公开了整个配置树。

Configula 支持这两种方式。您还可以为不同的环境编写自己的加载器。

加载多个环境变量

Configula 支持使用 getenv() 加载环境变量作为配置值。这是 12 Factor App 的一种做法。

此加载器的常见用例包括

  1. 将系统环境作为配置值加载
  2. 使用 phpDotEnvSymfony dotEnv 加载值
  3. 访问云提供商(Kubernetes、Docker、Heroku 等)注入到环境中的值

默认环境变量加载

默认行为是直接加载配置值

$config = ConfigFactory::loadEnv();

结果

MYAPP_MYSQL_USERNAME="..."   --> becomes --> $config->get('MYAPP_MYSQL_USERNAME')
MYAPP_MYSQL_PASSWORD="..."   --> becomes --> $config->get('MYAPP_MYSQL_PASSWORD')
MYAPP_MYSQL_HOST_PORT="..."  --> becomes --> $config->get('MYAPP_MYSQL_HOST_PORT')
MYAPP_MYSQL_HOST_NAME="..."  --> becomes --> $config->get('MYAPP_MYSQL_HOST_NAME')
SERVER_NAME="..."            --> becomes --> $config->get('SERVER_NAME')
etc..

只加载带有前缀的环境变量

您可以加载 带有特定前缀的环境变量。Configula 在加载配置时会删除前缀

$config = ConfigFactory::loadEnv('MYAPP_');

结果

MYAPP_MYSQL_USERNAME="..."   --> becomes --> $config->get('MYSQL_USERNAME')
MYAPP_MYSQL_PASSWORD="..."   --> becomes --> $config->get('MYSQL_PASSWORD')
MYAPP_MYSQL_HOST_PORT="..."  --> becomes --> $config->get('MYSQL_HOST_PORT')
MYAPP_MYSQL_HOST_NAME="..."  --> becomes --> $config->get('MYSQL_HOST_NAME')
SERVER_NAME="..."            --> ignored
etc..

将环境变量转换为嵌套配置

您可以通过传递分隔符来将扁平列表转换为嵌套配置值

$config = ConfigFactory::loadEnv('MYAPP', '_');

结果

MYAPP_MYSQL_USERNAME="..."   --> becomes --> $config->get('MYSQL.USERNAME')
MYAPP_MYSQL_PASSWORD="..."   --> becomes --> $config->get('MYSQL.PASSWORD')
MYAPP_MYSQL_HOST_PORT="..."  --> becomes --> $config->get('MYSQL.HOST.PORT')
MYAPP_MYSQL_HOST_NAME="..."  --> becomes --> $config->get('MYSQL.HOST.NAME')

这使得您可以以数组的形式访问嵌套值

$config = ConfigFactory::loadEnv('MY_APP', '_');
$dbConfig = $config->get('mysql.host');

// $dbConfig: ['host' => '...', 'port' => '...']

将环境变量转换为小写

您可以通过将 TRUE 作为最后一个参数传递来将所有值转换为小写

$config = ConfigFactory::loadEnv('MYAPP_', '_', true);

结果

MYAPP_MYSQL_USERNAME="..."   --> becomes --> $config->get('mysql.username')
MYAPP_MYSQL_PASSWORD="..."   --> becomes --> $config->get('mysql.password')
MYAPP_MYSQL_HOST_PORT="..."  --> becomes --> $config->get('mysql.host.port')
MYAPP_MYSQL_HOST_NAME="..."  --> becomes --> $config->get('mysql.host.name')

使用正则表达式模式加载环境变量

不用前缀,您可以通过传递正则表达式字符串来限制返回的值

$config = ConfigFactory::LoadEnvRegex('/.+_MYAPP_.+/', '_', true);

结果

MYAPP_MYSQL_USERNAME="..."   --> becomes --> $config->get('myapp.mysql.username')
MYAPP_MYSQL_PASSWORD="..."   --> becomes --> $config->get('myapp.mysql.password')
MYAPP_MYSQL_HOST_PORT="..."  --> becomes --> $config->get('myapp.mysql.host.port')
EMAIL_MYAPP_SERVER="..."     --> becomes --> $config->get('email.myapp.server')
SERVER_NAME="..."            --> ignored

加载单个JSON编码的环境变量

使用Configula\Loader\JsonEnvLoader来加载JSON环境变量

MY_ENV_VAR = '{"foo: "bar", "baz": "biz"}'
use Configula\Loader\JsonEnvLoader;

$values = (new JsonEnvLoader('MY_ENV_VAR'))->load();

echo $values->foo;
echo $values->get('foo'); // "bar"

从多个加载器中加载

您可以使用Configula\ConfigFactory::loadMultiple()从多个来源加载并合并结果。此方法接受一个迭代器,其中每个值都是以下之一

  • Configula\ConfigLoader\ConfigLoaderInterface的实例
  • 配置值数组
  • 字符串或SplFileInfo的实例,它是一个指向文件或目录的路径

迭代器中的任何其他值将触发一个\InvalidArgument异常

use Configula\ConfigFactory as Config;
use Configula\Loader;

$config = Config::loadMultiple([
    new Loader\EnvLoader('My_APP'),                 // Instance of LoaderInterface
    ['some' => 'values'],                           // Array of config vaules
    '/path/to/some/file.yml',                       // Path to file (must exist)
    new \SplFileInfo('/path/to/another/file.json')  // SplFileInfo
]);

// Alternatively, you can pass an iterator of `Configula\ConfigLoaderInterface` instances to
// `Configula\Loader\CascadingConfigLoader`.

错误处理

所有异常都扩展自Configula\Exception\ConfigException。您可以通过捕获此异常来捕获在加载和读取配置值期间发生的特定类型的Configula错误。

加载异常

  • 当加载配置时发生错误时,会抛出ConfigLoaderException
  • 当所需的配置文件或路径缺失时,会抛出ConfigFileNotFoundException。通常在将$required构造函数参数设置为true时,由FileLoader加载器抛出。
  • DecidingFileLoader抛出UnmappedFileExtensionException,用于具有未识别扩展名的文件。

注意: Configula 不会捕获非Configula异常并将它们转换为Configula异常。如果您想捕获加载配置时可能发生的所有错误,请在配置加载代码周围使用try...catch (\Throwable $e)

配置值异常

  • 尝试引用一个不存在的配置值名称且未指定默认值时,会抛出ConfigValueNotFoundException
  • 尝试通过数组更改配置时,会抛出ConfigLogicException
  • InvalidConfigValueException不是直接从任何Configula类中抛出的,但它作为实现库的选项提供(请参阅下面的“扩展ConfigValues类”)。
// These throw a ConfigValueNotFoundException
$config->get('non_existent_value');
$config['non_existent_value'];
$config->non_existent_value;

// This will not throw an exception, but instead return NULL
$config->find('non_existent_value');

// This will not throw an exception, but instead return 'default'
$config->get('non_existent_value', 'default');

扩展ConfigValues类(用于IDE类型提示)

虽然直接使用Configula\ConfigValues类是完全可能的,但您可能还希望提供一些特定于应用程序的方法来加载配置值。这可以提供更好的开发者体验。

use Configula\ConfigValues;
use Configula\Exception\InvalidConfigValueException;

class AppConfig extends ConfigValues
{
    /**
     * Is the app running in development mode?
     *
     * @return bool
     */
    public function isDevMode(): bool
    {
        // Get the value or assume false
        return (bool) $this->get('devmode', false);
    }
    
    /**
     * Get the encryption key (as 32-character alphanumeric string)
     *
     * @return string
     */
    public function getEncryptionKey(): string
    {
        // If the value doesn't exist, a `ConfigValueNotFoundException` is thrown
        $key = $this->get('encryption_key');
        
        // Let's do a little validation...
        if (strlen($key) != 32) {
            throw new InvalidConfigValueException('Encryption key must be 32 characters');
        }
        
        return $key;
    }
}

注意: 注意上面的示例使用了InvalidConfigValueException,这是Configula为这种用途包含的。

您可以使用以下方式使用自定义的AppConfig

use Configula\ConfigFactory;

// Build it
$config = AppConfig::fromConfigValues(ConfigFactory::loadPath('/some/path'));

// Use it (and enjoy the type-hinting in your IDE)
$config->getEncryptionKey();
$config->isDevMode();
// etc...

使用Symfony Config定义配置架构

在某些情况下,您可能希望严格控制允许的配置值,并在加载配置时验证这些值。Symfony Config组件提供了实现这一点的出色机制。

首先,在您的应用程序中包含Symfony Config组件库

composer require symfony/config

然后,创建一个提供您的配置树类的类。有关您可以在配置树中执行的所有酷炫操作,请参阅Symfony 文档

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class ConfigTree implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->getRootNode();
        
        $rootNode->children()
            ->boolean('devmode')->defaultValue(false)->end()
            ->scalarNode('encryption_key')->isRequired()->cannotBeEmpty()->end()
            ->arrayNode('db')
                ->children()
                    ->scalarNode('host')->cannotBeEmpty()->defaultValue('localhost')->end()
                    ->integerNode('port')->min(0)->defaultValue(3306)->end()
                    ->scalarNode('driver')->cannotBeEmpty()->defaultValue('mysql')->end()
                    ->scalarNode('dbname')->cannotBeEmpty()->end()
                    ->scalarNode('user')->cannotBeEmpty()->end()
                    ->scalarNode('password')->end()
                ->end()
            ->end() // End DB
        -end();
        
        return $treeBuilder;
    }
}

像往常一样加载您的配置,然后将生成的ConfigValues对象通过Symfony过滤器传递

use Configula\ConfigFactory;
use Configula\Util\SymfonyConfigFilter;

// Setup your config tree, and load your configuration
$configTree = new ConfigTree();
$config = ConfigFactory::loadPath('/path/to/config');

// Validate the configuration by filtering it through the allowed values
// If anything goes wrong here, a Symfony exception will be thrown (not a Configula exception)
$config = SymfonyConfigFilter::filter($configTree, $config);

编写自己的加载器

除了使用内置加载器之外,您还可以编写自己的加载器。有两种方法可以这样做

创建自己的文件加载器

扩展Configula\Loader\AbstractFileLoader来编写自己的加载器,该加载器从文件中读取数据。

use Configula\Loader\AbstractFileLoader;

class MyFileLoader extends AbstractFileLoader
{
        /**
         * Parse file contents
         *
         * @param string $rawFileContents
         * @return array
         */
        protected function parse(string $rawFileContents): array
        {
            // Parse the file contents and return an array of values.
        }
}

使用它

use Configula\ConfigFactory;

// use the factory..
$config = ConfigFactory::load(new MyFileLoader('/path/to/file'));

// ..or don't..
$config = (new MyFileLoader('/path/to/file'))->load();

如果您想使用FolderLoader并自动将新类型映射到文件扩展名,您可以这样操作

use Configula\Loader\FileLoader;
use Configula\Loader\FolderLoader;

// Map my custom file loader to the 'conf' extension type (case-insensitive)
$extensionMap = array_merge(FileLoader::DEFAULT_EXTENSION_MAP, [
    'conf' => MyFileLoader::class
]);

// Now any files encountered in the folder with .conf extension will use my custom file loader
$config = (new FolderLoader('/path/to/folder', true, $extensionMap))->load();

创建自己的自定义加载器

创建自己的Configula\Loader\ConfigLoaderInterface实现,然后您可以从任何地方加载配置

use Configula\Loader\ConfigLoaderInterface;
use Configula\Exception\ConfigLoaderException;
use Configula\ConfigValues;

class MyLoader implements ConfigLoaderInterface
{
    public function load(): ConfigValues
    {
        if (! $arrayOfValues = doWorkToLoadValuesHere()) {
            throw new ConfigLoaderException("Something went wrong..");
        }
        
        return new ConfigValues($arrayOfValues);
    }
}

使用它

use Configula\ConfigFactory;

// use the factory..
$config = ConfigFactory::load(new MyLoader());

// ..or use it directly.
$config = (new MyLoader())->load();

变更日志

有关最近更改的更多信息,请参阅变更日志

测试

$ composer test

贡献

请参阅贡献指南获取详细信息。

鸣谢

许可证

MIT许可证(MIT)。请参阅许可证文件获取更多信息。