├── .cs.php ├── .github └── workflows │ └── build.yml ├── LICENSE ├── README.md ├── composer.json ├── phpcs.xml ├── phpstan.neon └── src └── Configuration.php /.cs.php: -------------------------------------------------------------------------------- 1 | setUsingCache(false) 7 | ->setRiskyAllowed(true) 8 | ->setRules( 9 | [ 10 | '@PSR1' => true, 11 | '@PSR2' => true, 12 | '@Symfony' => true, 13 | 'psr_autoloading' => true, 14 | // custom rules 15 | 'align_multiline_comment' => ['comment_type' => 'phpdocs_only'], // psr-5 16 | 'phpdoc_to_comment' => false, 17 | 'no_superfluous_phpdoc_tags' => false, 18 | 'array_indentation' => true, 19 | 'array_syntax' => ['syntax' => 'short'], 20 | 'cast_spaces' => ['space' => 'none'], 21 | 'concat_space' => ['spacing' => 'one'], 22 | 'compact_nullable_typehint' => true, 23 | 'declare_equal_normalize' => ['space' => 'single'], 24 | 'general_phpdoc_annotation_remove' => [ 25 | 'annotations' => [ 26 | 'author', 27 | 'package', 28 | ], 29 | ], 30 | 'increment_style' => ['style' => 'post'], 31 | 'list_syntax' => ['syntax' => 'short'], 32 | 'echo_tag_syntax' => ['format' => 'long'], 33 | 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], 34 | 'phpdoc_align' => false, 35 | 'phpdoc_no_empty_return' => false, 36 | 'phpdoc_order' => true, // psr-5 37 | 'phpdoc_no_useless_inheritdoc' => false, 38 | 'protected_to_private' => false, 39 | 'yoda_style' => false, 40 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 41 | 'ordered_imports' => [ 42 | 'sort_algorithm' => 'alpha', 43 | 'imports_order' => ['class', 'const', 'function'], 44 | ], 45 | 'single_line_throw' => false, 46 | 'declare_strict_types' => false, 47 | 'blank_line_between_import_groups' => true, 48 | 'fully_qualified_strict_types' => true, 49 | 'no_null_property_initialization' => false, 50 | 'operator_linebreak' => [ 51 | 'only_booleans' => true, 52 | 'position' => 'beginning', 53 | ], 54 | 'global_namespace_import' => [ 55 | 'import_classes' => true, 56 | 'import_constants' => null, 57 | 'import_functions' => null 58 | ] 59 | ] 60 | ) 61 | ->setFinder( 62 | PhpCsFixer\Finder::create() 63 | ->in(__DIR__ . '/src') 64 | ->in(__DIR__ . '/tests') 65 | ->name('*.php') 66 | ->ignoreDotFiles(true) 67 | ->ignoreVCS(true) 68 | ); 69 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | run: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | matrix: 10 | operating-system: [ ubuntu-latest ] 11 | php-versions: [ '8.1', '8.2' ] 12 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v1 17 | 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php-versions }} 22 | extensions: mbstring, intl, zip 23 | coverage: none 24 | 25 | - name: Check PHP Version 26 | run: php -v 27 | 28 | - name: Check Composer Version 29 | run: composer -V 30 | 31 | - name: Check PHP Extensions 32 | run: php -m 33 | 34 | - name: Validate composer.json and composer.lock 35 | run: composer validate 36 | 37 | - name: Install dependencies 38 | run: composer install --prefer-dist --no-progress --no-suggest 39 | 40 | - name: Run test suite 41 | run: composer test:all 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 odan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # selective/config 2 | 3 | A strictly typed configuration component for PHP. Inspired by [Apache Commons Configuration](https://commons.apache.org/proper/commons-configuration/). 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/github/release/selective-php/config.svg)](https://packagist.org/packages/selective/config) 6 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 7 | [![Build Status](https://github.com/selective-php/config/workflows/build/badge.svg)](https://github.com/selective-php/config/actions) 8 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/selective-php/config.svg)](https://scrutinizer-ci.com/g/selective-php/config/code-structure) 9 | [![Quality Score](https://img.shields.io/scrutinizer/quality/g/selective-php/config.svg)](https://scrutinizer-ci.com/g/selective-php/config/?branch=master) 10 | [![Total Downloads](https://img.shields.io/packagist/dt/selective/config.svg)](https://packagist.org/packages/selective/config/stats) 11 | 12 | 13 | ## Requirements 14 | 15 | * PHP 8.1+ 16 | 17 | ## Installation 18 | 19 | ```bash 20 | composer require selective/config 21 | ``` 22 | 23 | ## Theory of Operation 24 | 25 | You can use the `Configuration` to read single values from a multidimensional 26 | array by passing the path to one of the `get{type}()` and `find{type}()` methods. 27 | 28 | Each `get*() / find*()` method takes a default value as second argument. 29 | If the path cannot be found in the original array, the default is used as return value. 30 | 31 | A `get*()` method returns only the declared return type. 32 | If the default value is not given and the element cannot be found, an exception is thrown. 33 | 34 | A `find*()` method returns only the declared return type or `null`. 35 | No exception is thrown if the element cannot be found. 36 | 37 | ## Usage 38 | 39 | ```php 40 | [ 46 | 'key2' => [ 47 | 'key3' => 'value1', 48 | ] 49 | ] 50 | ]); 51 | 52 | // Output: value1 53 | echo $config->getString('key1.key2.key3'); 54 | ``` 55 | 56 | ## Slim 4 integration 57 | 58 | Add this dependency injection container definition: 59 | 60 | ```php 61 | use Selective\Config\Configuration; 62 | 63 | // ... 64 | 65 | return [ 66 | // Application settings 67 | Configuration::class => function () { 68 | return new Configuration(require __DIR__ . '/settings.php'); 69 | }, 70 | 71 | // ... 72 | ]; 73 | ``` 74 | 75 | ## Examples 76 | 77 | ### Configuring a database connection 78 | 79 | The settings: 80 | 81 | ```php 82 | // Database settings 83 | $settings['db'] = [ 84 | 'driver' => 'mysql', 85 | 'host' => 'localhost', 86 | 'username' => 'root', 87 | 'database' => 'test', 88 | 'password' => '', 89 | 'charset' => 'utf8mb4', 90 | 'collation' => 'utf8mb4_unicode_ci', 91 | 'flags' => [ 92 | PDO::ATTR_PERSISTENT => false, 93 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 94 | ], 95 | ]; 96 | ``` 97 | 98 | The container definition: 99 | 100 | ```php 101 | use Selective\Config\Configuration; 102 | use PDO; 103 | 104 | return [ 105 | // ... 106 | 107 | PDO::class => static function (ContainerInterface $container) { 108 | $config = $container->get(Configuration::class); 109 | 110 | $host = $config->getString('db.host'); 111 | $dbname = $config->getString('db.database'); 112 | $username = $config->getString('db.username'); 113 | $password = $config->getString('db.password'); 114 | $charset = $config->getString('db.charset'); 115 | $flags = $config->getArray('db.flags'); 116 | $dsn = "mysql:host=$host;dbname=$dbname;charset=$charset"; 117 | 118 | return new PDO($dsn, $username, $password, $flags); 119 | }, 120 | 121 | // ... 122 | 123 | ]; 124 | ``` 125 | 126 | ### Injecting the configuration 127 | 128 | The settings: 129 | 130 | ```php 131 | $settings['module'] = [ 132 | 'key1' => 'my-value', 133 | ]; 134 | ``` 135 | 136 | The consumer class: 137 | 138 | ```php 139 | config = $config; 152 | } 153 | 154 | public function bar() 155 | { 156 | $myKey1 = $this->config->getString('module.key1'); 157 | 158 | // ... 159 | } 160 | } 161 | 162 | ``` 163 | 164 | ## Similar libraries 165 | 166 | * https://github.com/laminas/laminas-config 167 | * https://github.com/hassankhan/config 168 | 169 | ## License 170 | 171 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 172 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "selective/config", 3 | "description": "Config component, strictly typed", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "config", 8 | "configuration", 9 | "setting", 10 | "settings", 11 | "properties", 12 | "strict" 13 | ], 14 | "homepage": "https://github.com/selective-php/config", 15 | "require": { 16 | "php": "^8.1", 17 | "cakephp/chronos": "^2 || ^3" 18 | }, 19 | "require-dev": { 20 | "friendsofphp/php-cs-fixer": "^3", 21 | "phpstan/phpstan": "^1", 22 | "phpunit/phpunit": "^10", 23 | "squizlabs/php_codesniffer": "^3" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Selective\\Config\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Selective\\Config\\Test\\": "tests/" 33 | } 34 | }, 35 | "config": { 36 | "sort-packages": true 37 | }, 38 | "scripts": { 39 | "cs:check": [ 40 | "@putenv PHP_CS_FIXER_IGNORE_ENV=1", 41 | "php-cs-fixer fix --dry-run --format=txt --verbose --diff --config=.cs.php --ansi" 42 | ], 43 | "cs:fix": [ 44 | "@putenv PHP_CS_FIXER_IGNORE_ENV=1", 45 | "php-cs-fixer fix --config=.cs.php --ansi --verbose" 46 | ], 47 | "sniffer:check": "phpcs --standard=phpcs.xml", 48 | "sniffer:fix": "phpcbf --standard=phpcs.xml", 49 | "stan": "phpstan analyse -c phpstan.neon --no-progress --ansi", 50 | "test": "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always", 51 | "test:all": [ 52 | "@cs:check", 53 | "@sniffer:check", 54 | "@stan", 55 | "@test" 56 | ], 57 | "test:coverage": "php -d xdebug.mode=coverage -r \"require 'vendor/bin/phpunit';\" -- --configuration phpunit.xml --do-not-cache-result --colors=always --coverage-clover build/logs/clover.xml --coverage-html build/coverage" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ./src 10 | ./tests 11 | 12 | 13 | 14 | 15 | warning 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | reportUnmatchedIgnoredErrors: false 4 | paths: 5 | - src 6 | ignoreErrors: 7 | - '#Cannot cast mixed to int.#' 8 | - '#Cannot cast mixed to string.#' 9 | - '#Cannot cast mixed to float.#' 10 | - '#Cannot access offset string on mixed.#' 11 | - '#Parameter \#1 \$time of class Cake\\Chronos\\Chronos constructor expects DateTimeInterface\|int\|string\|null, mixed given.#' 12 | - '#Parameter \#1 \$data of class Selective\\Config\\Configuration constructor expects DateTimeInterface\|int\|string\|null, mixed given.#' 13 | - '#Parameter \#1 \$data of class Selective\\Config\\Configuration constructor expects array, mixed given.#' 14 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::findString\(\) expects string\|null, mixed given.#' 15 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::getString\(\) expects string\|null, mixed given.#' 16 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::findInt\(\) expects int\|null, mixed given.#' 17 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::getInt\(\) expects int\|null, mixed given.#' 18 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::findBool\(\) expects bool\|null, mixed given.#' 19 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::getBool\(\) expects bool\|null, mixed given.#' 20 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::findFloat\(\) expects float\|null, mixed given.#' 21 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::getFloat\(\) expects float\|null, mixed given.#' 22 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::findArray\(\) expects array\|null, mixed given.#' 23 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::getArray\(\) expects array\|null, mixed given.#' 24 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::findChronos\(\) expects Cake\\Chronos\\Chronos\|null, mixed given.#' 25 | - '#Parameter \#2 \$default of method Selective\\Config\\Configuration::getChronos\(\) expects Cake\\Chronos\\Chronos\|null, mixed given.#' 26 | -------------------------------------------------------------------------------- /src/Configuration.php: -------------------------------------------------------------------------------- 1 | The data 15 | */ 16 | private $data; 17 | 18 | /** 19 | * Constructor. 20 | * 21 | * @param array $data Data 22 | */ 23 | public function __construct(array $data = []) 24 | { 25 | $this->data = $data; 26 | } 27 | 28 | /** 29 | * Get value as integer. 30 | * 31 | * @param string $key The key 32 | * @param int|null $default The default value 33 | * 34 | * @throws InvalidArgumentException 35 | * 36 | * @return int The value 37 | */ 38 | public function getInt(string $key, ?int $default = null): int 39 | { 40 | $value = $this->find($key, $default); 41 | 42 | if ($this->isNullOrBlank($value)) { 43 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 44 | } 45 | 46 | return (int)$value; 47 | } 48 | 49 | /** 50 | * Get value as integer or null. 51 | * 52 | * @param string $key The key 53 | * @param int|null $default The default value 54 | * 55 | * @return int|null The value 56 | */ 57 | public function findInt(string $key, ?int $default = null) 58 | { 59 | $value = $this->find($key, $default); 60 | 61 | if ($this->isNullOrBlank($value)) { 62 | return null; 63 | } 64 | 65 | return (int)$value; 66 | } 67 | 68 | /** 69 | * Get value as string. 70 | * 71 | * @param string $key The key 72 | * @param string|null $default The default value 73 | * 74 | * @throws InvalidArgumentException 75 | * 76 | * @return string The value 77 | */ 78 | public function getString(string $key, ?string $default = null): string 79 | { 80 | $value = $this->find($key, $default); 81 | 82 | if ($value === null) { 83 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 84 | } 85 | 86 | return (string)$value; 87 | } 88 | 89 | /** 90 | * Get value as string or null. 91 | * 92 | * @param string $key The key 93 | * @param string $default The default value 94 | * 95 | * @return string|null The value 96 | */ 97 | public function findString(string $key, ?string $default = null) 98 | { 99 | $value = $this->find($key, $default); 100 | 101 | if ($value === null) { 102 | return null; 103 | } 104 | 105 | return (string)$value; 106 | } 107 | 108 | /** 109 | * Get value as array. 110 | * 111 | * @param string $key The key 112 | * @param array|null $default The default value 113 | * 114 | * @throws InvalidArgumentException 115 | * 116 | * @return array The value 117 | */ 118 | public function getArray(string $key, ?array $default = null): array 119 | { 120 | $value = $this->find($key, $default); 121 | 122 | if ($this->isNullOrBlank($value)) { 123 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 124 | } 125 | 126 | return (array)$value; 127 | } 128 | 129 | /** 130 | * Get value as array or null. 131 | * 132 | * @param string $key The key 133 | * @param array $default The default value 134 | * 135 | * @return array|null The value 136 | */ 137 | public function findArray(string $key, ?array $default = null) 138 | { 139 | $value = $this->find($key, $default); 140 | 141 | if ($this->isNullOrBlank($value)) { 142 | return null; 143 | } 144 | 145 | return (array)$value; 146 | } 147 | 148 | /** 149 | * Get value as float. 150 | * 151 | * @param string $key The key 152 | * @param float|null $default The default value 153 | * 154 | * @throws InvalidArgumentException 155 | * 156 | * @return float The value 157 | */ 158 | public function getFloat(string $key, ?float $default = null): float 159 | { 160 | $value = $this->find($key, $default); 161 | 162 | if ($this->isNullOrBlank($value)) { 163 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 164 | } 165 | 166 | return (float)$value; 167 | } 168 | 169 | /** 170 | * Get value as float or null. 171 | * 172 | * @param string $key The key 173 | * @param float $default The default value 174 | * 175 | * @return float|null The value 176 | */ 177 | public function findFloat(string $key, ?float $default = null) 178 | { 179 | $value = $this->find($key, $default); 180 | 181 | if ($this->isNullOrBlank($value)) { 182 | return null; 183 | } 184 | 185 | return (float)$value; 186 | } 187 | 188 | /** 189 | * Get value as boolean. 190 | * 191 | * @param string $key The key 192 | * @param bool|null $default The default value 193 | * 194 | * @throws InvalidArgumentException 195 | * 196 | * @return bool The value 197 | */ 198 | public function getBool(string $key, ?bool $default = null): bool 199 | { 200 | $value = $this->find($key, $default); 201 | 202 | if ($this->isNullOrBlank($value)) { 203 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 204 | } 205 | 206 | return (bool)$value; 207 | } 208 | 209 | /** 210 | * Get value as boolean or null. 211 | * 212 | * @param string $key The key 213 | * @param bool $default The default value 214 | * 215 | * @return bool|null The value 216 | */ 217 | public function findBool(string $key, ?bool $default = null) 218 | { 219 | $value = $this->find($key, $default); 220 | 221 | if ($this->isNullOrBlank($value)) { 222 | return null; 223 | } 224 | 225 | return (bool)$value; 226 | } 227 | 228 | /** 229 | * Get value as Chronos. 230 | * 231 | * @param string $key The key 232 | * @param Chronos|null $default The default value 233 | * 234 | * @throws InvalidArgumentException 235 | * 236 | * @return Chronos The value 237 | */ 238 | public function getChronos(string $key, ?Chronos $default = null): Chronos 239 | { 240 | $value = $this->find($key, $default); 241 | 242 | if ($this->isNullOrBlank($value)) { 243 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 244 | } 245 | 246 | if ($value instanceof Chronos) { 247 | return $value; 248 | } 249 | 250 | return new Chronos($value); 251 | } 252 | 253 | /** 254 | * Get value as Chronos or null. 255 | * 256 | * @param string $key The key 257 | * @param Chronos|null $default The default value 258 | * 259 | * @return Chronos|null The value 260 | */ 261 | public function findChronos(string $key, ?Chronos $default = null) 262 | { 263 | $value = $this->find($key, $default); 264 | 265 | if ($this->isNullOrBlank($value)) { 266 | return null; 267 | } 268 | 269 | if ($value instanceof Chronos) { 270 | return $value; 271 | } 272 | 273 | return new Chronos($value); 274 | } 275 | 276 | /** 277 | * Find mixed value. 278 | * 279 | * @param string $path The path 280 | * @param mixed|null $default The default value 281 | * 282 | * @return mixed|null The value 283 | */ 284 | public function find(string $path, $default = null) 285 | { 286 | if (array_key_exists($path, $this->data)) { 287 | return $this->data[$path] ?? $default; 288 | } 289 | 290 | if (strpos($path, '.') === false) { 291 | return $default; 292 | } 293 | 294 | $pathKeys = explode('.', $path); 295 | 296 | $arrayCopyOrValue = $this->data; 297 | 298 | foreach ($pathKeys as $pathKey) { 299 | if (!isset($arrayCopyOrValue[$pathKey])) { 300 | return $default; 301 | } 302 | $arrayCopyOrValue = $arrayCopyOrValue[$pathKey]; 303 | } 304 | 305 | return $arrayCopyOrValue; 306 | } 307 | 308 | /** 309 | * Return all settings as array. 310 | * 311 | * @return array The data 312 | */ 313 | public function all(): array 314 | { 315 | return $this->data; 316 | } 317 | 318 | /** 319 | * Is null or blank. 320 | * 321 | * @param mixed $value The value 322 | * 323 | * @return bool The status 324 | */ 325 | private function isNullOrBlank($value): bool 326 | { 327 | return $value === null || $value === ''; 328 | } 329 | } 330 | --------------------------------------------------------------------------------