├── .php_cs ├── .phpunit-watcher.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── logo.png ├── psalm.xml └── src ├── Builder.php ├── ComposerEventHandler.php ├── Config ├── ConfigOutput.php ├── ConfigOutputFactory.php ├── Constants.php ├── Envs.php ├── Params.php └── System.php ├── ContentWriter.php ├── Env.php ├── Exception ├── BadConfigurationException.php ├── CircularDependencyException.php ├── ConfigBuildException.php ├── Exception.php ├── FailedReadException.php ├── FailedWriteException.php └── UnsupportedFileTypeException.php ├── Merger ├── Merger.php └── Modifier │ ├── InsertValueBeforeKey.php │ ├── ModifierInterface.php │ ├── RemoveKeys.php │ ├── ReplaceValue.php │ ├── ReverseBlockMerge.php │ ├── ReverseValues.php │ └── UnsetValue.php ├── Package.php ├── Package └── PackageFinder.php ├── Plugin.php ├── Reader ├── AbstractReader.php ├── EnvReader.php ├── JsonReader.php ├── PhpReader.php ├── ReaderFactory.php ├── ReaderInterface.php └── YamlReader.php └── Util ├── BuilderRequireEncoder.php ├── ClosureEncoder.php ├── EnvEncoder.php ├── Helper.php ├── ObjectEncoder.php ├── PathHelper.php └── Resolver.php /.php_cs: -------------------------------------------------------------------------------- 1 | setUsingCache(true) 15 | ->setRiskyAllowed(true) 16 | ->setRules(array( 17 | '@Symfony' => true, 18 | 'header_comment' => [ 19 | 'header' => $header, 20 | 'separate' => 'bottom', 21 | 'location' => 'after_declare_strict', 22 | 'commentType' => 'PHPDoc', 23 | ], 24 | 'binary_operator_spaces' => [ 25 | 'default' => null, 26 | ], 27 | 'concat_space' => ['spacing' => 'one'], 28 | 'array_syntax' => ['syntax' => 'short'], 29 | 'phpdoc_no_alias_tag' => ['replacements' => ['type' => 'var']], 30 | 'blank_line_before_return' => false, 31 | 'phpdoc_align' => false, 32 | 'phpdoc_scalar' => false, 33 | 'phpdoc_separation' => false, 34 | 'phpdoc_to_comment' => false, 35 | 'method_argument_space' => false, 36 | 'ereg_to_preg' => true, 37 | 'blank_line_after_opening_tag' => true, 38 | 'single_blank_line_before_namespace' => true, 39 | 'ordered_imports' => true, 40 | 'phpdoc_order' => true, 41 | 'pre_increment' => true, 42 | 'strict_comparison' => true, 43 | 'strict_param' => true, 44 | 'no_multiline_whitespace_before_semicolons' => true, 45 | 'semicolon_after_instruction' => false, 46 | 'yoda_style' => false, 47 | )) 48 | ->setFinder( 49 | PhpCsFixer\Finder::create() 50 | ->in(__DIR__) 51 | ->notPath('vendor') 52 | ->notPath('runtime') 53 | ->notPath('web/assets') 54 | ) 55 | ; 56 | -------------------------------------------------------------------------------- /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | risky: true 3 | 4 | version: 8 5 | 6 | finder: 7 | exclude: 8 | - docs 9 | - vendor 10 | - resources 11 | - views 12 | - public 13 | - templates 14 | not-name: 15 | - UnionCar.php 16 | - TimerUnionTypes.php 17 | - schema1.php 18 | 19 | enabled: 20 | - alpha_ordered_traits 21 | - array_indentation 22 | - array_push 23 | - combine_consecutive_issets 24 | - combine_consecutive_unsets 25 | - combine_nested_dirname 26 | - declare_strict_types 27 | - dir_constant 28 | - fully_qualified_strict_types 29 | - function_to_constant 30 | - hash_to_slash_comment 31 | - is_null 32 | - logical_operators 33 | - magic_constant_casing 34 | - magic_method_casing 35 | - method_separation 36 | - modernize_types_casting 37 | - native_function_casing 38 | - native_function_type_declaration_casing 39 | - no_alias_functions 40 | - no_empty_comment 41 | - no_empty_phpdoc 42 | - no_empty_statement 43 | - no_extra_block_blank_lines 44 | - no_short_bool_cast 45 | - no_superfluous_elseif 46 | - no_unneeded_control_parentheses 47 | - no_unneeded_curly_braces 48 | - no_unneeded_final_method 49 | - no_unset_cast 50 | - no_unused_imports 51 | - no_unused_lambda_imports 52 | - no_useless_else 53 | - no_useless_return 54 | - normalize_index_brace 55 | - php_unit_dedicate_assert 56 | - php_unit_dedicate_assert_internal_type 57 | - php_unit_expectation 58 | - php_unit_mock 59 | - php_unit_mock_short_will_return 60 | - php_unit_namespaced 61 | - php_unit_no_expectation_annotation 62 | - phpdoc_no_empty_return 63 | - phpdoc_no_useless_inheritdoc 64 | - phpdoc_order 65 | - phpdoc_property 66 | - phpdoc_scalar 67 | - phpdoc_separation 68 | - phpdoc_singular_inheritdoc 69 | - phpdoc_trim 70 | - phpdoc_trim_consecutive_blank_line_separation 71 | - phpdoc_type_to_var 72 | - phpdoc_types 73 | - phpdoc_types_order 74 | - print_to_echo 75 | - regular_callable_call 76 | - return_assignment 77 | - self_accessor 78 | - self_static_accessor 79 | - set_type_to_cast 80 | - short_array_syntax 81 | - short_list_syntax 82 | - simplified_if_return 83 | - single_quote 84 | - standardize_not_equals 85 | - ternary_to_null_coalescing 86 | - trailing_comma_in_multiline_array 87 | - unalign_double_arrow 88 | - unalign_equals 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Composer config plugin changelog 2 | 3 | ## 0.6.1 under development 4 | 5 | ## 0.6.0 March 24, 2021 6 | 7 | - Enh #147: Add context to errors (xepozz) 8 | - Chg #34: Better name for output config classes (samdark) 9 | - Enh #157: Backport "config-plugin-options" -> "source-directory" support from `yiisoft/config` (SilverFire) 10 | 11 | ## 0.5.0 December 24, 2020 12 | 13 | - Chg: Changed namespace to `Yiisoft/Composer/Config` ([@samdark]) 14 | - Fix #1: Use Composer to determine vendor directory (roxblnfk) 15 | - Enh #47: Add support for runtime environment variables via Env::get() (xepozz) 16 | - Enh: Add Composer 2 support ([@samdark]) 17 | - Bug #21: Fix merge failures on first Composer install / update ([@samdark]) 18 | - Enh: Make base path detection more reliable (xepozz, [@samdark]) 19 | - Bug #54: Fix failure when using short closure (xepozz) 20 | - Bug #72: Fix serialization of objects with closure (xepozz) 21 | - Enh #70: Removed generation of aliases.php (xepozz) 22 | - Enh #85: Rebuild now works regardless of composer dump-autoload (yiiliveext, [@samdark]) 23 | - Enh #113: Add `Builder::require('my-config')` that can be used for sub-configs ([@samdark]) 24 | - Enh #121: Clear output directory before build configuration (vjik) 25 | - Bug #58: Add PHP 8 support ([@samdark]) 26 | 27 | ## 0.4.0 March 8, 2020 28 | 29 | - Fixed config assembling on Windows ([@samdark]) 30 | - Added configuring of output dir ([@hiqsol]) 31 | - Added building alternative configs ([@hiqsol]) 32 | - Added support for `vlucas/phpdotenv` v4 ([@jseliga]) 33 | - Better work with env vars ([@hiqsol]) 34 | - Used `riimu/kit-phpencoder` for variable exporting ([@hiqsol]) 35 | - Bug fixes ([@hiqsol], [@SilverFire], [@samdark], [@noname007], [@jomonkj], [@machour]) 36 | 37 | ## 0.3.0 April 11, 2019 38 | 39 | - Fixed config reading and merging ([@hiqsol]) 40 | - Added dev-only configs ([@hiqsol], [@samdark]) 41 | - Changed to use `defines` files as is to keep values ([@hiqsol]) 42 | - Reworked configuration files building ([@hiqsol], [@marclaporte], [@loveorigami]) 43 | 44 | ## 0.2.5 May 19, 2017 45 | 46 | - Added showing package dependencies hierarchy tree with `composer du -v` ([@hiqsol]) 47 | 48 | ## 0.2.4 May 18, 2017 49 | 50 | - Added proper resolving of config dependencies with `Resolver` class ([@hiqsol]) 51 | - Fixed exportVar closures in Windows ([@SilverFire], [@edgardmessias]) 52 | 53 | ## 0.2.3 April 18, 2017 54 | 55 | - Added vendor dir arg to `Builder::path` to get config path at given vendor dir ([@hiqsol]) 56 | 57 | ## 0.2.2 April 12, 2017 58 | 59 | - Improved README ([@hiqsol]) 60 | - Added support for `.env`, JSON and YAML ([@hiqsol]) 61 | 62 | ## 0.2.1 March 23, 2017 63 | 64 | - Fixed wrong call of `Composer\Config::get()` ([@SilverFire]) 65 | 66 | ## 0.2.0 March 15, 2017 67 | 68 | - Added initializaion of composer autoloading for project classes become usable in configs ([@hiqsol]) 69 | - Added work with `$config_name` paths for use of already built config ([@hiqsol]) 70 | - Renamed pathes -> paths everywhere ([@hiqsol]) 71 | - Added collecting dev aliases for root package ([@hiqsol]) 72 | 73 | ## 0.1.0 December 26, 2016 74 | 75 | - Added proper rebuild ([@hiqsol]) 76 | - Changed output dir to `composer-config-plugin-output` ([@hiqsol]) 77 | - Changed: splitted out `Builder` ([@hiqsol]) 78 | - Changed namespace to `hiqdev\composer\config` ([@hiqsol]) 79 | 80 | ## 0.0.9 September 22, 2016 81 | 82 | - Fixed infinite loop in case of circular dependencies in composer ([@hiqsol]) 83 | 84 | ## 0.0.8 August 27, 2016 85 | 86 | - Added showing ordered list of packages when verbose option ([@hiqsol]) 87 | 88 | ## 0.0.7 August 26, 2016 89 | 90 | - Fixed packages processing order again, used original `composer.json` ([@hiqsol]) 91 | 92 | ## 0.0.6 August 24, 2016 93 | 94 | - Fixed packages processing order ([@hiqsol]) 95 | 96 | ## 0.0.5 June 22, 2016 97 | 98 | - Added multiple defines ([@hiqsol]) 99 | 100 | ## 0.0.4 May 21, 2016 101 | 102 | - Added multiple configs and params ([@hiqsol]) 103 | 104 | ## 0.0.3 May 20, 2016 105 | 106 | - Changed aliases assembling ([@hiqsol]) 107 | 108 | ## 0.0.2 May 19, 2016 109 | 110 | - Removed replace composer-extension-plugin ([@hiqsol]) 111 | 112 | ## 0.0.1 May 18, 2016 113 | 114 | - Added basics ([@hiqsol]) 115 | 116 | [@SilverFire]: https://github.com/SilverFire 117 | [d.naumenko.a@gmail.com]: https://github.com/SilverFire 118 | [@tafid]: https://github.com/tafid 119 | [andreyklochok@gmail.com]: https://github.com/tafid 120 | [@BladeRoot]: https://github.com/BladeRoot 121 | [bladeroot@gmail.com]: https://github.com/BladeRoot 122 | [@hiqsol]: https://github.com/hiqsol 123 | [sol@hiqdev.com]: https://github.com/hiqsol 124 | [@edgardmessias]: https://github.com/edgardmessias 125 | [edgardmessias@gmail.com]: https://github.com/edgardmessias 126 | [@samdark]: https://github.com/samdark 127 | [sam@rmcreative.ru]: https://github.com/samdark 128 | [@loveorigami]: https://github.com/loveorigami 129 | [loveorigami@mail.ru]: https://github.com/loveorigami 130 | [@marclaporte]: https://github.com/marclaporte 131 | [marc@laporte.name]: https://github.com/marclaporte 132 | [@jseliga]: https://github.com/jseliga 133 | [seliga.honza@gmail.com]: https://github.com/jseliga 134 | [@machour]: https://github.com/machour 135 | [machour@gmail.com]: https://github.com/machour 136 | [@jomonkj]: https://github.com/jomonkj 137 | [jomon.entero@gmail.com]: https://github.com/jomonkj 138 | [@noname007]: https://github.com/noname007 139 | [soul11201@gmail.com]: https://github.com/noname007 140 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Composer Config Plugin

