├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer-require-checker.json
├── composer.json
├── psalm83-84.xml
├── rector.php
├── roave-bc-check.yaml
├── src
├── Command
│ ├── ConfigCommandProvider.php
│ ├── CopyCommand.php
│ ├── InfoCommand.php
│ └── RebuildCommand.php
├── Composer
│ ├── ConfigSettings.php
│ ├── EventHandler.php
│ ├── MergePlanProcess.php
│ ├── Options.php
│ ├── PackageFile.php
│ ├── PackageFilesProcess.php
│ ├── PackagesListBuilder.php
│ └── ProcessHelper.php
├── Config.php
├── ConfigInterface.php
├── ConfigPaths.php
├── Context.php
├── DataModifiers.php
├── FilesExtractor.php
├── MergePlan.php
├── Merger.php
└── Modifier
│ ├── RecursiveMerge.php
│ ├── RemoveFromVendor.php
│ ├── RemoveGroupsFromVendor.php
│ ├── RemoveKeysFromVendor.php
│ └── ReverseMerge.php
└── tools
├── .gitignore
└── composer-require-checker
└── composer.json
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Config Change Log
2 |
3 | ## 1.6.1 under development
4 |
5 | - no changes in this release.
6 |
7 | ## 1.6.0 February 05, 2025
8 |
9 | - New #173: Allow to use option "config-plugin-file" in packages (@vjik)
10 | - New #175: Add `yii-config-info` composer command (@vjik)
11 | - Chg #175: Raise minimum Composer version to 2.3 (@vjik)
12 | - Chg #187: Change PHP constraint in `composer.json` to `~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik)
13 | - Enh #172, #173: Refactoring: extract config settings reader to separate class (@vjik)
14 | - Enh #175: Minor refactoring of internal classes `Options` and `ProcessHelper` (@vjik)
15 | - Enh #186: Raise the minimum PHP version to 8.1 and minor refactoring (@vjik)
16 | - Bug #186: Explicitly mark nullable parameters (@vjik)
17 |
18 | ## 1.5.0 December 25, 2023
19 |
20 | - New #155: Add ability to specify recursion depth for recursive modifier (@vjik)
21 | - Enh #157: Remove unnecessary code in `PackagesListBuilder` (@vjik)
22 | - Bug #153: Do not throw "Duplicate key…" exception when using nested groups (@vjik)
23 | - Bug #163: References to another configs use reverse and recursive modifiers of root group now (@vjik)
24 |
25 | ## 1.4.0 November 17, 2023
26 |
27 | - Enh #152: Add plugin option "package-types" that define package types for process, by default "library" and
28 | "composer-plugin" (@vjik)
29 |
30 | ## 1.3.1 November 17, 2023
31 |
32 | - Bug #145: Use composer library and plugins only, instead of any packages before (@vjik)
33 | - Bug #150: Empty configuration groups from packages were not added to merge plan (@vjik)
34 |
35 | ## 1.3.0 February 11, 2023
36 |
37 | - Enh #131: Add ability to use `Config` without params (@vjik)
38 |
39 | ## 1.2.0 February 08, 2023
40 |
41 | - Enh #119: Improve performance of collecting data for `ReverseMerge` and `RecursiveMerge` (@samdark)
42 | - Enh #122: Raise minimal PHP version to 8.0 (@vjik, @xepozz)
43 | - Enh #130: Add ability to change merge plan file path (@vjik)
44 |
45 | ## 1.1.1 January 05, 2022
46 |
47 | - Enh #110: Improve the error message by displaying a name of the group where the error occurred when merging (@devanych)
48 |
49 | ## 1.1.0 December 31, 2021
50 |
51 | - New #108: Add `Yiisoft\Config\ConfigInterface` to allow custom implementations of a config loader (@devanych)
52 |
53 | ## 1.0.0 December 17, 2021
54 |
55 | - Initial release.
56 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software ()
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 |
4 |
5 |
6 |
Yii Config
7 |
8 |
9 |
10 | [](https://packagist.org/packages/yiisoft/config)
11 | [](https://packagist.org/packages/yiisoft/config)
12 | [](https://github.com/yiisoft/config/actions)
13 | [](https://codecov.io/gh/yiisoft/config)
14 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/config/master)
15 | [](https://github.com/yiisoft/config/actions?query=workflow%3A%22static+analysis%22)
16 | [](https://shepherd.dev/github/yiisoft/config)
17 |
18 | This [Composer](https://getcomposer.org) plugin provides assembling of configurations distributed with composer
19 | packages. It is implementing a plugin system which allows to provide the configuration needed to use a package directly when installing it to make it run out-of-the-box.
20 | The package becomes a plugin holding both the code and its default configuration.
21 |
22 | ## Requirements
23 |
24 | - PHP 8.1 or higher.
25 | - Composer 2.3 or higher.
26 |
27 | ## Installation
28 |
29 | ```shell
30 | composer require yiisoft/config
31 | ```
32 |
33 | ## How it works?
34 |
35 | The package consist of two parts: Composer plugin and config loader.
36 |
37 | After composer updates its autoload file, and that happens after `dump-autoload`, `require`, `update` or `remove`,
38 | Composer plugin:
39 |
40 | - Scans installed packages for `config-plugin` extra option in their `composer.json`.
41 | - Writes a merge plan into `config/.merge-plan.php`. It includes configuration from each package `composer.json`.
42 |
43 | In the application entry point, usually `index.php`, we create an instance of config loader and require a configuration
44 | we need:
45 |
46 | ```php
47 | use Yiisoft\Config\Config;
48 | use Yiisoft\Config\ConfigPaths;
49 |
50 | $config = new Config(
51 | new ConfigPaths(dirname(__DIR__)),
52 | );
53 |
54 | $web = $config->get('web');
55 | ```
56 |
57 | The `web` in the above is a config group. The config loader obtains it runtime according to the merge plan.
58 | The configuration consists of three layers that are loaded as follows:
59 |
60 | - Vendor configurations from each `vendor/package-name`. These provide default values.
61 | - Root package configurations from `config`. These may override vendor configurations.
62 | - Environment specific configurations from `config`. These may override root and vendor configurations.
63 |
64 | > Please note that same named keys are not allowed within a configuration layer.
65 |
66 | When calling the `get()` method, if the configuration group does not exist, an `\ErrorException` will be thrown.
67 | If you are not sure that the configuration group exists, then use the `has()` method:
68 |
69 | ```php
70 | use Yiisoft\Config\Config;
71 | use Yiisoft\Config\ConfigPaths;
72 |
73 | $config = new Config(
74 | new ConfigPaths(dirname(__DIR__)),
75 | );
76 |
77 | if ($config->has('web')) {
78 | $web = $config->get('web');
79 | }
80 | ```
81 |
82 | ## Config groups
83 |
84 | Each config group represents a set of configs that is merged into a single array. It is defined per package in
85 | each package `composer.json`:
86 |
87 | ```json
88 | "extra": {
89 | "config-plugin": {
90 | "params": [
91 | "params.php",
92 | "?params-local.php"
93 | ],
94 | "common": "common.php",
95 | "web": [
96 | "$common",
97 | "web.php",
98 | "../src/Modules/*/config/web.php"
99 | ],
100 | "other": "other.php"
101 | }
102 | }
103 | ```
104 |
105 | In the above example the mapping keys are config group names and the values are configuration files and references to other config groups.
106 | The file paths are relative to the [source-directory](#source-directory), which by default is the path where `composer.json` is located.
107 |
108 | ### Markers
109 |
110 | - `?` - marks optional files. Absence of files not marked with this marker will cause exception.
111 |
112 | ```php
113 | "params": [
114 | "params.php",
115 | "?params-local.php"
116 | ]
117 | ```
118 |
119 | It's okay if `params-local.php` will not be found, but it's not okay if `params.php` will be absent.
120 |
121 | - `*` - marks wildcard path. It means zero or more matches by wildcard mask.
122 |
123 | ```php
124 | "web": [
125 | "../src/Modules/*/config/web.php"
126 | ]
127 | ```
128 |
129 | It will collect all `web.php` in any sub-folders of `src/Modules/` in `config` folder.
130 | However, if the configuration folder is packaged as part of the `PHAR` archive, the configuration
131 | files will not be uploaded. In this case, you must explicitly specify each configuration file.
132 |
133 | - `$` - reference to another config by its group name.
134 |
135 | ```php
136 | "params": [
137 | "params.php",
138 | "?params-local.php"
139 | ],
140 | "params-console": [
141 | "$params",
142 | "params-console.php"
143 | ],
144 | "params-web": [
145 | "$params",
146 | "params-web.php"
147 | ]
148 | ```
149 |
150 | The config groups `params-console` and `params-web` will both contain the config values from `params.php` and `params-local.php` additional to their own configuration values.
151 |
152 | ***
153 |
154 | Define your configs like the following:
155 |
156 | ```php
157 | return [
158 | 'components' => [
159 | 'db' => [
160 | 'class' => \my\Db::class,
161 | 'name' => $params['db.name'],
162 | 'password' => $params['db.password'],
163 | ],
164 | ],
165 | ];
166 | ```
167 |
168 | A special variable `$params` is read from config (by default, group is named `params`).
169 |
170 | ### Using custom group for `$params`
171 |
172 | By default, `$params` variable is read from `params` group. You can customize the group name via constructor of `Config`:
173 |
174 | ```php
175 | $config = new Config(
176 | new ConfigPaths(__DIR__ . '/configs'),
177 | null,
178 | [],
179 | 'custom-params' // Group name for `$params`
180 | );
181 | ```
182 |
183 | You can pass `null` as `$params` group name. In this case `$params` will empty array.
184 |
185 | ### Using sub-configs
186 |
187 | In order to access a sub-config, use the following in your config:
188 |
189 | ```php
190 | 'routes' => $config->get('routes');
191 | ```
192 |
193 | ## Options
194 |
195 | A number of options is available both for Composer plugin and a config loader. Composer options are specified in
196 | `composer.json`:
197 |
198 | ```json
199 | "extra": {
200 | "config-plugin-options": {
201 | "source-directory": "config"
202 | },
203 | "config-plugin": {
204 | // ...
205 | }
206 | }
207 | ```
208 |
209 | ### `source-directory`
210 |
211 | The `source-directory` option specifies where to read the configs from for a package the option is specified for.
212 | It is available for all packages, including the root package, which is typically an application.
213 | The value is a path relative to where the `composer.json` file is located. The default value is an empty string.
214 |
215 | If you change the source directory for the root package, don't forget to adjust configs path when creating
216 | an instance of `Config`. Usually that is `index.php`:
217 |
218 | ```php
219 | use Yiisoft\Config\Config;
220 | use Yiisoft\Config\ConfigPaths;
221 |
222 | $config = new Config(
223 | new ConfigPaths(dirname(__DIR__), 'path/to/config/directory'),
224 | );
225 |
226 | $web = $config->get('web');
227 | ```
228 |
229 | ### `vendor-override-layer`
230 |
231 | The `vendor-override-layer` option adds a sublayer to the vendor, which allocates packages that will override
232 | the vendor's default configurations. This sublayer is located between the vendor and application layers.
233 |
234 | This can be useful if you need to redefine default configurations even before the application layer. To do this,
235 | you need to create your own package with configurations meant to override the default ones:
236 |
237 | ```json
238 | "name": "vendor-name/package-name",
239 | "extra": {
240 | "config-plugin": {
241 | // ...
242 | }
243 | }
244 | ```
245 |
246 | And in the root file `composer.json` of your application, specify this package in the `vendor-override-layer` option:
247 |
248 | ```json
249 | "require": {
250 | "vendor-name/package-name": "version",
251 | "yiisoft/config": "version"
252 | },
253 | "extra": {
254 | "config-plugin-options": {
255 | "vendor-override-layer": "vendor-name/package-name"
256 | },
257 | "config-plugin": {
258 | // ...
259 | }
260 | }
261 | ```
262 |
263 | In the same way, several packages can be added to this sublayer:
264 |
265 | ```json
266 | "extra": {
267 | "config-plugin-options": {
268 | "vendor-override-layer": [
269 | "vendor-name/package-1",
270 | "vendor-name/package-2"
271 | ]
272 | }
273 | }
274 | ```
275 |
276 | You can use wildcard pattern if there are too many packages:
277 |
278 | ```json
279 | "extra": {
280 | "config-plugin-options": {
281 | "vendor-override-layer": [
282 | "vendor-1/*",
283 | "vendor-2/config-*"
284 | ]
285 | }
286 | }
287 | ```
288 |
289 | For more information about the wildcard syntax, see the [yiisoft/strings](https://github.com/yiisoft/strings).
290 |
291 | > Please note that in this sublayer keys with the same names are not allowed similar to other layers.
292 |
293 | ### `merge-plan-file`
294 |
295 | This option allows you to override path to merge plan file. It is `.merge-plan.php` by default. To change it, set the value:
296 |
297 | ```json
298 | "extra": {
299 | "config-plugin-options": {
300 | "merge-plan-file": "custom/path/my-merge-plan.php"
301 | }
302 | }
303 | ```
304 |
305 | This can be useful when developing. Don't forget to set same path in `Config` constructor when changing this option.
306 |
307 | ### `build-merge-plan`
308 |
309 | The `build-merge-plan` option allows you to disable creation/updating of the `config/.merge-plan.php`.
310 | Enabled by default, to disable it, set the value to `false`:
311 |
312 | ```json
313 | "extra": {
314 | "config-plugin-options": {
315 | "build-merge-plan": false
316 | }
317 | }
318 | ```
319 |
320 | This can be useful when developing. If the config package is a dependency of your package,
321 | and you do not need to create a merge plan file when developing your package.
322 | For example, this is implemented in [yiisoft/yii-runner](https://github.com/yiisoft/yii-runner).
323 |
324 | ### `package-types`
325 |
326 | The `package-types` option define package types for process by composer plugin. By default, it is "library" and
327 | "composer-plugin". You can override default value by own types:
328 |
329 | ```json
330 | "extra": {
331 | "config-plugin-options": {
332 | "package-types": ["library", "my-extension"]
333 | }
334 | }
335 | ```
336 |
337 | ## Environments
338 |
339 | The plugin supports creating additional environments added to the base configuration. This allows you to create
340 | multiple configurations for the application such as `production` and `development`.
341 |
342 | > Note that environments are supported on application level only and are not read from configurations of packages.
343 |
344 | The environments are specified in the `composer.json` file of your application:
345 |
346 | ```json
347 | "extra": {
348 | "config-plugin-options": {
349 | "source-directory": "config"
350 | },
351 | "config-plugin": {
352 | "params": "params.php",
353 | "web": "web.php"
354 | },
355 | "config-plugin-environments": {
356 | "dev": {
357 | "params": "dev/params.php",
358 | "app": [
359 | "$web",
360 | "dev/app.php"
361 | ]
362 | },
363 | "prod": {
364 | "app": "prod/app.php"
365 | }
366 | }
367 | }
368 | ```
369 |
370 | Configuration defines the merge process. One of the environments from `config-plugin-environments`
371 | is merged with the main configuration defined by `config-plugin`. In given example, in the `dev` environment
372 | we use `$web` configuration from the main environment.
373 |
374 | This configuration has the following structure:
375 |
376 | ```
377 | config/ Configuration root directory.
378 | dev/ Development environment files.
379 | app.php Development environment app group configuration.
380 | params.php Development environment parameters.
381 | prod/ Production environment files.
382 | app.php Production environment app group configuration.
383 | params.php Main configuration parameters.
384 | web.php Мain configuration web group configuration.
385 | ```
386 |
387 | To choose an environment to be used you must specify its name when creating an instance of `Config`:
388 |
389 | ```php
390 | use Yiisoft\Config\Config;
391 | use Yiisoft\Config\ConfigPaths;
392 |
393 | $config = new Config(
394 | new ConfigPaths(dirname(__DIR__)),
395 | 'dev',
396 | );
397 |
398 | $app = $config->get('app');
399 | ```
400 |
401 | If defined in an environment, `params` will be merged with `params` from the main configuration,
402 | and could be used as `$params` in all configurations.
403 |
404 | ## Configuration in a PHP file
405 |
406 | You can define configuration in a PHP file. To do it, specify a PHP file path in the `extra` section of
407 | the `composer.json`:
408 |
409 | ```json
410 | "extra": {
411 | "config-plugin-file": "path/to/configuration/file.php"
412 | }
413 | ```
414 |
415 | Configurations are specified in the same way, only in PHP format:
416 |
417 | ```php
418 | return [
419 | 'config-plugin-options' => [
420 | 'source-directory' => 'config',
421 | ],
422 | 'config-plugin' => [
423 | 'params' => [
424 | 'params.php',
425 | '?params-local.php',
426 | ],
427 | 'web' => 'web.php',
428 | ],
429 | 'config-plugin-environments' => [
430 | 'dev' => [
431 | 'params' => 'dev/params.php',
432 | 'app' => [
433 | '$web',
434 | 'dev/app.php',
435 | ],
436 | ],
437 | 'prod' => [
438 | 'app' => 'prod/app.php',
439 | ],
440 | ],
441 | ];
442 | ```
443 |
444 | If you specify the file path, the remaining sections (`config-plugin-*`) in `composer.json` will be ignored and
445 | configurations will be read from the PHP file specified. The path is relative to where the `composer.json` file
446 | is located.
447 |
448 | ## Configuration modifiers
449 |
450 | ### Recursive merge of arrays
451 |
452 | By default, recursive merging of arrays in configuration files is not performed. If you want to recursively merge
453 | arrays in a certain group of configs, such as params, you must pass `RecursiveMerge` modifier with specified
454 | group names to the `Config` constructor:
455 |
456 | ```php
457 | use Yiisoft\Config\Config;
458 | use Yiisoft\Config\ConfigPaths;
459 | use Yiisoft\Config\Modifier\RecursiveMerge;
460 |
461 | $config = new Config(
462 | new ConfigPaths(dirname(__DIR__)),
463 | 'dev',
464 | [
465 | RecursiveMerge::groups('params', 'events', 'events-web', 'events-console'),
466 | ],
467 | );
468 |
469 | $params = $config->get('params'); // merged recursively
470 | ```
471 |
472 | If you want to recursively merge arrays to a certain depth, use the `RecursiveMerge::groupsWithDepth()` method:
473 |
474 | ```php
475 | RecursiveMerge::groups(['widgets-themes', 'my-custom-group'], 1)
476 | ```
477 |
478 | > Note: References to another configs use recursive modifier of root group.
479 |
480 | ### Reverse merge of arrays
481 |
482 | Result of reverse merge is being ordered descending by data source. It is useful for merging module config with
483 | base config where more specific config (i.e. module's) has more priority. One of such cases is merging events.
484 |
485 | To enable reverse merge pass `ReverseMerge` modifier with specified group names to the `Config` constructor:
486 |
487 | ```php
488 | use Yiisoft\Config\Config;
489 | use Yiisoft\Config\ConfigPaths;
490 | use Yiisoft\Config\Modifier\ReverseMerge;
491 |
492 | $config = new Config(
493 | new ConfigPaths(dirname(__DIR__)),
494 | 'dev',
495 | [
496 | ReverseMerge::groups('events', 'events-web', 'events-console'),
497 | ],
498 | );
499 |
500 | $events = $config->get('events-console'); // merged reversed
501 | ```
502 |
503 | > Note: References to another configs use reverse modifier of root group.
504 |
505 | ### Remove elements from vendor package configuration
506 |
507 | Sometimes it is necessary to remove some elements of vendor packages configuration. To do this,
508 | pass `RemoveFromVendor` modifier to the `Config` constructor.
509 |
510 | Remove specified key paths:
511 |
512 | ```php
513 | use Yiisoft\Config\Config;
514 | use Yiisoft\Config\ConfigPaths;
515 | use Yiisoft\Config\Modifier\RemoveFromVendor;
516 |
517 | $config = new Config(
518 | new ConfigPaths(dirname(__DIR__)),
519 | 'dev',
520 | [
521 | // Remove elements `key-for-remove` and `nested→key→for-remove` from all groups in all vendor packages
522 | RemoveFromVendor::keys(
523 | ['key-for-remove'],
524 | ['nested', 'key', 'for-remove'],
525 | ),
526 |
527 | // Remove elements `a` and `b` from all groups in package `yiisoft/auth`
528 | RemoveFromVendor::keys(['a'], ['b'])
529 | ->package('yiisoft/auth'),
530 |
531 | // Remove elements `c` and `d` from groups `params` and `web` in package `yiisoft/view`
532 | RemoveFromVendor::keys(['c'], ['d'])
533 | ->package('yiisoft/view', 'params', 'web'),
534 |
535 | // Remove elements `e` and `f` from all groups in package `yiisoft/auth`
536 | // and from groups `params` and `web` in package `yiisoft/view`
537 | RemoveFromVendor::keys(['e'], ['f'])
538 | ->package('yiisoft/auth')
539 | ->package('yiisoft/view', 'params', 'web'),
540 | ],
541 | );
542 |
543 | $params = $config->get('params');
544 | ```
545 |
546 | Remove specified configuration groups:
547 |
548 | ```php
549 | use Yiisoft\Config\Config;
550 | use Yiisoft\Config\ConfigPaths;
551 | use Yiisoft\Config\Modifier\RemoveFromVendor;
552 |
553 | $config = new Config(
554 | new ConfigPaths(dirname(__DIR__)),
555 | 'dev',
556 | [
557 | RemoveFromVendor::groups([
558 | // Remove group `params` from all vendor packages
559 | '*' => 'params',
560 |
561 | // Remove groups `common` and `web` from all vendor packages
562 | '*' => ['common', 'web'],
563 |
564 | // Remove all groups from package `yiisoft/auth`
565 | 'yiisoft/auth' => '*',
566 |
567 | // Remove groups `params` from package `yiisoft/http`
568 | 'yiisoft/http' => 'params',
569 |
570 | // Remove groups `params` and `common` from package `yiisoft/view`
571 | 'yiisoft/view' => ['params', 'common'],
572 | ]),
573 | ],
574 | );
575 | ```
576 |
577 | ### Combine modifiers
578 |
579 | `Config` supports simultaneous use of several modifiers:
580 |
581 | ```php
582 | use Yiisoft\Config\Config;
583 | use Yiisoft\Config\ConfigPaths;
584 | use Yiisoft\Config\Modifier\RecursiveMerge;
585 | use Yiisoft\Config\Modifier\RemoveFromVendor;
586 | use Yiisoft\Config\Modifier\ReverseMerge;
587 |
588 | $config = new Config(
589 | new ConfigPaths(dirname(__DIR__)),
590 | 'dev',
591 | [
592 | RecursiveMerge::groups('params', 'events', 'events-web', 'events-console'),
593 | ReverseMerge::groups('events', 'events-web', 'events-console'),
594 | RemoveFromVendor::keys(
595 | ['key-for-remove'],
596 | ['nested', 'key', 'for-remove'],
597 | ),
598 | ],
599 | );
600 | ```
601 |
602 | ## Commands
603 |
604 | ### `yii-config-copy`
605 |
606 | The plugin adds extra `yii-config-copy` command to Composer. It copies the package config files from the vendor
607 | to the config directory of the root package:
608 |
609 | ```shell
610 | composer yii-config-copy [target-path] [files]
611 | ```
612 |
613 | Copies all config files of the `yiisoft/view` package:
614 |
615 | ```shell
616 | # To the `config` directory
617 | composer yii-config-copy yiisoft/view
618 |
619 | # To the `config/my/path` directory
620 | composer yii-config-copy yiisoft/view my/path
621 | ```
622 |
623 | Copies the specified config files of the `yiisoft/view` package:
624 |
625 | ```shell
626 | # To the `config` directory
627 | composer yii-config-copy yiisoft/view / params.php web.php
628 |
629 | # To the `config/my/path` directory and without the file extension
630 | composer yii-config-copy yiisoft/view my/path params web
631 | ```
632 |
633 | In order to avoid conflicts with file names, a prefix is added to the names of the copied files:
634 | `yiisoft-view-params.php`, `yiisoft-view-web.php`.
635 |
636 | ### `yii-config-rebuild`
637 |
638 | The `yii-config-rebuild` command updates merge plan file. This command may be used if you have added files or directories
639 | to the application configuration file structure and these were not specified in `composer.json` of the root package.
640 | In this case you need to add to the information about new files to `composer.json` of the root package by executing the
641 | command:
642 |
643 | ```shell
644 | composer yii-config-rebuild
645 | ```
646 |
647 | ### `yii-config-info`
648 |
649 | The `yii-config-info` command displays application or package configuration details.
650 |
651 | ```shell
652 | composer yii-config-info
653 | composer yii-config-info yiisoft/widget
654 | ```
655 |
656 | ## Documentation
657 |
658 | - [Internals](docs/internals.md)
659 |
660 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for
661 | that. You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
662 |
663 | ## License
664 |
665 | The Yii Config package is free software. It is released under the terms of the BSD License.
666 | Please see [`LICENSE`](./LICENSE.md) for more information.
667 |
668 | Maintained by [Yii Software](https://www.yiiframework.com/).
669 |
670 | ## Credits
671 |
672 | The plugin is heavily inspired by [Composer config plugin](https://github.com/yiisoft/composer-config-plugin)
673 | originally created by HiQDev () in 2016 and then adopted by Yii.
674 |
675 | ## Support the project
676 |
677 | [](https://opencollective.com/yiisoft)
678 |
679 | ## Follow updates
680 |
681 | [](https://www.yiiframework.com/)
682 | [](https://twitter.com/yiiframework)
683 | [](https://t.me/yii3en)
684 | [](https://www.facebook.com/groups/yiitalk)
685 | [](https://yiiframework.com/go/slack)
686 |
--------------------------------------------------------------------------------
/composer-require-checker.json:
--------------------------------------------------------------------------------
1 | {
2 | "symbol-whitelist": [
3 | "Composer\\Command\\BaseCommand",
4 | "Composer\\Composer",
5 | "Composer\\EventDispatcher\\EventSubscriberInterface",
6 | "Composer\\Factory",
7 | "Composer\\IO\\IOInterface",
8 | "Composer\\Package\\BasePackage",
9 | "Composer\\Package\\CompletePackage",
10 | "Composer\\Package\\PackageInterface",
11 | "Composer\\Plugin\\Capability\\CommandProvider",
12 | "Composer\\Plugin\\Capable",
13 | "Composer\\Plugin\\CommandEvent",
14 | "Composer\\Plugin\\PluginEvents",
15 | "Composer\\Plugin\\PluginInterface",
16 | "Composer\\Script\\Event",
17 | "Composer\\Script\\ScriptEvents",
18 | "Composer\\Util\\Filesystem",
19 | "Symfony\\Component\\Console\\Input\\InputArgument",
20 | "Symfony\\Component\\Console\\Input\\InputInterface",
21 | "Symfony\\Component\\Console\\Output\\OutputInterface"
22 | ],
23 | "php-core-extensions": [
24 | "Core",
25 | "date",
26 | "json",
27 | "pcre",
28 | "Phar",
29 | "Reflection",
30 | "SPL",
31 | "standard"
32 | ],
33 | "scan-files": []
34 | }
35 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/config",
3 | "type": "composer-plugin",
4 | "description": "Composer plugin and a library for config assembling",
5 | "keywords": [
6 | "composer",
7 | "config",
8 | "plugin"
9 | ],
10 | "homepage": "https://github.com/yiisoft/config",
11 | "license": "BSD-3-Clause",
12 | "support": {
13 | "issues": "https://github.com/yiisoft/config/issues?state=open",
14 | "source": "https://github.com/yiisoft/config",
15 | "forum": "https://www.yiiframework.com/forum/",
16 | "wiki": "https://www.yiiframework.com/wiki/",
17 | "irc": "ircs://irc.libera.chat:6697/yii",
18 | "chat": "https://t.me/yii3en"
19 | },
20 | "funding": [
21 | {
22 | "type": "opencollective",
23 | "url": "https://opencollective.com/yiisoft"
24 | },
25 | {
26 | "type": "github",
27 | "url": "https://github.com/sponsors/yiisoft"
28 | }
29 | ],
30 | "require": {
31 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
32 | "composer-plugin-api": "^2.0",
33 | "symfony/console": "^5.4.11|^6.0.11|^7",
34 | "yiisoft/arrays": "^3.0",
35 | "yiisoft/strings": "^2.0",
36 | "yiisoft/var-dumper": "^1.1"
37 | },
38 | "require-dev": {
39 | "bamarni/composer-bin-plugin": "^1.8.2",
40 | "composer/composer": "^2.8.5",
41 | "phpunit/phpunit": "^10.5.44",
42 | "rector/rector": "^2.0.7",
43 | "roave/infection-static-analysis-plugin": "^1.35",
44 | "spatie/phpunit-watcher": "^1.24",
45 | "vimeo/psalm": "^5.26.1|^6.1"
46 | },
47 | "config": {
48 | "sort-packages": true,
49 | "bump-after-update": "dev",
50 | "allow-plugins": {
51 | "bamarni/composer-bin-plugin": true,
52 | "composer/package-versions-deprecated": true,
53 | "infection/extension-installer": true
54 | }
55 | },
56 | "autoload": {
57 | "psr-4": {
58 | "Yiisoft\\Config\\": "src"
59 | }
60 | },
61 | "autoload-dev": {
62 | "psr-4": {
63 | "Yiisoft\\Config\\Tests\\": "tests"
64 | }
65 | },
66 | "extra": {
67 | "class": "Yiisoft\\Config\\Composer\\EventHandler",
68 | "bump-after-update": "dev",
69 | "bamarni-bin": {
70 | "bin-links": true,
71 | "target-directory": "tools",
72 | "forward-command": true
73 | }
74 | },
75 | "scripts": {
76 | "test": "phpunit --testdox --no-interaction",
77 | "test-watch": "phpunit-watcher watch"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/psalm83-84.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | paths([
13 | __DIR__ . '/src',
14 | __DIR__ . '/tests',
15 | ]);
16 |
17 | // register a single rule
18 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
19 |
20 | // define sets of rules
21 | $rectorConfig->sets([
22 | LevelSetList::UP_TO_PHP_80,
23 | ]);
24 |
25 | $rectorConfig->skip([
26 | RemoveExtraParametersRector::class,
27 | ClosureToArrowFunctionRector::class,
28 | ]);
29 | };
30 |
--------------------------------------------------------------------------------
/roave-bc-check.yaml:
--------------------------------------------------------------------------------
1 | parameters:
2 | ignoreErrors:
3 | - '#\[BC\] SKIPPED: Roave\\BetterReflection\\Reflection\\ReflectionClass \"Composer\\Command\\BaseCommand\" could not be found in the located source#'
4 | - '#\[BC\] SKIPPED: Roave\\BetterReflection\\Reflection\\ReflectionClass \"Composer\\Plugin\\PluginInterface\" could not be found in the located source#'
5 |
--------------------------------------------------------------------------------
/src/Command/ConfigCommandProvider.php:
--------------------------------------------------------------------------------
1 | setName('yii-config-copy')
30 | ->setDescription('Copying package configuration files to the application.')
31 | ->setHelp('This command copies the package configuration files from the vendor to the application.')
32 | ->addArgument('package', InputArgument::REQUIRED, 'Package')
33 | ->addArgument('target', InputArgument::OPTIONAL, 'Target directory', '/')
34 | ->addArgument('files', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Files')
35 | ;
36 | }
37 |
38 | protected function execute(InputInterface $input, OutputInterface $output): int
39 | {
40 | /** @var string $package */
41 | $package = $input->getArgument('package');
42 |
43 | /** @var string $target */
44 | $target = $input->getArgument('target');
45 |
46 | /** @var string[] $selectedFileNames */
47 | $selectedFileNames = $input->getArgument('files');
48 |
49 | $builder = new PackageFilesProcess($this->requireComposer(), [$package]);
50 |
51 | $filesystem = new Filesystem();
52 | $targetPath = $builder
53 | ->paths()
54 | ->absolute(trim($target, '\/'));
55 | $filesystem->ensureDirectoryExists($targetPath);
56 | $prefix = str_replace('/', '-', $package);
57 |
58 | foreach ($this->prepareFiles($builder->files(), $selectedFileNames) as $file) {
59 | $filename = str_replace('/', '-', $file->filename());
60 | $filesystem->copy($file->absolutePath(), "$targetPath/$prefix-$filename");
61 | }
62 |
63 | return 0;
64 | }
65 |
66 | /**
67 | * @param PackageFile[] $packageFiles
68 | * @param string[] $selectedFileNames
69 | *
70 | * @return PackageFile[]
71 | */
72 | private function prepareFiles(array $packageFiles, array $selectedFileNames): array
73 | {
74 | if (empty($selectedFileNames)) {
75 | return $packageFiles;
76 | }
77 |
78 | $files = [];
79 |
80 | foreach ($selectedFileNames as $selectedFileName) {
81 | if (pathinfo($selectedFileName, PATHINFO_EXTENSION) === '') {
82 | $selectedFileName .= '.php';
83 | }
84 |
85 | foreach ($packageFiles as $file) {
86 | if (substr($file->relativePath(), 0 - strlen($selectedFileName)) === $selectedFileName) {
87 | $files[] = $file;
88 | }
89 | }
90 | }
91 |
92 | return $files;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Command/InfoCommand.php:
--------------------------------------------------------------------------------
1 | setName('yii-config-info')
23 | ->addArgument('package', InputArgument::OPTIONAL);
24 | }
25 |
26 | protected function execute(InputInterface $input, OutputInterface $output): int
27 | {
28 | $io = new SymfonyStyle($input, $output);
29 | $composer = $this->requireComposer();
30 |
31 | $packageName = $input->getArgument('package');
32 | if (is_string($packageName)) {
33 | $package = $composer->getRepositoryManager()->getLocalRepository()->findPackage($packageName, '*');
34 | if ($package === null) {
35 | $io->error('Package "' . $packageName . '" not found.');
36 | return 1;
37 | }
38 | return $this->vendorPackage($composer, $package, $io);
39 | }
40 |
41 | return $this->rootPackage($composer, $io);
42 | }
43 |
44 | private function vendorPackage(Composer $composer, BasePackage $package, SymfonyStyle $io): int
45 | {
46 | $settings = ConfigSettings::forVendorPackage($composer, $package);
47 | if (empty($settings->packageConfiguration())) {
48 | $io->writeln('');
49 | $io->writeln('Configuration don\'t found in package "' . $package->getName() . '".>');
50 | return 0;
51 | }
52 |
53 | $io->title('Yii Config — Package "' . $package->getName() . '"');
54 |
55 | $io->writeln('Source directory: ' . $settings->path() . '/' . $settings->options()->sourceDirectory());
56 |
57 | $io->section('Configuration groups');
58 | $this->writeConfiguration($io, $settings->packageConfiguration());
59 |
60 | return 0;
61 | }
62 |
63 | private function rootPackage(Composer $composer, SymfonyStyle $io): int
64 | {
65 | $settings = ConfigSettings::forRootPackage($composer);
66 | $options = $settings->options();
67 | $sourceDirectory = $settings->options()->sourceDirectory();
68 | $mergePlanFilePath = $settings->path() . '/'
69 | . (empty($sourceDirectory) ? '' : ($sourceDirectory . '/'))
70 | . $options->mergePlanFile();
71 |
72 | $io->title('Yii Config — Root Configuration');
73 |
74 | $io->section('Options');
75 | $io->table([], [
76 | [
77 | 'Build merge plan',
78 | $options->buildMergePlan() ? 'yes>' : 'no>',
79 | ],
80 | [
81 | 'Merge plan file path',
82 | file_exists($mergePlanFilePath)
83 | ? '' . $mergePlanFilePath . '>'
84 | : '' . $mergePlanFilePath . ' (not exists)>',
85 | ],
86 | [
87 | 'Package types',
88 | empty($options->packageTypes()) ? 'not set>' : implode(', ', $options->packageTypes()),
89 | ],
90 | [
91 | 'Source directory',
92 | $settings->path() . '/' . $options->sourceDirectory(),
93 | ],
94 | [
95 | 'Vendor override layer packages',
96 | empty($options->vendorOverrideLayerPackages())
97 | ? 'not set>'
98 | : implode(', ', $options->vendorOverrideLayerPackages()),
99 | ],
100 | ]);
101 |
102 | $io->section('Configuration groups');
103 | $this->writeConfiguration($io, $settings->packageConfiguration());
104 |
105 | $io->section('Environments');
106 | $environmentsConfiguration = $settings->environmentsConfiguration();
107 | if (empty($environmentsConfiguration)) {
108 | $io->writeln('not set>');
109 | } else {
110 | $isFirst = true;
111 | foreach ($environmentsConfiguration as $environment => $groups) {
112 | if ($isFirst) {
113 | $isFirst = false;
114 | } else {
115 | $io->newLine();
116 | }
117 | $io->write(' ' . $environment . '>');
118 | if (empty($groups)) {
119 | $io->writeln(' (empty)>');
120 | } else {
121 | $io->newLine();
122 | $this->writeConfiguration($io, $groups, offset: 2, addSeparateLine: false);
123 | }
124 | }
125 | }
126 |
127 | return 0;
128 | }
129 |
130 | /**
131 | * @psalm-param array $configuration
132 | */
133 | private function writeConfiguration(
134 | SymfonyStyle $io,
135 | array $configuration,
136 | int $offset = 1,
137 | bool $addSeparateLine = true,
138 | ): void {
139 | foreach ($configuration as $group => $values) {
140 | $this->writeGroup($io, $group, $values, $offset);
141 | if ($addSeparateLine) {
142 | $io->newLine();
143 | }
144 | }
145 | }
146 |
147 | /**
148 | * @param string|string[] $items
149 | */
150 | private function writeGroup(SymfonyStyle $io, string $group, array|string $items, int $offset): void
151 | {
152 | $prefix = str_repeat(' ', $offset);
153 | $items = (array) $items;
154 | $io->write($prefix . '' . $group . '>');
155 | if (empty($items)) {
156 | $io->write(' (empty)>');
157 | } else {
158 | foreach ($items as $item) {
159 | $io->newLine();
160 | $io->write($prefix . ' - ' . (Options::isVariable($item) ? '' . $item . '>' : $item));
161 | }
162 | }
163 | $io->newLine();
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/Command/RebuildCommand.php:
--------------------------------------------------------------------------------
1 | setName('yii-config-rebuild')
21 | ->setDescription('Crawls all the configuration files and updates the merge plan file.')
22 | ->setHelp('This command crawls all the configuration files and updates the merge plan file.')
23 | ;
24 | }
25 |
26 | protected function execute(InputInterface $input, OutputInterface $output): int
27 | {
28 | new MergePlanProcess($this->requireComposer());
29 | return 0;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Composer/ConfigSettings.php:
--------------------------------------------------------------------------------
1 | >
14 | * @psalm-type EnvironmentsConfigurationType = array>
15 | */
16 | final class ConfigSettings
17 | {
18 | private readonly Options $options;
19 |
20 | /**
21 | * @psalm-var PackageConfigurationType
22 | */
23 | private readonly array $packageConfiguration;
24 |
25 | /**
26 | * @psalm-var EnvironmentsConfigurationType
27 | */
28 | private readonly array $environmentsConfiguration;
29 |
30 | private function __construct(
31 | private readonly string $path,
32 | array $composerExtra,
33 | ) {
34 | if (isset($composerExtra['config-plugin-file'])) {
35 | /**
36 | * @var array $extra
37 | * @psalm-suppress UnresolvableInclude,MixedOperand
38 | */
39 | $extra = require $this->path . '/' . $composerExtra['config-plugin-file'];
40 | } else {
41 | $extra = $composerExtra;
42 | }
43 |
44 | $this->options = new Options($extra);
45 |
46 | /** @psalm-var PackageConfigurationType */
47 | $this->packageConfiguration = (array) ($extra['config-plugin'] ?? []);
48 |
49 | /** @psalm-var EnvironmentsConfigurationType */
50 | $this->environmentsConfiguration = $extra['config-plugin-environments'] ?? [];
51 | }
52 |
53 | public static function forRootPackage(Composer $composer): self
54 | {
55 | /** @psalm-suppress PossiblyFalseArgument */
56 | return new self(
57 | realpath(dirname(Factory::getComposerFile())),
58 | $composer->getPackage()->getExtra(),
59 | );
60 | }
61 |
62 | public static function forVendorPackage(Composer $composer, BasePackage $package): self
63 | {
64 | /**
65 | * @var string $rootPath Because we use library and composer-plugins only which always has installation path.
66 | * @see PackagesListBuilder::getAllPackages()
67 | */
68 | $rootPath = $composer->getInstallationManager()->getInstallPath($package);
69 | return new self($rootPath, $package->getExtra());
70 | }
71 |
72 | public function path(): string
73 | {
74 | return $this->path;
75 | }
76 |
77 | public function configPath(): string
78 | {
79 | $sourceDirectory = $this->options->sourceDirectory();
80 | return $this->path . (empty($sourceDirectory) ? '' : "/$sourceDirectory");
81 | }
82 |
83 | public function options(): Options
84 | {
85 | return $this->options;
86 | }
87 |
88 | /**
89 | * Returns the root package configuration.
90 | *
91 | * @return array The root package configuration.
92 | *
93 | * @psalm-return PackageConfigurationType
94 | */
95 | public function packageConfiguration(): array
96 | {
97 | return $this->packageConfiguration;
98 | }
99 |
100 | /**
101 | * Returns the environments configuration.
102 | *
103 | * @return array The environments configuration.
104 | *
105 | * @psalm-return EnvironmentsConfigurationType
106 | */
107 | public function environmentsConfiguration(): array
108 | {
109 | return $this->environmentsConfiguration;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/Composer/EventHandler.php:
--------------------------------------------------------------------------------
1 | 'onCommand',
31 | ScriptEvents::POST_AUTOLOAD_DUMP => 'onPostAutoloadDump',
32 | ScriptEvents::POST_INSTALL_CMD => 'onPostUpdateCommandDump',
33 | ScriptEvents::POST_UPDATE_CMD => 'onPostUpdateCommandDump',
34 | ];
35 | }
36 |
37 | /**
38 | * @codeCoverageIgnore This method runs via eval and does not get into coverage.
39 | */
40 | public function onCommand(CommandEvent $event): void
41 | {
42 | if ($event->getCommandName() === 'dump-autoload') {
43 | $this->runOnAutoloadDump = true;
44 | }
45 | }
46 |
47 | /**
48 | * @codeCoverageIgnore This method runs via eval and does not get into coverage.
49 | */
50 | public function onPostAutoloadDump(Event $event): void
51 | {
52 | if ($this->runOnAutoloadDump) {
53 | $this->processConfigs($event->getComposer());
54 | }
55 | }
56 |
57 | public function onPostUpdateCommandDump(Event $event): void
58 | {
59 | $this->processConfigs($event->getComposer());
60 | }
61 |
62 | public function getCapabilities(): array
63 | {
64 | return [CommandProvider::class => ConfigCommandProvider::class];
65 | }
66 |
67 | public function activate(Composer $composer, IOInterface $io): void
68 | {
69 | // do nothing
70 | }
71 |
72 | public function deactivate(Composer $composer, IOInterface $io): void
73 | {
74 | // do nothing
75 | }
76 |
77 | public function uninstall(Composer $composer, IOInterface $io): void
78 | {
79 | // do nothing
80 | }
81 |
82 | private function processConfigs(Composer $composer): void
83 | {
84 | new MergePlanProcess($composer);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Composer/MergePlanProcess.php:
--------------------------------------------------------------------------------
1 | mergePlan = new MergePlan();
36 | $this->helper = new ProcessHelper($composer);
37 |
38 | if (!$this->helper->shouldBuildMergePlan()) {
39 | return;
40 | }
41 |
42 | $this->addPackagesConfigsToMergePlan(false);
43 | $this->addPackagesConfigsToMergePlan(true);
44 |
45 | $this->addRootPackageConfigToMergePlan();
46 | $this->addEnvironmentsConfigsToMergePlan();
47 |
48 | $this->updateMergePlan();
49 | }
50 |
51 | private function addPackagesConfigsToMergePlan(bool $isVendorOverrideLayer): void
52 | {
53 | $packages = $isVendorOverrideLayer ? $this->helper->getVendorOverridePackages() : $this->helper->getVendorPackages();
54 |
55 | foreach ($packages as $name => $package) {
56 | $configSettings = ConfigSettings::forVendorPackage($this->composer, $package);
57 | $packageName = $isVendorOverrideLayer ? Options::VENDOR_OVERRIDE_PACKAGE_NAME : $name;
58 |
59 | foreach ($configSettings->packageConfiguration() as $group => $files) {
60 | $this->mergePlan->addGroup($group);
61 |
62 | foreach ((array) $files as $file) {
63 | $isOptional = false;
64 |
65 | if (Options::isOptional($file)) {
66 | $isOptional = true;
67 | $file = substr($file, 1);
68 | }
69 |
70 | if (Options::isVariable($file)) {
71 | $this->mergePlan->add($file, $packageName, $group);
72 | continue;
73 | }
74 |
75 | $absoluteFilePath = $configSettings->configPath() . '/' . $file;
76 |
77 | if (Options::containsWildcard($file)) {
78 | $matches = glob($absoluteFilePath);
79 |
80 | if (empty($matches)) {
81 | continue;
82 | }
83 |
84 | foreach ($matches as $match) {
85 | $this->mergePlan->add(
86 | $this->normalizePackageFilePath($package, $match, $isVendorOverrideLayer),
87 | $packageName,
88 | $group,
89 | );
90 | }
91 |
92 | continue;
93 | }
94 |
95 | if ($isOptional && !is_file($absoluteFilePath)) {
96 | continue;
97 | }
98 |
99 | $this->mergePlan->add(
100 | $this->normalizePackageFilePath($package, $absoluteFilePath, $isVendorOverrideLayer),
101 | $packageName,
102 | $group,
103 | );
104 | }
105 | }
106 | }
107 | }
108 |
109 | private function addRootPackageConfigToMergePlan(): void
110 | {
111 | foreach ($this->helper->getRootPackageConfig() as $group => $files) {
112 | $this->mergePlan->addMultiple(
113 | (array) $files,
114 | Options::ROOT_PACKAGE_NAME,
115 | $group,
116 | );
117 | }
118 | }
119 |
120 | private function addEnvironmentsConfigsToMergePlan(): void
121 | {
122 | foreach ($this->helper->getEnvironmentConfig() as $environment => $groups) {
123 | if ($environment === Options::DEFAULT_ENVIRONMENT) {
124 | continue;
125 | }
126 |
127 | if (empty($groups)) {
128 | $this->mergePlan->addEnvironmentWithoutConfigs($environment);
129 | continue;
130 | }
131 |
132 | foreach ($groups as $group => $files) {
133 | $this->mergePlan->addMultiple(
134 | (array) $files,
135 | Options::ROOT_PACKAGE_NAME,
136 | $group,
137 | $environment,
138 | );
139 | }
140 | }
141 | }
142 |
143 | private function updateMergePlan(): void
144 | {
145 | $mergePlan = $this->mergePlan->toArray();
146 | ksort($mergePlan);
147 |
148 | $filePath = $this->helper->getPaths()->absolute(
149 | $this->helper->getMergePlanFile()
150 | );
151 | (new Filesystem())->ensureDirectoryExists(dirname($filePath));
152 |
153 | /** @var string $oldContent */
154 | $oldContent = is_file($filePath) ? file_get_contents($filePath) : '';
155 |
156 | $content = 'export(true) . ";\n";
160 |
161 | if ($this->normalizeLineEndings($oldContent) !== $this->normalizeLineEndings($content)) {
162 | file_put_contents($filePath, $content, LOCK_EX);
163 | }
164 | }
165 |
166 | private function normalizeLineEndings(string $value): string
167 | {
168 | return strtr($value, [
169 | "\r\n" => "\n",
170 | "\r" => "\n",
171 | ]);
172 | }
173 |
174 | private function normalizePackageFilePath(
175 | PackageInterface $package,
176 | string $absoluteFilePath,
177 | bool $isVendorOverrideLayer
178 | ): string {
179 | if ($isVendorOverrideLayer) {
180 | return $this->helper->getRelativePackageFilePathWithPackageName($package, $absoluteFilePath);
181 | }
182 |
183 | return $this->helper->getRelativePackageFilePath($package, $absoluteFilePath);
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/Composer/Options.php:
--------------------------------------------------------------------------------
1 | mergePlanFile = (string) $options['merge-plan-file'];
50 | }
51 |
52 | if (isset($options['build-merge-plan'])) {
53 | $this->buildMergePlan = (bool) $options['build-merge-plan'];
54 | }
55 |
56 | if (isset($options['vendor-override-layer'])) {
57 | $this->vendorOverrideLayerPackages = array_filter(
58 | (array) $options['vendor-override-layer'],
59 | static fn(mixed $value): bool => is_string($value),
60 | );
61 | }
62 |
63 | if (isset($options['source-directory'])) {
64 | $this->sourceDirectory = $this->normalizePath((string) $options['source-directory']);
65 | }
66 |
67 | if (isset($options['package-types'])) {
68 | $this->packageTypes = array_filter(
69 | (array) $options['package-types'],
70 | static fn(mixed $value): bool => is_string($value),
71 | );
72 | }
73 | }
74 |
75 | public static function containsWildcard(string $file): bool
76 | {
77 | return str_contains($file, '*');
78 | }
79 |
80 | public static function isOptional(string $file): bool
81 | {
82 | return str_starts_with($file, '?');
83 | }
84 |
85 | public static function isVariable(string $file): bool
86 | {
87 | return str_starts_with($file, '$');
88 | }
89 |
90 | public function mergePlanFile(): string
91 | {
92 | return $this->mergePlanFile;
93 | }
94 |
95 | public function buildMergePlan(): bool
96 | {
97 | return $this->buildMergePlan;
98 | }
99 |
100 | /**
101 | * @return string[]
102 | */
103 | public function vendorOverrideLayerPackages(): array
104 | {
105 | return $this->vendorOverrideLayerPackages;
106 | }
107 |
108 | public function sourceDirectory(): string
109 | {
110 | return $this->sourceDirectory;
111 | }
112 |
113 | /**
114 | * @return string[]
115 | */
116 | public function packageTypes(): array
117 | {
118 | return $this->packageTypes;
119 | }
120 |
121 | private function normalizePath(string $value): string
122 | {
123 | return trim(str_replace('\\', '/', $value), '/');
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/Composer/PackageFile.php:
--------------------------------------------------------------------------------
1 | filename = str_replace($configSettings->configPath() . '/', '', $absolutePath);
20 | $this->relativePath = str_replace($configSettings->path() . '/', '', $absolutePath);
21 | }
22 |
23 | public function filename(): string
24 | {
25 | return $this->filename;
26 | }
27 |
28 | public function relativePath(): string
29 | {
30 | return $this->relativePath;
31 | }
32 |
33 | public function absolutePath(): string
34 | {
35 | return $this->absolutePath;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Composer/PackageFilesProcess.php:
--------------------------------------------------------------------------------
1 | helper = new ProcessHelper($composer);
37 | $this->process($packageNames);
38 | }
39 |
40 | /**
41 | * Returns the processed package configuration files.
42 | *
43 | * @return PackageFile[] The processed package configuration files.
44 | */
45 | public function files(): array
46 | {
47 | return $this->packageFiles;
48 | }
49 |
50 | /**
51 | * Returns the config paths instance.
52 | *
53 | * @return ConfigPaths The config paths instance.
54 | */
55 | public function paths(): ConfigPaths
56 | {
57 | return $this->helper->getPaths();
58 | }
59 |
60 | /**
61 | * @param string[] $packageNames The pretty package names to build.
62 | * If the array is empty, the files of all packages will be build.
63 | */
64 | private function process(array $packageNames): void
65 | {
66 | foreach ($this->helper->getPackages() as $package) {
67 | $configSettings = ConfigSettings::forVendorPackage($this->composer, $package);
68 | foreach ($configSettings->packageConfiguration() as $files) {
69 | $files = (array) $files;
70 |
71 | foreach ($files as $file) {
72 | $isOptional = false;
73 |
74 | if (Options::isOptional($file)) {
75 | $isOptional = true;
76 | $file = substr($file, 1);
77 | }
78 |
79 | if (
80 | Options::isVariable($file) ||
81 | (!empty($packageNames) && !in_array($package->getPrettyName(), $packageNames, true))
82 | ) {
83 | continue;
84 | }
85 |
86 | $absoluteFilePath = $configSettings->configPath() . '/' . $file;
87 |
88 | if (Options::containsWildcard($file)) {
89 | $matches = glob($absoluteFilePath);
90 |
91 | if (empty($matches)) {
92 | continue;
93 | }
94 |
95 | foreach ($matches as $match) {
96 | $this->packageFiles[] = new PackageFile($configSettings, $match);
97 | }
98 |
99 | continue;
100 | }
101 |
102 | if ($isOptional && !is_file($absoluteFilePath)) {
103 | continue;
104 | }
105 |
106 | $this->packageFiles[] = new PackageFile($configSettings, $absoluteFilePath);
107 | }
108 | }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/Composer/PackagesListBuilder.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | public function build(): array
31 | {
32 | $allPackages = $this->getAllPackages();
33 |
34 | $packageDepths = [];
35 | $this->calculatePackageDepths($allPackages, $packageDepths, 0, $this->composer->getPackage(), true);
36 |
37 | $result = [];
38 | foreach ($this->getSortedPackageNames($packageDepths) as $name) {
39 | if (array_key_exists($name, $allPackages)) {
40 | $result[$name] = $allPackages[$name];
41 | }
42 | }
43 |
44 | return $result;
45 | }
46 |
47 | /**
48 | * Get package names stable sorted by depth
49 | *
50 | * @param array $packageDepths
51 | *
52 | * @return string[]
53 | */
54 | private function getSortedPackageNames(array $packageDepths): array
55 | {
56 | $n = 0;
57 | foreach ($packageDepths as $name => $depth) {
58 | $packageDepths[$name] = [$depth, ++$n];
59 | }
60 |
61 | /** @psalm-var array $packageDepths */
62 |
63 | uasort($packageDepths, static function (array $a, array $b) {
64 | $result = $a[0] <=> $b[0];
65 | return $result === 0 ? $a[1] <=> $b[1] : $result;
66 | });
67 |
68 | return array_keys($packageDepths);
69 | }
70 |
71 | /**
72 | * @param array $allPackages
73 | * @param array $packageDepths
74 | */
75 | private function calculatePackageDepths(
76 | array $allPackages,
77 | array &$packageDepths,
78 | int $depth,
79 | PackageInterface $package,
80 | bool $includingDev
81 | ): void {
82 | $name = $package->getPrettyName();
83 |
84 | $packageProcessed = array_key_exists($name, $packageDepths);
85 |
86 | if (!$packageProcessed || $packageDepths[$name] < $depth) {
87 | $packageDepths[$name] = $depth;
88 | }
89 |
90 | // Prevent infinite loop in case of circular dependencies
91 | if ($packageProcessed) {
92 | return;
93 | }
94 |
95 | ++$depth;
96 |
97 | $dependencies = $includingDev
98 | ? [...array_keys($package->getRequires()), ...array_keys($package->getDevRequires())]
99 | : array_keys($package->getRequires());
100 |
101 | foreach ($dependencies as $dependency) {
102 | if (array_key_exists($dependency, $allPackages)) {
103 | $this->calculatePackageDepths($allPackages, $packageDepths, $depth, $allPackages[$dependency], false);
104 | }
105 | }
106 | }
107 |
108 | /**
109 | * @return array
110 | */
111 | private function getAllPackages(): array
112 | {
113 | $packages = $this->composer
114 | ->getRepositoryManager()
115 | ->getLocalRepository()
116 | ->getPackages();
117 |
118 | $result = [];
119 | foreach ($packages as $package) {
120 | if (!in_array($package->getType(), $this->packageTypes)) {
121 | continue;
122 | }
123 |
124 | $result[$package->getPrettyName()] = $package;
125 | }
126 |
127 | return $result;
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/Composer/ProcessHelper.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | private readonly array $packages;
29 |
30 | /**
31 | * @param Composer $composer The composer instance.
32 | */
33 | public function __construct(
34 | private readonly Composer $composer,
35 | ) {
36 | /** @psalm-suppress UnresolvableInclude, MixedOperand */
37 | require_once $composer->getConfig()->get('vendor-dir') . '/autoload.php';
38 |
39 | $this->appConfigSettings = ConfigSettings::forRootPackage($composer);
40 |
41 | $this->paths = new ConfigPaths(
42 | $this->appConfigSettings->path(),
43 | $this->appConfigSettings->options()->sourceDirectory(),
44 | );
45 | $this->packages = (new PackagesListBuilder(
46 | $this->composer,
47 | $this->appConfigSettings->options()->packageTypes()
48 | ))->build();
49 | }
50 |
51 | /**
52 | * Returns all vendor packages.
53 | *
54 | * @psalm-return array
55 | */
56 | public function getPackages(): array
57 | {
58 | return $this->packages;
59 | }
60 |
61 | /**
62 | * Returns vendor packages without packages from the vendor override sublayer.
63 | *
64 | * @psalm-return array
65 | */
66 | public function getVendorPackages(): array
67 | {
68 | $vendorPackages = [];
69 |
70 | foreach ($this->packages as $name => $package) {
71 | if (!$this->isVendorOverridePackage($name)) {
72 | $vendorPackages[$name] = $package;
73 | }
74 | }
75 |
76 | return $vendorPackages;
77 | }
78 |
79 | /**
80 | * Returns vendor packages only from the vendor override sublayer.
81 | *
82 | * @psalm-return array
83 | */
84 | public function getVendorOverridePackages(): array
85 | {
86 | $vendorOverridePackages = [];
87 |
88 | foreach ($this->packages as $name => $package) {
89 | if ($this->isVendorOverridePackage($name)) {
90 | $vendorOverridePackages[$name] = $package;
91 | }
92 | }
93 |
94 | return $vendorOverridePackages;
95 | }
96 |
97 | /**
98 | * Returns the relative path to the package file including the source directory {@see Options::sourceDirectory()}.
99 | *
100 | * @param PackageInterface $package The package instance.
101 | * @param string $filePath The absolute path to the package file.
102 | *
103 | * @return string The relative path to the package file including the source directory.
104 | */
105 | public function getRelativePackageFilePath(PackageInterface $package, string $filePath): string
106 | {
107 | return str_replace("{$this->getPackageRootDirectoryPath($package)}/", '', $filePath);
108 | }
109 |
110 | /**
111 | * Returns the relative path to the package file including the package name.
112 | *
113 | * @param PackageInterface $package The package instance.
114 | * @param string $filePath The absolute path to the package file.
115 | *
116 | * @return string The relative path to the package file including the package name.
117 | */
118 | public function getRelativePackageFilePathWithPackageName(PackageInterface $package, string $filePath): string
119 | {
120 | return "{$package->getPrettyName()}/{$this->getRelativePackageFilePath($package, $filePath)}";
121 | }
122 |
123 | /**
124 | * Returns the root package configuration.
125 | *
126 | * @return array The root package configuration.
127 | *
128 | * @psalm-return PackageConfigurationType
129 | */
130 | public function getRootPackageConfig(): array
131 | {
132 | return $this->appConfigSettings->packageConfiguration();
133 | }
134 |
135 | /**
136 | * Returns the environment configuration.
137 | *
138 | * @return array The environment configuration.
139 | *
140 | * @psalm-return EnvironmentsConfigurationType
141 | */
142 | public function getEnvironmentConfig(): array
143 | {
144 | return $this->appConfigSettings->environmentsConfiguration();
145 | }
146 |
147 | /**
148 | * Returns the config paths instance.
149 | *
150 | * @return ConfigPaths The config paths instance.
151 | */
152 | public function getPaths(): ConfigPaths
153 | {
154 | return $this->paths;
155 | }
156 |
157 | /**
158 | * Checks whether to build a merge plan.
159 | *
160 | * @return bool Whether to build a merge plan.
161 | */
162 | public function shouldBuildMergePlan(): bool
163 | {
164 | return $this->appConfigSettings->options()->buildMergePlan();
165 | }
166 |
167 | /**
168 | * @return string The merge plan filepath.
169 | */
170 | public function getMergePlanFile(): string
171 | {
172 | return $this->appConfigSettings->options()->mergePlanFile();
173 | }
174 |
175 | /**
176 | * Returns the absolute path to the package root directory.
177 | *
178 | * @param PackageInterface $package The package instance.
179 | *
180 | * @return string The absolute path to the package root directory.
181 | */
182 | private function getPackageRootDirectoryPath(PackageInterface $package): string
183 | {
184 | /**
185 | * @var string Because we use library and composer-plugins only ({@see PackagesListBuilder::getAllPackages()}),
186 | * which always has installation path.
187 | */
188 | return $this->composer
189 | ->getInstallationManager()
190 | ->getInstallPath($package);
191 | }
192 |
193 | /**
194 | * Checks whether the package is in the vendor override sublayer.
195 | *
196 | * @param string $package The package name.
197 | *
198 | * @return bool Whether the package is in the vendor override sublayer.
199 | */
200 | private function isVendorOverridePackage(string $package): bool
201 | {
202 | foreach ($this->appConfigSettings->options()->vendorOverrideLayerPackages() as $pattern) {
203 | if ($package === $pattern || (new WildcardPattern($pattern))->match($package)) {
204 | return true;
205 | }
206 | }
207 |
208 | return false;
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/src/Config.php:
--------------------------------------------------------------------------------
1 |
30 | */
31 | private array $build = [];
32 |
33 | /**
34 | * @param ConfigPaths $paths The config paths instance.
35 | * @param string|null $environment The environment name.
36 | * @param object[] $modifiers Modifiers that affect merge process.
37 | * @param string|null $paramsGroup Group name for `$params`. If it is `null`, then `$params` will be empty array.
38 | * @param string $mergePlanFile The merge plan filepath.
39 | *
40 | * @throws ErrorException If the environment does not exist.
41 | */
42 | public function __construct(
43 | ConfigPaths $paths,
44 | ?string $environment = null,
45 | array $modifiers = [],
46 | private readonly ?string $paramsGroup = 'params',
47 | string $mergePlanFile = Options::DEFAULT_MERGE_PLAN_FILE,
48 | ) {
49 | $environment = empty($environment) ? Options::DEFAULT_ENVIRONMENT : $environment;
50 |
51 | /** @psalm-suppress UnresolvableInclude, MixedArgument */
52 | $mergePlan = new MergePlan(require $paths->absolute($mergePlanFile));
53 |
54 | if (!$mergePlan->hasEnvironment($environment)) {
55 | $this->throwException(sprintf('The "%s" configuration environment does not exist.', $environment));
56 | }
57 |
58 | $dataModifiers = new DataModifiers($modifiers);
59 | $this->merger = new Merger($paths, $dataModifiers);
60 | $this->filesExtractor = new FilesExtractor($paths, $mergePlan, $dataModifiers, $environment);
61 | }
62 |
63 | /**
64 | * {@inheritDoc}
65 | *
66 | * @throws ErrorException If the group does not exist or an error occurred during the build.
67 | */
68 | public function get(string $group): array
69 | {
70 | if (isset($this->build[$group])) {
71 | return $this->build[$group];
72 | }
73 |
74 | $this->runBuildParams();
75 |
76 | $this->merger->reset();
77 | $this->build[$group] = $this->buildGroup($group);
78 |
79 | return $this->build[$group];
80 | }
81 |
82 | public function has(string $group): bool
83 | {
84 | return $this->filesExtractor->hasGroup($group);
85 | }
86 |
87 | /**
88 | * @throws ErrorException If an error occurred during the build.
89 | */
90 | private function runBuildParams(): void
91 | {
92 | if ($this->paramsGroup !== null && !isset($this->build[$this->paramsGroup])) {
93 | $this->isBuildingParams = true;
94 | $this->build[$this->paramsGroup] = $this->buildGroup($this->paramsGroup);
95 | $this->isBuildingParams = false;
96 | }
97 | }
98 |
99 | /**
100 | * Builds the configuration of the group.
101 | *
102 | * @param string $group The group name.
103 | *
104 | * @throws ErrorException If an error occurred during the build.
105 | */
106 | private function buildGroup(string $group, array $result = [], ?string $originalGroup = null): array
107 | {
108 | foreach ($this->filesExtractor->extract($group) as $file => $context) {
109 | if ($context->isVariable()) {
110 | $variable = $this->prepareVariable($file, $group);
111 | $result = $this->buildGroup($variable, $result, $originalGroup ?? $group);
112 | } else {
113 | $result = $this->merger->merge(
114 | $context->setOriginalGroup($originalGroup ?? $group),
115 | $result,
116 | $this->buildFile($file),
117 | );
118 | }
119 | }
120 |
121 | return $result;
122 | }
123 |
124 | /**
125 | * Checks the configuration variable and returns its name.
126 | *
127 | * @param string $variable The variable.
128 | * @param string $group The group name.
129 | *
130 | * @throws ErrorException If the variable name is not valid.
131 | *
132 | * @return string The variable name.
133 | */
134 | private function prepareVariable(string $variable, string $group): string
135 | {
136 | $name = substr($variable, 1);
137 |
138 | if ($name === $group) {
139 | $this->throwException(sprintf(
140 | 'The variable "%s" must not be located inside the "%s" config group.',
141 | "$variable",
142 | "$name",
143 | ));
144 | }
145 |
146 | return $name;
147 | }
148 |
149 | /**
150 | * Builds the configuration from the file.
151 | *
152 | * @param string $filePath The file path.
153 | *
154 | * @throws ErrorException If an error occurred during the build.
155 | *
156 | * @return array The configuration from the file.
157 | */
158 | private function buildFile(string $filePath): array
159 | {
160 | $scopeRequire = static function (): array {
161 | /** @psalm-suppress InvalidArgument, MissingClosureParamType */
162 | set_error_handler(static function (int $errorNumber, string $errorString, string $errorFile, int $errorLine) {
163 | throw new ErrorException($errorString, $errorNumber, 0, $errorFile, $errorLine);
164 | });
165 |
166 | /** @psalm-suppress MixedArgument, PossiblyFalseArgument */
167 | extract(func_get_arg(1), EXTR_SKIP);
168 |
169 | /**
170 | * @psalm-suppress UnresolvableInclude
171 | * @psalm-var array
172 | */
173 | $result = require func_get_arg(0);
174 | restore_error_handler();
175 | return $result;
176 | };
177 |
178 | $scope = [];
179 |
180 | if (!$this->isBuildingParams) {
181 | $scope['config'] = $this;
182 | $scope['params'] = $this->paramsGroup === null ? [] : $this->build[$this->paramsGroup];
183 | }
184 |
185 | /** @psalm-suppress TooManyArguments */
186 | return $scopeRequire($filePath, $scope);
187 | }
188 |
189 | /**
190 | * @throws ErrorException
191 | */
192 | private function throwException(string $message): void
193 | {
194 | throw new ErrorException($message, 0, E_USER_ERROR);
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/src/ConfigInterface.php:
--------------------------------------------------------------------------------
1 | configPath = $rootPath . ($configDirectory === '' ? '' : "/$configDirectory");
33 |
34 | $vendorDirectory = trim($vendorDirectory ?? Options::DEFAULT_VENDOR_DIRECTORY, '/');
35 | $this->vendorPath = $rootPath . ($vendorDirectory === '' ? '' : "/$vendorDirectory");
36 | }
37 |
38 | /**
39 | * Returns the absolute path to the configuration file.
40 | *
41 | * @param string $file Config file.
42 | * @param string $package Name of the package. {@see Options::ROOT_PACKAGE_NAME} stands for the root package.
43 | *
44 | * @return string The absolute path to the configuration file.
45 | */
46 | public function absolute(string $file, string $package = Options::ROOT_PACKAGE_NAME): string
47 | {
48 | if ($package === Options::ROOT_PACKAGE_NAME) {
49 | return "$this->configPath/$file";
50 | }
51 |
52 | if ($package === Options::VENDOR_OVERRIDE_PACKAGE_NAME) {
53 | return "$this->vendorPath/$file";
54 | }
55 |
56 | return "$this->vendorPath/$package/$file";
57 | }
58 |
59 | /**
60 | * Returns the relative path to the configuration file.
61 | *
62 | * @param string $file Config file.
63 | *
64 | * @return string The relative path to the configuration file.
65 | */
66 | public function relative(string $file): string
67 | {
68 | return str_starts_with($file, "$this->rootPath/")
69 | ? substr($file, strlen("$this->rootPath/"))
70 | : $file;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Context.php:
--------------------------------------------------------------------------------
1 | originalGroup = $group;
31 | return $this;
32 | }
33 |
34 | public function originalGroup(): string
35 | {
36 | return $this->originalGroup;
37 | }
38 |
39 | public function group(): string
40 | {
41 | return $this->group;
42 | }
43 |
44 | public function package(): string
45 | {
46 | return $this->package;
47 | }
48 |
49 | public function layer(): int
50 | {
51 | return $this->layer;
52 | }
53 |
54 | public function file(): string
55 | {
56 | return $this->file;
57 | }
58 |
59 | public function isVariable(): bool
60 | {
61 | return $this->isVariable;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/DataModifiers.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | private readonly array $mergedGroupsRecursionDepthMap;
27 |
28 | /**
29 | * @psalm-var array
30 | */
31 | private readonly array $reverseMergeGroupsIndex;
32 |
33 | /**
34 | * @psalm-var array
35 | */
36 | private array $removeFromVendorGroupsIndex = [];
37 |
38 | /**
39 | * @psalm-var array>
40 | */
41 | private array $removeFromVendorKeysIndex = [];
42 |
43 | /**
44 | * @param object[] $modifiers Modifiers that affect merge process.
45 | */
46 | public function __construct(array $modifiers = [])
47 | {
48 | $reverseMergeGroups = [];
49 | $recursiveMergeGroups = [];
50 |
51 | foreach ($modifiers as $modifier) {
52 | if ($modifier instanceof ReverseMerge) {
53 | array_unshift($reverseMergeGroups, $modifier->getGroups());
54 | continue;
55 | }
56 |
57 | if ($modifier instanceof RecursiveMerge) {
58 | array_unshift(
59 | $recursiveMergeGroups,
60 | array_fill_keys($modifier->getGroups(), $modifier->getDepth())
61 | );
62 | continue;
63 | }
64 |
65 | if ($modifier instanceof RemoveGroupsFromVendor) {
66 | foreach ($modifier->getGroups() as $package => $groups) {
67 | foreach ($groups as $group) {
68 | $this->removeFromVendorGroupsIndex[$package . '~' . $group] = true;
69 | }
70 | }
71 | continue;
72 | }
73 |
74 | if ($modifier instanceof RemoveKeysFromVendor) {
75 | $configPaths = [];
76 |
77 | if ($modifier->getPackages() === []) {
78 | $configPaths[] = '*';
79 | } else {
80 | foreach ($modifier->getPackages() as $configPath) {
81 | $package = array_shift($configPath);
82 |
83 | if ($configPath === []) {
84 | $configPaths[] = $package . '~*';
85 | } else {
86 | foreach ($configPath as $group) {
87 | $configPaths[] = $package . '~' . $group;
88 | }
89 | }
90 | }
91 | }
92 |
93 | foreach ($modifier->getKeys() as $keyPath) {
94 | foreach ($configPaths as $configPath) {
95 | $this->removeFromVendorKeysIndex[$configPath] ??= [];
96 | ArrayHelper::setValue($this->removeFromVendorKeysIndex[$configPath], $keyPath, true);
97 | }
98 | }
99 | }
100 | }
101 |
102 | $this->reverseMergeGroupsIndex = array_flip(array_merge(...$reverseMergeGroups));
103 | $this->mergedGroupsRecursionDepthMap = array_merge(...$recursiveMergeGroups);
104 | }
105 |
106 | /**
107 | * @return false|int|null
108 | * - `int` - depth limited by specified number.
109 | * - `null` - depth is not limited (infinite recursion).
110 | * - `false` - recursion is disabled.
111 | */
112 | public function getRecursionDepth(string $group): int|null|false
113 | {
114 | if (!array_key_exists($group, $this->mergedGroupsRecursionDepthMap)) {
115 | return false;
116 | }
117 |
118 | return $this->mergedGroupsRecursionDepthMap[$group];
119 | }
120 |
121 | public function isReverseMergeGroup(string $group): bool
122 | {
123 | return array_key_exists($group, $this->reverseMergeGroupsIndex);
124 | }
125 |
126 | public function shouldRemoveGroupFromVendor(string $package, string $group, int $layer): bool
127 | {
128 | if ($layer !== Context::VENDOR) {
129 | return false;
130 | }
131 |
132 | return array_key_exists('*~*', $this->removeFromVendorGroupsIndex)
133 | || array_key_exists('*~' . $group, $this->removeFromVendorGroupsIndex)
134 | || array_key_exists($package . '~*', $this->removeFromVendorGroupsIndex)
135 | || array_key_exists($package . '~' . $group, $this->removeFromVendorGroupsIndex);
136 | }
137 |
138 | /**
139 | * @psalm-param non-empty-array $keyPath
140 | */
141 | public function shouldRemoveKeyFromVendor(Context $context, array $keyPath): bool
142 | {
143 | if ($context->layer() !== Context::VENDOR) {
144 | return false;
145 | }
146 |
147 | $configPaths = [
148 | '*',
149 | $context->package() . '~*',
150 | $context->package() . '~' . $context->group(),
151 | ];
152 |
153 | foreach ($configPaths as $configPath) {
154 | if (ArrayHelper::getValue($this->removeFromVendorKeysIndex[$configPath] ?? [], $keyPath) === true) {
155 | return true;
156 | }
157 | }
158 |
159 | return false;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/FilesExtractor.php:
--------------------------------------------------------------------------------
1 |
38 | */
39 | public function extract(string $group): array
40 | {
41 | $environment = $this->prepareEnvironment($group);
42 |
43 | $result = $this->process(Options::DEFAULT_ENVIRONMENT, $group, $this->mergePlan->getGroup($group));
44 |
45 | if ($environment !== Options::DEFAULT_ENVIRONMENT) {
46 | $result = array_merge(
47 | $result,
48 | $this->process(
49 | $environment,
50 | $group,
51 | $this->mergePlan->getGroup($group, $environment)
52 | )
53 | );
54 | }
55 |
56 | return $result;
57 | }
58 |
59 | /**
60 | * Checks whether the group exists in the merge plan.
61 | *
62 | * @param string $group The group name.
63 | *
64 | * @return bool Whether the group exists in the merge plan.
65 | */
66 | public function hasGroup(string $group): bool
67 | {
68 | return $this->mergePlan->hasGroup($group, $this->environment) || (
69 | $this->environment !== Options::DEFAULT_ENVIRONMENT &&
70 | $this->mergePlan->hasGroup($group, Options::DEFAULT_ENVIRONMENT)
71 | );
72 | }
73 |
74 | /**
75 | * @psalm-param array $data
76 | *
77 | * @throws ErrorException If an error occurred during the process.
78 | *
79 | * @psalm-return array
80 | */
81 | private function process(string $environment, string $group, array $data): array
82 | {
83 | $result = [];
84 |
85 | foreach ($data as $package => $items) {
86 | $layer = $this->detectLayer($environment, $package);
87 |
88 | if ($this->dataModifiers->shouldRemoveGroupFromVendor($package, $group, $layer)) {
89 | continue;
90 | }
91 |
92 | foreach ($items as $item) {
93 | if (Options::isVariable($item)) {
94 | $result[$item] = new Context($group, $package, $layer, $item, true);
95 | continue;
96 | }
97 |
98 | $isOptional = Options::isOptional($item);
99 |
100 | if ($isOptional) {
101 | $item = substr($item, 1);
102 | }
103 |
104 | $filePath = $this->paths->absolute($item, $package);
105 | /** @psalm-var list $files */
106 | $files = Options::containsWildcard($item) ? glob($filePath) : [$filePath];
107 |
108 | foreach ($files as $file) {
109 | if (is_file($file)) {
110 | $result[$file] = new Context($group, $package, $layer, $file, false);
111 | } elseif (!$isOptional) {
112 | $this->throwException(sprintf('The "%s" file does not found.', $file));
113 | }
114 | }
115 | }
116 | }
117 |
118 | return $result;
119 | }
120 |
121 | /**
122 | * Calculates the layer for the context.
123 | *
124 | * @param string $environment The environment name.
125 | * @param string $package The package name.
126 | *
127 | * @return int The layer for the context.
128 | */
129 | private function detectLayer(string $environment, string $package): int
130 | {
131 | if ($package !== Options::ROOT_PACKAGE_NAME) {
132 | return $package === Options::VENDOR_OVERRIDE_PACKAGE_NAME ? Context::VENDOR_OVERRIDE : Context::VENDOR;
133 | }
134 |
135 | if ($environment === Options::DEFAULT_ENVIRONMENT) {
136 | return Context::APPLICATION;
137 | }
138 |
139 | return Context::ENVIRONMENT;
140 | }
141 |
142 | /**
143 | * Checks the group name and returns actual environment name.
144 | *
145 | * @param string $group The group name.
146 | *
147 | * @throws ErrorException If the group does not exist.
148 | *
149 | * @return string The actual environment name.
150 | */
151 | private function prepareEnvironment(string $group): string
152 | {
153 | if (!$this->mergePlan->hasGroup($group, $this->environment)) {
154 | if (
155 | $this->environment === Options::DEFAULT_ENVIRONMENT ||
156 | !$this->mergePlan->hasGroup($group, Options::DEFAULT_ENVIRONMENT)
157 | ) {
158 | $this->throwException(sprintf('The "%s" configuration group does not exist.', $group));
159 | }
160 |
161 | return Options::DEFAULT_ENVIRONMENT;
162 | }
163 |
164 | return $this->environment;
165 | }
166 |
167 | /**
168 | * @throws ErrorException
169 | */
170 | private function throwException(string $message): void
171 | {
172 | throw new ErrorException($message, 0, E_USER_ERROR);
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/MergePlan.php:
--------------------------------------------------------------------------------
1 | >> $mergePlan
16 | */
17 | public function __construct(
18 | private array $mergePlan = [],
19 | ) {
20 | }
21 |
22 | /**
23 | * Adds an item to the merge plan.
24 | *
25 | * @param string $file The config file.
26 | * @param string $package The package name.
27 | * @param string $group The group name.
28 | * @param string $environment The environment name.
29 | */
30 | public function add(
31 | string $file,
32 | string $package,
33 | string $group,
34 | string $environment = Options::DEFAULT_ENVIRONMENT
35 | ): void {
36 | $this->mergePlan[$environment][$group][$package][] = $file;
37 | }
38 |
39 | /**
40 | * Adds a multiple items to the merge plan.
41 | *
42 | * @param string[] $files The config files.
43 | * @param string $package The package name.
44 | * @param string $group The group name.
45 | * @param string $environment The environment name.
46 | */
47 | public function addMultiple(
48 | array $files,
49 | string $package,
50 | string $group,
51 | string $environment = Options::DEFAULT_ENVIRONMENT
52 | ): void {
53 | $this->mergePlan[$environment][$group][$package] = $files;
54 | }
55 |
56 | /**
57 | * Adds an empty environment item to the merge plan.
58 | *
59 | * @param string $environment The environment name.
60 | */
61 | public function addEnvironmentWithoutConfigs(string $environment): void
62 | {
63 | $this->mergePlan[$environment] = [];
64 | }
65 |
66 | /**
67 | * Add empty group if it not exists.
68 | *
69 | * @param string $group The group name.
70 | * @param string $environment The environment name.
71 | */
72 | public function addGroup(string $group, string $environment = Options::DEFAULT_ENVIRONMENT): void
73 | {
74 | if (!isset($this->mergePlan[$environment][$group])) {
75 | $this->mergePlan[$environment][$group] = [];
76 | }
77 | }
78 |
79 | /**
80 | * Returns the merge plan group.
81 | *
82 | * @param string $group The group name.
83 | * @param string $environment The environment name.
84 | *
85 | * @return array
86 | */
87 | public function getGroup(string $group, string $environment = Options::DEFAULT_ENVIRONMENT): array
88 | {
89 | return $this->mergePlan[$environment][$group] ?? [];
90 | }
91 |
92 | /**
93 | * Returns the merge plan as an array.
94 | *
95 | * @psalm-return array>>
96 | */
97 | public function toArray(): array
98 | {
99 | return $this->mergePlan;
100 | }
101 |
102 | /**
103 | * Checks whether the group exists in the merge plan.
104 | *
105 | * @param string $group The group name.
106 | * @param string $environment The environment name.
107 | *
108 | * @return bool Whether the group exists in the merge plan.
109 | */
110 | public function hasGroup(string $group, string $environment): bool
111 | {
112 | return isset($this->mergePlan[$environment][$group]);
113 | }
114 |
115 | /**
116 | * Checks whether the environment exists in the merge plan.
117 | *
118 | * @param string $environment The environment name.
119 | *
120 | * @return bool Whether the environment exists in the merge plan.
121 | */
122 | public function hasEnvironment(string $environment): bool
123 | {
124 | return isset($this->mergePlan[$environment]);
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/Merger.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | private array $cacheKeys = [];
29 |
30 | /**
31 | * @param ConfigPaths $configPaths The config paths instance.
32 | * @param DataModifiers $dataModifiers The data modifiers that affect merge process.
33 | */
34 | public function __construct(
35 | private readonly ConfigPaths $configPaths,
36 | private readonly DataModifiers $dataModifiers,
37 | ) {
38 | }
39 |
40 | public function reset(): void
41 | {
42 | $this->cacheKeys = [];
43 | }
44 |
45 | /**
46 | * Merges two or more arrays into one recursively.
47 | *
48 | * @param Context $context Context containing the name of the file, package, group and environment.
49 | * @param array $arrayA First array to merge.
50 | * @param array $arrayB Second array to merge.
51 | *
52 | * @throws ErrorException If an error occurred during the merge.
53 | *
54 | * @return array The merged array.
55 | */
56 | public function merge(Context $context, array $arrayA, array $arrayB): array
57 | {
58 | $recursionDepth = $this->dataModifiers->getRecursionDepth($context->originalGroup());
59 | $isReverseMerge = $this->dataModifiers->isReverseMergeGroup($context->originalGroup());
60 |
61 | if ($isReverseMerge) {
62 | $arrayB = $this->prepareArrayForReverse($context, [], $arrayB, $recursionDepth !== false);
63 | }
64 |
65 | return $this->performMerge(
66 | $context,
67 | [],
68 | $isReverseMerge ? $arrayB : $arrayA,
69 | $isReverseMerge ? $arrayA : $arrayB,
70 | $recursionDepth,
71 | $isReverseMerge,
72 | );
73 | }
74 |
75 | /**
76 | * @param Context $context Context containing the name of the file, package, group and environment.
77 | * @param string[] $recursiveKeyPath The key path for recursive merging of arrays in configuration files.
78 | * @param array $arrayA First array to merge.
79 | * @param array $arrayB Second array to merge.
80 | *
81 | * @throws ErrorException If an error occurred during the merge.
82 | *
83 | * @return array The merged array.
84 | */
85 | private function performMerge(
86 | Context $context,
87 | array $recursiveKeyPath,
88 | array $arrayA,
89 | array $arrayB,
90 | int|null|false $recursionDepth,
91 | bool $isReverseMerge,
92 | int $depth = 0,
93 | ): array {
94 | $result = $arrayA;
95 | foreach ($arrayB as $k => $v) {
96 | if (is_int($k)) {
97 | if (array_key_exists($k, $result) && $result[$k] !== $v) {
98 | $result[] = $v;
99 | } else {
100 | $result[$k] = $v;
101 | }
102 | continue;
103 | }
104 |
105 | $fullKeyPath = array_merge($recursiveKeyPath, [$k]);
106 |
107 | if (
108 | $recursionDepth !== false
109 | && is_array($v)
110 | && ($recursionDepth === null || $depth < $recursionDepth)
111 | && (
112 | !array_key_exists($k, $result)
113 | || is_array($result[$k])
114 | )
115 | ) {
116 | /** @var array $array */
117 | $array = $result[$k] ?? [];
118 | $this->setValue(
119 | $context,
120 | $fullKeyPath,
121 | $result,
122 | $k,
123 | $this->performMerge(
124 | $context,
125 | $fullKeyPath,
126 | $array,
127 | $v,
128 | $recursionDepth,
129 | $isReverseMerge,
130 | $depth + 1,
131 | )
132 | );
133 | continue;
134 | }
135 |
136 | $existKey = array_key_exists($k, $result);
137 |
138 | if ($existKey && !$isReverseMerge) {
139 | /** @var string|null $file */
140 | $file = ArrayHelper::getValue(
141 | $this->cacheKeys,
142 | array_merge([$context->layer()], $fullKeyPath)
143 | );
144 |
145 | if ($file !== null) {
146 | $this->throwDuplicateKeyErrorException($context->originalGroup(), $fullKeyPath, [$file, $context->file()]);
147 | }
148 | }
149 |
150 | if (!$isReverseMerge || !$existKey) {
151 | $isSet = $this->setValue($context, $fullKeyPath, $result, $k, $v);
152 |
153 | if ($isSet && !$isReverseMerge && !$context->isVariable()) {
154 | /** @psalm-suppress MixedPropertyTypeCoercion */
155 | ArrayHelper::setValue(
156 | $this->cacheKeys,
157 | array_merge([$context->layer()], $fullKeyPath),
158 | $context->file()
159 | );
160 | }
161 | }
162 | }
163 |
164 | return $result;
165 | }
166 |
167 | /**
168 | * @param string[] $recursiveKeyPath
169 | *
170 | * @throws ErrorException If an error occurred during prepare.
171 | */
172 | private function prepareArrayForReverse(
173 | Context $context,
174 | array $recursiveKeyPath,
175 | array $array,
176 | bool $isRecursiveMerge
177 | ): array {
178 | $result = [];
179 |
180 | foreach ($array as $key => $value) {
181 | if (is_int($key)) {
182 | $result[$key] = $value;
183 | continue;
184 | }
185 |
186 | if ($this->dataModifiers->shouldRemoveKeyFromVendor($context, array_merge($recursiveKeyPath, [$key]))) {
187 | continue;
188 | }
189 |
190 | if ($isRecursiveMerge && is_array($value)) {
191 | $result[$key] = $this->prepareArrayForReverse(
192 | $context,
193 | array_merge($recursiveKeyPath, [$key]),
194 | $value,
195 | true,
196 | );
197 | continue;
198 | }
199 |
200 | $recursiveKeyPath[] = $key;
201 |
202 | /** @var string|null $file */
203 | $file = ArrayHelper::getValue(
204 | $this->cacheKeys,
205 | array_merge([$context->layer()], $recursiveKeyPath)
206 | );
207 |
208 | if ($file !== null) {
209 | $this->throwDuplicateKeyErrorException($context->originalGroup(), $recursiveKeyPath, [$file, $context->file()]);
210 | }
211 |
212 | $result[$key] = $value;
213 |
214 | /** @psalm-suppress MixedPropertyTypeCoercion */
215 | ArrayHelper::setValue(
216 | $this->cacheKeys,
217 | array_merge([$context->layer()], $recursiveKeyPath),
218 | $context->file()
219 | );
220 | }
221 |
222 | return $result;
223 | }
224 |
225 | /**
226 | * @psalm-param non-empty-array $keyPath
227 | */
228 | private function setValue(Context $context, array $keyPath, array &$array, string $key, mixed $value): bool
229 | {
230 | if ($this->dataModifiers->shouldRemoveKeyFromVendor($context, $keyPath)) {
231 | return false;
232 | }
233 |
234 | /** @var mixed */
235 | $array[$key] = $value;
236 |
237 | return true;
238 | }
239 |
240 | /**
241 | * Generates a duplicate key error message and throws an exception.
242 | *
243 | * @param string $currentGroupName The name of the group that the error occurred when merging.
244 | * @param string[] $recursiveKeyPath The key path for recursive merging of arrays in configuration files.
245 | * @param string[] $absoluteFilePaths The absolute paths to the files in which duplicates are found.
246 | *
247 | * @throws ErrorException With a duplicate key error message.
248 | */
249 | private function throwDuplicateKeyErrorException(
250 | string $currentGroupName,
251 | array $recursiveKeyPath,
252 | array $absoluteFilePaths
253 | ): void {
254 | $filePaths = array_map(
255 | fn (string $filePath) => ' - ' . $this->configPaths->relative($filePath),
256 | $absoluteFilePaths,
257 | );
258 |
259 | usort($filePaths, static function (string $a, string $b) {
260 | $countDirsA = substr_count($a, '/');
261 | $countDirsB = substr_count($b, '/');
262 | return $countDirsA === $countDirsB ? $a <=> $b : $countDirsA <=> $countDirsB;
263 | });
264 |
265 | $message = sprintf(
266 | "Duplicate key \"%s\" in the following configs while building \"%s\" group:\n%s",
267 | implode(' => ', $recursiveKeyPath),
268 | $currentGroupName,
269 | implode("\n", $filePaths),
270 | );
271 |
272 | throw new ErrorException($message, 0, E_USER_ERROR);
273 | }
274 | }
275 |
--------------------------------------------------------------------------------
/src/Modifier/RecursiveMerge.php:
--------------------------------------------------------------------------------
1 | [
31 | * 'a' => 1,
32 | * 'b' => 2,
33 | * ],
34 | * ];
35 | * ```
36 | *
37 | * - configuration in vendor package:
38 | *
39 | * ```
40 | * "config-plugin": {
41 | * "params": "params.php",
42 | * }
43 | * ```
44 | *
45 | * - vendor package `params.php` contents:
46 | *
47 | * ```php
48 | * return [
49 | * 'key' => [
50 | * 'c' => 3,
51 | * 'd' => 4,
52 | * ],
53 | * ];
54 | * ```
55 | *
56 | * - getting configuration:
57 | *
58 | * ```php
59 | * $config = new Config(new ConfigPaths($configsDir), null, [
60 | * RecursiveMerge::groups('params')
61 | * ]);
62 | *
63 | * $result = $config->get('params');
64 | * ```
65 | *
66 | * The result will be:
67 | *
68 | * ```php
69 | * [
70 | * 'key' => [
71 | * 'c' => 3,
72 | * 'd' => 4,
73 | * 'a' => 1,
74 | * 'b' => 2,
75 | * ],
76 | * ]
77 | * ```
78 | */
79 | final class RecursiveMerge
80 | {
81 | /**
82 | * @param string[] $groups
83 | * @psalm-param positive-int|null $depth
84 | */
85 | private function __construct(
86 | private readonly array $groups,
87 | private readonly ?int $depth = null,
88 | ) {
89 | }
90 |
91 | public static function groups(string ...$groups): self
92 | {
93 | return new self($groups);
94 | }
95 |
96 | /**
97 | * @param string[] $groups
98 | * @psalm-param positive-int|null $depth
99 | */
100 | public static function groupsWithDepth(array $groups, ?int $depth): self
101 | {
102 | return new self($groups, $depth);
103 | }
104 |
105 | /**
106 | * @return string[]
107 | */
108 | public function getGroups(): array
109 | {
110 | return $this->groups;
111 | }
112 |
113 | /**
114 | * @psalm-return positive-int|null
115 | */
116 | public function getDepth(): ?int
117 | {
118 | return $this->depth;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/Modifier/RemoveFromVendor.php:
--------------------------------------------------------------------------------
1 | package('yiisoft/auth'),
25 | *
26 | * // Remove elements `c` and `d` from groups `params` and `web` in package `yiisoft/view`
27 | * RemoveFromVendor::keys(['c'], ['d'])
28 | * ->package('yiisoft/view', 'params', 'web'),
29 | *
30 | * // Remove elements `e` and `f` from all groups in package `yiisoft/auth`
31 | * // and from groups `params` and `web` in package `yiisoft/view`
32 | * RemoveFromVendor::keys(['e'], ['f'])
33 | * ->package('yiisoft/auth')
34 | * ->package('yiisoft/view', 'params', 'web'),
35 | * ```
36 | *
37 | * For example:
38 | *
39 | * - configuration in application `composer.json`:
40 | *
41 | * ```
42 | * "config-plugin": {
43 | * "events": "events.php",
44 | * "params": "params.php",
45 | * }
46 | * ```
47 | *
48 | * - application `events.php` contents:
49 | *
50 | * ```php
51 | * return ['a' => 1, 'b' => 2];
52 | * ```
53 | *
54 | * - configuration in vendor package:
55 | *
56 | * ```
57 | * "config-plugin": {
58 | * "events": "events.php",
59 | * }
60 | * ```
61 | *
62 | * - vendor package `events.php` contents:
63 | *
64 | * ```php
65 | * return ['c' => 3, 'd' => 4, 'e' => 5];
66 | * ```
67 | *
68 | * - getting configuration:
69 | *
70 | * ```php
71 | * $config = new Config(new ConfigPaths($configsDir), null, [
72 | * RemoveFromVendor::keys(
73 | * ['d'],
74 | * ['e'],
75 | * )
76 | * ]);
77 | *
78 | * $result = $config->get('events');
79 | * ```
80 | *
81 | * The result will be:
82 | *
83 | * ```php
84 | * [
85 | * 'c' => 3,
86 | * 'a' => 1,
87 | * 'b' => 2,
88 | * ]
89 | * ```
90 | *
91 | * @param string[] ...$keys
92 | */
93 | public static function keys(array ...$keys): RemoveKeysFromVendor
94 | {
95 | return new RemoveKeysFromVendor(...$keys);
96 | }
97 |
98 | /**
99 | * Marks specified groups to be ignored when reading configuration from vendor packages.
100 | *
101 | * The modifier should be specified as
102 | *
103 | * ```php
104 | * RemoveFromVendor::groups([
105 | * // Remove group `params` from all vendor packages
106 | * '*' => 'params',
107 | *
108 | * // Remove groups `common` and `web` from all vendor packages
109 | * '*' => ['common', 'web'],
110 | *
111 | * // Remove all groups from package `yiisoft/auth`
112 | * 'yiisoft/auth' => '*',
113 | *
114 | * // Remove groups `params` from package `yiisoft/http`
115 | * 'yiisoft/http' => 'params',
116 | *
117 | * // Remove groups `params` and `common` from package `yiisoft/view`
118 | * 'yiisoft/view' => ['params', 'common'],
119 | * ]),
120 | * ```
121 | *
122 | * @psalm-param array $groups
123 | */
124 | public static function groups(array $groups): RemoveGroupsFromVendor
125 | {
126 | return new RemoveGroupsFromVendor($groups);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Modifier/RemoveGroupsFromVendor.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | private array $groups = [];
16 |
17 | /**
18 | * @psalm-param array $groups
19 | */
20 | public function __construct(array $groups)
21 | {
22 | foreach ($groups as $package => $groupNames) {
23 | $this->groups[$package] = (array) $groupNames;
24 | }
25 | }
26 |
27 | /**
28 | * @return array
29 | */
30 | public function getGroups(): array
31 | {
32 | return $this->groups;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Modifier/RemoveKeysFromVendor.php:
--------------------------------------------------------------------------------
1 | >
22 | */
23 | private array $packages = [];
24 |
25 | /**
26 | * @param string[] ...$keys
27 | */
28 | public function __construct(array ...$keys)
29 | {
30 | $this->keys = $keys;
31 | }
32 |
33 | public function package(string ...$package): self
34 | {
35 | if ($package === []) {
36 | throw new InvalidArgumentException('Package should be in format "packageName[, group][, group]".');
37 | }
38 | $this->packages[] = $package;
39 | return $this;
40 | }
41 |
42 | /**
43 | * @return string[][]
44 | */
45 | public function getKeys(): array
46 | {
47 | return $this->keys;
48 | }
49 |
50 | /**
51 | * @return string[][]
52 | * @psalm-return list>
53 | */
54 | public function getPackages(): array
55 | {
56 | return $this->packages;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Modifier/ReverseMerge.php:
--------------------------------------------------------------------------------
1 | 1, 'b' => 2];
32 | * ```
33 | *
34 | * - configuration in vendor package:
35 | *
36 | * ```
37 | * "config-plugin": {
38 | * "events": "events.php",
39 | * }
40 | * ```
41 | *
42 | * - vendor package `events.php` contents:
43 | *
44 | * ```php
45 | * return ['c' => 3, 'd' => 4];
46 | * ```
47 | *
48 | * - getting configuration:
49 | *
50 | * ```php
51 | * $config = new Config(new ConfigPaths($configsDir), null, [
52 | * ReverseMerge::groups('events'),
53 | * ]);
54 | *
55 | * $result = $config->get('events');
56 | * ```
57 | *
58 | * The result will be:
59 | *
60 | * ```php
61 | * [
62 | * 'a' => 1,
63 | * 'b' => 2,
64 | * 'c' => 3,
65 | * 'd' => 4,
66 | * ]
67 | * ```
68 | */
69 | final class ReverseMerge
70 | {
71 | /**
72 | * @param string[] $groups
73 | */
74 | private function __construct(
75 | private readonly array $groups,
76 | ) {
77 | }
78 |
79 | public static function groups(string ...$groups): self
80 | {
81 | return new self($groups);
82 | }
83 |
84 | /**
85 | * @return string[]
86 | */
87 | public function getGroups(): array
88 | {
89 | return $this->groups;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tools/.gitignore:
--------------------------------------------------------------------------------
1 | /*/vendor
2 | /*/composer.lock
3 |
--------------------------------------------------------------------------------
/tools/composer-require-checker/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require-dev": {
3 | "maglnet/composer-require-checker": "^4.7.1"
4 | },
5 | "config": {
6 | "bump-after-update": "dev"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------