├── .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 | [](https://packagist.org/packages/yiisoft/composer-config-plugin)
12 | [](https://packagist.org/packages/yiisoft/composer-config-plugin)
13 | [](https://github.com/yiisoft/composer-config-plugin/actions)
14 | [](https://scrutinizer-ci.com/g/yiisoft/composer-config-plugin/?branch=master)
15 | [](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 |
--------------------------------------------------------------------------------