4 |
5 |

6 | 7 | Composer plugin for config assembling. 8 | 9 | > ⚠️ The plugin is no longer supported in favor of [yiisoft/config](https://github.com/yiisoft/config). 10 | 11 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/composer-config-plugin/v/stable)](https://packagist.org/packages/yiisoft/composer-config-plugin) 12 | [![Total Downloads](https://poser.pugx.org/yiisoft/composer-config-plugin/downloads)](https://packagist.org/packages/yiisoft/composer-config-plugin) 13 | [![Build status](https://github.com/yiisoft/composer-config-plugin/workflows/build/badge.svg)](https://github.com/yiisoft/composer-config-plugin/actions) 14 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/yiisoft/composer-config-plugin/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/composer-config-plugin/?branch=master) 15 | [![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/composer-config-plugin/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/composer-config-plugin/?branch=master) 16 | 17 | This [Composer] plugin provides assembling 18 | of configurations distributed with composer packages. 19 | It allows putting configuration needed to use a package right inside of 20 | the package thus implementing a plugin system. The package becomes a plugin 21 | holding both the code and its configuration. 22 | 23 | ### Documentation 24 | 25 | - [English](docs/en/README.md) 26 | - [Russian](docs/ru/README.md) 27 | 28 | ## How it works? 29 | 30 | - Scans installed packages for `config-plugin` extra option in their 31 | `composer.json`. 32 | - Loads `.env` files to set `$_ENV` variables. 33 | - Requires `constants` files to set constants. 34 | - Requires `params` files. 35 | - Requires config files. 36 | - Options collected during earlier steps could and should be used in later 37 | steps, e.g. `$_ENV` should be used for constants and parameters, which 38 | in turn should be used for configs. 39 | - File processing order is crucial to achieve expected behavior: options 40 | in root package have priority over options from included packages. It is described 41 | below in **File processing order** section. 42 | - Collected configs are written as PHP files in 43 | `vendor/yiisoft/composer-config-plugin-output` 44 | directory along with information needed to rebuild configs on demand. 45 | - Then assembled configs are ready to be loaded into application using `require`. 46 | 47 | **Read more** about the general idea behind this plugin in [English] or 48 | [Russian]. 49 | 50 | [composer]: https://getcomposer.org/ 51 | [English]: https://hiqdev.com/pages/articles/app-organization 52 | [Russian]: https://habrahabr.ru/post/329286/ 53 | 54 | ## Installation 55 | 56 | ```sh 57 | composer require "yiisoft/composer-config-plugin" 58 | ``` 59 | 60 | Out of the box this plugin supports configs in PHP and JSON formats. 61 | 62 | To enable additional formats require: 63 | 64 | - [vlucas/phpdotenv] - for `.env` files. 65 | - [symfony/yaml] - for YAML files, `.yml` and `.yaml`. 66 | 67 | [vlucas/phpdotenv]: https://github.com/vlucas/phpdotenv 68 | [symfony/yaml]: https://github.com/symfony/yaml 69 | 70 | ## Usage 71 | 72 | List your config files in `composer.json` like the following: 73 | 74 | ```json 75 | "extra": { 76 | "config-plugin-output-dir": "path/relative-to-composer-json", 77 | "config-plugin": { 78 | "envs": "db.env", 79 | "params": [ 80 | "config/params.php", 81 | "?config/params-local.php" 82 | ], 83 | "common": "config/common.php", 84 | "web": [ 85 | "$common", 86 | "config/web.php" 87 | "../src/Modules/*/config/web.php" 88 | ], 89 | "other": "config/other.php" 90 | } 91 | }, 92 | ``` 93 | 94 | ### Markers 95 | 96 | - `?` - marks optional files. Absence of files not marked with it will cause exception. 97 | ``` 98 | "params": [ 99 | "params.php", 100 | "?params-local.php" 101 | ] 102 | ``` 103 | It's okay if `params-local.php` will not found, but it's not okay if `params.php` will be absent. 104 | 105 | - `*` - marks wildcard path. It means zero or more matches by wildcard mask. 106 | ``` 107 | "web": [ 108 | "../src/Modules/*/config/web.php" 109 | ] 110 | ``` 111 | It will collect all `web.php` in any subfolders of `src/Modules/` in `config` folder. 112 | 113 | - `$` - reference to another config. 114 | ``` 115 | "params": [ 116 | "params.php", 117 | "?params-local.php" 118 | ], 119 | "params-console": [ 120 | "$params", 121 | "params-console.php" 122 | ], 123 | "params-web": [ 124 | "$params", 125 | "params-web.php" 126 | ] 127 | ``` 128 | Output files `params-console.php` and `params-web.php` will contain `params.php` and `params-local.php`. 129 | 130 | *** 131 | 132 | Define your configs like the following: 133 | 134 | ```php 135 | [ 139 | 'db' => [ 140 | 'class' => \my\Db::class, 141 | 'name' => $params['db.name'], 142 | 'password' => $params['db.password'], 143 | ], 144 | ], 145 | ]; 146 | ``` 147 | 148 | A special variable `$params` is read from `params` config. 149 | 150 | To load assembled configs in your application use `require`: 151 | 152 | ```php 153 | $config = require Yiisoft\Composer\Config\Builder::path('web'); 154 | ``` 155 | 156 | ### Using sub-configs 157 | 158 | In some cases it is convenient to extract part of your config into another file. For example, we want to extract database 159 | configuration into `db.php`. To do it add the config to `composer.json`: 160 | 161 | ```json 162 | "extra": { 163 | "config-plugin-output-dir": "path/relative-to-composer-json", 164 | "config-plugin": { 165 | "envs": "db.env", 166 | "params": [ 167 | "config/params.php", 168 | "?config/params-local.php" 169 | ], 170 | "common": "config/common.php", 171 | "web": [ 172 | "$common", 173 | "config/web.php" 174 | ], 175 | "other": "config/other.php", 176 | "db": "config/db.php" 177 | } 178 | }, 179 | ``` 180 | 181 | Create `db.php`: 182 | 183 | ```php 184 | \my\Db::class, 188 | 'name' => $params['db.name'], 189 | 'password' => $params['db.password'], 190 | ]; 191 | ``` 192 | 193 | Then in the config use `Builder::require()`: 194 | 195 | ```php 196 | [ 202 | 'db' => Builder::require('db'), 203 | ], 204 | ]; 205 | ``` 206 | 207 | ### Refreshing config 208 | 209 | Plugin uses composer `POST_AUTOLOAD_DUMP` event i.e. composer runs this plugin on `install`, `update` and `dump-autoload` 210 | commands. As the result configs are ready to be used right after package installation or update. 211 | 212 | When you make changes to any of configs you may want to reassemble configs manually. In order to do it run: 213 | 214 | ```sh 215 | composer dump-autoload 216 | ``` 217 | 218 | Above can be shortened to `composer du`. 219 | 220 | If you need to force config rebuilding from your application, you can do it like the following: 221 | 222 | ```php 223 | // Don't do it in production, assembling takes it's time 224 | if (getenv('APP_ENV') === 'dev') { 225 | Yiisoft\Composer\Config\Builder::rebuild(); 226 | } 227 | ``` 228 | 229 | ### File processing order 230 | 231 | Config files are processed in proper order to achieve naturally expected 232 | behavior: 233 | 234 | - Options in outer packages override options from inner packages. 235 | - Plugin respects the order your configs are listed in `composer.json` with. 236 | - Different types of options are processed in the following order: 237 | - Environment variables from `envs`. 238 | - Constants from `constants`. 239 | - Parameters from `params`. 240 | - Configs are processed last of all. 241 | 242 | ### Debugging 243 | 244 | There are several ways to debug config building internals. 245 | 246 | - Plugin can show detected package dependencies hierarchy by running: 247 | 248 | ```sh 249 | composer dump-autoload --verbose 250 | ``` 251 | 252 | Above can be shortened to `composer du -v`. 253 | 254 | - You can see the assembled configs in the output directory which is 255 | `vendor/yiisoft/composer-config-plugin-output` by default and can be configured 256 | with `config-plugin-output-dir` extra option in `composer.json`. 257 | 258 | ## Known issues 259 | 260 | This plugin treats configs as simple PHP arrays. No specific 261 | structure or semantics are expected and handled. 262 | It is simple and straightforward, but I'm in doubt... 263 | What about errors and typos? 264 | I think about adding config validation rules provided together with 265 | plugins. Will it solve all the problems? 266 | 267 | ## License 268 | 269 | This project is released under the terms of the BSD-3-Clause [license](LICENSE). 270 | Read more [here](http://choosealicense.com/licenses/bsd-3-clause). 271 | 272 | Copyright © 2016-2020, HiQDev (http://hiqdev.com/) 273 | Copyright © 2020, Yiisoft (https://www.yiiframework.com/) 274 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/composer-config-plugin", 3 | "type": "composer-plugin", 4 | "description": "Composer plugin for config assembling", 5 | "keywords": [ 6 | "composer", 7 | "config", 8 | "assembling", 9 | "plugin" 10 | ], 11 | "homepage": "https://github.com/yiisoft/composer-config-plugin", 12 | "license": "BSD-3-Clause", 13 | "minimum-stability": "dev", 14 | "prefer-stable": true, 15 | "support": { 16 | "issues": "https://github.com/yiisoft/composer-config-plugin/issues?state=open", 17 | "forum": "https://www.yiiframework.com/forum/", 18 | "wiki": "https://www.yiiframework.com/wiki/", 19 | "irc": "irc://irc.freenode.net/yii", 20 | "source": "https://github.com/yiisoft/composer-config-plugin" 21 | }, 22 | "require": { 23 | "php": "^7.4|^8.0", 24 | "ext-json": "*", 25 | "composer-plugin-api": "^1.0|^2.0", 26 | "composer/composer": "^1.0|^2.0", 27 | "opis/closure": "3.6.x-dev@dev", 28 | "riimu/kit-phpencoder": "^2.4", 29 | "yiisoft/files": "^1.0" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^9.4", 33 | "roave/infection-static-analysis-plugin": "^1.6", 34 | "spatie/phpunit-watcher": "^1.23", 35 | "vimeo/psalm": "^4.3" 36 | }, 37 | "suggest": { 38 | "vlucas/phpdotenv": "^2.0 for `.env` files support", 39 | "symfony/yaml": "^2.0 || ^3.0 || ^4.0 for YAML files support" 40 | }, 41 | "config": { 42 | "sort-packages": true 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Yiisoft\\Composer\\Config\\": "src" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Yiisoft\\Composer\\Config\\Tests\\": "tests" 52 | } 53 | }, 54 | "extra": { 55 | "class": "Yiisoft\\Composer\\Config\\ComposerEventHandler", 56 | "branch-alias": { 57 | "dev-master": "1.0.x-dev" 58 | } 59 | }, 60 | "scripts": { 61 | "test": "phpunit --testdox --no-interaction", 62 | "test-watch": "phpunit-watcher watch" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiisoft/composer-config-plugin/781fa30e7fee5b3398da8049d9e19ddecc56dff2/logo.png -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | configFactory = $configFactory; 48 | $this->baseDir = $baseDir; 49 | $this->outputDir = self::findOutputDir($baseDir); 50 | } 51 | 52 | public function createAlternative($name): self 53 | { 54 | $alt = new static($this->configFactory, $this->baseDir); 55 | $alt->setOutputDir($this->outputDir . DIRECTORY_SEPARATOR . $name); 56 | $alt->configs['packages'] = $this->getConfig('packages')->clone($alt); 57 | 58 | return $alt; 59 | } 60 | 61 | public function setOutputDir(?string $outputDir): void 62 | { 63 | $this->outputDir = $outputDir 64 | ? static::buildAbsPath($this->getBaseDir(), $outputDir) 65 | : static::findOutputDir($this->getBaseDir()); 66 | } 67 | 68 | public static function rebuild(?string $baseDir = null): void 69 | { 70 | // Ensure COMPOSER_HOME is set in case web server does not give PHP OS environment variables 71 | if (!(getenv('APPDATA') || getenv('HOME') || getenv('COMPOSER_HOME'))) { 72 | $path = sys_get_temp_dir() . '/.composer'; 73 | if (!is_dir($path) && !mkdir($path)) { 74 | throw new \RuntimeException(sprintf('Directory "%s" was not created', $path)); 75 | } 76 | putenv('COMPOSER_HOME=' . $path); 77 | } 78 | 79 | Plugin::buildAllConfigs($baseDir ?? self::findBaseDir()); 80 | } 81 | 82 | /** 83 | * Returns default output dir. 84 | * 85 | * @param string|null $baseDir path to the root Composer package. When `null`, 86 | * 87 | * @throws JsonException 88 | * 89 | * @return string 90 | */ 91 | private static function findOutputDir(string $baseDir = null): string 92 | { 93 | if ($baseDir === null) { 94 | $baseDir = static::findBaseDir(); 95 | } 96 | $path = $baseDir . DIRECTORY_SEPARATOR . 'composer.json'; 97 | $data = @json_decode(file_get_contents($path), true); 98 | $dir = $data['extra'][Package::EXTRA_OUTPUT_DIR_OPTION_NAME] ?? null; 99 | 100 | return $dir ? static::buildAbsPath($baseDir, $dir) : static::defaultOutputDir($baseDir); 101 | } 102 | 103 | private static function findBaseDir(): string 104 | { 105 | $candidates = [ 106 | // normal relative path 107 | dirname(__DIR__, 4), 108 | // console 109 | getcwd(), 110 | // symlinked web 111 | dirname(getcwd()), 112 | ]; 113 | 114 | foreach ($candidates as $baseDir) { 115 | if (file_exists($baseDir . DIRECTORY_SEPARATOR . 'composer.json')) { 116 | return $baseDir; 117 | } 118 | } 119 | 120 | throw new \RuntimeException('Cannot find directory that contains composer.json'); 121 | } 122 | 123 | /** 124 | * Returns default output dir. 125 | * 126 | * @param string|null $baseDir path to base directory 127 | * 128 | * @return string 129 | */ 130 | private static function defaultOutputDir(string $baseDir = null): string 131 | { 132 | if ($baseDir) { 133 | $dir = $baseDir . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'yiisoft' . DIRECTORY_SEPARATOR . basename(dirname(__DIR__)); 134 | } else { 135 | $dir = dirname(__DIR__); 136 | } 137 | 138 | return $dir . static::OUTPUT_DIR_SUFFIX; 139 | } 140 | 141 | /** 142 | * Returns full path to assembled config file. 143 | * 144 | * @param string $filename name of config 145 | * @param string|null $baseDir path to base dir 146 | * 147 | * @throws JsonException 148 | * 149 | * @return string absolute path 150 | */ 151 | public static function path(string $filename, string $baseDir = null): string 152 | { 153 | return static::buildAbsPath(static::findOutputDir($baseDir), $filename . '.php'); 154 | } 155 | 156 | private static function buildAbsPath(string $dir, string $file): string 157 | { 158 | return self::isAbsolutePath($file) ? $file : $dir . DIRECTORY_SEPARATOR . $file; 159 | } 160 | 161 | private static function isAbsolutePath(string $path): bool 162 | { 163 | return strpos($path, '/') === 0 || strpos($path, ':') === 1 || strpos($path, '\\\\') === 0; 164 | } 165 | 166 | /** 167 | * Builds all (user and system) configs by given files list. 168 | * 169 | * @param array $files files to process: config name => list of files 170 | */ 171 | public function buildAllConfigs(array $files): void 172 | { 173 | if (is_dir($this->outputDir)) { 174 | FileHelper::clearDirectory($this->outputDir); 175 | } 176 | 177 | $this->buildUserConfigs($files); 178 | $this->buildSystemConfigs(); 179 | } 180 | 181 | /** 182 | * Builds configs by given files list. 183 | * 184 | * @param array $files files to process: config name => list of files 185 | * 186 | * @return array 187 | */ 188 | private function buildUserConfigs(array $files): array 189 | { 190 | $resolver = new Resolver($files); 191 | $files = $resolver->get(); 192 | foreach ($files as $name => $paths) { 193 | $this->getConfig($name)->load($paths)->build()->write(); 194 | } 195 | 196 | return $files; 197 | } 198 | 199 | private function buildSystemConfigs(): void 200 | { 201 | $this->getConfig('packages')->build()->write(); 202 | } 203 | 204 | public function getOutputPath(string $name): string 205 | { 206 | return $this->outputDir . DIRECTORY_SEPARATOR . $name . '.php'; 207 | } 208 | 209 | public function getConfig(string $name) 210 | { 211 | if (!array_key_exists($name, $this->configs)) { 212 | $this->configs[$name] = $this->configFactory->create($this, $name); 213 | } 214 | 215 | return $this->configs[$name]; 216 | } 217 | 218 | public function getVars(): array 219 | { 220 | $vars = []; 221 | foreach ($this->configs as $name => $config) { 222 | $vars[$name] = $config->getValues(); 223 | } 224 | 225 | return $vars; 226 | } 227 | 228 | public function setPackage(string $name, array $data): void 229 | { 230 | $this->getConfig('packages')->setValue($name, $data); 231 | } 232 | 233 | /** 234 | * @return string a full path to the project root 235 | */ 236 | public function getBaseDir(): string 237 | { 238 | return $this->baseDir; 239 | } 240 | 241 | /** 242 | * Require another configuration by name. 243 | * 244 | * It will result in "require 'my-config' in the assembled configuration file. 245 | * 246 | * @param string $config config name 247 | * 248 | * @return callable 249 | */ 250 | public static function require(string $config): callable 251 | { 252 | return static fn () => require $config; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/ComposerEventHandler.php: -------------------------------------------------------------------------------- 1 | [ 28 | ['onPostAutoloadDump', 0], 29 | ], 30 | ]; 31 | } 32 | 33 | public function activate(Composer $composer, IOInterface $io): void 34 | { 35 | $this->composer = $composer; 36 | $this->io = $io; 37 | } 38 | 39 | public function onPostAutoloadDump(Event $event): void 40 | { 41 | require_once $event->getComposer()->getConfig()->get('vendor-dir') . '/autoload.php'; 42 | 43 | $plugin = new Plugin($this->composer, $this->io); 44 | $plugin->build(); 45 | } 46 | 47 | public function deactivate(Composer $composer, IOInterface $io): void 48 | { 49 | // do nothing 50 | } 51 | 52 | public function uninstall(Composer $composer, IOInterface $io): void 53 | { 54 | // do nothing 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Config/ConfigOutput.php: -------------------------------------------------------------------------------- 1 | >>'; 21 | 22 | /** 23 | * @var string config name 24 | */ 25 | private string $name; 26 | 27 | /** 28 | * @var array sources - paths to config source files 29 | */ 30 | private array $sources = []; 31 | 32 | /** 33 | * @var array config value 34 | */ 35 | protected array $values = []; 36 | 37 | protected Builder $builder; 38 | 39 | protected ContentWriter $contentWriter; 40 | 41 | public function __construct(Builder $builder, string $name) 42 | { 43 | $this->builder = $builder; 44 | $this->name = $name; 45 | $this->contentWriter = new ContentWriter(); 46 | } 47 | 48 | public function clone(Builder $builder): self 49 | { 50 | $config = new self($builder, $this->name); 51 | $config->sources = $this->sources; 52 | $config->values = $this->values; 53 | 54 | return $config; 55 | } 56 | 57 | public function getValues(): array 58 | { 59 | return $this->values; 60 | } 61 | 62 | public function load(array $paths = []): self 63 | { 64 | $this->sources = $this->loadFiles($paths); 65 | 66 | return $this; 67 | } 68 | 69 | private function loadFiles(array $paths): array 70 | { 71 | switch (count($paths)) { 72 | case 0: 73 | return []; 74 | case 1: 75 | $path = reset($paths); 76 | if ($this->containsWildcard($path) === false) { 77 | return [$this->loadFile(reset($paths))]; 78 | } 79 | } 80 | 81 | $configs = []; 82 | foreach ($paths as $path) { 83 | $cs = $this->loadFiles($this->glob($path)); 84 | foreach ($cs as $config) { 85 | if (!empty($config)) { 86 | $configs[] = $config; 87 | } 88 | } 89 | } 90 | 91 | return $configs; 92 | } 93 | 94 | private function glob(string $path): array 95 | { 96 | if ($this->containsWildcard($path) === false) { 97 | return [$path]; 98 | } 99 | 100 | return glob($path); 101 | } 102 | 103 | private function containsWildcard(string $path): bool 104 | { 105 | return strpos($path, '*') !== false; 106 | } 107 | 108 | /** 109 | * Reads config file. 110 | * 111 | * @param string $path 112 | * 113 | * @return array configuration read from file 114 | */ 115 | protected function loadFile(string $path): array 116 | { 117 | $reader = ReaderFactory::get($this->builder, $path); 118 | 119 | try { 120 | return $reader->read($path); 121 | } catch (\Throwable $e) { 122 | throw new ConfigBuildException($e); 123 | } 124 | } 125 | 126 | /** 127 | * Merges given configs and writes at given name. 128 | * 129 | * @return ConfigOutput 130 | */ 131 | public function build(): self 132 | { 133 | $this->values = $this->calcValues($this->sources); 134 | 135 | return $this; 136 | } 137 | 138 | public function write(): self 139 | { 140 | $this->writeFile($this->getOutputPath(), $this->values); 141 | 142 | return $this; 143 | } 144 | 145 | protected function calcValues(array $sources): array 146 | { 147 | $values = Merger::merge(...$sources); 148 | 149 | return $this->substituteOutputDirs($values); 150 | } 151 | 152 | protected function writeFile(string $path, array $data): void 153 | { 154 | $depth = $this->findDepth(); 155 | $baseDir = $depth > 0 ? "dirname(__DIR__, $depth)" : '__DIR__'; 156 | 157 | $envs = $this->envsRequired() ? "\$_ENV = array_merge((array) require __DIR__ . '/envs.php', \$_ENV);" : ''; 158 | $constants = $this->constantsRequired() ? $this->builder->getConfig('constants')->buildRequires() : ''; 159 | $params = $this->paramsRequired() ? "\$params = (array) require __DIR__ . '/params.php';" : ''; 160 | $variables = Helper::exportVar($data); 161 | 162 | $content = <<contentWriter->write($path, $this->replaceMarkers($content) . "\n"); 177 | } 178 | 179 | protected function envsRequired(): bool 180 | { 181 | return true; 182 | } 183 | 184 | protected function constantsRequired(): bool 185 | { 186 | return true; 187 | } 188 | 189 | protected function paramsRequired(): bool 190 | { 191 | return true; 192 | } 193 | 194 | private function findDepth(): int 195 | { 196 | $outDir = PathHelper::realpath(dirname($this->getOutputPath())); 197 | $diff = substr($outDir, strlen(PathHelper::realpath($this->getBaseDir()))); 198 | 199 | return substr_count($diff, '/'); 200 | } 201 | 202 | private function replaceMarkers(string $content): string 203 | { 204 | return str_replace( 205 | ["'" . self::BASE_DIR_MARKER, "'?" . self::BASE_DIR_MARKER], 206 | ["\$baseDir . '", "'?' . \$baseDir . '"], 207 | $content 208 | ); 209 | } 210 | 211 | /** 212 | * Substitute output paths in given data array recursively with marker. 213 | * 214 | * @param array $data 215 | * 216 | * @return array 217 | */ 218 | protected function substituteOutputDirs(array $data): array 219 | { 220 | $dir = PathHelper::normalize($this->getBaseDir()); 221 | 222 | return $this->substitutePaths($data, $dir); 223 | } 224 | 225 | /** 226 | * Substitute all paths in given array recursively with marker if applicable. 227 | * 228 | * @param array $data 229 | * @param string $dir 230 | * 231 | * @return array 232 | */ 233 | private function substitutePaths($data, $dir): array 234 | { 235 | $res = []; 236 | foreach ($data as $key => $value) { 237 | $res[$this->substitutePath($key, $dir)] = $this->substitutePath($value, $dir); 238 | } 239 | 240 | return $res; 241 | } 242 | 243 | /** 244 | * Substitute all paths in given value if applicable. 245 | * 246 | * @param mixed $value 247 | * @param string $dir 248 | * 249 | * @return mixed 250 | */ 251 | private function substitutePath($value, $dir) 252 | { 253 | if (is_string($value)) { 254 | return $this->substitutePathInString($value, $dir); 255 | } 256 | if (is_array($value)) { 257 | return $this->substitutePaths($value, $dir); 258 | } 259 | 260 | return $value; 261 | } 262 | 263 | /** 264 | * Substitute path with marker in string if applicable. 265 | * 266 | * @param string $path 267 | * @param string $dir 268 | * 269 | * @return string 270 | */ 271 | private function substitutePathInString($path, $dir): string 272 | { 273 | $end = $dir . '/'; 274 | $skippable = 0 === strncmp($path, '?', 1); 275 | if ($skippable) { 276 | $path = substr($path, 1); 277 | } 278 | if ($path === $dir) { 279 | $result = self::BASE_DIR_MARKER; 280 | } elseif (strpos($path, $end) === 0) { 281 | $result = self::BASE_DIR_MARKER . substr($path, strlen($end) - 1); 282 | } else { 283 | $result = $path; 284 | } 285 | 286 | return ($skippable ? '?' : '') . $result; 287 | } 288 | 289 | private function getBaseDir(): string 290 | { 291 | return $this->builder->getBaseDir(); 292 | } 293 | 294 | protected function getOutputPath(string $name = null): string 295 | { 296 | return $this->builder->getOutputPath($name ?: $this->name); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/Config/ConfigOutputFactory.php: -------------------------------------------------------------------------------- 1 | System::class, 17 | 'packages' => System::class, 18 | 'envs' => Envs::class, 19 | 'params' => Params::class, 20 | 'constants' => Constants::class, 21 | ]; 22 | 23 | public function create(Builder $builder, string $name): ConfigOutput 24 | { 25 | $class = self::KNOWN_TYPES[$name] ?? ConfigOutput::class; 26 | 27 | return new $class($builder, $name); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Config/Constants.php: -------------------------------------------------------------------------------- 1 | values as $path) { 26 | $res[] = "require_once '$path';"; 27 | } 28 | 29 | return implode("\n", $res); 30 | } 31 | 32 | protected function constantsRequired(): bool 33 | { 34 | return false; 35 | } 36 | 37 | protected function paramsRequired(): bool 38 | { 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Config/Envs.php: -------------------------------------------------------------------------------- 1 | contentWriter->write($path, $content . PHP_EOL); 24 | } 25 | 26 | protected function envsRequired(): bool 27 | { 28 | return false; 29 | } 30 | 31 | protected function constantsRequired(): bool 32 | { 33 | return false; 34 | } 35 | 36 | protected function paramsRequired(): bool 37 | { 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Config/Params.php: -------------------------------------------------------------------------------- 1 | pushEnvVars(parent::calcValues($sources)); 15 | } 16 | 17 | protected function pushEnvVars(array $data): array 18 | { 19 | if (empty($data)) { 20 | return []; 21 | } 22 | 23 | $env = $this->builder->getConfig('envs')->getValues(); 24 | 25 | return self::pushValues($data, $env); 26 | } 27 | 28 | public static function pushValues(array $data, array $values, string $prefix = null) 29 | { 30 | foreach ($data as $key => &$value) { 31 | $subkey = $prefix===null ? $key : "${prefix}_$key"; 32 | 33 | $envkey = self::getEnvKey($subkey); 34 | if (isset($values[$envkey])) { 35 | $value = $values[$envkey]; 36 | } elseif (is_array($value)) { 37 | $value = self::pushValues($value, $values, $subkey); 38 | } 39 | } 40 | 41 | return $data; 42 | } 43 | 44 | private static function getEnvKey(string $key): string 45 | { 46 | return strtoupper(strtr($key, '.-', '__')); 47 | } 48 | 49 | protected function paramsRequired(): bool 50 | { 51 | return false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Config/System.php: -------------------------------------------------------------------------------- 1 | values[$name] = $value; 16 | 17 | return $this; 18 | } 19 | 20 | public function setValues(array $values): self 21 | { 22 | $this->values = $values; 23 | 24 | return $this; 25 | } 26 | 27 | public function mergeValues(array $values): self 28 | { 29 | $this->values = array_merge($this->values, $values); 30 | 31 | return $this; 32 | } 33 | 34 | public function load(array $paths = []): self 35 | { 36 | $path = $this->getOutputPath(); 37 | if (!file_exists($path)) { 38 | return $this; 39 | } 40 | 41 | $this->values = array_merge($this->loadFile($path), $this->values); 42 | 43 | return $this; 44 | } 45 | 46 | public function build(): self 47 | { 48 | $this->values = $this->substituteOutputDirs($this->values); 49 | 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ContentWriter.php: -------------------------------------------------------------------------------- 1 | $_ENV[$key] ?? $default; 13 | } 14 | 15 | return static fn () => $_ENV[$key]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exception/BadConfigurationException.php: -------------------------------------------------------------------------------- 1 | getFile(), 22 | $previous->getLine(), 23 | $previous->getMessage() 24 | ); 25 | 26 | parent::__construct($message, (int)$previous->getCode()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | $v) { 45 | if (is_int($k)) { 46 | if (array_key_exists($k, $res) && $res[$k] !== $v) { 47 | /** @var mixed */ 48 | $res[] = $v; 49 | } else { 50 | /** @var mixed */ 51 | $res[$k] = $v; 52 | } 53 | } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { 54 | $res[$k] = self::performMerge($res[$k], $v); 55 | } else { 56 | /** @var mixed */ 57 | $res[$k] = $v; 58 | } 59 | } 60 | } 61 | 62 | return $res; 63 | } 64 | 65 | private static function performReverseBlockMerge(array ...$args): array 66 | { 67 | $res = array_pop($args) ?: []; 68 | while (!empty($args)) { 69 | /** @psalm-var mixed $v */ 70 | foreach (array_pop($args) as $k => $v) { 71 | if (is_int($k)) { 72 | if (array_key_exists($k, $res) && $res[$k] !== $v) { 73 | /** @var mixed */ 74 | $res[] = $v; 75 | } else { 76 | /** @var mixed */ 77 | $res[$k] = $v; 78 | } 79 | } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { 80 | $res[$k] = self::performReverseBlockMerge($v, $res[$k]); 81 | } elseif (!isset($res[$k])) { 82 | /** @var mixed */ 83 | $res[$k] = $v; 84 | } 85 | } 86 | } 87 | 88 | return $res; 89 | } 90 | 91 | /** 92 | * Apply modifiers (classes that implement {@link ModifierInterface}) in array. 93 | * 94 | * For example, {@link \Yiisoft\Composer\Config\Merger\Modifier\UnsetValue} to unset value from previous 95 | * array or {@link \Yiisoft\Composer\Config\Merger\Modifier\ReplaceArrayValue} to force replace former 96 | * value instead of recursive merging. 97 | * 98 | * @param array $data 99 | * 100 | * @return array 101 | * 102 | * @see ModifierInterface 103 | */ 104 | private static function applyModifiers(array $data): array 105 | { 106 | $modifiers = []; 107 | /** @psalm-var mixed $v */ 108 | foreach ($data as $k => $v) { 109 | if ($v instanceof ModifierInterface) { 110 | $modifiers[$k] = $v; 111 | unset($data[$k]); 112 | } elseif (is_array($v)) { 113 | $data[$k] = self::applyModifiers($v); 114 | } 115 | } 116 | ksort($modifiers); 117 | foreach ($modifiers as $key => $modifier) { 118 | $data = $modifier->apply($data, $key); 119 | } 120 | return $data; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Merger/Modifier/InsertValueBeforeKey.php: -------------------------------------------------------------------------------- 1 | new InsertValueBeforeKey('some-value', 'a-key-to-insert-before'), 14 | * ``` 15 | * 16 | * ```php 17 | * $a = [ 18 | * 'name' => 'Yii', 19 | * 'version' => '1.0', 20 | * ]; 21 | * 22 | * $b = [ 23 | * 'version' => '1.1', 24 | * 'options' => [], 25 | * 'vendor' => new InsertValueBeforeKey('Yiisoft', 'name'), 26 | * ]; 27 | * 28 | * $result = Merger::merge($a, $b); 29 | * ``` 30 | * 31 | * Will result in: 32 | * 33 | * ```php 34 | * [ 35 | * 'vendor' => 'Yiisoft', 36 | * 'name' => 'Yii', 37 | * 'version' => '1.1', 38 | * 'options' => [], 39 | * ]; 40 | */ 41 | final class InsertValueBeforeKey implements ModifierInterface 42 | { 43 | /** @var mixed value of any type */ 44 | private $value; 45 | 46 | private string $key; 47 | 48 | /** 49 | * @param mixed $value value of any type 50 | * @param string $key 51 | */ 52 | public function __construct($value, string $key) 53 | { 54 | $this->value = $value; 55 | $this->key = $key; 56 | } 57 | 58 | public function apply(array $data, $key): array 59 | { 60 | $res = []; 61 | /** @psalm-var mixed $v */ 62 | foreach ($data as $k => $v) { 63 | if ($k === $this->key) { 64 | /** @var mixed */ 65 | $res[$key] = $this->value; 66 | } 67 | /** @var mixed */ 68 | $res[$k] = $v; 69 | } 70 | 71 | return $res; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Merger/Modifier/ModifierInterface.php: -------------------------------------------------------------------------------- 1 | new RemoveKeys(), 14 | * ``` 15 | * 16 | * ```php 17 | * $a = [ 18 | * 'name' => 'Yii', 19 | * 'version' => '1.0', 20 | * ]; 21 | * 22 | * $b = [ 23 | * 'version' => '1.1', 24 | * 'options' => [], 25 | * RemoveKeys::class => new RemoveKeys(), 26 | * ]; 27 | * 28 | * $result = Merger::merge($a, $b); 29 | * ``` 30 | * 31 | * Will result in: 32 | * 33 | * ```php 34 | * [ 35 | * 'Yii', 36 | * '1.1', 37 | * [], 38 | * ]; 39 | */ 40 | final class RemoveKeys implements ModifierInterface 41 | { 42 | public function apply(array $data, $key): array 43 | { 44 | return array_values($data); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Merger/Modifier/ReplaceValue.php: -------------------------------------------------------------------------------- 1 | [ 15 | * 1, 16 | * ], 17 | * 'validDomains' => [ 18 | * 'example.com', 19 | * 'www.example.com', 20 | * ], 21 | * ]; 22 | * 23 | * $array2 = [ 24 | * 'ids' => [ 25 | * 2, 26 | * ], 27 | * 'validDomains' => new \Yiisoft\Composer\Config\Merger\Modifier\ReplaceValue([ 28 | * 'yiiframework.com', 29 | * 'www.yiiframework.com', 30 | * ]), 31 | * ]; 32 | * 33 | * $result = Merger::merge($array1, $array2); 34 | * ``` 35 | * 36 | * The result will be 37 | * 38 | * ```php 39 | * [ 40 | * 'ids' => [ 41 | * 1, 42 | * 2, 43 | * ], 44 | * 'validDomains' => [ 45 | * 'yiiframework.com', 46 | * 'www.yiiframework.com', 47 | * ], 48 | * ] 49 | * ``` 50 | */ 51 | final class ReplaceValue implements ModifierInterface 52 | { 53 | /** 54 | * @var mixed value used as replacement 55 | */ 56 | public $value; 57 | 58 | /** 59 | * @param mixed $value value used as replacement 60 | */ 61 | public function __construct($value) 62 | { 63 | $this->value = $value; 64 | } 65 | 66 | public function apply(array $data, $key): array 67 | { 68 | /** @var mixed */ 69 | $data[$key] = $this->value; 70 | 71 | return $data; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Merger/Modifier/ReverseBlockMerge.php: -------------------------------------------------------------------------------- 1 | new ReverseBlockMerge(), 16 | * ``` 17 | * 18 | * For example: 19 | * 20 | * ```php 21 | * $one = [ 22 | * 'f' => 'f1', 23 | * 'b' => [ 24 | * 'b1' => 'b11', 25 | * 'b2' => 'b33', 26 | * ], 27 | * 'g' => 'g1', 28 | * 'h', 29 | * ]; 30 | * 31 | * $two = [ 32 | * 'a' => 'a1', 33 | * 'b' => [ 34 | * 'b1' => 'bv1', 35 | * 'b2' => 'bv2', 36 | * ], 37 | * 'd', 38 | * ]; 39 | * 40 | * $three = [ 41 | * 'a' => 'a2', 42 | * 'c' => 'c1', 43 | * 'b' => [ 44 | * 'b2' => 'bv22', 45 | * 'b3' => 'bv3', 46 | * ], 47 | * 'e', 48 | * ReverseBlockMerge::class => new ReverseBlockMerge(), 49 | * ]; 50 | * 51 | * $result = Merger::merge($one, $two, $three); 52 | * ``` 53 | * 54 | * Will result in: 55 | * 56 | * ```php 57 | * [ 58 | * 'a' => 'a2', 59 | * 'c' => 'c1', 60 | * 'b' => [ 61 | * 'b2' => 'bv22', 62 | * 'b3' => 'bv3', 63 | * 'b1' => 'bv1', 64 | * ] 65 | * 0 => 'e', 66 | * 1 => 'd', 67 | * 'f' => 'f1', 68 | * 'g' => 'g1', 69 | * 2 => 'h', 70 | * ] 71 | * ``` 72 | * 73 | * @see Merger::performReverseBlockMerge() 74 | */ 75 | final class ReverseBlockMerge implements ModifierInterface 76 | { 77 | public function apply(array $data, $key): array 78 | { 79 | return $data; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Merger/Modifier/ReverseValues.php: -------------------------------------------------------------------------------- 1 | new ReverseValues(), 16 | * ``` 17 | * 18 | * Usage example: 19 | * 20 | * ```php 21 | * 22 | * use Yiisoft\Composer\Config\Merger\ReverseValues; 23 | * 24 | * $array1 = [ 25 | * 'paths' => [ 26 | * '/tmp/tmp', 27 | * ReverseValues::class => new ReverseValues(), 28 | * ], 29 | * ]; 30 | * 31 | * $array2 = [ 32 | * 'paths' => [ 33 | * '/usr/bin', 34 | * ], 35 | * ]; 36 | * 37 | * $result = Merger::merge($array1, $array2); 38 | * ``` 39 | * 40 | * The result will be 41 | * 42 | * ```php 43 | * [ 44 | * 'paths' => [ 45 | * '/usr/bin', 46 | * '/tmp/tmp', 47 | * ], 48 | * ] 49 | * ``` 50 | */ 51 | class ReverseValues implements ModifierInterface 52 | { 53 | public function apply(array $data, $key): array 54 | { 55 | return array_reverse($data); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Merger/Modifier/UnsetValue.php: -------------------------------------------------------------------------------- 1 | [ 15 | * 1, 16 | * ], 17 | * 'validDomains' => [ 18 | * 'example.com', 19 | * 'www.example.com', 20 | * ], 21 | * ]; 22 | * 23 | * $array2 = [ 24 | * 'ids' => [ 25 | * 2, 26 | * ], 27 | * 'validDomains' => new \Yiisoft\Composer\Config\Merger\Modifier\UnsetValue(), 28 | * ]; 29 | * 30 | * $result = Merger::merge($array1, $array2); 31 | * ``` 32 | * 33 | * The result will be 34 | * 35 | * ```php 36 | * [ 37 | * 'ids' => [ 38 | * 1, 39 | * 2, 40 | * ], 41 | * ] 42 | * ``` 43 | */ 44 | final class UnsetValue implements ModifierInterface 45 | { 46 | public function apply(array $data, $key): array 47 | { 48 | unset($data[$key]); 49 | 50 | return $data; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Package.php: -------------------------------------------------------------------------------- 1 | package = $package; 48 | $this->filesystem = new Filesystem(); 49 | 50 | $this->vendorDir = $this->filesystem->normalizePath($vendorDir); 51 | $this->baseDir = dirname($this->vendorDir); 52 | $this->data = $this->readRawData(); 53 | } 54 | 55 | /** 56 | * @return string package pretty name, like: vendor/name 57 | */ 58 | public function getPrettyName(): string 59 | { 60 | return $this->package->getPrettyName(); 61 | } 62 | 63 | /** 64 | * @return string package version, like: 3.0.16.0, 9999999-dev 65 | */ 66 | public function getVersion(): string 67 | { 68 | return $this->package->getVersion(); 69 | } 70 | 71 | /** 72 | * @return string package CVS revision, like: 3a4654ac9655f32888efc82fb7edf0da517d8995 73 | */ 74 | public function getSourceReference(): ?string 75 | { 76 | return $this->package->getSourceReference(); 77 | } 78 | 79 | /** 80 | * @return string package dist revision, like: 3a4654ac9655f32888efc82fb7edf0da517d8995 81 | */ 82 | public function getDistReference(): ?string 83 | { 84 | return $this->package->getDistReference(); 85 | } 86 | 87 | /** 88 | * @return bool is package complete 89 | */ 90 | public function isComplete(): bool 91 | { 92 | return $this->package instanceof CompletePackageInterface; 93 | } 94 | 95 | /** 96 | * @return bool is this a root package 97 | */ 98 | public function isRoot(): bool 99 | { 100 | return $this->package instanceof RootPackageInterface; 101 | } 102 | 103 | /** 104 | * @return array autoload configuration array 105 | */ 106 | public function getAutoload(): array 107 | { 108 | return $this->getRawValue('autoload') ?? $this->package->getAutoload(); 109 | } 110 | 111 | /** 112 | * @return array autoload-dev configuration array 113 | */ 114 | public function getDevAutoload(): array 115 | { 116 | return $this->getRawValue('autoload-dev') ?? $this->package->getDevAutoload(); 117 | } 118 | 119 | /** 120 | * @return array require configuration array 121 | */ 122 | public function getRequires(): array 123 | { 124 | return $this->getRawValue('require') ?? $this->package->getRequires(); 125 | } 126 | 127 | /** 128 | * @return array require-dev configuration array 129 | */ 130 | public function getDevRequires(): array 131 | { 132 | return $this->getRawValue('require-dev') ?? $this->package->getDevRequires(); 133 | } 134 | 135 | /** 136 | * @return array files array 137 | */ 138 | public function getFiles(): array 139 | { 140 | return $this->getExtraValue(self::EXTRA_FILES_OPTION_NAME, []); 141 | } 142 | 143 | /** 144 | * @return array dev-files array 145 | */ 146 | public function getDevFiles(): array 147 | { 148 | return $this->getExtraValue(self::EXTRA_DEV_FILES_OPTION_NAME, []); 149 | } 150 | 151 | /** 152 | * @return mixed alternatives array or path to config 153 | */ 154 | public function getAlternatives() 155 | { 156 | return $this->getExtraValue(self::EXTRA_ALTERNATIVES_OPTION_NAME); 157 | } 158 | 159 | private function getConfigSourceDirectory(): ?string 160 | { 161 | $sourceDirPrefix = $this->getExtraValue(self::EXTRA_OPTIONS_MAP_NAME)['source-directory'] ?? ''; 162 | 163 | $sourceDir = $this->getPackageDirectory() . '/' . $sourceDirPrefix; 164 | 165 | return $this->filesystem->normalizePath($sourceDir); 166 | } 167 | 168 | /** 169 | * Get extra configuration value or default 170 | * 171 | * @param string $key key to look for in extra configuration 172 | * @param mixed $default default to return if there's no extra configuration value 173 | * 174 | * @return mixed extra configuration value or default 175 | */ 176 | private function getExtraValue(string $key, $default = null) 177 | { 178 | return $this->getExtra()[$key] ?? $default; 179 | } 180 | 181 | /** 182 | * @return array extra configuration array 183 | */ 184 | private function getExtra(): array 185 | { 186 | return $this->getRawValue('extra') ?? $this->package->getExtra(); 187 | } 188 | 189 | /** 190 | * @param string $name option name 191 | * 192 | * @return mixed raw value from composer.json if available 193 | */ 194 | private function getRawValue(string $name) 195 | { 196 | return $this->data[$name] ?? null; 197 | } 198 | 199 | /** 200 | * @throws \JsonException 201 | * 202 | * @return array composer.json contents as array 203 | */ 204 | private function readRawData(): array 205 | { 206 | $path = $this->preparePath('composer.json'); 207 | if (file_exists($path)) { 208 | return json_decode(file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); 209 | } 210 | 211 | return []; 212 | } 213 | 214 | /** 215 | * Builds path inside of a package. 216 | * 217 | * @param string $file 218 | * 219 | * @return string absolute paths will stay untouched 220 | */ 221 | public function preparePath(string $file): string 222 | { 223 | if (!$this->filesystem->isAbsolutePath($file)) { 224 | $file = $this->getPackageDirectory() . '/' . $file; 225 | } 226 | 227 | return $this->filesystem->normalizePath($file); 228 | } 229 | 230 | public function prepareConfigFilePath(string $file): string 231 | { 232 | if (0 === strncmp($file, '$', 1)) { 233 | return $file; 234 | } 235 | 236 | $skippable = 0 === strncmp($file, '?', 1) ? '?' : ''; 237 | if ($skippable) { 238 | $file = substr($file, 1); 239 | } 240 | 241 | if (!$this->filesystem->isAbsolutePath($file)) { 242 | $file = $this->getConfigSourceDirectory() . '/' . $file; 243 | } 244 | 245 | return $skippable . $this->filesystem->normalizePath($file); 246 | } 247 | 248 | public function getVendorDir(): string 249 | { 250 | return $this->vendorDir; 251 | } 252 | 253 | public function getBaseDir(): string 254 | { 255 | return $this->baseDir; 256 | } 257 | 258 | private function getPackageDirectory(): string 259 | { 260 | return $this->isRoot() 261 | ? $this->baseDir 262 | : $this->vendorDir . '/' . $this->getPrettyName(); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/Package/PackageFinder.php: -------------------------------------------------------------------------------- 1 | depth. 20 | * For order description see {@see findPackages()}. 21 | */ 22 | private array $orderedList = []; 23 | 24 | private PackageInterface $rootPackage; 25 | 26 | /** 27 | * @var PackageInterface[] 28 | */ 29 | private array $packages; 30 | 31 | private string $vendorDir; 32 | 33 | public function __construct(string $vendorDir, PackageInterface $rootPackage, array $packages) 34 | { 35 | $this->rootPackage = $rootPackage; 36 | $this->packages = $packages; 37 | $this->vendorDir = $vendorDir; 38 | } 39 | 40 | /** 41 | * Returns ordered list of packages: 42 | 43 | * - Packages listed earlier in the composer.json will get earlier in the list. 44 | * - Children are listed before parents. 45 | * 46 | * @return Package[] 47 | */ 48 | public function findPackages(): array 49 | { 50 | $root = new Package($this->rootPackage, $this->vendorDir); 51 | $this->plainList[$root->getPrettyName()] = $root; 52 | foreach ($this->packages as $package) { 53 | $this->plainList[$package->getPrettyName()] = new Package($package, $this->vendorDir); 54 | } 55 | $this->orderedList = []; 56 | $this->iteratePackage($root, true); 57 | 58 | $result = []; 59 | foreach (array_keys($this->orderedList) as $name) { 60 | /** @psalm-var array-key $name */ 61 | $result[] = $this->plainList[$name]; 62 | } 63 | 64 | return $result; 65 | } 66 | 67 | /** 68 | * Iterates through package dependencies. 69 | * 70 | * @param Package $package to iterate. 71 | * @param bool $includingDev process development dependencies, defaults to not process. 72 | */ 73 | private function iteratePackage(Package $package, bool $includingDev = false): void 74 | { 75 | $name = $package->getPrettyName(); 76 | 77 | // prevent infinite loop in case of circular dependencies 78 | static $processed = []; 79 | if (array_key_exists($name, $processed)) { 80 | return; 81 | } 82 | 83 | $processed[$name] = 1; 84 | 85 | // package depth in dependency hierarchy 86 | static $depth = 0; 87 | ++$depth; 88 | 89 | $this->iterateDependencies($package); 90 | if ($includingDev) { 91 | $this->iterateDependencies($package, true); 92 | } 93 | if (!array_key_exists($name, $this->orderedList)) { 94 | $this->orderedList[$name] = $depth; 95 | } 96 | 97 | --$depth; 98 | } 99 | 100 | /** 101 | * Iterates dependencies of the given package. 102 | * 103 | * @param Package $package 104 | * @param bool $dev type of dependencies to iterate: true - dev, default - general. 105 | */ 106 | private function iterateDependencies(Package $package, bool $dev = false): void 107 | { 108 | $dependencies = $dev ? $package->getDevRequires() : $package->getRequires(); 109 | foreach (array_keys($dependencies) as $target) { 110 | if (array_key_exists($target, $this->plainList) && empty($this->orderedList[$target])) { 111 | $this->iteratePackage($this->plainList[$target]); 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | list of files 30 | * Important: defines config files processing order: 31 | * envs then constants then params then other configs 32 | */ 33 | private array $files = [ 34 | 'envs' => [], 35 | 'constants' => [], 36 | 'params' => [], 37 | ]; 38 | 39 | /** 40 | * @var array package name => configs as listed in `composer.json` 41 | */ 42 | private array $originalFiles = []; 43 | 44 | private Builder $builder; 45 | 46 | /** 47 | * @var IOInterface 48 | */ 49 | private IOInterface $io; 50 | 51 | /** 52 | * Initializes the plugin object with the passed $composer and $io. 53 | * 54 | * @param Composer $composer 55 | * @param IOInterface $io 56 | */ 57 | public function __construct(Composer $composer, IOInterface $io) 58 | { 59 | $baseDir = dirname($composer->getConfig()->get('vendor-dir')) . DIRECTORY_SEPARATOR; 60 | $this->builder = new Builder(new ConfigOutputFactory(), realpath($baseDir)); 61 | $this->io = $io; 62 | $this->collectPackages($composer); 63 | } 64 | 65 | public static function buildAllConfigs(string $projectRootPath): void 66 | { 67 | $factory = new \Composer\Factory(); 68 | $output = $factory::createOutput(); 69 | $input = new \Symfony\Component\Console\Input\ArgvInput([]); 70 | $helperSet = new \Symfony\Component\Console\Helper\HelperSet(); 71 | $io = new \Composer\IO\ConsoleIO($input, $output, $helperSet); 72 | $composer = $factory->createComposer($io, $projectRootPath . '/composer.json', true, $projectRootPath, false); 73 | $plugin = new self($composer, $io); 74 | $plugin->build(); 75 | } 76 | 77 | public function build(): void 78 | { 79 | $this->io->overwriteError('Assembling config files'); 80 | 81 | $this->scanPackages(); 82 | $this->reorderFiles(); 83 | 84 | $this->builder->buildAllConfigs($this->files); 85 | 86 | $saveFiles = $this->files; 87 | $saveEnv = $_ENV; 88 | foreach ($this->alternatives as $name => $files) { 89 | $this->files = $saveFiles; 90 | $_ENV = $saveEnv; 91 | $builder = $this->builder->createAlternative($name); 92 | /** @psalm-suppress PossiblyNullArgument */ 93 | $this->addFiles($this->rootPackage, $files); 94 | $this->reorderFiles(); 95 | $builder->buildAllConfigs($this->files); 96 | } 97 | } 98 | 99 | private function scanPackages(): void 100 | { 101 | foreach ($this->packages as $package) { 102 | if ($package->isComplete()) { 103 | $this->processPackage($package); 104 | } 105 | } 106 | } 107 | 108 | private function reorderFiles(): void 109 | { 110 | foreach (array_keys($this->files) as $name) { 111 | $this->files[$name] = $this->getAllFiles($name); 112 | } 113 | foreach ($this->files as $name => $files) { 114 | $this->files[$name] = $this->orderFiles($files); 115 | } 116 | } 117 | 118 | private function getAllFiles(string $name, array $stack = []): array 119 | { 120 | if (empty($this->files[$name])) { 121 | return []; 122 | } 123 | $res = []; 124 | foreach ($this->files[$name] as $file) { 125 | if (strncmp($file, '$', 1) === 0) { 126 | if (!in_array($name, $stack, true)) { 127 | $res = array_merge($res, $this->getAllFiles(substr($file, 1), array_merge($stack, [$name]))); 128 | } 129 | } else { 130 | $res[] = $file; 131 | } 132 | } 133 | 134 | return $res; 135 | } 136 | 137 | private function orderFiles(array $files): array 138 | { 139 | if ($files === []) { 140 | return []; 141 | } 142 | $keys = array_combine($files, $files); 143 | $res = []; 144 | foreach ($this->orderedFiles as $file) { 145 | if (array_key_exists($file, $keys)) { 146 | $res[$file] = $file; 147 | } 148 | } 149 | 150 | return array_values($res); 151 | } 152 | 153 | /** 154 | * Scans the given package and collects packages data. 155 | * 156 | * @param Package $package 157 | */ 158 | private function processPackage(Package $package): void 159 | { 160 | $files = $package->getFiles(); 161 | $this->originalFiles[$package->getPrettyName()] = $files; 162 | 163 | if (!empty($files)) { 164 | $this->addFiles($package, $files); 165 | } 166 | if ($package->isRoot()) { 167 | $this->rootPackage = $package; 168 | $this->loadDotEnv($package); 169 | $devFiles = $package->getDevFiles(); 170 | if (!empty($devFiles)) { 171 | $this->addFiles($package, $devFiles); 172 | } 173 | $alternatives = $package->getAlternatives(); 174 | if (is_string($alternatives)) { 175 | $this->alternatives = $this->readConfig($package, $alternatives); 176 | } elseif (is_array($alternatives)) { 177 | $this->alternatives = $alternatives; 178 | } elseif (!empty($alternatives)) { 179 | throw new BadConfigurationException('Alternatives must be array or path to configuration file.'); 180 | } 181 | } 182 | 183 | $this->builder->setPackage($package->getPrettyName(), array_filter([ 184 | 'name' => $package->getPrettyName(), 185 | 'version' => $package->getVersion(), 186 | 'reference' => $package->getSourceReference() ?: $package->getDistReference(), 187 | ])); 188 | } 189 | 190 | private function readConfig(Package $package, string $file): array 191 | { 192 | $path = $package->prepareConfigFilePath($file); 193 | if (!file_exists($path)) { 194 | throw new FailedReadException("failed read file: $file"); 195 | } 196 | $reader = ReaderFactory::get($this->builder, $path); 197 | 198 | try { 199 | return $reader->read($path); 200 | } catch (\Throwable $e) { 201 | throw new ConfigBuildException($e); 202 | } 203 | } 204 | 205 | private function loadDotEnv(Package $package): void 206 | { 207 | $path = $package->preparePath('.env'); 208 | if (file_exists($path) && class_exists(Dotenv::class)) { 209 | $this->addFile($package, 'envs', $path); 210 | } 211 | } 212 | 213 | /** 214 | * Adds given files to the list of files to be processed. 215 | * Prepares `constants` in reversed order (outer package first) because 216 | * constants cannot be redefined. 217 | * 218 | * @param Package $package 219 | * @param array $files 220 | */ 221 | private function addFiles(Package $package, array $files): void 222 | { 223 | foreach ($files as $name => $paths) { 224 | $paths = (array) $paths; 225 | if ('constants' === $name) { 226 | $paths = array_reverse($paths); 227 | } 228 | foreach ($paths as $path) { 229 | $this->addFile($package, $name, $path); 230 | } 231 | } 232 | } 233 | 234 | private array $orderedFiles = []; 235 | 236 | private function addFile(Package $package, string $name, string $path): void 237 | { 238 | $path = $package->prepareConfigFilePath($path); 239 | if (!array_key_exists($name, $this->files)) { 240 | $this->files[$name] = []; 241 | } 242 | if (in_array($path, $this->files[$name], true)) { 243 | return; 244 | } 245 | if ('constants' === $name) { 246 | array_unshift($this->orderedFiles, $path); 247 | array_unshift($this->files[$name], $path); 248 | } else { 249 | $this->orderedFiles[] = $path; 250 | $this->files[$name][] = $path; 251 | } 252 | } 253 | 254 | private function collectPackages(Composer $composer): void 255 | { 256 | $vendorDir = $composer->getConfig()->get('vendor-dir'); 257 | $rootPackage = $composer->getPackage(); 258 | $packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages(); 259 | $packageFinder = new PackageFinder($vendorDir, $rootPackage, $packages); 260 | 261 | $this->packages = $packageFinder->findPackages(); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/Reader/AbstractReader.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 20 | } 21 | 22 | public function read($path): array 23 | { 24 | $skippable = 0 === strncmp($path, '?', 1); 25 | if ($skippable) { 26 | $path = substr($path, 1); 27 | } 28 | 29 | if (is_readable($path)) { 30 | $res = $this->readRaw($path); 31 | 32 | return is_array($res) ? $res : []; 33 | } 34 | 35 | if (!$skippable) { 36 | throw new FailedReadException("Failed read file: $path"); 37 | } 38 | 39 | return []; 40 | } 41 | 42 | protected function getFileContents(string $path): string 43 | { 44 | $res = file_get_contents($path); 45 | if (false === $res) { 46 | throw new FailedReadException("Failed read file: $path"); 47 | } 48 | 49 | return $res; 50 | } 51 | 52 | abstract protected function readRaw(string $path); 53 | } 54 | -------------------------------------------------------------------------------- /src/Reader/EnvReader.php: -------------------------------------------------------------------------------- 1 | loadEnvs($info['dirname'], $info['basename']); 22 | 23 | return $_ENV; 24 | } 25 | 26 | /** 27 | * Creates and loads Dotenv object. 28 | * Supports all 2, 3 and 4 version of `phpdotenv` 29 | * 30 | * @param mixed $dir 31 | * @param mixed $file 32 | */ 33 | private function loadEnvs(string $dir, string $file): void 34 | { 35 | /** @psalm-suppress UndefinedClass */ 36 | if (method_exists(Dotenv::class, 'createMutable')) { 37 | Dotenv::createMutable($dir, $file)->load(); 38 | } elseif (method_exists(Dotenv::class, 'create')) { 39 | Dotenv::create($dir, $file)->overload(); 40 | } else { 41 | (new Dotenv($dir, $file))->overload(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Reader/JsonReader.php: -------------------------------------------------------------------------------- 1 | getFileContents($path), true, 512, JSON_THROW_ON_ERROR); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Reader/PhpReader.php: -------------------------------------------------------------------------------- 1 | builder->getVars()['params'] ?? []; 15 | 16 | $result = static function (array $params) { 17 | return require func_get_arg(1); 18 | }; 19 | 20 | /** @psalm-suppress TooManyArguments */ 21 | return $result($params, $path); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Reader/ReaderFactory.php: -------------------------------------------------------------------------------- 1 | */ 16 | private static array $loaders = []; 17 | 18 | private static array $knownReaders = [ 19 | 'env' => EnvReader::class, 20 | 'php' => PhpReader::class, 21 | 'json' => JsonReader::class, 22 | 'yaml' => YamlReader::class, 23 | 'yml' => YamlReader::class, 24 | ]; 25 | 26 | public static function get(Builder $builder, string $path): ReaderInterface 27 | { 28 | $class = static::findClass($path); 29 | 30 | $uniqid = $class . ':' . spl_object_hash($builder); 31 | if (empty(self::$loaders[$uniqid])) { 32 | /** @psalm-var ReaderInterface */ 33 | self::$loaders[$uniqid] = new $class($builder); 34 | } 35 | 36 | /** @psalm-var ReaderInterface */ 37 | return self::$loaders[$uniqid]; 38 | } 39 | 40 | private static function detectType(string $path): string 41 | { 42 | if (strncmp(basename($path), '.env.', 5) === 0) { 43 | return 'env'; 44 | } 45 | 46 | return pathinfo($path, PATHINFO_EXTENSION); 47 | } 48 | 49 | private static function findClass(string $path): string 50 | { 51 | $type = static::detectType($path); 52 | if (!array_key_exists($type, static::$knownReaders)) { 53 | throw new UnsupportedFileTypeException("Unsupported file type for \"$path\"."); 54 | } 55 | 56 | return static::$knownReaders[$type]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Reader/ReaderInterface.php: -------------------------------------------------------------------------------- 1 | getFileContents($path)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Util/BuilderRequireEncoder.php: -------------------------------------------------------------------------------- 1 | getClosureScopeClass(); 27 | 28 | if (null === $closureReflection) { 29 | return false; 30 | } 31 | 32 | $closureClassOwnerName = $closureReflection->getName(); 33 | 34 | return is_a($closureClassOwnerName, Builder::class, true); 35 | } 36 | 37 | public function encode($value, $depth, array $options, callable $encode) 38 | { 39 | $reflection = new ReflectionClosure($value); 40 | $variables = $reflection->getStaticVariables(); 41 | $config = $variables['config']; 42 | 43 | return str_replace( 44 | ['$config'], 45 | ["'$config.php'"], 46 | substr( 47 | $reflection->getCode(), 48 | 16 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Util/ClosureEncoder.php: -------------------------------------------------------------------------------- 1 | getCode(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Util/EnvEncoder.php: -------------------------------------------------------------------------------- 1 | getClosureScopeClass(); 27 | 28 | if (null === $closureReflection) { 29 | return false; 30 | } 31 | 32 | $closureClassOwnerName = $closureReflection->getName(); 33 | 34 | return is_a($closureClassOwnerName, Env::class, true); 35 | } 36 | 37 | public function encode($value, $depth, array $options, callable $encode) 38 | { 39 | $reflection = new ReflectionClosure($value); 40 | $variables = $reflection->getStaticVariables(); 41 | $key = $variables['key']; 42 | $default = $variables['default'] ?? null; 43 | 44 | return str_replace( 45 | ['$key', '$default'], 46 | ["'$key'", Helper::exportVar($default)], 47 | substr( 48 | $reflection->getCode(), 49 | 16 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Util/Helper.php: -------------------------------------------------------------------------------- 1 | encode($value); 28 | } 29 | 30 | private static ?PHPEncoder $encoder = null; 31 | 32 | private static function getEncoder(): PHPEncoder 33 | { 34 | if (self::$encoder === null) { 35 | self::$encoder = static::createEncoder(); 36 | } 37 | 38 | return self::$encoder; 39 | } 40 | 41 | private static function createEncoder(): PHPEncoder 42 | { 43 | $encoder = new PHPEncoder([ 44 | 'object.format' => 'serialize', 45 | ]); 46 | $encoder->addEncoder(new ClosureEncoder(), true); 47 | $encoder->addEncoder(new EnvEncoder(), true); 48 | $encoder->addEncoder(new ObjectEncoder(), true); 49 | $encoder->addEncoder(new BuilderRequireEncoder(), true); 50 | 51 | return $encoder; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Util/ObjectEncoder.php: -------------------------------------------------------------------------------- 1 | files = $files; 28 | 29 | $this->collectDependencies($files); 30 | foreach (array_keys($files) as $name) { 31 | $this->followDependencies($name); 32 | } 33 | } 34 | 35 | public function get(): array 36 | { 37 | $result = []; 38 | foreach ($this->dependenciesOrder as $name) { 39 | $result[$name] = $this->resolveDependencies($this->files[$name]); 40 | } 41 | 42 | return $result; 43 | } 44 | 45 | private function resolveDependencies(array $paths): array 46 | { 47 | foreach ($paths as &$path) { 48 | if ($this->isDependency($path)) { 49 | $dependency = $this->parseDependencyName($path); 50 | 51 | $path = Builder::path($dependency); 52 | } 53 | } 54 | 55 | return $paths; 56 | } 57 | 58 | private function followDependencies(string $name): void 59 | { 60 | if (array_key_exists($name, $this->dependenciesOrder)) { 61 | return; 62 | } 63 | if (array_key_exists($name, $this->following)) { 64 | throw new CircularDependencyException($name . ' ' . implode(',', $this->following)); 65 | } 66 | $this->following[$name] = $name; 67 | if (array_key_exists($name, $this->dependencies)) { 68 | foreach ($this->dependencies[$name] as $dependency) { 69 | $this->followDependencies($dependency); 70 | } 71 | } 72 | $this->dependenciesOrder[$name] = $name; 73 | unset($this->following[$name]); 74 | } 75 | 76 | private function collectDependencies(array $files): void 77 | { 78 | foreach ($files as $name => $paths) { 79 | foreach ($paths as $path) { 80 | if ($this->isDependency($path)) { 81 | $dependencyName = $this->parseDependencyName($path); 82 | if (!array_key_exists($name, $this->dependencies)) { 83 | $this->dependencies[$name] = []; 84 | } 85 | $this->dependencies[$name][$dependencyName] = $dependencyName; 86 | } 87 | } 88 | } 89 | } 90 | 91 | private function isDependency(string $path): bool 92 | { 93 | return 0 === strncmp($path, '$', 1); 94 | } 95 | 96 | private function parseDependencyName(string $path): string 97 | { 98 | return substr($path, 1); 99 | } 100 | } 101 | --------------------------------------------------------------------------------