├── .editorconfig ├── .github └── workflows │ └── php.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── UPGRADE.md ├── composer.json ├── coverage.svg └── src ├── ConfigFactory.php ├── ConfigLoaderInterface.php ├── ConfigValues.php ├── Exception ├── ConfigExceptionInterface.php ├── ConfigFileNotFoundException.php ├── ConfigLoaderException.php ├── ConfigLogicException.php ├── ConfigValueNotFoundException.php ├── InvalidConfigValueException.php └── UnmappedFileExtensionException.php ├── Filter ├── ExtractTopLevelItemsFilter.php ├── RemovePrefixFilter.php └── SymfonyConfigFilter.php ├── Loader ├── AbstractFileLoader.php ├── ArrayValuesLoader.php ├── CascadingConfigLoader.php ├── EnvLoader.php ├── FileListLoader.php ├── FileLoader.php ├── FileLoaderInterface.php ├── FolderLoader.php ├── IniFileLoader.php ├── JsonEnvLoader.php ├── JsonFileLoader.php ├── PhpFileLoader.php └── YamlFileLoader.php └── Util ├── ArrayUtils.php └── LocalDistFileIterator.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at https://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Github Build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | php: ['8.2', '8.3', '8.4'] 10 | name: PHP ${{ matrix.php }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Install PHP 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: ${{ matrix.php }} 17 | coverage: xdebug 18 | - name: Report PHP version 19 | run: php -v 20 | - name: Validate composer.json and composer.lock 21 | run: composer validate 22 | - name: Get Composer Cache Directory 23 | id: composer-cache 24 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 25 | - name: Cache dependencies 26 | uses: actions/cache@v4 27 | with: 28 | path: ${{ steps.composer-cache.outputs.dir }} 29 | key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} 30 | restore-keys: ${{ matrix.php }}-composer- 31 | - name: Install dependencies 32 | run: composer install --prefer-dist --no-progress --no-suggest 33 | - name: Run PHPStan 34 | run: composer phpstan 35 | - name: Run PHP-CS 36 | run: composer check-style 37 | - name: Run test suite 38 | run: vendor/bin/phpunit 39 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `Configula` will be documented in this file since v3.0 4 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 5 | 6 | ## [5.0] (2024-12-07) 7 | ### Changed 8 | - Refactored all code to PHP 8.2 standards 9 | - Updated all tests to support latest PHPUnit 11 10 | - Replaced `ConfigException` with `ConfigExceptionInterface` 11 | ### Removed 12 | - Support for PHP < 8.2 13 | - Support for PHPUnit < 11 14 | - Support for Symfony < 6.4 15 | 16 | ## [4.3.1] (2024-12-07) 17 | ### Changed 18 | - Added GitHub build action for PHP 8.4 19 | - Updated GitHub build `action/checkout` and `action/cache` 20 | - Code syntax and comment cleanup 21 | ### Fixed 22 | - Implicit null values in `SymfonyConfigFilter` and `YamlFileLoader` classes 23 | - `IniFileLoader` behavior (not sure why the tests _ever_ passed) 24 | - Remove `ReturnTypeWillChange` import statements for those implementors still using PHP7 25 | 26 | ## [4.3.0] (2024-02-03) 27 | ### Added 28 | - Added support for Symfony v7 29 | - Added test builds for PHP 8.2 and 8.3 30 | 31 | ## [4.2] (2022-08-16) 32 | ### Added 33 | - Symfony Config 6.x tests 34 | - Explicit `XDEBUG_MODE=coverage` environment variable to `composer test` script 35 | ### Changed 36 | - Code syntax cleanup and improvements 37 | - Updated PHPUnit config file (`phpunit.xml.dist`) to v9.x schema 38 | ### Fixed 39 | - Added attribute to fix PHP Deprecation warning in v8 (thanks @usox!) 40 | 41 | ## [4.1] (2021-11-29) 42 | ### Added 43 | - Ability for PhpFileLoader to load arrays directly from included PHP files (thanks @thedumbtechguy!) 44 | - Support for PHP v8.1 45 | - Support for Symfony 6 46 | - Updated PHPStan to v1.2 and fixed code issues 47 | ### Removed 48 | - Unused `extensionMap` property in `FolderLoader` 49 | 50 | ## [4.0.1] (2021-02-22) 51 | ### Fixed 52 | - Merge `$_ENV` and `getenv()` arrays in `EnvLoader.php`; fixes an issue with Symfony dotEnv loader 53 | 54 | ## [4.0] (2021-02-06) 55 | ### Added 56 | - Support for PHP v8 57 | - Support for `dflydev/dot-access-data` version 3.0 and newer 58 | - PHPStan, which replaces Scrutinizer 59 | - Some additional tests 60 | ### Changed 61 | - *BREAKING:* Added `final` keyword for `ConfigValues` constructor, and added `protected init()` method to keep any custom 62 | logic that was previously in the controller (see ) 63 | - *BREAKING:* Made all concrete loader classes final (see ) 64 | - Added support for `vlucas/dotenv` v5.0 and newer 65 | - Beginning to implement GitHub Workflows in order to replace Travis-CI 66 | - Refactored logic in `PhpFileLoader` 67 | ### Removed 68 | - Travis checks and Scrutinizer checks (replaced by GitHub builds and PHPStan) 69 | - Support for PHP < 7.3 70 | - Support for PHPUnit < 9.x 71 | - Support for Symfony v3 and < v4.4 72 | 73 | ## [3.1.0] (2020-01-14) 74 | ### Added 75 | - PHP 7.4 test in `travis.ci` 76 | - Support for Symfony v5 77 | - `.editorconfig` file (to make developers' lives easier :) 78 | - Strict types declaration on every file (`declare(strict_types=1);`) 79 | ### Changed 80 | - Update to PSR-12 coding standard (from PSR-2) 81 | ### Fixed 82 | - Explicitly cast results to string in order to facilitate strict types 83 | 84 | ## [3.0.0] (2019-04-11) 85 | ### Added 86 | - Added this changelog 87 | - Added `ConfigValues::__invoke()` method 88 | - Added `ConfigValues::get()` method 89 | - Added `ConfigValues::hasValue()` method 90 | - Added `ConfigValues::find()` method 91 | - Added `ConfigValues::has()` method 92 | - Added `ConfigValues::getIterator()` method 93 | - Added `ConfigValues::fromConfigValues()` method 94 | - Added `ConfigValues::getArrayCopy()` method 95 | - Added Validator and Symfony Configuration Validator Bridge 96 | - Added `ConfigFactory` to replace v2 functionality and add additional utility methods 97 | - Added `ConfigLoaderInterface` and all loaders in `Loader` namespace to replace drivers 98 | - Added ability to load values from environment and any arbitrary source (via `ConfigLoaderInterface`) 99 | - Added filters, including `SymfonyConfigFilter` 100 | ### Changed 101 | - Main config class changed from `Config` to `ConfigValues` 102 | - Configula now requires PHP v7.1 or newer. Use Configula v2.0 for PHP5.x/7.0 support. 103 | - Configula now requires Symfony 3.4 or newer. 104 | - All configuration loading has been moved from `ConfigValues` constructor into separate classes. 105 | - Invalid files or other load errors now throw a `ConfigLoaderException` 106 | - Switched from PSR-0 to PSR-4 107 | - `ConfigValues` constructor now accepts an array of values instead of a file path 108 | - `ConfigValues` now implements `IteratorAggregate` instead of `Iterator` 109 | - Use `dfyldev/data` for access to nested values via dot-notation (removed `getNestedVar` method) 110 | ### Removed 111 | - Removed `Config(Values)::loadConfig()`. Use loaders now. 112 | - Removed `Config(Values)::loadConfgFile()`. Use loaders now. 113 | - Removed `Config(Values)::parseConfigFile()`. 114 | - Removed `DriverInterface` and all drivers. Use loaders now. 115 | - Removed `ConfigulaException`. All exceptions now extend `Exception\ConfigException`. 116 | ### Deprecated 117 | - `ConfigValues::getItems()` is now deprecated. Use `ConfigValues::getArrayCopy()` instead. 118 | - `ConfigValues::getItem()` is now deprecated. Use `ConfigValues::get()` instead. 119 | - `ConfigValues::valid()` is now deprecated. Use `ConfigValues::has()` instead. 120 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Casey McLaughlin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Configula 2 | 3 | Configula is a configuration library for PHP 7.3+. 4 | 5 | [![Latest Version on Packagist][ico-version]][link-packagist] 6 | [![Software License][ico-license]](LICENSE.md) 7 | [![Github Build][ico-ghbuild]][link-ghbuild] 8 | [![Code coverage][ico-coverage]](coverage.svg) 9 | [![PHPStan Level 8][ico-phpstan]][link-phpstan] 10 | [![Total Downloads][ico-downloads]][link-downloads] 11 | 12 | Use this library when you want to load configuration from the filesystem, environment, and other sources. It implements 13 | your configuration values as an immutable object in PHP. It is a framework-independent tool, and can be easily used in 14 | any PHP application. 15 | 16 | ## Features 17 | 18 | * Load configuration from a variety of sources: 19 | * Load values from _.php_, _.ini_, _.json_, and _.yml_ configuration file types 20 | * Load values from the environment, and _.env_ files using a DotEnv library ([vlucas](https://github.com/vlucas/phpdotenv) or [Symfony](https://github.com/symfony/dotenv)) 21 | * Easily write your own loaders to support other file types and sources 22 | * Cascade/deep merge values from multiple sources (e.g. array, files, environment, etc) 23 | * Optionally use in combination with [Symfony Config Component](http://symfony.com/doc/current/components/config/introduction.html) 24 | to validate configuration values and/or cache them 25 | * Creates an immutable object to access configuration values in your application: 26 | * Array-access (read-only) 27 | * `get(val)`, `has(val)`, and `hasValue(val)` methods 28 | * Magic methods (`__get(val)`, `__isset(val)`, and `__invoke(val)`) 29 | * Implements `Traversable` and `Countable` interfaces 30 | * Provides simple dot-based access to nested values (e.g. `$config->get('application.sitename.prefix');`) 31 | * Code quality standards: PSR-12, complete unit test coverage 32 | 33 | ## Installation 34 | 35 | ```bash 36 | composer require caseyamcl/configula 37 | ``` 38 | 39 | ## Upgrading? 40 | 41 | Refer to [UPGRADE.md](UPGRADE.md) for notes on upgrading from Version 2.x, 3.x, or 4.x to v5. 42 | 43 | ## Need PHP v7.x, 5.x, or older Symfony compatibility? 44 | 45 | Configula v5.x is compatible with PHP v8.2+. 46 | 47 | * If you need PHP 7.3+ compatibility, instruct Composer to use the **4.x** version of this library: 48 | ```composer require caseyamcl/configula:^4.2``` 49 | * If you need PHP 7-7.2+ compatibility, instruct Composer to use the **3.x** version of this library: 50 | ```composer require caseyamcl/configula:^3.2``` 51 | * If you need PHP 5.x support, you can use the **2.x** version of this library (no longer maintained): 52 | ```composer require caseyamcl/configula:^2.4``` 53 | 54 | ## Loading Configuration 55 | 56 | You can use the `Configula\ConfigFactory` to load configuration from files, the environment or other sources: 57 | 58 | ```php 59 | use Configula\ConfigFactory as Config; 60 | 61 | // Load all .yml, .php, .json, and .ini files from directory (recursive) 62 | // Supports '.local' and '.dist' modifiers to load config in correct order 63 | $config = Config::loadPath('/path/to/config/files', ['optional' => 'defaults', ...]); 64 | 65 | // Load all .yml, .php, .json, and .in files from directory (non-recursive) 66 | // Supports '.local' and '.dist' modifiers to load config in correct order 67 | $config = Config::loadSingleDirectory('/path/to/config/files', ['optional' => 'defaults', ...]); 68 | 69 | // Load from array 70 | $config = Config::fromArray(['some' => 'values']); 71 | 72 | // Chain loaders -- performs deep merge 73 | $config = Config::fromArray(['some' => 'values']) 74 | ->merge(Config::loadPath('/some/path')) 75 | ->merge(Config::loadEnv('MY_APP')); 76 | ``` 77 | 78 | Or, if you are loading an array, you can instantiate `Configula\ConfigValues` directly: 79 | 80 | ```php 81 | $config = new Configula\ConfigValues(['array' => 'values']); 82 | ``` 83 | 84 | Or, you can manually invoke any of the loaders in the `Configula\Loader` namespace: 85 | 86 | ```php 87 | $config = (new Configula\Loader\FileListLoader(['file-1.yml', 'file-2.json']))->load(); 88 | ``` 89 | 90 | 91 | ## Accessing Values 92 | 93 | The `Configula\ConfigValues` object provides several ways to access your configuration values: 94 | 95 | ```php 96 | // get method - throws exception if value does not exist 97 | $config->get('some_value'); 98 | 99 | // get method with default - returns default if value does not exist 100 | $config->get('some_value', 'default'); 101 | 102 | // find method - returns NULL if value does not exist 103 | $config->find('some_value'); 104 | 105 | // has method - returns TRUE or FALSE 106 | $config->has('some_value'); 107 | 108 | // hasValue method - returns TRUE if value exists and is not empty (NULL, [], "") 109 | $config->hasValue('some_value'); 110 | 111 | ``` 112 | 113 | ### Accessing values using dot notation 114 | 115 | Configula supports accessing values via dot-notation (e.g `some.nested.var`): 116 | 117 | ```php 118 | // Here is a nested array: 119 | $values = [ 120 | 'debug' => true, 121 | 'db' => [ 122 | 'platform' => 'mysql', 123 | 'credentials' => [ 124 | 'username' => 'some', 125 | 'password' => 'thing' 126 | ] 127 | ], 128 | ]; 129 | 130 | // Load it into Configula 131 | $config = new \Configula\ConfigValues($values); 132 | 133 | // Access top-level item 134 | $values->get('debug'); // bool; TRUE 135 | 136 | // Access nested item 137 | $values->get('db.platform'); // string; 'mysql' 138 | 139 | // Access deeply nested item 140 | $values->get('db.credentials.username'); // string: 'some' 141 | 142 | // Get item as array 143 | $values->get('db'); // array ['platform' => 'mysql', 'credentials' => ...] 144 | 145 | // has/hasValue work too 146 | $values->has('db.credentials.key'); // false 147 | $values->hasValue('db.credentials.key'); // false 148 | ``` 149 | 150 | Property-like access to your config settings via `__get()` and `__isset()`: 151 | 152 | ```php 153 | // Access configuration values 154 | $config = Config::loadPath('/path/to/config/files'); 155 | 156 | // Throws exception if value does not exist 157 | $some_value = $config->some_key; 158 | 159 | // Returns TRUE or FALSE 160 | isset($config->some_key); 161 | ``` 162 | 163 | Iterator and count access to your config settings: 164 | 165 | ```php 166 | // Basic iteration 167 | foreach ($config as $item => $value) { 168 | echo "
  • {$item} is {$value}
  • "; 169 | } 170 | 171 | // Count 172 | count($config); /* or */ $config->count(); 173 | ``` 174 | 175 | Callable access to your config settings via `__invoke()`: 176 | 177 | ```php 178 | // Throws exception if value does not exist 179 | $value = $config('some_value'); 180 | 181 | // Returns default value if value does not exist 182 | $value = $config('some_value', 'default'); 183 | ``` 184 | 185 | Array access to your config settings: 186 | 187 | ```php 188 | // Throws exception if value does not exist 189 | $some_value = $config['some_key']; 190 | 191 | // Returns TRUE or FALSE 192 | $exists = isset($config['some_key']); 193 | 194 | // Not allowed; always throws exception (config is immutable) 195 | $config['some_key'] = 'foobar'; // Configula\Exception\ConfigLogicException 196 | unset($config['some_key']); // Configula\Exception\ConfigLogicException 197 | ``` 198 | 199 | ## Merging Configuration 200 | 201 | Since `Configula\ConfigValues` is an immutable object, you cannot mutate the configuration 202 | once it is set. However, you can merge values and get a new copy of the object using the `merge` 203 | or `mergeValues` methods: 204 | 205 | ```php 206 | use Configula\ConfigValues; 207 | 208 | $config = new ConfigValues(['foo' => 'bar', 'baz' => 'biz']); 209 | 210 | // Merge configuration using merge() 211 | $newConfig = $config->merge(new ConfigValues(['baz' => 'buzz', 'cad' => 'cuzz'])); 212 | 213 | // For convenience, you can pass in an array using mergeValues() 214 | $newConfig = $config->mergeValues(['baz' => 'buzz', 'cad' => ['some' => 'thing']]); 215 | ``` 216 | 217 | Configula performs a *deep merge*. Nested arrays are traversed and the last value always takes precedence. 218 | 219 | Note that Configula does not deep merge nested objects, only arrays. 220 | 221 | ## Iterator and Count 222 | 223 | The built-in `ConfigValues::getIterator()` and `ConfigValues::count()` methods flattens nested values when iterating 224 | or counting: 225 | 226 | ```php 227 | // Here is a nested array 228 | $config = new Configula\ConfigValues([ 229 | 'debug' => true, 230 | 'db' => [ 231 | 'platform' => 'mysql', 232 | 'credentials' => [ 233 | 'username' => 'some', 234 | 'password' => 'thing' 235 | ] 236 | ], 237 | ]); 238 | 239 | // --------------------- 240 | 241 | foreach ($config as $path => $value) { 242 | echo "\n" . $path . ": " . $value; 243 | } 244 | 245 | // Output: 246 | // 247 | // debug: 1 248 | // db.platform: mysql 249 | // db.credentials.username: some 250 | // db.credentials.password: thing 251 | // 252 | 253 | echo count($config); 254 | 255 | // Output: 4 256 | 257 | ``` 258 | 259 | If you want to iterate only the top-level items in your configuration, you can use the `getArrayCopy()` method: 260 | 261 | ```php 262 | foreach ($config->getArrayCopy() as $path => $value) { 263 | echo "\n" . $path . ": " . $value; 264 | } 265 | 266 | // Output: 267 | // 268 | // debug: 1 269 | // db: Array 270 | // 271 | 272 | ``` 273 | 274 | ## Using the Folder Loader - Config Folder Layout 275 | 276 | The folder loaders in Configula will load files with the following extensions (you can add your own custom loaders; see below): 277 | 278 | * `php` - Configula will look for an array called `$config` in this file. 279 | * `json` - Uses the built-in PHP `json_decode()` function 280 | * `yaml` or `yml` - Uses the [Symfony YAML parser](https://symfony.com/doc/current/components/yaml.html) 281 | * `ini` - Uses the built-in PHP `parse_ini_file()` function 282 | 283 | The `ConfigFactory::loadPath($path)` method will traverse directories in your configuration path recursively. 284 | 285 | The `ConfigFactory::loadSingleDirectory($path)` method will load your configuration in a single directory 286 | non-recursively. 287 | 288 | ### Local Configuration Files 289 | 290 | In some cases, you may want to have local configuration files that override the default configuration files. There are 291 | two ways to do this: 292 | 293 | 1. prefix the default configuration file extension with `.dist` (e.g. `config.dist.yml`), and name the local 294 | configuration file normally: `config.yml` 295 | 2. name the default configuration file normally (e.g. `config.yml`) and prefix `.local` to the extension for the local 296 | configuration file: `config.local.yml`. 297 | 298 | Either way will work, and you could even combine approaches if you want. The file iterator will always cascade merge 299 | files in this order: 300 | 301 | * `FILENAME.dist.EXT` 302 | * `FILENAME.EXT` 303 | * `FILENAME.local.EXT` 304 | 305 | This is useful if you want to keep local configuration files out of revision control. Choose a paradigm, 306 | and simply add the following to your `.gitignore` 307 | 308 | ```bash 309 | # If keeping .dist files... 310 | [CONFIGDIR]/* 311 | [!CONFIGDIR]/*.dist.* 312 | 313 | # or, if ignoring .local files... 314 | [CONFIGDIR]/*.local.* 315 | ``` 316 | 317 | ### Example 318 | 319 | Consider the following directory layout... 320 | 321 | ``` 322 | /my/app/config 323 | ├config.php 324 | ├config.dist.php 325 | └/subfolder 326 | ├database.yml 327 | └database.dist.yml 328 | ``` 329 | 330 | If you use `ConfigFactory::loadPath('/my/app/config')`, the files will be parsed according to their extension and 331 | values will be merged in the following order (values in files that are later in the list will clobber earlier values): 332 | 333 | ``` 334 | - /config.dist.php 335 | - /subfolder/database.dist.yml 336 | - /config.php 337 | - /subfolder/database.yml 338 | ``` 339 | 340 | ## Loading from environment variables 341 | 342 | There are two common ways that configuration is generally stored in environment: 343 | 344 | 1. As multiple environment variables (perhaps loaded by phpDotEnv or Symfony dotEnv, or exposed by Heroku/Kubernetes/etc.). 345 | 2. As a single environment variable with a JSON-encoded value, which exposes the entire configuration tree. 346 | 347 | Configula supports both. You can also write your own loader if your environment is different. 348 | 349 | ## Loading multiple environment variables 350 | 351 | Configula supports loading environment variables as configuration values using `getenv()`. This is the 352 | [12 Factor App](https://12factor.net/config) way of doing things. 353 | 354 | Common use-cases for this loader include: 355 | 356 | 1. Loading system environment as config values 357 | 2. Loading values using [phpDotEnv](https://github.com/vlucas/phpdotenv) or [Symfony dotEnv](https://symfony.com/doc/current/components/dotenv.html) 358 | 3. Accessing values injected into the environment by a cloud provider (Kubernetes, Docker, Heroku, etc.) 359 | 360 | ### Default environment variable loading 361 | 362 | The default behavior is to load the configuration values directly: 363 | 364 | ```php 365 | $config = ConfigFactory::loadEnv(); 366 | ``` 367 | 368 | Result: 369 | 370 | ``` 371 | MYAPP_MYSQL_USERNAME="..." --> becomes --> $config->get('MYAPP_MYSQL_USERNAME') 372 | MYAPP_MYSQL_PASSWORD="..." --> becomes --> $config->get('MYAPP_MYSQL_PASSWORD') 373 | MYAPP_MYSQL_HOST_PORT="..." --> becomes --> $config->get('MYAPP_MYSQL_HOST_PORT') 374 | MYAPP_MYSQL_HOST_NAME="..." --> becomes --> $config->get('MYAPP_MYSQL_HOST_NAME') 375 | SERVER_NAME="..." --> becomes --> $config->get('SERVER_NAME') 376 | etc.. 377 | ``` 378 | 379 | ### Load only environment variables with prefix 380 | 381 | You can load *ONLY* environment variables with a specific prefix. Configula will remove the prefix 382 | when loading the configuration: 383 | 384 | ```php 385 | $config = ConfigFactory::loadEnv('MYAPP_'); 386 | ``` 387 | 388 | Result: 389 | 390 | ``` 391 | MYAPP_MYSQL_USERNAME="..." --> becomes --> $config->get('MYSQL_USERNAME') 392 | MYAPP_MYSQL_PASSWORD="..." --> becomes --> $config->get('MYSQL_PASSWORD') 393 | MYAPP_MYSQL_HOST_PORT="..." --> becomes --> $config->get('MYSQL_HOST_PORT') 394 | MYAPP_MYSQL_HOST_NAME="..." --> becomes --> $config->get('MYSQL_HOST_NAME') 395 | SERVER_NAME="..." --> ignored 396 | etc.. 397 | ``` 398 | 399 | ### Convert environment variables to nested configuration 400 | 401 | You can convert a flat list into nested config values by passing a delimiter to break on: 402 | 403 | ```php 404 | $config = ConfigFactory::loadEnv('MYAPP', '_'); 405 | ``` 406 | 407 | Result: 408 | 409 | ``` 410 | MYAPP_MYSQL_USERNAME="..." --> becomes --> $config->get('MYSQL.USERNAME') 411 | MYAPP_MYSQL_PASSWORD="..." --> becomes --> $config->get('MYSQL.PASSWORD') 412 | MYAPP_MYSQL_HOST_PORT="..." --> becomes --> $config->get('MYSQL.HOST.PORT') 413 | MYAPP_MYSQL_HOST_NAME="..." --> becomes --> $config->get('MYSQL.HOST.NAME') 414 | ``` 415 | 416 | This allows you to access nested values as an array: 417 | 418 | ```php 419 | $config = ConfigFactory::loadEnv('MY_APP', '_'); 420 | $dbConfig = $config->get('mysql.host'); 421 | 422 | // $dbConfig: ['host' => '...', 'port' => '...'] 423 | ``` 424 | 425 | ### Transform environment variables to lower-case 426 | 427 | You can transform all the values to lower-case by passing TRUE as the last argument: 428 | 429 | ```php 430 | $config = ConfigFactory::loadEnv('MYAPP_', '_', true); 431 | ``` 432 | 433 | Result: 434 | 435 | ``` 436 | MYAPP_MYSQL_USERNAME="..." --> becomes --> $config->get('mysql.username') 437 | MYAPP_MYSQL_PASSWORD="..." --> becomes --> $config->get('mysql.password') 438 | MYAPP_MYSQL_HOST_PORT="..." --> becomes --> $config->get('mysql.host.port') 439 | MYAPP_MYSQL_HOST_NAME="..." --> becomes --> $config->get('mysql.host.name') 440 | ``` 441 | 442 | ### Loading environment variables with regex pattern 443 | 444 | Instead of using a prefix, you can pass a regex string to limit returned values: 445 | 446 | ```php 447 | $config = ConfigFactory::LoadEnvRegex('/.+_MYAPP_.+/', '_', true); 448 | ``` 449 | 450 | Result: 451 | 452 | ``` 453 | MYAPP_MYSQL_USERNAME="..." --> becomes --> $config->get('myapp.mysql.username') 454 | MYAPP_MYSQL_PASSWORD="..." --> becomes --> $config->get('myapp.mysql.password') 455 | MYAPP_MYSQL_HOST_PORT="..." --> becomes --> $config->get('myapp.mysql.host.port') 456 | EMAIL_MYAPP_SERVER="..." --> becomes --> $config->get('email.myapp.server') 457 | SERVER_NAME="..." --> ignored 458 | ``` 459 | 460 | ## Loading a single JSON-encoded environment variable 461 | 462 | Use the `Configula\Loader\JsonEnvLoader` to load a JSON environment variable: 463 | 464 | ``` 465 | MY_ENV_VAR = '{"foo: "bar", "baz": "biz"}' 466 | ``` 467 | 468 | ```php 469 | use Configula\Loader\JsonEnvLoader; 470 | 471 | $values = (new JsonEnvLoader('MY_ENV_VAR'))->load(); 472 | 473 | echo $values->foo; 474 | echo $values->get('foo'); // "bar" 475 | ``` 476 | 477 | ## Loading from multiple loaders 478 | 479 | You can use the `Configula\ConfigFactory::loadMultiple()` to load from multiple sources and merge results. 480 | This method accepts an iterator where each value is one of the following: 481 | 482 | * Instance of `Configula\ConfigLoader\ConfigLoaderInterface` 483 | * Array of configuration values 484 | * String or instance of `SplFileInfo` that is a path to a file or directory 485 | 486 | Any other value in the iterator will trigger an `\InvalidArgument` exception 487 | 488 | ```php 489 | use Configula\ConfigFactory as Config; 490 | use Configula\Loader; 491 | 492 | $config = Config::loadMultiple([ 493 | new Loader\EnvLoader('My_APP'), // Instance of LoaderInterface 494 | ['some' => 'values'], // Array of config vaules 495 | '/path/to/some/file.yml', // Path to file (must exist) 496 | new \SplFileInfo('/path/to/another/file.json') // SplFileInfo 497 | ]); 498 | 499 | // Alternatively, you can pass an iterator of `Configula\ConfigLoaderInterface` instances to 500 | // `Configula\Loader\CascadingConfigLoader`. 501 | ``` 502 | 503 | ## Handling Errors 504 | 505 | All exceptions implement `Configula\Exception\ConfigExceptionInterface` and extend `\RuntimeException`. 506 | You can catch this exception to catch certain types of Configula errors during loading and reading of configuration values. 507 | 508 | ### Loading Exceptions: 509 | 510 | * `ConfigLoaderException` is thrown when an error occurs during loading configuration. 511 | * `ConfigFileNotFoundException` is thrown when a required configuration file or path is missing. It is generally 512 | thrown from the `FileLoader` loader when the `$required` constructor parameter is set to `true`. 513 | * `UnmappedFileExtensionException` is thrown from the `DecidingFileLoader` for files with unrecognized extensions. 514 | 515 | *NOTE:* Configula does NOT catch non-Configula exceptions and convert them to Configula exceptions. If you wan to 516 | catch all conceivable errors when loading configuration, you should surround your configuration loading code 517 | with a `try...catch (\Throwable $e)`. 518 | 519 | ### Config Value Exceptions: 520 | 521 | * `ConfigValueNotFoundException` is thrown when trying to reference a non-existent configuration value name and 522 | no default value is specified. 523 | * `ConfigLogicException` is thrown when attempting to mutate configuration via array 524 | * `InvalidConfigValueException` is not thrown from any Configula class directly, but provided as an option for 525 | implementing libraries (see "_Extending the `ConfigValues` class_" below). 526 | 527 | ```php 528 | // These throw a ConfigValueNotFoundException 529 | $config->get('non_existent_value'); 530 | $config['non_existent_value']; 531 | $config->non_existent_value; 532 | 533 | // This will not throw an exception, but instead return NULL 534 | $config->find('non_existent_value'); 535 | 536 | // This will not throw an exception, but instead return 'default' 537 | $config->get('non_existent_value', 'default'); 538 | ``` 539 | 540 | ## Extending the `ConfigValues` class (for IDE type-hinting) 541 | 542 | While it is entirely possible to use the `Configula\ConfigValues` class directly, you might also want to provide 543 | some application-specific methods to load configuration values. This creates a better developer experience. 544 | 545 | ```php 546 | use Configula\ConfigValues; 547 | use Configula\Exception\InvalidConfigValueException; 548 | 549 | class AppConfig extends ConfigValues 550 | { 551 | /** 552 | * Is the app running in development mode? 553 | * 554 | * @return bool 555 | */ 556 | public function isDevMode(): bool 557 | { 558 | // Get the value or assume false 559 | return (bool) $this->get('devmode', false); 560 | } 561 | 562 | /** 563 | * Get the encryption key (as 32-character alphanumeric string) 564 | * 565 | * @return string 566 | */ 567 | public function getEncryptionKey(): string 568 | { 569 | // If the value doesn't exist, a `ConfigValueNotFoundException` is thrown 570 | $key = $this->get('encryption_key'); 571 | 572 | // Let's do a little validation... 573 | if (strlen($key) != 32) { 574 | throw new InvalidConfigValueException('Encryption key must be 32 characters'); 575 | } 576 | 577 | return $key; 578 | } 579 | } 580 | ``` 581 | 582 | *Note:* Notice that the example above uses the `InvalidConfigValueException`, which is included with Configula for 583 | exactly this use-case. 584 | 585 | You can use your custom `AppConfig` class as follows: 586 | 587 | ```php 588 | use Configula\ConfigFactory; 589 | 590 | // Build it 591 | $config = AppConfig::fromConfigValues(ConfigFactory::loadPath('/some/path')); 592 | 593 | // Use it (and enjoy the type-hinting in your IDE) 594 | $config->getEncryptionKey(); 595 | $config->isDevMode(); 596 | // etc... 597 | ``` 598 | 599 | ## Using Symfony Config to define a configuration schema 600 | 601 | In some cases, you may wish to strictly control what configuration values are allowed and also validate those values 602 | when loading the configuration. The [Symfony Config Component](http://symfony.com/doc/current/components/config.html) 603 | provides an excellent mechanism for accomplishing this. 604 | 605 | First, include the Symfony Config Component library in your application: 606 | 607 | ``` 608 | composer require symfony/config 609 | ``` 610 | 611 | Then, create a class that provides your configuration tree. Refer to the [Symfony Docs](http://symfony.com/doc/current/components/config/definition.html#defining-a-hierarchy-of-configuration-values-using-the-treebuilder) 612 | for all the cool stuff you can do in your configuration tree: 613 | 614 | ```php 615 | 616 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 617 | use Symfony\Component\Config\Definition\ConfigurationInterface; 618 | 619 | class ConfigTree implements ConfigurationInterface 620 | { 621 | public function getConfigTreeBuilder() 622 | { 623 | $treeBuilder = new TreeBuilder(); 624 | $rootNode = $treeBuilder->getRootNode(); 625 | 626 | $rootNode->children() 627 | ->boolean('devmode')->defaultValue(false)->end() 628 | ->scalarNode('encryption_key')->isRequired()->cannotBeEmpty()->end() 629 | ->arrayNode('db') 630 | ->children() 631 | ->scalarNode('host')->cannotBeEmpty()->defaultValue('localhost')->end() 632 | ->integerNode('port')->min(0)->defaultValue(3306)->end() 633 | ->scalarNode('driver')->cannotBeEmpty()->defaultValue('mysql')->end() 634 | ->scalarNode('dbname')->cannotBeEmpty()->end() 635 | ->scalarNode('user')->cannotBeEmpty()->end() 636 | ->scalarNode('password')->end() 637 | ->end() 638 | ->end() // End DB 639 | -end(); 640 | 641 | return $treeBuilder; 642 | } 643 | } 644 | 645 | ``` 646 | 647 | Load your configuration as you normally would, and then pass the resulting `ConfigValues` object through 648 | the Symfony filter: 649 | 650 | ```php 651 | 652 | use Configula\ConfigFactory; 653 | use Configula\Util\SymfonyConfigFilter; 654 | 655 | // Setup your config tree, and load your configuration 656 | $configTree = new ConfigTree(); 657 | $config = ConfigFactory::loadPath('/path/to/config'); 658 | 659 | // Validate the configuration by filtering it through the allowed values 660 | // If anything goes wrong here, a Symfony exception will be thrown (not a Configula exception) 661 | $config = SymfonyConfigFilter::filter($configTree, $config); 662 | ``` 663 | 664 | ## Writing your own loader 665 | 666 | In addition to using the built-in loaders, you can write your own loader. There are two ways to do this: 667 | 668 | ### Create your own file loader 669 | 670 | Extend the `Configula\Loader\AbstractFileLoader` to write your own loader that reads data from a file. 671 | 672 | ```php 673 | 674 | use Configula\Loader\AbstractFileLoader; 675 | 676 | class MyFileLoader extends AbstractFileLoader 677 | { 678 | /** 679 | * Parse file contents 680 | * 681 | * @param string $rawFileContents 682 | * @return array 683 | */ 684 | protected function parse(string $rawFileContents): array 685 | { 686 | // Parse the file contents and return an array of values. 687 | } 688 | } 689 | 690 | ``` 691 | 692 | Use it: 693 | 694 | ```php 695 | 696 | use Configula\ConfigFactory; 697 | 698 | // use the factory.. 699 | $config = ConfigFactory::load(new MyFileLoader('/path/to/file')); 700 | 701 | // ..or don't.. 702 | $config = (new MyFileLoader('/path/to/file'))->load(); 703 | ``` 704 | 705 | If you want to use the `FolderLoader` and automatically map your new type to a file extension, you can do so: 706 | 707 | ```php 708 | 709 | use Configula\Loader\FileLoader; 710 | use Configula\Loader\FolderLoader; 711 | 712 | // Map my custom file loader to the 'conf' extension type (case-insensitive) 713 | $extensionMap = array_merge(FileLoader::DEFAULT_EXTENSION_MAP, [ 714 | 'conf' => MyFileLoader::class 715 | ]); 716 | 717 | // Now any files encountered in the folder with .conf extension will use my custom file loader 718 | $config = (new FolderLoader('/path/to/folder', true, $extensionMap))->load(); 719 | 720 | ``` 721 | 722 | ### Create your own custom loader 723 | 724 | Create your own implementation of `Configula\Loader\ConfigLoaderInterface`, and you can load configuration from anywhere: 725 | 726 | ```php 727 | 728 | use Configula\Loader\ConfigLoaderInterface; 729 | use Configula\Exception\ConfigLoaderException; 730 | use Configula\ConfigValues; 731 | 732 | class MyLoader implements ConfigLoaderInterface 733 | { 734 | public function load(): ConfigValues 735 | { 736 | if (! $arrayOfValues = doWorkToLoadValuesHere()) { 737 | throw new ConfigLoaderException("Something went wrong.."); 738 | } 739 | 740 | return new ConfigValues($arrayOfValues); 741 | } 742 | } 743 | 744 | ``` 745 | Use it: 746 | 747 | ```php 748 | 749 | use Configula\ConfigFactory; 750 | 751 | // use the factory.. 752 | $config = ConfigFactory::load(new MyLoader()); 753 | 754 | // ..or use it directly. 755 | $config = (new MyLoader())->load(); 756 | ``` 757 | 758 | ## Change log 759 | 760 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 761 | 762 | ## Testing 763 | 764 | ``` bash 765 | $ composer test 766 | ``` 767 | 768 | ## Contributing 769 | 770 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 771 | 772 | ## Credits 773 | 774 | - [Casey McLaughlin][link-author] 775 | - [All Contributors][link-contributors] 776 | 777 | ## License 778 | 779 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 780 | 781 | [ico-version]: https://img.shields.io/packagist/v/caseyamcl/configula.svg 782 | [ico-downloads]: https://img.shields.io/packagist/dt/caseyamcl/configula.svg 783 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg 784 | [ico-ghbuild]: https://github.com/caseyamcl/configula/workflows/Github%20Build/badge.svg 785 | [ico-phpstan]: https://img.shields.io/badge/PHPStan-level%205-brightgreen.svg 786 | [ico-coverage]: https://github.com/caseyamcl/configula/blob/master/coverage.svg 787 | 788 | [link-packagist]: https://packagist.org/packages/caseyamcl/configula 789 | [link-downloads]: https://packagist.org/packages/caseyamcl/configula 790 | [link-author]: https://github.com/caseyamcl 791 | [link-contributors]: ../../contributors 792 | [link-phpstan]: https://phpstan.org/ 793 | [link-ghbuild]: https://github.com/caseyamcl/configula/actions?query=workflow%3A%22Github+Build%22 794 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrading from v4.x to v5.x 2 | 3 | Configula v5 requires a minimum PHP8.2, and adds all the typing and syntax features of modern PHP. 4 | Some changes were made from v4 to the public-facing API: 5 | 6 | ## Changes to Exceptions 7 | 8 | The abstract `ConfigException` class was replaced by `ConfigExceptionInterface`, which all exception classes 9 | implement. In your client replace all instances of `ConfigException` to `ConfigExceptionInterface`. 10 | 11 | # Upgrading from v3.x to v4.x 12 | 13 | Configula v4 introduces PHP8 support, and while there was an attempt to keep breaking changes to a minimum, 14 | there are a few to address: 15 | 16 | ## Move custom logic from `ConfigValues::__construct()` into `ConfigValues::init()` 17 | 18 | If you subclass `ConfigValues` and have custom logic in the `__construct()` method, you'll need to move that logic into 19 | the new `init()` method. The `ConfigValues::__construct()` is now declared final. 20 | 21 | ## Refactor any classes that extend concrete `Loader` classes 22 | 23 | The `*Loader` classes are all final now, except for `AbstractFileLoader`. If you extend any of them, you may 24 | need to refactor to use a composition-style rather than inheritance-style design pattern (e.g. decorator, etc.) 25 | 26 | # Upgrading from v2.x to v3.x 27 | 28 | Configula v3 is quite different from v2. But, you can replicate the behavior of Configula v2 29 | with minimal code changes as follows: 30 | 31 | ## Class name changes 32 | 33 | `Configula\Config` has now become `Configula\ConfigValues`. Update all references. 34 | 35 | ## Loading configuration 36 | 37 | Before upgrade: 38 | 39 | ```php 40 | use Configula\Config; 41 | 42 | $config = new Config('/path/to/config/files', ['default' => 'values']); 43 | ``` 44 | 45 | After upgrade: 46 | 47 | ```php 48 | use Configula\ConfigFactory; 49 | 50 | $config = ConfigFactory::loadPath('/path/to/config/files', ['default' => 'values']); 51 | ``` 52 | 53 | One behavior change in v3 is that `loadPath()` will now recursively load configuration files from your configuration 54 | path. If you want to read configuration files only from the top-level directory of your config path, you can do the 55 | following: 56 | 57 | ```php 58 | use Configula\ConfigFactory; 59 | 60 | $config = ConfigFactory::loadSingleDirectory('/path/to/config/files', ['default' => 'values']); 61 | ``` 62 | 63 | ## Adding configuration to an existing instance 64 | 65 | Before upgrade: 66 | 67 | ```php 68 | $config->loadConfig('/some/path'); 69 | ``` 70 | 71 | After upgrade: 72 | 73 | ```php 74 | // Note that the ConfigValues class is now immutable, so you need to use the instance that 75 | // is returned from the merge method. 76 | $config = $config->merge((new DecidingFileLoader('/some/path')->load())); 77 | ``` 78 | 79 | ## Getting a value 80 | 81 | Before upgrade: 82 | 83 | ```php 84 | $config->getItem('some_item', 'default'); 85 | ``` 86 | 87 | After upgrade: 88 | 89 | ```php 90 | // same behavior as v2 getItem() 91 | $config->get('some_item', 'default'); 92 | 93 | // returns NULL 94 | $config->find('non_existent_item'); 95 | 96 | // throws exception 97 | $config->get('non_existent_item'); 98 | ``` 99 | 100 | ## Checking if a value exists 101 | 102 | Before upgrade: 103 | 104 | ```php 105 | $config->valid('some_item'); 106 | ``` 107 | 108 | After upgrade: 109 | 110 | ```php 111 | $config->has('some_item'); 112 | ``` 113 | 114 | ## Getting an array copy of values 115 | 116 | Before upgrade: 117 | 118 | ```php 119 | $config->getItems(); 120 | ``` 121 | 122 | After upgrade: 123 | 124 | ```php 125 | $config->getArrayCopy(); 126 | ``` 127 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "caseyamcl/configula", 3 | "type": "library", 4 | "description": "A simple, but versatile, PHP config loader", 5 | "keywords": ["config", "configuration"], 6 | "homepage": "https://github.com/caseyamcl/Configula", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Casey McLaughlin", 11 | "email": "caseyamcl@gmail.com", 12 | "homepage": "https://caseymclaughlin.com", 13 | "role": "Developer" 14 | } 15 | ], 16 | "require": { 17 | "php" : "^8.2", 18 | "ext-json": "*", 19 | 20 | "symfony/yaml" : "^6.4|^7.0", 21 | "dflydev/dot-access-data": "^3.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit" : "^11.0", 25 | "vlucas/phpdotenv" : "^5.5", 26 | "symfony/config" : "^6.4|^7.0", 27 | "squizlabs/php_codesniffer" : "^3.5", 28 | "jaschilz/php-coverage-badger": "^2.0", 29 | "phpstan/phpstan" : "^2.0" 30 | }, 31 | "suggest": { 32 | "vlucas/phpdotenv": "Allows loading values from .env files", 33 | "symfony/config" : "Allows using the Symfony loader to set configuration file rules" 34 | }, 35 | "autoload": { 36 | "psr-4": { "Configula\\": "src/" } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { "Configula\\": "tests/" } 40 | }, 41 | "scripts": { 42 | "test": "XDEBUG_MODE=coverage vendor/bin/phpunit; vendor/bin/php-coverage-badger ./build/logs/clover.xml ./coverage.svg", 43 | "check-style": "vendor/bin/phpcs src tests", 44 | "fix-style": "vendor/bin/phpcbf src tests", 45 | "phpstan": "vendor/bin/phpstan analyse -l 5 src tests" 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 100% 19 | 100% 20 | 21 | -------------------------------------------------------------------------------- /src/ConfigFactory.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula; 21 | 22 | use Configula\Exception\ConfigFileNotFoundException; 23 | use Configula\Loader\CascadingConfigLoader; 24 | use Configula\Loader\EnvLoader; 25 | use Configula\Loader\FileListLoader; 26 | use Configula\Loader\FolderLoader; 27 | use Configula\Loader\FileLoader; 28 | use SplFileInfo; 29 | 30 | /** 31 | * Config Facade Class 32 | * 33 | * Provides convenience methods to load configuration using many different strategies in one-step 34 | */ 35 | class ConfigFactory 36 | { 37 | /** 38 | * Build configuration from array 39 | */ 40 | public static function fromArray(array $items): ConfigValues 41 | { 42 | return new ConfigValues($items); 43 | } 44 | 45 | /** 46 | * Load config using single loader 47 | * 48 | * This is the same as simply calling `$loader->load()` 49 | */ 50 | public static function load(ConfigLoaderInterface $loader): ConfigValues 51 | { 52 | return $loader->load(); 53 | } 54 | 55 | /** 56 | * Load from multiple sources 57 | * 58 | * Pass in an iterable list of multiple loaders, file names, or arrays of values 59 | * 60 | * @param iterable $items 61 | * @return ConfigValues 62 | */ 63 | public static function loadMultiple(iterable $items): ConfigValues 64 | { 65 | return CascadingConfigLoader::build($items)->load(); 66 | } 67 | 68 | /** 69 | * Load from an iterator of files 70 | * 71 | * Values are loaded in cascading fashion, with files later in the iterator taking precedence 72 | * 73 | * Missing or unreadable files are ignored. 74 | * 75 | * @param iterable $files 76 | * @return ConfigValues 77 | */ 78 | public static function loadFiles(iterable $files): ConfigValues 79 | { 80 | return (new FileListLoader($files))->load(); 81 | } 82 | 83 | /** 84 | * Build configuration by reading a single directory of files (non-recursive; ignores subdirectories) 85 | */ 86 | public static function loadSingleDirectory(string $configDirectory, array $defaults = []): ConfigValues 87 | { 88 | return (new ConfigValues($defaults))->merge((new FolderLoader($configDirectory, false))->load()); 89 | } 90 | 91 | /** 92 | * Build configuration by recursively reading a directory of files 93 | */ 94 | public static function loadPath(string $configPath = '', array $defaults = []): ConfigValues 95 | { 96 | // If path, use default behavior... 97 | if (is_dir($configPath)) { 98 | $pathValues = (new FolderLoader($configPath))->load(); 99 | } elseif (is_file($configPath)) { // Elseif file, then just load that single file... 100 | $pathValues = (new FileLoader($configPath))->load(); 101 | } elseif ($configPath === '') { // Else, no path provided, so empty values 102 | $pathValues = new ConfigValues([]); 103 | } else { 104 | throw new ConfigFileNotFoundException('Cannot resolve config path: ' . $configPath); 105 | } 106 | 107 | // Merge defaults and return 108 | return (new ConfigValues($defaults))->merge($pathValues); 109 | } 110 | 111 | /** 112 | * Load from environment looking only for those values with a specified prefix (and remove prefix) 113 | * 114 | * @param string $prefix Specify a prefix, and only environment variables with this prefix will be read 115 | * (e.g. "MYAPP_" means that this will only read env vars starting with 116 | * "MYAPP_") 117 | * @param string $delimiter Split variable names on this string into a nested array. (e.g. "MYSQL_HOST" 118 | * would become the key, "MYSQL.HOST" (empty string to not delimit) 119 | * @param bool $toLower Convert all keys to lower-case 120 | * 121 | * @return ConfigValues 122 | */ 123 | public static function loadEnv(string $prefix = '', string $delimiter = '', bool $toLower = false): ConfigValues 124 | { 125 | return ($prefix) 126 | ? EnvLoader::loadUsingPrefix($prefix, $delimiter, $toLower) 127 | : (new EnvLoader('', $delimiter, $toLower))->load(); 128 | } 129 | 130 | /** 131 | * Load configuration from environment variables using regex 132 | * 133 | * @param string $regex Optionally filter values based on some regex pattern 134 | * @param string $delimiter Split variable names on this string into a nested array. (e.g. "MYSQL_HOST" 135 | * would become the key, "MYSQL.HOST" (empty string to not delimit) 136 | * @param bool $toLower Convert all keys to lower-case 137 | * @return ConfigValues 138 | */ 139 | public static function loadEnvRegex(string $regex, string $delimiter = '', bool $toLower = false): ConfigValues 140 | { 141 | return (new EnvLoader($regex, $delimiter, $toLower))->load(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/ConfigLoaderInterface.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula; 21 | 22 | use Configula\Exception\ConfigLoaderException; 23 | 24 | interface ConfigLoaderInterface 25 | { 26 | /** 27 | * Load config 28 | * @throws ConfigLoaderException If loading fails for whatever reason, throw this exception 29 | */ 30 | public function load(): ConfigValues; 31 | } 32 | -------------------------------------------------------------------------------- /src/ConfigValues.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula; 21 | 22 | use ArrayAccess; 23 | use Configula\Exception\ConfigLogicException; 24 | use Configula\Util\ArrayUtils; 25 | use Countable; 26 | use Dflydev\DotAccessData\Data; 27 | use Configula\Exception\ConfigValueNotFoundException; 28 | use IteratorAggregate; 29 | use ReturnTypeWillChange; 30 | 31 | /** 32 | * Config Values Class 33 | * 34 | * Immutable class for storing configuration values 35 | * 36 | * @package Configula 37 | */ 38 | class ConfigValues implements IteratorAggregate, Countable, ArrayAccess 39 | { 40 | // Silly value, but we need one to reasonably ensure there is no collision with actual data 41 | public const NOT_SET = '__THe_VALUe___iS__Not_SET_l33t__'; 42 | 43 | private Data $values; 44 | 45 | /** 46 | * Construct from other config values 47 | * 48 | * @param ConfigValues $configValues 49 | * @return ConfigValues|static 50 | */ 51 | public static function fromConfigValues(ConfigValues $configValues): self 52 | { 53 | return new static($configValues->getArrayCopy()); 54 | } 55 | 56 | /** 57 | * Config constructor. 58 | * 59 | * @param array $values 60 | */ 61 | final public function __construct(array $values) 62 | { 63 | $this->values = new Data($values); 64 | $this->init(); 65 | } 66 | 67 | /** 68 | * Get a value 69 | * 70 | * @param string $path Accepts '.' path notation for nested values 71 | * @param mixed $default 72 | * @return mixed 73 | */ 74 | public function get(string $path, mixed $default = self::NOT_SET): mixed 75 | { 76 | return match (true) { 77 | // Check for top-level, even if it has a dot in the name 78 | isset($this->values->export()[$path]) => $this->values->export()[$path], 79 | 80 | // Use default dot-notation behavior for dot-access-data 81 | $this->values->has($path) => $this->values->get($path), 82 | 83 | // Return default if specified 84 | $default !== static::NOT_SET => $default, 85 | 86 | // Give up 87 | default => throw new ConfigValueNotFoundException('Config value not found: ' . $path), 88 | }; 89 | } 90 | 91 | /** 92 | * Find a configuration value, or return NULL if not found 93 | * 94 | * This is different from the get() method in that it will not throw an exception if the value doesn't exist. 95 | * 96 | * @param string $path Accepts '.' path notation for nested values 97 | */ 98 | public function find(string $path): mixed 99 | { 100 | return $this->get($path, null); 101 | } 102 | 103 | /** 104 | * Check if value exists (even if it is NULL) 105 | * 106 | * @param string $path 107 | * @return bool 108 | */ 109 | public function has(string $path): bool 110 | { 111 | return isset($this->values->export()[$path]) || $this->values->has($path); 112 | } 113 | 114 | /** 115 | * Check if value exists and has non-empty value 116 | * 117 | * Returns FALSE if value is NULL, empty array, or empty string 118 | * 119 | * @param string $path 120 | * @return bool 121 | */ 122 | public function hasValue(string $path): bool 123 | { 124 | $result = $this->get($path, null); 125 | return (! in_array($result, [null, '', []], true)); 126 | } 127 | 128 | /** 129 | * Get an array copy of config values 130 | * 131 | * @return array 132 | */ 133 | public function getArrayCopy(): array 134 | { 135 | return $this->values->export(); 136 | } 137 | 138 | // -------------------------------------------------------------- 139 | // Magic Method Access 140 | 141 | /** 142 | * Magic method - Get a value or path, or throw an exception 143 | * 144 | * @param string $path Accepts '.' path notation for nested values 145 | * @return mixed 146 | * @throws ConfigValueNotFoundException If the config value is not found 147 | */ 148 | public function __get(string $path) 149 | { 150 | return $this->get($path); 151 | } 152 | 153 | /** 154 | * Magic method - Check if a value or path exists 155 | * 156 | * @param string $path Accepts '.' path notation for nested values 157 | * @return bool 158 | */ 159 | public function __isset(string $path): bool 160 | { 161 | return $this->has($path); 162 | } 163 | 164 | /** 165 | * Magic method - Get a value or path, or throw an exception 166 | * 167 | * @param string $path Accepts '.' path notation for nested values 168 | * @param string $default 169 | * @return mixed 170 | */ 171 | public function __invoke(string $path, string $default = self::NOT_SET): mixed 172 | { 173 | return $this->get($path, $default); 174 | } 175 | 176 | // -------------------------------------------------------------- 177 | // Merging ConfigValues 178 | 179 | /** 180 | * Merge config values and return a new Config instance 181 | * 182 | * This is a recursive merge. Any sub-arrays will be cascade-merged. 183 | * 184 | * @param ConfigValues $config 185 | * @return static|ConfigValues 186 | */ 187 | public function merge(ConfigValues $config): ConfigValues 188 | { 189 | return new static(ArrayUtils::merge($this->getArrayCopy(), $config->getArrayCopy())); 190 | } 191 | 192 | /** 193 | * Merge values and return a new Config instance 194 | * 195 | * This is a recursive merge. Any sub-arrays will be cascade-merged. 196 | * 197 | * @param array $values 198 | * @return static|ConfigValues 199 | */ 200 | public function mergeValues(array $values): ConfigValues 201 | { 202 | return $this->merge(new ConfigValues($values)); 203 | } 204 | 205 | // -------------------------------------------------------------- 206 | // Iterating and counting interfaces 207 | 208 | /** 209 | * Iterator access 210 | * 211 | * Flattens the structure and implodes paths 212 | */ 213 | #[ReturnTypeWillChange] 214 | public function getIterator(): iterable 215 | { 216 | return ArrayUtils::flattenAndIterate($this->getArrayCopy()); 217 | } 218 | 219 | /** 220 | * Array-access to check if configuration value exists 221 | */ 222 | public function offsetExists(mixed $offset): bool 223 | { 224 | return $this->has($offset); 225 | } 226 | 227 | /** 228 | * Array-access to a configuration value 229 | */ 230 | #[ReturnTypeWillChange] 231 | public function offsetGet(mixed $offset): mixed 232 | { 233 | return $this->get($offset); 234 | } 235 | 236 | /** 237 | * Always throw exception. Cannot set config values in immutable object 238 | */ 239 | public function offsetSet(mixed $offset, mixed $value): void 240 | { 241 | throw new ConfigLogicException("Cannot set config value: " . $offset . "; config values are immutable"); 242 | } 243 | 244 | /** 245 | * Always throw exception. Cannot set config values in immutable object 246 | */ 247 | public function offsetUnset(mixed $offset): void 248 | { 249 | throw new ConfigLogicException("Cannot unset config value: " . $offset . "; config values are immutable"); 250 | } 251 | 252 | /** 253 | * Count values 254 | * 255 | * Counts all paths (not just the top-level paths) 256 | * 257 | * @return int 258 | */ 259 | public function count(): int 260 | { 261 | return count(iterator_to_array($this->getIterator())); 262 | } 263 | 264 | /** 265 | * Do nothing by default. 266 | * 267 | * This is to account for the fact that the `__construct()` method is now final, so 268 | * additional logic should go in here. 269 | */ 270 | protected function init(): void 271 | { 272 | // pass.. 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/Exception/ConfigExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Exception; 21 | 22 | /** 23 | * All exception classes in this library implement ConfigExceptionInterface 24 | */ 25 | interface ConfigExceptionInterface 26 | { 27 | // pass... 28 | } 29 | -------------------------------------------------------------------------------- /src/Exception/ConfigFileNotFoundException.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Exception; 21 | 22 | /** 23 | * Config File not found 24 | * 25 | * This is thrown when an expected config file is missing 26 | * 27 | * @author Casey McLaughlin 28 | */ 29 | class ConfigFileNotFoundException extends ConfigLoaderException 30 | { 31 | // pass ... 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/ConfigLoaderException.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Exception; 21 | 22 | use RuntimeException; 23 | 24 | /** 25 | * Class ConfigParseException 26 | * 27 | * Typically, this is thrown when there is a parsing error when loading configuration from various sources 28 | * 29 | * @package Configula\Exception 30 | */ 31 | class ConfigLoaderException extends RuntimeException implements ConfigExceptionInterface 32 | { 33 | // pass ... 34 | } 35 | -------------------------------------------------------------------------------- /src/Exception/ConfigLogicException.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Exception; 21 | 22 | use RuntimeException; 23 | 24 | /** 25 | * Config Logic Exception 26 | * 27 | * This exception is thrown when you attempt to use this library incorrectly. 28 | */ 29 | class ConfigLogicException extends RuntimeException implements ConfigExceptionInterface 30 | { 31 | // pass ... 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/ConfigValueNotFoundException.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Exception; 21 | 22 | use RuntimeException; 23 | 24 | /** 25 | * Class ConfigValueNotFoundException 26 | * 27 | * This is thrown when a configuration value is not found (or doesn't exist) 28 | */ 29 | class ConfigValueNotFoundException extends RuntimeException implements ConfigExceptionInterface 30 | { 31 | // pass ... 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/InvalidConfigValueException.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Exception; 21 | 22 | use RuntimeException; 23 | 24 | /** 25 | * Invalid Config Value Exception 26 | * 27 | * This is not used in the Configula source itself, but provided for implementing libraries 28 | * (see Configula documentation) 29 | */ 30 | class InvalidConfigValueException extends RuntimeException implements ConfigExceptionInterface 31 | { 32 | // pass ... 33 | } 34 | -------------------------------------------------------------------------------- /src/Exception/UnmappedFileExtensionException.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Exception; 21 | 22 | /** 23 | * Class UnmappedFileExtensionException 24 | * 25 | * @author Casey McLaughlin 26 | */ 27 | class UnmappedFileExtensionException extends ConfigLoaderException 28 | { 29 | // pass ... 30 | } 31 | -------------------------------------------------------------------------------- /src/Filter/ExtractTopLevelItemsFilter.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Filter; 21 | 22 | use Configula\ConfigValues; 23 | use Configula\Exception\ConfigLoaderException; 24 | 25 | /** 26 | * Extract items from given top-level item and elevate them to the top level 27 | * 28 | * @author Casey McLaughlin 29 | */ 30 | readonly class ExtractTopLevelItemsFilter 31 | { 32 | /** 33 | * Extract Top Level Item constructor. 34 | * 35 | * @param string $topLevelItemName 36 | * @param string $delimiter 37 | */ 38 | public function __construct( 39 | private string $topLevelItemName, 40 | private string $delimiter = '' 41 | ) { 42 | } 43 | 44 | /** 45 | * Remove top-level item and elevate its children to the top level 46 | * 47 | * @param ConfigValues $values 48 | * @return ConfigValues 49 | */ 50 | public function __invoke(ConfigValues $values): ConfigValues 51 | { 52 | $items = $values->getArrayCopy(); 53 | $delimiterParts = $this->delimiter 54 | ? array_filter(explode($this->delimiter, $this->topLevelItemName)) 55 | : [$this->topLevelItemName]; 56 | 57 | while ($current = array_shift($delimiterParts)) { 58 | $items = $this->elevate($items, $current); 59 | } 60 | 61 | return new ConfigValues($items); 62 | } 63 | 64 | /** 65 | * @param array $items 66 | * @param string $key 67 | * @return array 68 | */ 69 | private function elevate(array $items, string $key): array 70 | { 71 | if (! isset($items[$key]) or ! is_array($items[$key])) { 72 | return $items; 73 | } 74 | 75 | // Elevate all the sub-items from the array that are children of the prefix to remove 76 | foreach ($items[$key] as $k => $v) { 77 | // If item already exists in root node, throw exception 78 | if (isset($items[$k])) { 79 | throw new ConfigLoaderException( 80 | sprintf( 81 | 'Name collision (%s) when removing %s.%s from config values', 82 | $k, 83 | $this->topLevelItemName, 84 | $k 85 | ) 86 | ); 87 | } 88 | 89 | $items[$k] = $v; 90 | } 91 | 92 | unset($items[$key]); 93 | return $items; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Filter/RemovePrefixFilter.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Filter; 21 | 22 | use Configula\ConfigValues; 23 | 24 | /** 25 | * Class RemovePrefixFilter 26 | * 27 | * @author Casey McLaughlin 28 | */ 29 | readonly class RemovePrefixFilter 30 | { 31 | /** 32 | * Extract Top Level Item constructor. 33 | * 34 | * @param string $prefix 35 | */ 36 | public function __construct( 37 | private string $prefix 38 | ) { 39 | } 40 | 41 | /** 42 | * @param ConfigValues $values 43 | * @return ConfigValues 44 | */ 45 | public function __invoke(ConfigValues $values): ConfigValues 46 | { 47 | $newValues = []; 48 | $pattern = sprintf('/^%s/', preg_quote($this->prefix)); 49 | 50 | foreach ($values->getArrayCopy() as $name => $val) { 51 | $newValues[preg_replace($pattern, '', $name)] = $val; 52 | } 53 | 54 | return new ConfigValues($newValues); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Filter/SymfonyConfigFilter.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Filter; 21 | 22 | use Configula\ConfigValues; 23 | use Symfony\Component\Config\Definition\ConfigurationInterface; 24 | use Symfony\Component\Config\Definition\Processor; 25 | 26 | /** 27 | * Class SymfonyConfigFilter 28 | * 29 | * @package Configula\Util 30 | */ 31 | readonly class SymfonyConfigFilter 32 | { 33 | /** 34 | * Filter method allows single-call static usage of this class using default Processor 35 | */ 36 | public static function filter(ConfigurationInterface $configuration, ConfigValues $values): ConfigValues 37 | { 38 | $that = new static($configuration); 39 | return $that($values); 40 | } 41 | 42 | final public function __construct( 43 | private ConfigurationInterface $configTree, 44 | private ?Processor $processor = null 45 | ) { 46 | } 47 | 48 | /** 49 | * Process configuration through Symfony 50 | * 51 | * @param ConfigValues $values 52 | * @return ConfigValues 53 | */ 54 | public function __invoke(ConfigValues $values): ConfigValues 55 | { 56 | $processor = $this->processor ?: new Processor(); 57 | return new ConfigValues($processor->processConfiguration($this->configTree, $values->getArrayCopy())); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Loader/AbstractFileLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\ConfigValues; 23 | use Configula\Exception\ConfigFileNotFoundException; 24 | use Configula\Exception\ConfigLoaderException; 25 | 26 | /** 27 | * Class AbstractFileLoader 28 | * 29 | * @package Configula\Loader 30 | */ 31 | abstract class AbstractFileLoader implements FileLoaderInterface 32 | { 33 | public function __construct( 34 | private readonly string $filePath, 35 | private readonly bool $ensureExists = true 36 | ) { 37 | } 38 | 39 | /** 40 | * Load config 41 | */ 42 | public function load(): ConfigValues 43 | { 44 | if (! is_readable($this->filePath)) { 45 | if ($this->ensureExists) { 46 | throw (file_exists($this->filePath)) 47 | ? new ConfigLoaderException("Could not read configuration file: " . $this->filePath) 48 | : new ConfigFileNotFoundException('Config file not found: ' . $this->filePath); 49 | } else { 50 | return new ConfigValues([]); 51 | } 52 | } 53 | 54 | $values = $this->parse(file_get_contents($this->filePath)); 55 | return new ConfigValues($values); 56 | } 57 | 58 | /** 59 | * Parse the contents 60 | * 61 | * @param string $rawFileContents 62 | * @return array 63 | */ 64 | abstract protected function parse(string $rawFileContents): array; 65 | 66 | /** 67 | * @return string 68 | */ 69 | protected function getFilePath(): string 70 | { 71 | return $this->filePath; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Loader/ArrayValuesLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\ConfigLoaderInterface; 23 | use Configula\ConfigValues; 24 | 25 | /** 26 | * Class ArrayValuesLoader 27 | * 28 | * @package Configula\Loader 29 | */ 30 | final readonly class ArrayValuesLoader implements ConfigLoaderInterface 31 | { 32 | /** 33 | * ArrayValuesLoader constructor. 34 | * 35 | * @param array $configValues 36 | */ 37 | public function __construct( 38 | private array $configValues 39 | ) { 40 | } 41 | 42 | /** 43 | * Load config 44 | * 45 | * @return ConfigValues 46 | */ 47 | public function load(): ConfigValues 48 | { 49 | return new ConfigValues($this->configValues); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Loader/CascadingConfigLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\ConfigLoaderInterface; 23 | use Configula\ConfigValues; 24 | use InvalidArgumentException; 25 | use SplFileInfo; 26 | 27 | readonly class CascadingConfigLoader implements ConfigLoaderInterface 28 | { 29 | /** 30 | * Pass in an iterable list of multiple loaders, file names, or arrays of values 31 | * 32 | * @param iterable $items 33 | * @return CascadingConfigLoader 34 | */ 35 | public static function build(iterable $items): CascadingConfigLoader 36 | { 37 | foreach ($items as $item) { 38 | $loaders[] = match (true) { 39 | $item instanceof ConfigLoaderInterface => $item, 40 | is_array($item) => new ArrayValuesLoader($item), 41 | is_string($item) && file_exists($item) => new FileLoader($item), 42 | $item instanceof SplFileInfo => ($item->isDir()) ? new FolderLoader($item) : new FileLoader( 43 | (string)$item 44 | ), 45 | default => throw new InvalidArgumentException( 46 | sprintf( 47 | 'Invalid config source (allowed: array, string (filepath), \SplFileInfo, or config loader): %s', 48 | gettype($item) 49 | ) 50 | ), 51 | }; 52 | } 53 | 54 | return new static($loaders ?? []); 55 | } 56 | 57 | /** 58 | * CascadingLoader constructor. 59 | * 60 | * @param iterable $loaders Loaders, in the order that you want to load them 61 | */ 62 | final public function __construct( 63 | private iterable $loaders 64 | ) { 65 | } 66 | 67 | /** 68 | * Load config 69 | * 70 | * @return ConfigValues 71 | */ 72 | public function load(): ConfigValues 73 | { 74 | $config = new ConfigValues([]); 75 | 76 | foreach ($this->loaders as $loader) { 77 | $config = $config->merge($loader->load()); 78 | } 79 | 80 | return $config; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Loader/EnvLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\ConfigLoaderInterface; 23 | use Configula\ConfigValues; 24 | use Configula\Filter\ExtractTopLevelItemsFilter; 25 | use Configula\Filter\RemovePrefixFilter; 26 | use Dflydev\DotAccessData\Data; 27 | 28 | /** 29 | * Env Loader 30 | * 31 | * Loads configuration from each environment variable 32 | * 33 | * @package Configula\Loader 34 | */ 35 | final class EnvLoader implements ConfigLoaderInterface 36 | { 37 | private string $regex; 38 | private string $delimiter; 39 | private bool $toLower; 40 | 41 | /** 42 | * Load from environment looking only for those values with a specified prefix (and remove prefix) 43 | * 44 | * @param string $prefix Specify a prefix, and only environment variables with this prefix will be read 45 | * (e.g. "MYAPP_" means that this will only read env vars starting with 46 | * "MYAPP_") 47 | * @param string $delimiter Split variable names on this string into a nested array. (e.g. "MYSQL_HOST" 48 | * would become the key, "MYSQL.HOST" (empty string to not delimit) 49 | * @param bool $toLower Convert all keys to lower-case 50 | * @return ConfigValues 51 | */ 52 | public static function loadUsingPrefix(string $prefix, string $delimiter = '', bool $toLower = false): ConfigValues 53 | { 54 | $prefix = preg_quote($prefix); 55 | $values = (new EnvLoader("/^$prefix/", $delimiter, $toLower))->load(); 56 | 57 | return $delimiter 58 | ? (new ExtractTopLevelItemsFilter($toLower ? strtolower($prefix) : $prefix, $delimiter))->__invoke($values) 59 | : (new RemovePrefixFilter($toLower ? strtolower($prefix) : $prefix))->__invoke($values); 60 | } 61 | 62 | /** 63 | * Environment Loader Constructor 64 | * 65 | * @param string $regex Optionally filter values based on some regex pattern 66 | * @param string $delimiter Split variable names on this string into a nested array. (e.g. "MYSQL_HOST" 67 | * would become the key, "MYSQL.HOST" (empty string to not delimit) 68 | * @param bool $toLower Convert all keys to lower-case 69 | */ 70 | public function __construct(string $regex = '', string $delimiter = '', bool $toLower = false) 71 | { 72 | $this->regex = $regex; 73 | $this->delimiter = $delimiter; 74 | $this->toLower = $toLower; 75 | } 76 | 77 | /** 78 | * @return ConfigValues 79 | */ 80 | public function load(): ConfigValues 81 | { 82 | $configValues = new Data(); 83 | 84 | // Make sure we capture *ALL* environment values 85 | $envValues = array_merge(getenv(), $_ENV); 86 | 87 | foreach ($envValues as $valName => $valVal) { 88 | if ($this->regex && ! preg_match($this->regex, $valName)) { 89 | continue; 90 | } 91 | 92 | $valName = ($this->delimiter) ? str_replace($this->delimiter, '.', $valName) : $valName; 93 | $valName = ($this->toLower) ? strtolower($valName) : $valName; 94 | $configValues->set($valName, $this->prepareVal((string) $valVal)); 95 | } 96 | 97 | return new ConfigValues($configValues->export()); 98 | } 99 | 100 | /** 101 | * Prepare string value 102 | */ 103 | private function prepareVal(string $value): bool|float|int|string|null 104 | { 105 | if (is_numeric($value)) { 106 | return filter_var($value, FILTER_VALIDATE_INT) !== false ? (int) $value : (float) $value; 107 | } 108 | 109 | return match (strtolower($value)) { 110 | 'null' => null, 111 | 'false' => false, 112 | 'true' => true, 113 | default => $value, 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Loader/FileListLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\ConfigLoaderInterface; 23 | use Configula\ConfigValues; 24 | use Configula\Exception\ConfigFileNotFoundException; 25 | use Configula\Exception\ConfigLoaderException; 26 | use Configula\Exception\UnmappedFileExtensionException; 27 | use SplFileInfo; 28 | 29 | /** 30 | * Loader that loads from files, but ignores missing/unmapped files 31 | * 32 | * @package Configula\Loader 33 | */ 34 | final readonly class FileListLoader implements ConfigLoaderInterface 35 | { 36 | /** 37 | * FileListLoader constructor. 38 | * 39 | * @param iterable $files 40 | * @param array $extensionMap 41 | */ 42 | public function __construct( 43 | private iterable $files, 44 | private array $extensionMap = FileLoader::DEFAULT_EXTENSION_MAP 45 | ) { 46 | } 47 | 48 | /** 49 | * Load config 50 | * 51 | * @return ConfigValues 52 | * @throws ConfigLoaderException If loading fails for any reason, throw this exception 53 | */ 54 | public function load(): ConfigValues 55 | { 56 | $values = new ConfigValues([]); 57 | 58 | foreach ($this->files as $file) { 59 | $fileInfo = ($file instanceof SplFileInfo) ? $file : new SplFileInfo($file); 60 | try { 61 | $newValues = (new FileLoader((string) $fileInfo->getRealPath(), $this->extensionMap))->load(); 62 | $values = $values->merge($newValues); 63 | } catch (ConfigFileNotFoundException | UnmappedFileExtensionException) { 64 | // pass.. 65 | } 66 | } 67 | 68 | return $values; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Loader/FileLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\ConfigValues; 23 | use Configula\Exception\ConfigLoaderException; 24 | use Configula\Exception\UnmappedFileExtensionException; 25 | use Error; 26 | use SplFileInfo; 27 | 28 | /** 29 | * File Loader 30 | * 31 | * Strategy class to load a file based on its extension 32 | * 33 | * @package Configula\Loader 34 | */ 35 | final readonly class FileLoader implements FileLoaderInterface 36 | { 37 | public const DEFAULT_EXTENSION_MAP = [ 38 | 'yml' => YamlFileLoader::class, 39 | 'yaml' => YamlFileLoader::class, 40 | 'json' => JsonFileLoader::class, 41 | 'php' => PhpFileLoader::class, 42 | 'ini' => IniFileLoader::class 43 | ]; 44 | 45 | /** 46 | * FileLoader constructor. 47 | * 48 | * @param string $filePath 49 | * @param array|string[] $extensionMap Keys are case-insensitive extensions; values are class names 50 | */ 51 | public function __construct( 52 | private string $filePath, 53 | private array $extensionMap = self::DEFAULT_EXTENSION_MAP 54 | ) { 55 | } 56 | 57 | /** 58 | * Load config 59 | * 60 | * @return ConfigValues 61 | * @throws ConfigLoaderException If file is missing, not readable, or if no registered loader for file extension 62 | */ 63 | public function load(): ConfigValues 64 | { 65 | $file = new SplFileInfo($this->filePath); 66 | 67 | if (array_key_exists(strtolower($file->getExtension()), $this->extensionMap)) { 68 | $className = $this->extensionMap[strtolower($file->getExtension())]; 69 | 70 | try { 71 | return (new $className((string) $file->getRealPath()))->load(); 72 | } catch (Error $e) { 73 | if (! is_a($className, AbstractFileLoader::class, true)) { 74 | throw new ConfigLoaderException( 75 | sprintf( 76 | 'File loader class %s must be instance of %s', 77 | $className, 78 | AbstractFileLoader::class 79 | ) 80 | ); 81 | } else { 82 | throw $e; 83 | } 84 | } 85 | } else { 86 | throw new UnmappedFileExtensionException( 87 | sprintf( 88 | "Error parsing file (no loader for extension '%s'): %s", 89 | $file->getExtension(), 90 | $file 91 | ) 92 | ); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Loader/FileLoaderInterface.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\ConfigLoaderInterface; 23 | 24 | /** 25 | * Interface FileLoaderInterface 26 | * 27 | * @author Casey McLaughlin 28 | */ 29 | interface FileLoaderInterface extends ConfigLoaderInterface 30 | { 31 | /** 32 | * FileLoaderInterface constructor. 33 | * 34 | * @param string $filePath 35 | */ 36 | public function __construct(string $filePath); 37 | } 38 | -------------------------------------------------------------------------------- /src/Loader/FolderLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use CallbackFilterIterator; 23 | use Configula\ConfigLoaderInterface; 24 | use Configula\ConfigValues; 25 | use Configula\Exception\ConfigLoaderException; 26 | use Configula\Util\LocalDistFileIterator; 27 | use FilesystemIterator; 28 | use RecursiveDirectoryIterator; 29 | use RecursiveIteratorIterator; 30 | use SplFileInfo; 31 | 32 | /** 33 | * Config Folder Files Loader 34 | * 35 | * This provides v2.x functionality/compatibility to v3.x/v4.x 36 | * 37 | * Loads all known 38 | * 39 | * @package Configula\Loader 40 | */ 41 | final readonly class FolderLoader implements ConfigLoaderInterface 42 | { 43 | /** 44 | * ConfigFolderFilesLoader constructor. 45 | * 46 | * @param string|SplFileInfo $path 47 | * @param bool $recursive 48 | */ 49 | public function __construct( 50 | private string|SplFileInfo $path, 51 | private bool $recursive = true 52 | ) { 53 | } 54 | 55 | /** 56 | * Load config 57 | * 58 | * @return ConfigValues 59 | */ 60 | public function load(): ConfigValues 61 | { 62 | // Check valid path 63 | if (! is_readable($this->path) or ! is_dir($this->path)) { 64 | throw new ConfigLoaderException( 65 | 'Cannot read from folder path (does it exist? is it a directory?): ' . $this->path 66 | ); 67 | } 68 | 69 | // Build either a recursive directory iterator a single directory (files only) iterator 70 | $innerIterator = $this->recursive 71 | ? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->path)) 72 | : new CallbackFilterIterator(new FilesystemIterator($this->path), function (SplFileInfo $info) { 73 | return $info->isFile(); 74 | }); 75 | 76 | // Use FileListLoader to load configuration respecting local/dist extension ordering 77 | return (new FileListLoader(new LocalDistFileIterator($innerIterator)))->load(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Loader/IniFileLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\ConfigValues; 23 | use Configula\Exception\ConfigLoaderException; 24 | 25 | final readonly class IniFileLoader implements FileLoaderInterface 26 | { 27 | public function __construct( 28 | private string $filePath, 29 | private bool $processSections = true 30 | ) { 31 | } 32 | 33 | public function load(): ConfigValues 34 | { 35 | $values = @parse_ini_file($this->filePath, $this->processSections, INI_SCANNER_TYPED); 36 | 37 | if ($values === false) { 38 | throw new ConfigLoaderException("Error parsing INI file: " . $this->filePath); 39 | } 40 | 41 | return new ConfigValues($values); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Loader/JsonEnvLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\ConfigLoaderInterface; 23 | use Configula\ConfigValues; 24 | use Configula\Exception\ConfigLoaderException; 25 | 26 | final readonly class JsonEnvLoader implements ConfigLoaderInterface 27 | { 28 | public function __construct( 29 | private string $envValueName, 30 | private bool $asAssoc = false 31 | ) { 32 | } 33 | 34 | public function load(): ConfigValues 35 | { 36 | $rawContent = getenv($this->envValueName); 37 | 38 | if (! $decoded = @json_decode($rawContent, $this->asAssoc)) { 39 | throw new ConfigLoaderException("Could not parse JSON from environment variable: " . $this->envValueName); 40 | } 41 | 42 | return new ConfigValues((array) $decoded); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Loader/JsonFileLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\Exception\ConfigLoaderException; 23 | 24 | /** 25 | * Class JsonFileLoader 26 | * 27 | * @package Configula\Loader 28 | */ 29 | final class JsonFileLoader extends AbstractFileLoader 30 | { 31 | protected function parse(string $rawFileContents): array 32 | { 33 | if (trim($rawFileContents) === '') { 34 | return []; 35 | } elseif (! $decoded = @json_decode($rawFileContents)) { 36 | throw new ConfigLoaderException("Could not parse JSON file: " . $this->getFilePath()); 37 | } 38 | 39 | return (array) $decoded; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Loader/PhpFileLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\ConfigValues; 23 | use Configula\Exception\ConfigLoaderException; 24 | use Throwable; 25 | 26 | /** 27 | * Class PhpFileLoader 28 | * 29 | * @package Configula\Loader 30 | */ 31 | final readonly class PhpFileLoader implements FileLoaderInterface 32 | { 33 | public function __construct( 34 | private string $filePath 35 | ) { 36 | } 37 | 38 | public function load(): ConfigValues 39 | { 40 | // If file is not readable for any reason (permissions, etc.), throw an exception 41 | if (! is_readable($this->filePath)) { 42 | throw new ConfigLoaderException("Error reading config from file (check permissions?): " . $this->filePath); 43 | } 44 | 45 | // If file is empty, just return empty ConfigValues object 46 | if (trim(file_get_contents($this->filePath)) === "") { 47 | return new ConfigValues([]); 48 | } 49 | 50 | try { 51 | $config = null; 52 | 53 | // Loading the file will either overwrite the $config variable or the file itself will return an array 54 | ob_start(); 55 | $configFromReturn = include $this->filePath; 56 | ob_end_clean(); 57 | 58 | /** @phpstan-ignore-next-line Ignore because we are doing some things that PHPStan doesn't understand */ 59 | if (!is_array($config)) { 60 | $config = $configFromReturn; 61 | } 62 | 63 | // If the config file still isn't an array, throw an exception 64 | if (!is_array($config)) { 65 | throw new ConfigLoaderException("Missing or invalid \$config array in file: " . $this->filePath); 66 | } 67 | return new ConfigValues($config); 68 | } catch (Throwable $e) { 69 | throw new ConfigLoaderException( 70 | "Error loading configuration from file: " . $this->filePath, 71 | $e->getCode(), 72 | $e 73 | ); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Loader/YamlFileLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Loader; 21 | 22 | use Configula\Exception\ConfigLoaderException; 23 | use Symfony\Component\Yaml\Exception\ParseException; 24 | use Symfony\Component\Yaml\Parser; 25 | use Symfony\Component\Yaml\Yaml; 26 | 27 | /** 28 | * Class YamlFileLoader 29 | * 30 | * @package FandF\Config 31 | */ 32 | final class YamlFileLoader extends AbstractFileLoader 33 | { 34 | public function __construct( 35 | string $yamlFilePath, 36 | bool $required = true, 37 | private readonly ?Parser $yamlParser = null 38 | ) { 39 | parent::__construct($yamlFilePath, $required); 40 | } 41 | 42 | protected function parse(string $rawFileContents): array 43 | { 44 | $parser = $this->yamlParser ?? new Parser(); 45 | 46 | try { 47 | return (array) $parser->parse($rawFileContents, Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE); 48 | } catch (ParseException $e) { 49 | throw new ConfigLoaderException("Could not parse YAML file: " . $this->getFilePath(), $e->getCode(), $e); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Util/ArrayUtils.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Util; 21 | 22 | use Generator; 23 | 24 | /** 25 | * Configula Utilities Class 26 | * 27 | * @author Casey McLaughlin 28 | */ 29 | class ArrayUtils 30 | { 31 | /** 32 | * Flatten and iterate 33 | */ 34 | public static function flattenAndIterate(array $array, string $delimiter = '.', string $basePath = ''): Generator 35 | { 36 | foreach ($array as $key => $value) { 37 | $fullKey = implode($delimiter, array_filter([$basePath, $key])); 38 | if (is_array($value)) { 39 | yield from static::flattenAndIterate($value, $delimiter, $fullKey); 40 | } else { 41 | yield $fullKey => $value; 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Merge configuration arrays 48 | * 49 | * What I would wish that array_merge_recursive actually does. 50 | * 51 | * This is a cascading merge, with individual values being overwritten. 52 | * From: http://www.php.net/manual/en/function.array-merge-recursive.php#102379 53 | */ 54 | public static function merge(array $arr1, array $arr2): array 55 | { 56 | foreach ($arr2 as $key => $value) { 57 | if (array_key_exists($key, $arr1) && is_array($value) && is_array($arr1[$key])) { 58 | $arr1[$key] = static::merge($arr1[$key], $value); 59 | } else { 60 | $arr1[$key] = $value; 61 | } 62 | } 63 | 64 | return $arr1; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Util/LocalDistFileIterator.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, - please view the LICENSE.md 13 | * file that was distributed with this source code. 14 | * 15 | * ------------------------------------------------------------------ 16 | */ 17 | 18 | declare(strict_types=1); 19 | 20 | namespace Configula\Util; 21 | 22 | use Generator; 23 | use IteratorAggregate; 24 | use ReturnTypeWillChange; 25 | use SplFileInfo; 26 | 27 | /** 28 | * Local/Dist file iterator 29 | * 30 | * Iterates over files in the following order: 31 | * 32 | * *.dist.EXT (.dist is configurable) 33 | * *.EXT 34 | * *.local.EXT (.local is configurable) 35 | * 36 | * @author Casey McLaughlin 37 | */ 38 | readonly class LocalDistFileIterator implements IteratorAggregate 39 | { 40 | /** 41 | * LocalDistFileIterator constructor. 42 | * @param iterable|SplFileInfo[]|string[] $fileIterator Iterate either file paths or SplFileInfo instances 43 | * @param string $localSuffix File suffix denoting 'local' (high priority) files (always comes before extension) 44 | * @param string $distSuffix File suffix denoting 'dist' (low priority) files (always comes before extension) 45 | */ 46 | public function __construct( 47 | private iterable $fileIterator, 48 | private string $localSuffix = '.local', 49 | private string $distSuffix = '.dist' 50 | ) { 51 | } 52 | 53 | /** 54 | * @return Generator 55 | */ 56 | #[ReturnTypeWillChange] 57 | public function getIterator(): Generator 58 | { 59 | $localFiles = []; 60 | $normalFiles = []; 61 | 62 | foreach ($this->fileIterator as $file) { 63 | $basename = rtrim($file->getBasename(strtolower($file->getExtension())), '.'); 64 | 65 | if (strcasecmp(substr($basename, 0 - strlen($this->localSuffix)), $this->localSuffix) === 0) { 66 | $localFiles[] = $file; 67 | } elseif (strcasecmp(substr($basename, 0 - strlen($this->distSuffix)), $this->distSuffix) === 0) { 68 | yield $file; 69 | } else { 70 | $normalFiles[] = $file; 71 | } 72 | } 73 | 74 | foreach (array_merge($normalFiles, $localFiles) as $item) { 75 | yield $item; 76 | } 77 | } 78 | } 79 | --------------------------------------------------------------------------------