├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Collection.php ├── ComponentInstaller.php ├── ConfigDiscovery.php ├── ConfigDiscovery ├── AbstractDiscovery.php ├── ApplicationConfig.php ├── ConfigAggregator.php ├── DevelopmentConfig.php ├── DevelopmentWorkConfig.php ├── DiscoveryChain.php ├── DiscoveryChainInterface.php ├── DiscoveryInterface.php ├── ExpressiveConfig.php └── ModulesConfig.php ├── ConfigOption.php ├── Exception └── RuntimeException.php └── Injector ├── AbstractInjector.php ├── ApplicationConfigInjector.php ├── ConditionalDiscoveryTrait.php ├── ConfigAggregatorInjector.php ├── ConfigInjectorChain.php ├── DevelopmentConfigInjector.php ├── DevelopmentWorkConfigInjector.php ├── ExpressiveConfigInjector.php ├── InjectorInterface.php ├── ModulesConfigInjector.php └── NoopInjector.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 2.1.3 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 2.1.2 - 2019-09-04 28 | 29 | ### Added 30 | 31 | - [#57](https://github.com/zendframework/zend-component-installer/pull/57) adds support for PHP 7.3. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - Nothing. 48 | 49 | ## 2.1.1 - 2018-03-21 50 | 51 | ### Added 52 | 53 | - Nothing. 54 | 55 | ### Changed 56 | 57 | - Nothing. 58 | 59 | ### Deprecated 60 | 61 | - Nothing. 62 | 63 | ### Removed 64 | 65 | - Nothing. 66 | 67 | ### Fixed 68 | 69 | - [#54](https://github.com/zendframework/zend-component-installer/pull/54) fixes 70 | issues when run with symfony/console v4 releases. 71 | 72 | ## 2.1.0 - 2018-02-08 73 | 74 | ### Added 75 | 76 | - [#52](https://github.com/zendframework/zend-component-installer/pull/52) adds 77 | the ability to whitelist packages exposing config providers and/or modules. 78 | When whitelisted, the installer will not prompt to inject configuration, but 79 | instead do it automatically. This is done at the root package level, using the 80 | following configuration: 81 | 82 | ```json 83 | "extra": { 84 | "zf": { 85 | "component-whitelist": [ 86 | "some/package" 87 | ] 88 | } 89 | } 90 | ``` 91 | 92 | ### Changed 93 | 94 | - Nothing. 95 | 96 | ### Deprecated 97 | 98 | - Nothing. 99 | 100 | ### Removed 101 | 102 | - Nothing. 103 | 104 | ### Fixed 105 | 106 | - Nothing. 107 | 108 | ## 2.0.0 - 2018-02-06 109 | 110 | ### Added 111 | 112 | - Nothing. 113 | 114 | ### Changed 115 | 116 | - [#49](https://github.com/zendframework/zend-component-installer/pull/49) 117 | modifies the default options for installer prompts. If providers and/or 118 | modules are discovered, the installer uses the first discovered as the default 119 | option, instead of the "Do not inject" option. Additionally, the "remember 120 | this selection" prompt now defaults to "y" instead of "n". 121 | 122 | ### Deprecated 123 | 124 | - Nothing. 125 | 126 | ### Removed 127 | 128 | - [#50](https://github.com/zendframework/zend-component-installer/pull/50) 129 | removes support for PHP versions 5.6 and 7.0. 130 | 131 | ### Fixed 132 | 133 | - Nothing. 134 | 135 | ## 1.1.1 - 2018-01-11 136 | 137 | ### Added 138 | 139 | - Nothing. 140 | 141 | ### Changed 142 | 143 | - Nothing. 144 | 145 | ### Deprecated 146 | 147 | - Nothing. 148 | 149 | ### Removed 150 | 151 | - Nothing. 152 | 153 | ### Fixed 154 | 155 | - [#47](https://github.com/zendframework/zend-component-installer/pull/47) fixes 156 | an issue during package removal when a package defines multiple targets (e.g., 157 | both "component" and "config-provider") and a `ConfigInjectorChain` is thus 158 | used by the plugin; previously, an error was raised due to an attempt to call 159 | a method the `ConfigInjectorChain` does not define. 160 | 161 | ## 1.1.0 - 2017-11-06 162 | 163 | ### Added 164 | 165 | - [#42](https://github.com/zendframework/zend-component-installer/pull/42) 166 | adds support for PHP 7.2. 167 | 168 | ### Deprecated 169 | 170 | - Nothing. 171 | 172 | ### Removed 173 | 174 | - [#42](https://github.com/zendframework/zend-component-installer/pull/42) 175 | removes support for HHVM. 176 | 177 | ### Fixed 178 | 179 | - [#40](https://github.com/zendframework/zend-component-installer/pull/40) and 180 | [#44](https://github.com/zendframework/zend-component-installer/pull/44) fix 181 | an issue whereby packages that define an array of paths for a PSR-0 or PSR-4 182 | autoloader would cause the installer to error. The installer now properly 183 | handles these situations. 184 | 185 | ## 1.0.0 - 2017-04-25 186 | 187 | First stable release. 188 | 189 | ### Added 190 | 191 | - Nothing. 192 | 193 | ### Deprecated 194 | 195 | - Nothing. 196 | 197 | ### Removed 198 | 199 | - Nothing. 200 | 201 | ### Fixed 202 | 203 | - Nothing. 204 | 205 | ## 0.7.1 - 2017-04-11 206 | 207 | ### Added 208 | 209 | - Nothing. 210 | 211 | ### Deprecated 212 | 213 | - Nothing. 214 | 215 | ### Removed 216 | 217 | - Nothing. 218 | 219 | ### Fixed 220 | 221 | - [#38](https://github.com/zendframework/zend-component-installer/pull/38) fixes 222 | an issue with detection of config providers in `ConfigAggregator`-based 223 | configuration files. Previously, entries that were globally qualified 224 | (prefixed with `\\`) were not properly detected, leading to the installer 225 | re-asking to inject. 226 | 227 | ## 0.7.0 - 2017-02-22 228 | 229 | ### Added 230 | 231 | - [#34](https://github.com/zendframework/zend-component-installer/pull/34) adds 232 | support for applications using [zendframework/zend-config-aggregator](https://github.com/zendframework/zend-config-aggregator). 233 | 234 | ### Changes 235 | 236 | - [#34](https://github.com/zendframework/zend-component-installer/pull/34) 237 | updates the internal architecture such that the Composer `IOInterface` no 238 | longer needs to be passed during config discovery or injection; instead, 239 | try/catch blocks are used within code exercising these classes, which already 240 | composes `IOInterface` instances. As such, a number of public methods that 241 | were receiving `IOInterface` instances now remove that argument. If you were 242 | extending any of these classes, you will need to update accordingly. 243 | 244 | ### Deprecated 245 | 246 | - Nothing. 247 | 248 | ### Removed 249 | 250 | - Nothing. 251 | 252 | ### Fixed 253 | 254 | - Nothing. 255 | 256 | ## 0.6.0 - 2017-01-09 257 | 258 | ### Added 259 | 260 | - [#31](https://github.com/zendframework/zend-component-installer/pull/31) adds 261 | support for [zend-config-aggregator](https://github.com/zendframework/zend-config-aggregator)-based 262 | application configuration. 263 | 264 | ### Deprecated 265 | 266 | - Nothing. 267 | 268 | ### Removed 269 | 270 | - Nothing. 271 | 272 | ### Fixed 273 | 274 | - Nothing. 275 | 276 | ## 0.5.1 - 2016-12-20 277 | 278 | ### Added 279 | 280 | - Nothing. 281 | 282 | ### Changes 283 | 284 | - [#29](https://github.com/zendframework/zend-component-installer/pull/29) 285 | updates the composer/composer dependency to `^1.2.2`, and, internally, uses 286 | `Composer\Installer\PackageEvent` instead of the deprecated 287 | `Composer\Script\PackageEvent`. 288 | 289 | ### Deprecated 290 | 291 | - Nothing. 292 | 293 | ### Removed 294 | 295 | - Nothing. 296 | 297 | ### Fixed 298 | 299 | - Nothing. 300 | 301 | ## 0.5.0 - 2016-10-17 302 | 303 | ### Added 304 | 305 | - [#24](https://github.com/zendframework/zend-component-installer/pull/24) adds 306 | a new method to the `InjectorInterface`: `setModuleDependencies(array $modules)`. 307 | This method is used in the `ComponentInstaller` when module dependencies are 308 | discovered, and by the injectors to provide dependency order during 309 | configuration injection. 310 | 311 | ### Deprecated 312 | 313 | - Nothing. 314 | 315 | ### Removed 316 | 317 | - Nothing. 318 | 319 | ### Fixed 320 | 321 | - [#22](https://github.com/zendframework/zend-component-installer/pull/22) and 322 | [#25](https://github.com/zendframework/zend-component-installer/pull/25) fix 323 | a bug whereby escaped namespace separators caused detection of a module in 324 | existing configuration to produce a false negative. 325 | - [#24](https://github.com/zendframework/zend-component-installer/pull/24) fixes 326 | an issue resulting from the additions from [#20](https://github.com/zendframework/zend-component-installer/pull/20) 327 | for detecting module dependencies. Since autoloading may not be setup yet, the 328 | previous approach could cause failures during installation. The patch provided 329 | in this version introduces a static analysis approach to prevent autoloading 330 | issues. 331 | 332 | ## 0.4.0 - 2016-10-11 333 | 334 | ### Added 335 | 336 | - [#12](https://github.com/zendframework/zend-component-installer/pull/12) adds 337 | a `DiscoveryChain`, for allowing discovery to use multiple discovery sources 338 | to answer the question of whether or not the application can inject 339 | configuration for the module or component. The stated use is for injection 340 | into development configuration. 341 | - [#12](https://github.com/zendframework/zend-component-installer/pull/12) adds 342 | a `ConfigInjectorChain`, which allows injecting a module or component into 343 | multiple configuration sources. The stated use is for injection into 344 | development configuration. 345 | - [#16](https://github.com/zendframework/zend-component-installer/pull/16) adds 346 | support for defining both a module and a component in the same package, 347 | ensuring that they are both injected, and at the appropriate positions in the 348 | module list. 349 | - [#20](https://github.com/zendframework/zend-component-installer/pull/20) adds 350 | support for modules that define `getModuleDependencies()`. When such a module 351 | is encountered, the installer will now also inject entries for these modules 352 | into the application module list, such that they *always* appear before the 353 | current module. This change ensures that dependencies are loaded in the 354 | correct order. 355 | 356 | ### Deprecated 357 | 358 | - Nothing. 359 | 360 | ### Removed 361 | 362 | - Nothing. 363 | 364 | ### Fixed 365 | 366 | - Nothing. 367 | 368 | ## 0.3.1 - 2016-09-12 369 | 370 | ### Added 371 | 372 | - Nothing. 373 | 374 | ### Deprecated 375 | 376 | - Nothing. 377 | 378 | ### Removed 379 | 380 | - Nothing. 381 | 382 | ### Fixed 383 | 384 | - [#15](https://github.com/zendframework/zend-component-installer/pull/15) fixes 385 | how modules are injected into configuration, ensuring they go (as documented) 386 | to the bottom of the module list, and not to the top. 387 | 388 | ## 0.3.0 - 2016-06-27 389 | 390 | ### Added 391 | 392 | - Nothing. 393 | 394 | ### Deprecated 395 | 396 | - Nothing. 397 | 398 | ### Removed 399 | 400 | - [#4](https://github.com/zendframework/zend-component-installer/pull/4) removes 401 | support for PHP 5.5. 402 | 403 | ### Fixed 404 | 405 | - [#8](https://github.com/zendframework/zend-component-installer/pull/8) fixes 406 | how the `DevelopmentConfig` discovery and injection works. Formerly, these 407 | were looking for the `development.config.php` file; however, this was 408 | incorrect. zf-development-mode has `development.config.php.dist` checked into 409 | the repository, but specifically excludes `development.config.php` from it in 410 | order to allow toggling it from the `.dist` file. The code now correctly does 411 | this. 412 | 413 | ## 0.2.0 - 2016-06-02 414 | 415 | ### Added 416 | 417 | - [#5](https://github.com/zendframework/zend-component-installer/pull/5) adds 418 | support for arrays of components/modules/config-providers, in the format: 419 | 420 | ```json 421 | { 422 | "extra": { 423 | "zf": { 424 | "component": [ 425 | "Some\\Component", 426 | "Other\\Component" 427 | ] 428 | } 429 | } 430 | } 431 | ``` 432 | 433 | This feature should primarily be used for metapackages, or config-providers 434 | where some configuration might not be required, and which could then be split 435 | into multiple providers. 436 | 437 | ### Deprecated 438 | 439 | - Nothing. 440 | 441 | ### Removed 442 | 443 | - Nothing. 444 | 445 | ### Fixed 446 | 447 | - Nothing. 448 | 449 | ## 0.1.0 - TBD 450 | 451 | First tagged release. 452 | 453 | Previously, PHAR releases were created from each push to the master branch. 454 | Starting in 0.1.0, the architecture changes to implement a 455 | [composer plugin](https://getcomposer.org/doc/articles/plugins.md). As such, 456 | tagged releases now make more sense, as plugins are installed via composer 457 | (either per-project or globally). 458 | 459 | ### Added 460 | 461 | - [#2](https://github.com/zendframework/zend-component-installer/pull/2) adds: 462 | - All classes in the `Zend\ComponentInstaller\ConfigDiscovery` namespace. 463 | These are used to determine which configuration files are present and 464 | injectable in the project. 465 | - All classes in the `Zend\ComponentInstaller\Injector` namespace. These are 466 | used to perform the work of injecting and removing values from configuration 467 | files. 468 | - `Zend\ComponentInstaller\ConfigOption`, a value object mapping prompt text 469 | to its related injector. 470 | - `Zend\ComponentInstaller\ConfigDiscovery`, a class that loops over known 471 | configuration discovery types to return a list of `ConfigOption` instances 472 | 473 | ### Deprecated 474 | 475 | - Nothing. 476 | 477 | ### Removed 478 | 479 | - [#2](https://github.com/zendframework/zend-component-installer/pull/2) removes 480 | all classes in the `Zend\ComponentInstaller\Command` namespace. 481 | - [#2](https://github.com/zendframework/zend-component-installer/pull/2) removes 482 | the various `bin/` scripts. 483 | - [#2](https://github.com/zendframework/zend-component-installer/pull/2) removes 484 | the PHAR distribution. 485 | 486 | ### Fixed 487 | 488 | - [#2](https://github.com/zendframework/zend-component-installer/pull/2) updates 489 | `Zend\ComponentInstaller\ComponentInstaller`: 490 | - to act as a Composer plugin. 491 | - to add awareness of additional configuration locations: 492 | - `modules.config.php` (Apigility) 493 | - `development.config.php` (zf-development-mode) 494 | - `config.php` (Expressive with expressive-config-manager) 495 | - to discover and prompt for known configuration locations when installing a 496 | package. 497 | - to allow re-using a configuration selection for remaining packages in the 498 | current install session. 499 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2019, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Component Installer for Zend Framework 3 and Expressive Applications 2 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-component-installer.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-component-installer) 3 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-component-installer/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-component-installer?branch=master) 4 | 5 | > ## Repository abandoned 2019-12-31 6 | > 7 | > This repository has moved to [laminas/laminas-component-installer](https://github.com/laminas/laminas-component-installer). 8 | 9 | This repository contains the Composer plugin class `Zend\ComponentInstaller\ComponentInstaller`, 10 | which provides Composer event hooks for the events: 11 | 12 | - post-package-install 13 | - post-package-uninstall 14 | 15 | ## Via Composer global install 16 | 17 | To install the utility for use with all projects you use: 18 | 19 | ```bash 20 | $ composer global require zendframework/zend-component-installer 21 | ``` 22 | 23 | ## Per project installation 24 | 25 | To install the utility for use with a specific project already managed by 26 | composer: 27 | 28 | ```bash 29 | $ composer require zendframework/zend-component-installer 30 | ``` 31 | 32 | ## Writing packages that utilize the installer 33 | 34 | Packages can opt-in to the workflow from zend-component-installer by defining 35 | one or more of the following keys under the `extra.zf` configuration in their 36 | `composer.json` file: 37 | 38 | ```json 39 | "extra": { 40 | "zf": { 41 | "component": "Component\\Namespace", 42 | "config-provider": "Classname\\For\\ConfigProvider", 43 | "module": "Module\\Namespace" 44 | } 45 | } 46 | ``` 47 | 48 | - A **component** is for use specifically with zend-mvc + zend-modulemanager; 49 | a `Module` class **must** be present in the namespace associated with it. 50 | The setting indicates a low-level component that should be injected to the top 51 | of the modules list of one of: 52 | - `config/application.config.php` 53 | - `config/modules.config.php` 54 | - `config/development.config.php` 55 | 56 | - A **module** is for use specifically with zend-mvc + zend-modulemanager; 57 | a `Module` class **must** be present in the namespace associated with it. 58 | The setting indicates a userland or third-party module that should be injected 59 | to the bottom of the modules list of one of: 60 | - `config/application.config.php` 61 | - `config/modules.config.php` 62 | - `config/development.config.php` 63 | 64 | - A **config-provider** is for use with applications that utilize 65 | [expressive-config-manager](https://github.com/mtymek/expressive-config-manager) 66 | or [zend-config-aggregator](https://github.com/zendframework/zend-config-aggregator) 67 | (which may or may not be Expressive applications). The class listed must be an 68 | invokable that returns an array of configuration, and will be injected at the 69 | top of: 70 | - `config/config.php` 71 | 72 | ## Whitelisting packages to install automatically 73 | 74 | At the project level, you can mark packages that expose configuration providers 75 | and modules that you want to automatically inject via the `component-whitelist` 76 | key: 77 | 78 | ```json 79 | "extra": { 80 | "zf": { 81 | "component-whitelist": [ 82 | "zendframework/zend-expressive", 83 | "zendframework/zend-expressive-helpers" 84 | ] 85 | } 86 | } 87 | ``` 88 | 89 | This configuration must be made at the root package level (the package 90 | _consuming_ configuration providing packages). 91 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-component-installer", 3 | "description": "Composer plugin for injecting modules and configuration providers into application configuration", 4 | "type": "composer-plugin", 5 | "license": "BSD-3-Clause", 6 | "keywords": [ 7 | "zf", 8 | "zendframework", 9 | "component installer", 10 | "composer", 11 | "plugin" 12 | ], 13 | "support": { 14 | "docs": "https://docs.zendframework.com/zend-component-installer/", 15 | "issues": "https://github.com/zendframework/zend-component-installer/issues", 16 | "source": "https://github.com/zendframework/zend-component-installer", 17 | "rss": "https://github.com/zendframework/zend-component-installer/releases.atom", 18 | "chat": "https://zendframework-slack.herokuapp.com", 19 | "forum": "https://discourse.zendframework.com/c/questions/components" 20 | }, 21 | "require": { 22 | "php": "^7.1", 23 | "composer-plugin-api": "^1.0" 24 | }, 25 | "require-dev": { 26 | "composer/composer": "^1.5.2", 27 | "malukenho/docheader": "^0.1.6", 28 | "mikey179/vfsstream": "^1.6.7", 29 | "phpunit/phpunit": "^7.5.15 || ^8.3.4", 30 | "zendframework/zend-coding-standard": "~1.0.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Zend\\ComponentInstaller\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "ZendTest\\ComponentInstaller\\": "test/" 40 | } 41 | }, 42 | "config": { 43 | "sort-packages": true 44 | }, 45 | "extra": { 46 | "branch-alias": { 47 | "dev-master": "2.1.x-dev", 48 | "dev-develop": "2.2.x-dev" 49 | }, 50 | "class": "Zend\\ComponentInstaller\\ComponentInstaller" 51 | }, 52 | "scripts": { 53 | "check": [ 54 | "@license-check", 55 | "@cs-check", 56 | "@test" 57 | ], 58 | "cs-check": "phpcs", 59 | "cs-fix": "phpcbf", 60 | "test": "phpunit --colors=always", 61 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", 62 | "license-check": "docheader check src/" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | items = $items; 52 | } 53 | 54 | /** 55 | * Factory method 56 | * 57 | * @param array|Traversable 58 | * @return static 59 | */ 60 | public static function create($items) 61 | { 62 | return new static($items); 63 | } 64 | 65 | /** 66 | * Cast collection to an array. 67 | * 68 | * @return array 69 | */ 70 | public function toArray() 71 | { 72 | return $this->items; 73 | } 74 | 75 | /** 76 | * Apply a callback to each item in the collection. 77 | * 78 | * @param callable $callback 79 | * @return self 80 | */ 81 | public function each(callable $callback) 82 | { 83 | foreach ($this->items as $key => $item) { 84 | $callback($item, $key); 85 | } 86 | return $this; 87 | } 88 | 89 | /** 90 | * Reduce the collection to a single value. 91 | * 92 | * @param callable $callback 93 | * @param mixed $initial Initial value. 94 | * @return mixed 95 | */ 96 | public function reduce(callable $callback, $initial = null) 97 | { 98 | $accumulator = $initial; 99 | 100 | foreach ($this->items as $key => $item) { 101 | $accumulator = $callback($accumulator, $item, $key); 102 | } 103 | 104 | return $accumulator; 105 | } 106 | 107 | /** 108 | * Filter the collection using a callback. 109 | * 110 | * Filter callback should return true for values to keep. 111 | * 112 | * @param callable $callback 113 | * @return static 114 | */ 115 | public function filter(callable $callback) 116 | { 117 | return $this->reduce(function ($filtered, $item, $key) use ($callback) { 118 | if ($callback($item, $key)) { 119 | $filtered[$key] = $item; 120 | } 121 | return $filtered; 122 | }, new static([])); 123 | } 124 | 125 | /** 126 | * Filter the collection using a callback; reject any items matching the callback. 127 | * 128 | * Filter callback should return true for values to reject. 129 | * 130 | * @param callable $callback 131 | * @return static 132 | */ 133 | public function reject(callable $callback) 134 | { 135 | return $this->reduce(function ($filtered, $item, $key) use ($callback) { 136 | if (! $callback($item, $key)) { 137 | $filtered[$key] = $item; 138 | } 139 | return $filtered; 140 | }, new static([])); 141 | } 142 | 143 | /** 144 | * Transform each value in the collection. 145 | * 146 | * Callback should return the new value to use. 147 | * 148 | * @param callable $callback 149 | * @return static 150 | */ 151 | public function map(callable $callback) 152 | { 153 | return $this->reduce(function ($results, $item, $key) use ($callback) { 154 | $results[$key] = $callback($item, $key); 155 | return $results; 156 | }, new static([])); 157 | } 158 | 159 | /** 160 | * Return a new collection containing only unique items. 161 | * 162 | * @return static 163 | */ 164 | public function unique() 165 | { 166 | return new static(array_unique($this->items)); 167 | } 168 | 169 | /** 170 | * Merge an array of values with the current collection. 171 | * 172 | * @param array $values 173 | * @return Collection 174 | */ 175 | public function merge(array $values) 176 | { 177 | $this->items = array_merge($this->items, $values); 178 | return $this; 179 | } 180 | 181 | /** 182 | * Prepend a value to the collection. 183 | * 184 | * @param mixed $value 185 | * @return Collection 186 | */ 187 | public function prepend($value) 188 | { 189 | array_unshift($this->items, $value); 190 | return $this; 191 | } 192 | 193 | /** 194 | * ArrayAccess: isset() 195 | * 196 | * @param string|int $offset 197 | * @return bool 198 | */ 199 | public function offsetExists($offset) 200 | { 201 | return array_key_exists($offset, $this->items); 202 | } 203 | 204 | /** 205 | * ArrayAccess: retrieve by key 206 | * 207 | * @param string|int $offset 208 | * @return mixed 209 | * @throws OutOfRangeException 210 | */ 211 | public function offsetGet($offset) 212 | { 213 | if (! $this->offsetExists($offset)) { 214 | throw new OutOfRangeException(sprintf( 215 | 'Offset %s does not exist in the collection', 216 | $offset 217 | )); 218 | } 219 | 220 | return $this->items[$offset]; 221 | } 222 | 223 | /** 224 | * ArrayAccess: set by key 225 | * 226 | * If $offset is null, pushes the item onto the stack. 227 | * 228 | * @param string|int $offset 229 | * @param mixed $value 230 | * @return void 231 | */ 232 | public function offsetSet($offset, $value) 233 | { 234 | if (null === $offset) { 235 | $this->items[] = $value; 236 | return; 237 | } 238 | 239 | $this->items[$offset] = $value; 240 | } 241 | 242 | /** 243 | * ArrayAccess: unset() 244 | * 245 | * @param string|int $offset 246 | * @return void 247 | */ 248 | public function offsetUnset($offset) 249 | { 250 | if ($this->offsetExists($offset)) { 251 | unset($this->items[$offset]); 252 | } 253 | } 254 | 255 | /** 256 | * Countable: number of items in the collection. 257 | * 258 | * @return int 259 | */ 260 | public function count() 261 | { 262 | return count($this->items); 263 | } 264 | 265 | /** 266 | * Is the collection empty? 267 | * 268 | * @return bool 269 | */ 270 | public function isEmpty() 271 | { 272 | return 0 === $this->count(); 273 | } 274 | 275 | /** 276 | * Traversable: Iterate the collection. 277 | * 278 | * @return ArrayIterator 279 | */ 280 | public function getIterator() 281 | { 282 | return new ArrayIterator($this->items); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/ComponentInstaller.php: -------------------------------------------------------------------------------- 1 | 59 | * { 60 | * "extra": { 61 | * "zf": { 62 | * "component": "Zend\\Form", 63 | * "module": "ZF\\Apigility\\ContentNegotiation", 64 | * "config-provider": "Zend\\Expressive\\PlatesRenderer\\ConfigProvider" 65 | * } 66 | * } 67 | * } 68 | * 69 | * 70 | * With regards to components and modules, for this to work correctly, the 71 | * package MUST define a `Module` in the namespace listed in either the 72 | * extra.zf.component or extra.zf.module definition. 73 | * 74 | * Components are added to the TOP of the modules list, to ensure that userland 75 | * code and/or modules can override the settings. Modules are added to the 76 | * BOTTOM of the modules list. Config providers are added to the TOP of 77 | * configuration providers. 78 | * 79 | * In either case, you can edit the appropriate configuration file when 80 | * complete to create a specific order. 81 | */ 82 | class ComponentInstaller implements 83 | EventSubscriberInterface, 84 | PluginInterface 85 | { 86 | /** 87 | * Cached injectors to re-use for packages installed later in the current process. 88 | * 89 | * @var Injector\InjectorInterface[] 90 | */ 91 | private $cachedInjectors = []; 92 | 93 | /** 94 | * @var Composer 95 | */ 96 | private $composer; 97 | 98 | /** 99 | * @var IOInterface 100 | */ 101 | private $io; 102 | 103 | /** 104 | * Map of known package types to composer config keys. 105 | * 106 | * @var string[] 107 | */ 108 | private $packageTypes = [ 109 | Injector\InjectorInterface::TYPE_CONFIG_PROVIDER => 'config-provider', 110 | Injector\InjectorInterface::TYPE_COMPONENT => 'component', 111 | Injector\InjectorInterface::TYPE_MODULE => 'module', 112 | ]; 113 | 114 | /** 115 | * Project root in which to install. 116 | * 117 | * @var string 118 | */ 119 | private $projectRoot; 120 | 121 | /** 122 | * Constructor 123 | * 124 | * Optionally accept the project root into which to install. 125 | * 126 | * @param string $projectRoot 127 | */ 128 | public function __construct($projectRoot = '') 129 | { 130 | if (is_string($projectRoot) && ! empty($projectRoot) && is_dir($projectRoot)) { 131 | $this->projectRoot = $projectRoot; 132 | } 133 | } 134 | 135 | /** 136 | * Activate plugin. 137 | * 138 | * Sets internal pointers to Composer and IOInterface instances, and resets 139 | * cached injector map. 140 | * 141 | * @param Composer $composer 142 | * @param IOInterface $io 143 | * @return void 144 | */ 145 | public function activate(Composer $composer, IOInterface $io) 146 | { 147 | $this->composer = $composer; 148 | $this->io = $io; 149 | $this->cachedInjectors = []; 150 | } 151 | 152 | /** 153 | * Return list of event handlers in this class. 154 | * 155 | * @return string[] 156 | */ 157 | public static function getSubscribedEvents() 158 | { 159 | return [ 160 | 'post-package-install' => 'onPostPackageInstall', 161 | 'post-package-uninstall' => 'onPostPackageUninstall', 162 | ]; 163 | } 164 | 165 | /** 166 | * post-package-install event hook. 167 | * 168 | * This routine exits early if any of the following conditions apply: 169 | * 170 | * - Executed in non-development mode 171 | * - No config/application.config.php is available 172 | * - The composer.json does not define one of either extra.zf.component 173 | * or extra.zf.module 174 | * - The value used for either extra.zf.component or extra.zf.module are 175 | * empty or not strings. 176 | * 177 | * Otherwise, it will attempt to update the application configuration 178 | * using the value(s) discovered in extra.zf.component and/or extra.zf.module, 179 | * writing their values into the `modules` list. 180 | * 181 | * @param PackageEvent $event 182 | * @return void 183 | */ 184 | public function onPostPackageInstall(PackageEvent $event) 185 | { 186 | if (! $event->isDevMode()) { 187 | // Do nothing in production mode. 188 | return; 189 | } 190 | 191 | $package = $event->getOperation()->getPackage(); 192 | $name = $package->getName(); 193 | $extra = $this->getExtraMetadata($package->getExtra()); 194 | 195 | if (empty($extra)) { 196 | // Package does not define anything of interest; do nothing. 197 | return; 198 | } 199 | 200 | $packageTypes = $this->discoverPackageTypes($extra); 201 | $options = (new ConfigDiscovery()) 202 | ->getAvailableConfigOptions($packageTypes, $this->projectRoot); 203 | 204 | if ($options->isEmpty()) { 205 | // No configuration options found; do nothing. 206 | return; 207 | } 208 | 209 | $dependencies = $this->loadModuleClassesDependencies($package); 210 | $applicationModules = $this->findApplicationModules(); 211 | 212 | $this->marshalInstallableModules($extra, $options) 213 | ->each(function ($module) use ($name) { 214 | }) 215 | // Create injectors 216 | ->reduce(function ($injectors, $module) use ($options, $packageTypes, $name) { 217 | // Get extra from root package 218 | $rootExtra = $this->getExtraMetadata($this->composer->getPackage()->getExtra()); 219 | $whitelist = $rootExtra['component-whitelist'] ?? []; 220 | $packageType = $packageTypes[$module]; 221 | $injectors[$module] = $this->promptForConfigOption($module, $options, $packageType, $name, $whitelist); 222 | return $injectors; 223 | }, new Collection([])) 224 | // Inject modules into configuration 225 | ->each(function ($injector, $module) use ($name, $packageTypes, $applicationModules, $dependencies) { 226 | if (isset($dependencies[$module])) { 227 | $injector->setModuleDependencies($dependencies[$module]); 228 | } 229 | 230 | $injector->setApplicationModules($applicationModules); 231 | $this->injectModuleIntoConfig($name, $module, $injector, $packageTypes[$module]); 232 | }); 233 | } 234 | 235 | /** 236 | * Find all Module classes in the package and their dependencies 237 | * via method `getModuleDependencies` of Module class. 238 | * 239 | * These dependencies are used later 240 | * @see \Zend\ComponentInstaller\Injector\AbstractInjector::injectAfterDependencies 241 | * to add component in a correct order on the module list - after dependencies. 242 | * 243 | * It works with PSR-0, PSR-4, 'classmap' and 'files' composer autoloading. 244 | * 245 | * @param PackageInterface $package 246 | * @return array 247 | */ 248 | private function loadModuleClassesDependencies(PackageInterface $package) 249 | { 250 | $dependencies = new ArrayObject([]); 251 | $installer = $this->composer->getInstallationManager(); 252 | $packagePath = $installer->getInstallPath($package); 253 | 254 | $this->mapAutoloaders($package->getAutoload(), $dependencies, $packagePath); 255 | 256 | return $dependencies->getArrayCopy(); 257 | } 258 | 259 | /** 260 | * Find all modules of the application. 261 | * 262 | * @return array 263 | */ 264 | private function findApplicationModules() 265 | { 266 | $modulePath = is_string($this->projectRoot) && ! empty($this->projectRoot) 267 | ? sprintf('%s/module', $this->projectRoot) 268 | : 'module'; 269 | 270 | $modules = []; 271 | 272 | if (is_dir($modulePath)) { 273 | $directoryIterator = new DirectoryIterator($modulePath); 274 | foreach ($directoryIterator as $file) { 275 | if ($file->isDot() || ! $file->isDir()) { 276 | continue; 277 | } 278 | 279 | $modules[] = $file->getBasename(); 280 | } 281 | } 282 | 283 | return $modules; 284 | } 285 | 286 | /** 287 | * post-package-uninstall event hook 288 | * 289 | * This routine exits early if any of the following conditions apply: 290 | * 291 | * - Executed in non-development mode 292 | * - No config/application.config.php is available 293 | * - The composer.json does not define one of either extra.zf.component 294 | * or extra.zf.module 295 | * - The value used for either extra.zf.component or extra.zf.module are 296 | * empty or not strings. 297 | * 298 | * Otherwise, it will attempt to update the application configuration 299 | * using the value(s) discovered in extra.zf.component and/or extra.zf.module, 300 | * removing their values from the `modules` list. 301 | * 302 | * @param PackageEvent $event 303 | * @return void 304 | */ 305 | public function onPostPackageUninstall(PackageEvent $event) 306 | { 307 | if (! $event->isDevMode()) { 308 | // Do nothing in production mode. 309 | return; 310 | } 311 | 312 | $options = (new ConfigDiscovery()) 313 | ->getAvailableConfigOptions( 314 | new Collection(array_keys($this->packageTypes)), 315 | $this->projectRoot 316 | ); 317 | 318 | if ($options->isEmpty()) { 319 | // No configuration options found; do nothing. 320 | return; 321 | } 322 | 323 | $package = $event->getOperation()->getPackage(); 324 | $name = $package->getName(); 325 | $extra = $this->getExtraMetadata($package->getExtra()); 326 | $this->removePackageFromConfig($name, $extra, $options); 327 | } 328 | 329 | /** 330 | * Retrieve the zf-specific metadata from the "extra" section 331 | * 332 | * @param array $extra 333 | * @return array 334 | */ 335 | private function getExtraMetadata(array $extra) 336 | { 337 | return isset($extra['zf']) && is_array($extra['zf']) 338 | ? $extra['zf'] 339 | : [] 340 | ; 341 | } 342 | 343 | /** 344 | * Discover what package types are relevant based on what the package 345 | * exposes in the extra configuration. 346 | * 347 | * @param string[] $extra 348 | * @return Collection Collection of Injector\InjectorInterface::TYPE_* constants. 349 | */ 350 | private function discoverPackageTypes(array $extra) 351 | { 352 | $packageTypes = array_flip($this->packageTypes); 353 | $knownTypes = array_keys($packageTypes); 354 | return Collection::create($extra) 355 | ->filter(function ($packages, $type) use ($knownTypes) { 356 | return in_array($type, $knownTypes, true); 357 | }) 358 | ->reduce(function ($discoveredTypes, $packages, $type) use ($packageTypes) { 359 | $packages = is_array($packages) ? $packages : [$packages]; 360 | 361 | foreach ($packages as $package) { 362 | $discoveredTypes[$package] = $packageTypes[$type]; 363 | } 364 | return $discoveredTypes; 365 | }, new Collection([])); 366 | } 367 | 368 | /** 369 | * Marshal a collection of defined package types. 370 | * 371 | * @param array $extra extra.zf value 372 | * @return Collection 373 | */ 374 | private function marshalPackageTypes(array $extra) 375 | { 376 | // Create a collection of types registered in the package. 377 | return Collection::create($this->packageTypes) 378 | ->filter(function ($configKey, $type) use ($extra) { 379 | return $this->metadataForKeyIsValid($configKey, $extra); 380 | }); 381 | } 382 | 383 | /** 384 | * Marshal a collection of package modules. 385 | * 386 | * @param array $extra extra.zf value 387 | * @param Collection $packageTypes 388 | * @param Collection $options ConfigOption instances 389 | * @return Collection 390 | */ 391 | private function marshalPackageModules(array $extra, Collection $packageTypes, Collection $options) 392 | { 393 | // We only want to list modules that the application can configure. 394 | $supportedTypes = $options 395 | ->reduce(function ($allowed, $option) { 396 | return $allowed->merge($option->getInjector()->getTypesAllowed()); 397 | }, new Collection([])) 398 | ->unique() 399 | ->toArray(); 400 | 401 | return $packageTypes 402 | ->reduce(function ($modules, $configKey, $type) use ($extra, $supportedTypes) { 403 | if (! in_array($type, $supportedTypes, true)) { 404 | return $modules; 405 | } 406 | return $modules->merge((array) $extra[$configKey]); 407 | }, new Collection([])) 408 | // Make sure the list is unique 409 | ->unique(); 410 | } 411 | 412 | /** 413 | * Prepare a list of modules to install/register with configuration. 414 | * 415 | * @param string[] $extra 416 | * @param Collection $options 417 | * @return string[] List of packages to install 418 | */ 419 | private function marshalInstallableModules(array $extra, Collection $options) 420 | { 421 | return $this->marshalPackageModules($extra, $this->marshalPackageTypes($extra), $options) 422 | // Filter out modules that do not have a registered injector 423 | ->reject(function ($module) use ($options) { 424 | return $options->reduce(function ($registered, $option) use ($module) { 425 | return $registered || $option->getInjector()->isRegistered($module); 426 | }, false); 427 | }); 428 | } 429 | 430 | /** 431 | * Prompt for the user to select a configuration location to update. 432 | * 433 | * @param string $name 434 | * @param Collection $options 435 | * @param int $packageType 436 | * @param string $packageName 437 | * @param array $whitelist 438 | * @return Injector\InjectorInterface 439 | */ 440 | private function promptForConfigOption( 441 | string $name, 442 | Collection $options, 443 | int $packageType, 444 | string $packageName, 445 | array $whitelist 446 | ) { 447 | if ($cachedInjector = $this->getCachedInjector($packageType)) { 448 | return $cachedInjector; 449 | } 450 | 451 | // If package is whitelisted, don't ask... 452 | if (in_array($packageName, $whitelist, true)) { 453 | return $options[1]->getInjector(); 454 | } 455 | 456 | // Default to first discovered option; index 0 is always "Do not inject" 457 | $default = $options->count() > 1 ? 1 : 0; 458 | $ask = $options->reduce(function ($ask, $option, $index) { 459 | $ask[] = sprintf( 460 | " [%d] %s\n", 461 | $index, 462 | $option->getPromptText() 463 | ); 464 | return $ask; 465 | }, []); 466 | 467 | array_unshift($ask, sprintf( 468 | "\n Please select which config file you wish to inject '%s' into:\n", 469 | $name 470 | )); 471 | $ask[] = sprintf(' Make your selection (default is %d):', $default); 472 | 473 | while (true) { 474 | $answer = $this->io->ask(implode($ask), $default); 475 | 476 | if (is_numeric($answer) && isset($options[(int) $answer])) { 477 | $injector = $options[(int) $answer]->getInjector(); 478 | $this->promptToRememberOption($injector, $packageType); 479 | return $injector; 480 | } 481 | 482 | $this->io->write('Invalid selection'); 483 | } 484 | } 485 | 486 | /** 487 | * Prompt the user to determine if the selection should be remembered for later packages. 488 | * 489 | * @todo Will need to store selection in filesystem and remove when all packages are complete 490 | * @param Injector\InjectorInterface $injector 491 | * @param int $packageType 492 | * return void 493 | */ 494 | private function promptToRememberOption(Injector\InjectorInterface $injector, $packageType) 495 | { 496 | $ask = ["\n Remember this option for other packages of the same type? (Y/n)"]; 497 | 498 | while (true) { 499 | $answer = strtolower($this->io->ask(implode($ask), 'y')); 500 | 501 | switch ($answer) { 502 | case 'y': 503 | $this->cacheInjector($injector, $packageType); 504 | return; 505 | case 'n': 506 | // intentionally fall-through 507 | default: 508 | return; 509 | } 510 | } 511 | } 512 | 513 | /** 514 | * Inject a module into available configuration. 515 | * 516 | * @param string $package Package name 517 | * @param string $module Module to install in configuration 518 | * @param Injector\InjectorInterface $injector Injector to use. 519 | * @param int $packageType 520 | * @return void 521 | */ 522 | private function injectModuleIntoConfig($package, $module, Injector\InjectorInterface $injector, $packageType) 523 | { 524 | $this->io->write(sprintf(' Installing %s from package %s', $module, $package)); 525 | 526 | try { 527 | if (! $injector->inject($module, $packageType)) { 528 | $this->io->write(' Package is already registered; skipping'); 529 | } 530 | } catch (Exception\RuntimeException $ex) { 531 | $this->io->write(sprintf( 532 | ' %s', 533 | $ex->getMessage() 534 | )); 535 | } 536 | } 537 | 538 | /** 539 | * Remove a package from configuration. 540 | * 541 | * @param string $package Package name 542 | * @param array $metadata Metadata pulled from extra.zf 543 | * @param Collection $configOptions Discovered configuration options from 544 | * which to remove package. 545 | * @return void 546 | */ 547 | private function removePackageFromConfig($package, array $metadata, Collection $configOptions) 548 | { 549 | // Create a collection of types registered in the package. 550 | $packageTypes = $this->marshalPackageTypes($metadata); 551 | 552 | // Create a collection of configured injectors for the package types 553 | // registered. 554 | $injectors = $configOptions 555 | ->map(function ($configOption) { 556 | return $configOption->getInjector(); 557 | }) 558 | ->filter(function ($injector) use ($packageTypes) { 559 | return $packageTypes->reduce(function ($registered, $key, $type) use ($injector) { 560 | return $registered || $injector->registersType($type); 561 | }, false); 562 | }); 563 | 564 | // Create a collection of unique modules based on the package types present, 565 | // and remove each from configuration. 566 | $this->marshalPackageModules($metadata, $packageTypes, $configOptions) 567 | ->each(function ($module) use ($package, $injectors) { 568 | $this->removeModuleFromConfig($module, $package, $injectors); 569 | }); 570 | } 571 | 572 | /** 573 | * Remove an individual module defined in a package from configuration. 574 | * 575 | * @param string $module Module to remove 576 | * @param string $package Package in which module is defined 577 | * @param Collection $injectors Injectors to use for removal 578 | * @return void 579 | */ 580 | private function removeModuleFromConfig($module, $package, Collection $injectors) 581 | { 582 | $injectors->each(function (InjectorInterface $injector) use ($module, $package) { 583 | $this->io->write(sprintf(' Removing %s from package %s', $module, $package)); 584 | 585 | if ($injector->remove($module)) { 586 | $this->io->write(sprintf( 587 | ' Removed package from %s', 588 | $this->getInjectorConfigFileName($injector) 589 | )); 590 | } 591 | }); 592 | } 593 | 594 | /** 595 | * @param InjectorInterface $injector 596 | * @return string 597 | * @todo remove after InjectorInterface has getConfigName defined 598 | */ 599 | private function getInjectorConfigFileName(InjectorInterface $injector) 600 | { 601 | if ($injector instanceof ConfigInjectorChain) { 602 | return $this->getInjectorChainConfigFileName($injector); 603 | } elseif ($injector instanceof AbstractInjector) { 604 | return $this->getAbstractInjectorConfigFileName($injector); 605 | } 606 | 607 | return ''; 608 | } 609 | 610 | /** 611 | * @param ConfigInjectorChain $injector 612 | * @return string 613 | * @todo remove after InjectorInterface has getConfigName defined 614 | */ 615 | private function getInjectorChainConfigFileName(ConfigInjectorChain $injector) 616 | { 617 | return implode(', ', array_map(function ($item) { 618 | return $this->getInjectorConfigFileName($item); 619 | }, $injector->getCollection()->toArray())); 620 | } 621 | 622 | /** 623 | * @param AbstractInjector $injector 624 | * @return string 625 | * @todo remove after InjectorInterface has getConfigName defined 626 | */ 627 | private function getAbstractInjectorConfigFileName(AbstractInjector $injector) 628 | { 629 | return $injector->getConfigFile(); 630 | } 631 | 632 | /** 633 | * Is a given module name valid? 634 | * 635 | * @param string $module 636 | * @return bool 637 | */ 638 | private function moduleIsValid($module) 639 | { 640 | return is_string($module) && ! empty($module); 641 | } 642 | 643 | /** 644 | * Is a given metadata value (extra.zf.*) valid? 645 | * 646 | * @param string $key Key to examine in metadata 647 | * @param array $metadata 648 | * @return bool 649 | */ 650 | private function metadataForKeyIsValid($key, array $metadata) 651 | { 652 | if (! isset($metadata[$key])) { 653 | return false; 654 | } 655 | 656 | if (is_string($metadata[$key])) { 657 | return $this->moduleIsValid($metadata[$key]); 658 | } 659 | 660 | if (! is_array($metadata[$key])) { 661 | return false; 662 | } 663 | 664 | return Collection::create($metadata[$key]) 665 | ->reduce(function ($valid, $value) { 666 | if (false === $valid) { 667 | return $valid; 668 | } 669 | return $this->moduleIsValid($value); 670 | }, null); 671 | } 672 | 673 | /** 674 | * Attempt to retrieve a cached injector for the current package type. 675 | * 676 | * @param int $packageType 677 | * @return null|Injector\InjectorInterface 678 | */ 679 | private function getCachedInjector($packageType) 680 | { 681 | if (isset($this->cachedInjectors[$packageType])) { 682 | return $this->cachedInjectors[$packageType]; 683 | } 684 | 685 | return null; 686 | } 687 | 688 | /** 689 | * Cache an injector for later use. 690 | * 691 | * @param Injector\InjectorInterface $injector 692 | * @param int $packageType 693 | * @return void 694 | */ 695 | private function cacheInjector(Injector\InjectorInterface $injector, $packageType) 696 | { 697 | $this->cachedInjectors[$packageType] = $injector; 698 | } 699 | 700 | /** 701 | * Iterate through each autoloader type to find dependencies. 702 | * 703 | * @param array $autoload List of autoloader types and associated autoloader definitions. 704 | * @param ArrayObject $dependencies Module dependencies defined by the module. 705 | * @param string $packagePath Path to the package on the filesystem. 706 | * @return void 707 | */ 708 | private function mapAutoloaders(array $autoload, ArrayObject $dependencies, $packagePath) 709 | { 710 | foreach ($autoload as $type => $map) { 711 | $this->mapType($map, $type, $dependencies, $packagePath); 712 | } 713 | } 714 | 715 | /** 716 | * Iterate through a single autolaoder type to find dependencies. 717 | * 718 | * @param array $map Map of namespace => path(s) pairs. 719 | * @param string $type Type of autoloader being iterated. 720 | * @param ArrayObject $dependencies Module dependencies defined by the module. 721 | * @param string $packagePath Path to the package on the filesystem. 722 | * @return void 723 | */ 724 | private function mapType(array $map, $type, ArrayObject $dependencies, $packagePath) 725 | { 726 | foreach ($map as $namespace => $paths) { 727 | $paths = (array) $paths; 728 | $this->mapNamespacePaths($paths, $namespace, $type, $dependencies, $packagePath); 729 | } 730 | } 731 | 732 | /** 733 | * Iterate through the paths defined for a given namespace. 734 | * 735 | * @param array $paths Paths defined for the given namespace. 736 | * @param string $namespace PHP namespace to which the paths map. 737 | * @param string $type Type of autoloader being iterated. 738 | * @param ArrayObject $dependencies Module dependencies defined by the module. 739 | * @param string $packagePath Path to the package on the filesystem. 740 | * @return void 741 | */ 742 | private function mapNamespacePaths(array $paths, $namespace, $type, ArrayObject $dependencies, $packagePath) 743 | { 744 | foreach ($paths as $path) { 745 | $this->mapPath($path, $namespace, $type, $dependencies, $packagePath); 746 | } 747 | } 748 | 749 | /** 750 | * Find module dependencies for a given namespace for a given path. 751 | * 752 | * @param string $path Path to inspect. 753 | * @param string $namespace PHP namespace to which the paths map. 754 | * @param string $type Type of autoloader being iterated. 755 | * @param ArrayObject $dependencies Module dependencies defined by the module. 756 | * @param string $packagePath Path to the package on the filesystem. 757 | * @return void 758 | */ 759 | private function mapPath($path, $namespace, $type, ArrayObject $dependencies, $packagePath) 760 | { 761 | switch ($type) { 762 | case 'classmap': 763 | $fullPath = sprintf('%s/%s', $packagePath, $path); 764 | if (substr($path, -10) === 'Module.php') { 765 | $modulePath = $fullPath; 766 | break; 767 | } 768 | 769 | $modulePath = sprintf('%s/Module.php', rtrim($fullPath, '/')); 770 | break; 771 | case 'files': 772 | if (substr($path, -10) !== 'Module.php') { 773 | return; 774 | } 775 | $modulePath = sprintf('%s/%s', $packagePath, $path); 776 | break; 777 | case 'psr-0': 778 | $modulePath = sprintf( 779 | '%s/%s%s%s', 780 | $packagePath, 781 | $path, 782 | str_replace('\\', '/', $namespace), 783 | 'Module.php' 784 | ); 785 | break; 786 | case 'psr-4': 787 | $modulePath = sprintf( 788 | '%s/%s%s', 789 | $packagePath, 790 | $path, 791 | 'Module.php' 792 | ); 793 | break; 794 | default: 795 | return; 796 | } 797 | 798 | if (! file_exists($modulePath)) { 799 | return; 800 | } 801 | 802 | $result = $this->getModuleDependencies($modulePath); 803 | 804 | if (empty($result)) { 805 | return; 806 | } 807 | 808 | // Mimic array + array operation in ArrayObject 809 | $dependencies->exchangeArray($dependencies->getArrayCopy() + $result); 810 | } 811 | 812 | /** 813 | * @param string $file 814 | * @return array 815 | */ 816 | private function getModuleDependencies($file) 817 | { 818 | $content = file_get_contents($file); 819 | if (preg_match('/namespace\s+([^\s]+)\s*;/', $content, $m)) { 820 | $moduleName = $m[1]; 821 | 822 | // @codingStandardsIgnoreStart 823 | $regExp = '/public\s+function\s+getModuleDependencies\s*\(\s*\)\s*{[^}]*return\s*(?:array\(|\[)([^})\]]*)(\)|\])/'; 824 | // @codingStandardsIgnoreEnd 825 | if (preg_match($regExp, $content, $m)) { 826 | $dependencies = array_filter( 827 | explode(',', stripslashes(rtrim(preg_replace('/[\s"\']/', '', $m[1]), ','))) 828 | ); 829 | 830 | if ($dependencies) { 831 | return [$moduleName => $dependencies]; 832 | } 833 | } 834 | } 835 | 836 | return []; 837 | } 838 | } 839 | -------------------------------------------------------------------------------- /src/ConfigDiscovery.php: -------------------------------------------------------------------------------- 1 | ConfigDiscovery\ApplicationConfig::class, 21 | 'config/modules.config.php' => ConfigDiscovery\ModulesConfig::class, 22 | 'config/development.config.php.dist' => [ 23 | 'dist' => ConfigDiscovery\DevelopmentConfig::class, 24 | 'work' => ConfigDiscovery\DevelopmentWorkConfig::class, 25 | ], 26 | 'config/config.php' => [ 27 | 'aggregator' => ConfigDiscovery\ConfigAggregator::class, 28 | 'manager' => ConfigDiscovery\ExpressiveConfig::class, 29 | ], 30 | ]; 31 | 32 | /** 33 | * Map of config files to injectors 34 | * 35 | * @var string[] 36 | */ 37 | private $injectors = [ 38 | 'config/application.config.php' => Injector\ApplicationConfigInjector::class, 39 | 'config/modules.config.php' => Injector\ModulesConfigInjector::class, 40 | 'config/development.config.php.dist' => [ 41 | 'dist' => Injector\DevelopmentConfigInjector::class, 42 | 'work' => Injector\DevelopmentWorkConfigInjector::class, 43 | ], 44 | 'config/config.php' => [ 45 | 'aggregator' => Injector\ConfigAggregatorInjector::class, 46 | 'manager' => Injector\ExpressiveConfigInjector::class, 47 | ] 48 | ]; 49 | 50 | /** 51 | * Return a list of available configuration options. 52 | * 53 | * @param Collection $availableTypes Collection of Injector\InjectorInterface::TYPE_* 54 | * constants indicating valid package types that could be injected. 55 | * @param string $projectRoot Path to the project root; assumes PWD by 56 | * default. 57 | * @return Collection Collection of ConfigOption instances. 58 | */ 59 | public function getAvailableConfigOptions(Collection $availableTypes, $projectRoot = '') 60 | { 61 | // Create an initial collection to which we'll append. 62 | // This approach is used to ensure indexes are sane. 63 | $discovered = new Collection([ 64 | new ConfigOption('Do not inject', new Injector\NoopInjector()), 65 | ]); 66 | 67 | Collection::create($this->discovery) 68 | // Create a discovery class for the discovery type 69 | ->map(function ($discoveryClass) use ($projectRoot) { 70 | if (is_array($discoveryClass)) { 71 | return new ConfigDiscovery\DiscoveryChain($discoveryClass, $projectRoot); 72 | } 73 | return new $discoveryClass($projectRoot); 74 | }) 75 | // Use only those where we can locate a corresponding config file 76 | ->filter(function ($discovery) { 77 | return $discovery->locate(); 78 | }) 79 | // Create an injector for the config file 80 | ->map(function ($discovery, $file) use ($projectRoot, $availableTypes) { 81 | // Look up the injector based on the file type 82 | $injectorClass = $this->injectors[$file]; 83 | if (is_array($injectorClass)) { 84 | return new Injector\ConfigInjectorChain( 85 | $injectorClass, 86 | $discovery, 87 | $availableTypes, 88 | $projectRoot 89 | ); 90 | } 91 | return new $injectorClass($projectRoot); 92 | }) 93 | // Keep only those injectors that match types available for the package 94 | ->filter(function ($injector) use ($availableTypes) { 95 | return $availableTypes->reduce(function ($flag, $type) use ($injector) { 96 | return $flag || $injector->registersType($type); 97 | }, false); 98 | }) 99 | // Create a config option using the file and injector 100 | ->each(function ($injector, $file) use ($discovered) { 101 | $discovered[] = new ConfigOption($file, $injector); 102 | }); 103 | 104 | return 1 === $discovered->count() 105 | ? new Collection([]) 106 | : $discovered; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/ConfigDiscovery/AbstractDiscovery.php: -------------------------------------------------------------------------------- 1 | configFile = sprintf( 48 | '%s/%s', 49 | $projectDirectory, 50 | $this->configFile 51 | ); 52 | } 53 | } 54 | 55 | /** 56 | * Determine if the configuration file exists and contains modules. 57 | * 58 | * @return bool 59 | */ 60 | public function locate() 61 | { 62 | if (! is_file($this->configFile)) { 63 | return false; 64 | } 65 | 66 | $config = file_get_contents($this->configFile); 67 | return (1 === preg_match($this->expected, $config)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ConfigDiscovery/ApplicationConfig.php: -------------------------------------------------------------------------------- 1 | \s*(array\(|\[))\s*$/m'; 25 | } 26 | -------------------------------------------------------------------------------- /src/ConfigDiscovery/ConfigAggregator.php: -------------------------------------------------------------------------------- 1 | expected = sprintf( 34 | '/new (?:%s?%s)?ConfigAggregator\(\s*(?:array\(|\[)/s', 35 | preg_quote('\\'), 36 | preg_quote('Zend\ConfigAggregator\\') 37 | ); 38 | 39 | parent::__construct($projectDirectory); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ConfigDiscovery/DevelopmentConfig.php: -------------------------------------------------------------------------------- 1 | chain = Collection::create($discovery) 33 | // Create a discovery class for the dicovery type 34 | ->map(function ($discoveryClass) use ($projectDirectory) { 35 | return new $discoveryClass($projectDirectory); 36 | }) 37 | // Use only those where we can locate a corresponding config file 38 | ->filter(function ($discovery) { 39 | return $discovery->locate(); 40 | }); 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function locate() 47 | { 48 | return $this->chain->count() > 0; 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | public function discoveryExists($name) 55 | { 56 | return $this->chain->offsetExists($name); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ConfigDiscovery/DiscoveryChainInterface.php: -------------------------------------------------------------------------------- 1 | expected = sprintf( 34 | '/new (?:%s?%s)?ConfigManager\(\s*(?:array\(|\[)/s', 35 | preg_quote('\\'), 36 | preg_quote('Zend\Expressive\ConfigManager\\') 37 | ); 38 | 39 | parent::__construct($projectDirectory); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ConfigDiscovery/ModulesConfig.php: -------------------------------------------------------------------------------- 1 | promptText = $promptText; 29 | $this->injector = $injector; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getPromptText() 36 | { 37 | return $this->promptText; 38 | } 39 | 40 | /** 41 | * @return Injector\InjectorInterface 42 | */ 43 | public function getInjector() 44 | { 45 | return $this->injector; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 'regular expression', 51 | * 'replacement' => 'preg_replace replacement', 52 | * ], 53 | * ``` 54 | * 55 | * @var string[] 56 | */ 57 | protected $cleanUpPatterns = [ 58 | 'pattern' => "/(array\(|\[|,)(\r?\n){2}/s", 59 | 'replacement' => "\$1\n", 60 | ]; 61 | 62 | /** 63 | * Configuration file to update. 64 | * 65 | * Implementations MUST overwrite this value. 66 | * 67 | * @var string 68 | */ 69 | protected $configFile; 70 | 71 | /** 72 | * Patterns and replacements to use when registering a code item. 73 | * 74 | * Implementations MUST overwrite this value. 75 | * 76 | * Structure MUST be: 77 | * 78 | * ``` 79 | * [ 80 | * TYPE_CONSTANT => [ 81 | * 'pattern' => 'regular expression', 82 | * 'replacement' => 'preg_replace replacement, with %s placeholder for package', 83 | * ], 84 | * ] 85 | * ``` 86 | * 87 | * @var string[] 88 | */ 89 | protected $injectionPatterns = []; 90 | 91 | /** 92 | * Pattern to use to determine if the code item is registered. 93 | * 94 | * Implementations MUST overwrite this value. 95 | * 96 | * @var string 97 | */ 98 | protected $isRegisteredPattern; 99 | 100 | /** 101 | * Patterns and replacements to use when removing a code item. 102 | * 103 | * Implementations MUST overwrite this value. 104 | * 105 | * Structure MUST be: 106 | * 107 | * ``` 108 | * [ 109 | * 'pattern' => 'regular expression, with %s placeholder for component namespace/configuration class', 110 | * 'replacement' => 'preg_replace replacement, usually an empty string', 111 | * ], 112 | * ``` 113 | * 114 | * @var string[] 115 | */ 116 | protected $removalPatterns = []; 117 | 118 | /** 119 | * Modules of the application. 120 | * 121 | * @var array 122 | */ 123 | protected $applicationModules = []; 124 | 125 | /** 126 | * Dependencies of the module. 127 | * 128 | * @var array 129 | */ 130 | protected $moduleDependencies = []; 131 | 132 | /** 133 | * Constructor 134 | * 135 | * Optionally accept the project root directory; if non-empty, it is used 136 | * to prefix the $configFile. 137 | * 138 | * @param string $projectRoot 139 | */ 140 | public function __construct($projectRoot = '') 141 | { 142 | if (is_string($projectRoot) && ! empty($projectRoot)) { 143 | $this->configFile = sprintf('%s/%s', $projectRoot, $this->configFile); 144 | } 145 | } 146 | 147 | /** 148 | * {@inheritDoc} 149 | */ 150 | public function registersType($type) 151 | { 152 | return in_array($type, $this->allowedTypes, true); 153 | } 154 | 155 | /** 156 | * {@inheritDoc} 157 | */ 158 | public function getTypesAllowed() 159 | { 160 | return $this->allowedTypes; 161 | } 162 | 163 | /** 164 | * {@inheritDoc} 165 | */ 166 | public function isRegistered($package) 167 | { 168 | $config = file_get_contents($this->configFile); 169 | return $this->isRegisteredInConfig($package, $config); 170 | } 171 | 172 | /** 173 | * {@inheritDoc} 174 | */ 175 | public function inject($package, $type) 176 | { 177 | $config = file_get_contents($this->configFile); 178 | 179 | if ($this->isRegisteredInConfig($package, $config)) { 180 | return false; 181 | } 182 | 183 | if ($type === self::TYPE_COMPONENT 184 | && $this->moduleDependencies 185 | ) { 186 | return $this->injectAfterDependencies($package, $config); 187 | } 188 | 189 | if ($type === self::TYPE_MODULE 190 | && ($firstApplicationModule = $this->findFirstEnabledApplicationModule($this->applicationModules, $config)) 191 | ) { 192 | return $this->injectBeforeApplicationModules($package, $config, $firstApplicationModule); 193 | } 194 | 195 | $pattern = $this->injectionPatterns[$type]['pattern']; 196 | $replacement = sprintf( 197 | $this->injectionPatterns[$type]['replacement'], 198 | $package 199 | ); 200 | 201 | $config = preg_replace($pattern, $replacement, $config); 202 | file_put_contents($this->configFile, $config); 203 | 204 | return true; 205 | } 206 | 207 | /** 208 | * Injects component $package into $config after all other dependencies. 209 | * 210 | * If any dependencies are not registered, the method throws 211 | * Exception\RuntimeException. 212 | * 213 | * @param string $package 214 | * @param string $config 215 | * @return true 216 | * @throws Exception\RuntimeException 217 | */ 218 | private function injectAfterDependencies($package, $config) 219 | { 220 | foreach ($this->moduleDependencies as $dependency) { 221 | if (! $this->isRegisteredInConfig($dependency, $config)) { 222 | throw new Exception\RuntimeException(sprintf( 223 | 'Dependency %s is not registered in the configuration', 224 | $dependency 225 | )); 226 | } 227 | } 228 | 229 | $lastDependency = $this->findLastDependency($this->moduleDependencies, $config); 230 | 231 | $pattern = sprintf( 232 | $this->injectionPatterns[self::TYPE_DEPENDENCY]['pattern'], 233 | preg_quote($lastDependency, '/') 234 | ); 235 | $replacement = sprintf( 236 | $this->injectionPatterns[self::TYPE_DEPENDENCY]['replacement'], 237 | $package 238 | ); 239 | 240 | $config = preg_replace($pattern, $replacement, $config); 241 | file_put_contents($this->configFile, $config); 242 | 243 | return true; 244 | } 245 | 246 | /** 247 | * Find which of dependency packages is the last one on the module list. 248 | * 249 | * @param array $dependencies 250 | * @param string $config 251 | * @return string 252 | */ 253 | private function findLastDependency(array $dependencies, $config) 254 | { 255 | if (count($dependencies) === 1) { 256 | return reset($dependencies); 257 | } 258 | 259 | $longLength = 0; 260 | $last = null; 261 | foreach ($dependencies as $dependency) { 262 | preg_match(sprintf($this->isRegisteredPattern, preg_quote($dependency, '/')), $config, $matches); 263 | 264 | $length = strlen($matches[0]); 265 | if ($length > $longLength) { 266 | $longLength = $length; 267 | $last = $dependency; 268 | } 269 | } 270 | 271 | return $last; 272 | } 273 | 274 | /** 275 | * Inject module $package into $config before the first found application module 276 | * and return true. 277 | * If there is no any enabled application module, this method will return false. 278 | * 279 | * @param string $package 280 | * @param string $config 281 | * @param string $firstApplicationModule 282 | * @return bool 283 | */ 284 | private function injectBeforeApplicationModules($package, $config, $firstApplicationModule) 285 | { 286 | $pattern = sprintf( 287 | $this->injectionPatterns[self::TYPE_BEFORE_APPLICATION]['pattern'], 288 | preg_quote($firstApplicationModule, '/') 289 | ); 290 | $replacement = sprintf( 291 | $this->injectionPatterns[self::TYPE_BEFORE_APPLICATION]['replacement'], 292 | $package 293 | ); 294 | 295 | $config = preg_replace($pattern, $replacement, $config); 296 | file_put_contents($this->configFile, $config); 297 | 298 | return true; 299 | } 300 | 301 | /** 302 | * Find the first enabled application module from list $modules in the $config. 303 | * If any module is not found method will return null. 304 | * 305 | * @param array $modules 306 | * @param string $config 307 | * @return string|null 308 | */ 309 | private function findFirstEnabledApplicationModule(array $modules, $config) 310 | { 311 | $shortest = strlen($config); 312 | $first = null; 313 | foreach ($modules as $module) { 314 | if (! $this->isRegistered($module)) { 315 | continue; 316 | } 317 | 318 | preg_match(sprintf($this->isRegisteredPattern, preg_quote($module, '/')), $config, $matches); 319 | 320 | $length = strlen($matches[0]); 321 | if ($length < $shortest) { 322 | $shortest = $length; 323 | $first = $module; 324 | } 325 | } 326 | 327 | return $first; 328 | } 329 | 330 | /** 331 | * {@inheritDoc} 332 | */ 333 | public function setApplicationModules(array $modules) 334 | { 335 | $this->applicationModules = $modules; 336 | 337 | return $this; 338 | } 339 | 340 | /** 341 | * {@inheritDoc} 342 | */ 343 | public function setModuleDependencies(array $modules) 344 | { 345 | $this->moduleDependencies = $modules; 346 | 347 | return $this; 348 | } 349 | 350 | /** 351 | * Removes a package from the configuration. 352 | * Returns true if successfully removed, 353 | * false when package is not registered. 354 | * 355 | * @param string $package Package name. 356 | * @return bool 357 | */ 358 | public function remove($package) 359 | { 360 | $config = file_get_contents($this->configFile); 361 | 362 | if (! $this->isRegisteredInConfig($package, $config)) { 363 | return false; 364 | } 365 | 366 | $config = preg_replace( 367 | sprintf($this->removalPatterns['pattern'], preg_quote($package)), 368 | $this->removalPatterns['replacement'], 369 | $config 370 | ); 371 | 372 | $config = preg_replace( 373 | $this->cleanUpPatterns['pattern'], 374 | $this->cleanUpPatterns['replacement'], 375 | $config 376 | ); 377 | 378 | file_put_contents($this->configFile, $config); 379 | 380 | return true; 381 | } 382 | 383 | /** 384 | * Returns config file name of the injector. 385 | * 386 | * @return string 387 | */ 388 | public function getConfigFile() 389 | { 390 | return $this->configFile; 391 | } 392 | 393 | /** 394 | * Is the code item registered in the configuration already? 395 | * 396 | * @var string $package Package name 397 | * @var string $config 398 | * @return bool 399 | */ 400 | protected function isRegisteredInConfig($package, $config) 401 | { 402 | return preg_match(sprintf($this->isRegisteredPattern, preg_quote($package, '/')), $config) 403 | || preg_match(sprintf($this->isRegisteredPattern, preg_quote(addslashes($package), '/')), $config); 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/Injector/ApplicationConfigInjector.php: -------------------------------------------------------------------------------- 1 | [ 26 | 'pattern' => '/^(\s+)(\'modules\'\s*\=\>\s*(?:array\s*\(|\[))\s*$/m', 27 | 'replacement' => "\$1\$2\n\$1 '%s',", 28 | ], 29 | self::TYPE_MODULE => [ 30 | 'pattern' => "/('modules'\s*\=\>\s*(?:array\s*\(|\[).*?)\n(\s+)(\)|\])/s", 31 | 'replacement' => "\$1\n\$2 '%s',\n\$2\$3", 32 | ], 33 | self::TYPE_DEPENDENCY => [ 34 | 'pattern' => '/^(\s+)(\'modules\'\s*\=\>\s*(?:array\s*\(|\[)[^)\]]*\'%s\')/m', 35 | 'replacement' => "\$1\$2,\n\$1 '%s'", 36 | ], 37 | self::TYPE_BEFORE_APPLICATION => [ 38 | 'pattern' => '/^(\s+)(\'modules\'\s*\=\>\s*(?:array\s*\(|\[)[^)\]]*)(\'%s\')/m', 39 | 'replacement' => "\$1\$2'%s',\n$1 \$3", 40 | ], 41 | ]; 42 | 43 | /** 44 | * Pattern to use to determine if the code item is registered. 45 | * 46 | * @var string 47 | */ 48 | protected $isRegisteredPattern = '/\'modules\'\s*\=\>\s*(?:array\(|\[)[^)\]]*\'%s\'/s'; 49 | 50 | /** 51 | * Patterns and replacements to use when removing a code item. 52 | * 53 | * @var string[] 54 | */ 55 | protected $removalPatterns = [ 56 | 'pattern' => '/^\s+\'%s\',\s*$/m', 57 | 'replacement' => '', 58 | ]; 59 | } 60 | -------------------------------------------------------------------------------- /src/Injector/ConditionalDiscoveryTrait.php: -------------------------------------------------------------------------------- 1 | validConfigAggregatorConfig()) { 23 | return false; 24 | } 25 | 26 | return parent::inject('\\' . $package, $type); 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | * 32 | * Prepends the package with a `\\` in order to ensure it is fully 33 | * qualified, preventing issues in config files that are namespaced. 34 | */ 35 | public function remove($package) 36 | { 37 | if (! $this->validConfigAggregatorConfig()) { 38 | return false; 39 | } 40 | 41 | return parent::remove('\\' . $package); 42 | } 43 | 44 | /** 45 | * Does the config file hold valid ConfigAggregator configuration? 46 | * 47 | * @return bool 48 | */ 49 | private function validConfigAggregatorConfig() 50 | { 51 | $discoveryClass = $this->discoveryClass; 52 | $discovery = new $discoveryClass($this->getProjectRoot()); 53 | return $discovery->locate(); 54 | } 55 | 56 | /** 57 | * Calculate the project root from the config file 58 | * 59 | * @return string 60 | */ 61 | private function getProjectRoot() 62 | { 63 | if (static::DEFAULT_CONFIG_FILE === $this->configFile) { 64 | return ''; 65 | } 66 | return str_replace('/' . static::DEFAULT_CONFIG_FILE, '', $this->configFile); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Injector/ConfigAggregatorInjector.php: -------------------------------------------------------------------------------- 1 | [ 52 | 'pattern' => '', 53 | 'replacement' => "\$1\n\$2%s::class,\n\$2", 54 | ], 55 | ]; 56 | 57 | /** 58 | * Pattern to use to determine if the code item is registered. 59 | * 60 | * Set in constructor due to PCRE quoting issues. 61 | * 62 | * @var string 63 | */ 64 | protected $isRegisteredPattern = ''; 65 | 66 | /** 67 | * Patterns and replacements to use when removing a code item. 68 | * 69 | * @var string[] 70 | */ 71 | protected $removalPatterns = [ 72 | 'pattern' => '/^\s+%s::class,\s*$/m', 73 | 'replacement' => '', 74 | ]; 75 | 76 | /** 77 | * {@inheritDoc} 78 | * 79 | * Sets $isRegisteredPattern and pattern for $injectionPatterns to ensure 80 | * proper PCRE quoting. 81 | */ 82 | public function __construct($projectRoot = '') 83 | { 84 | $ns = preg_quote('\\'); 85 | $this->isRegisteredPattern = '/new (?:' 86 | . $ns 87 | . '?' 88 | . preg_quote('Zend\ConfigAggregator\\') 89 | . ')?ConfigAggregator\(\s*(?:array\(|\[).*\s+' 90 | . $ns 91 | . '?%s::class/s'; 92 | 93 | $this->injectionPatterns[self::TYPE_CONFIG_PROVIDER]['pattern'] = sprintf( 94 | "/(new (?:%s?%s)?ConfigAggregator\(\s*(?:array\(|\[)\s*)(?:\r|\n|\r\n)(\s*)/", 95 | preg_quote('\\'), 96 | preg_quote('Zend\ConfigAggregator\\') 97 | ); 98 | 99 | parent::__construct($projectRoot); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Injector/ConfigInjectorChain.php: -------------------------------------------------------------------------------- 1 | chain = Collection::create($injectors) 51 | // Keep only those injectors that discovery exists in discoveryChain 52 | ->filter(function ($injector, $file) use ($discoveryChain) { 53 | return $discoveryChain->discoveryExists($file); 54 | }) 55 | // Create an injector for the config file 56 | ->map(function ($injector) use ($projectRoot) { 57 | return new $injector($projectRoot); 58 | }) 59 | // Keep only those injectors that match types available for the package 60 | ->filter(function ($injector) use ($availableTypes) { 61 | return $availableTypes->reduce(function ($flag, $type) use ($injector) { 62 | return $flag || $injector->registersType($type); 63 | }, false); 64 | }); 65 | } 66 | 67 | /** 68 | * {@inheritDoc} 69 | */ 70 | public function registersType($type) 71 | { 72 | return in_array($type, $this->getTypesAllowed(), true); 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function getTypesAllowed() 79 | { 80 | if ($this->allowedTypes) { 81 | return $this->allowedTypes; 82 | } 83 | $allowedTypes = []; 84 | foreach ($this->chain->getIterator() as $injector) { 85 | $allowedTypes = $allowedTypes + $injector->getTypesAllowed(); 86 | } 87 | $this->allowedTypes = $allowedTypes; 88 | return $allowedTypes; 89 | } 90 | 91 | /** 92 | * {@inheritDoc} 93 | */ 94 | public function isRegistered($package) 95 | { 96 | $isRegisteredCount = $this->chain 97 | ->filter(function ($injector) use ($package) { 98 | return $injector->isRegistered($package); 99 | }) 100 | ->count(); 101 | return $this->chain->count() === $isRegisteredCount; 102 | } 103 | 104 | /** 105 | * {@inheritDoc} 106 | */ 107 | public function inject($package, $type) 108 | { 109 | $injected = false; 110 | 111 | $this->chain 112 | ->each(function ($injector) use ($package, $type, &$injected) { 113 | $injected = $injector->inject($package, $type) || $injected; 114 | }); 115 | 116 | return $injected; 117 | } 118 | 119 | /** 120 | * {@inheritDoc} 121 | */ 122 | public function remove($package) 123 | { 124 | $removed = false; 125 | 126 | $this->chain 127 | ->each(function ($injector) use ($package, &$removed) { 128 | $removed = $injector->remove($package) || $removed; 129 | }); 130 | 131 | return $removed; 132 | } 133 | 134 | /** 135 | * 136 | * @return Collection 137 | */ 138 | public function getCollection() 139 | { 140 | return $this->chain; 141 | } 142 | 143 | /** 144 | * {@inheritDoc} 145 | */ 146 | public function setApplicationModules(array $modules) 147 | { 148 | return $this; 149 | } 150 | 151 | /** 152 | * {@inheritDoc} 153 | */ 154 | public function setModuleDependencies(array $modules) 155 | { 156 | return $this; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Injector/DevelopmentConfigInjector.php: -------------------------------------------------------------------------------- 1 | [ 52 | 'pattern' => '', 53 | 'replacement' => "\$1\n %s::class,", 54 | ], 55 | ]; 56 | 57 | /** 58 | * Pattern to use to determine if the code item is registered. 59 | * 60 | * Set in constructor due to PCRE quoting issues. 61 | * 62 | * @var string 63 | */ 64 | protected $isRegisteredPattern = ''; 65 | 66 | /** 67 | * Patterns and replacements to use when removing a code item. 68 | * 69 | * @var string[] 70 | */ 71 | protected $removalPatterns = [ 72 | 'pattern' => '/^\s+%s::class,\s*$/m', 73 | 'replacement' => '', 74 | ]; 75 | 76 | /** 77 | * {@inheritDoc} 78 | * 79 | * Sets $isRegisteredPattern and pattern for $injectionPatterns to ensure 80 | * proper PCRE quoting. 81 | */ 82 | public function __construct($projectRoot = '') 83 | { 84 | $this->isRegisteredPattern = '/new (?:' 85 | . preg_quote('\\') 86 | . '?' 87 | . preg_quote('Zend\Expressive\ConfigManager\\') 88 | . ')?ConfigManager\(\s*(?:array\(|\[).*\s+%s::class/s'; 89 | 90 | $this->injectionPatterns[self::TYPE_CONFIG_PROVIDER]['pattern'] = sprintf( 91 | '/(new (?:%s?%s)?ConfigManager\(\s*(?:array\(|\[)\s*)$/m', 92 | preg_quote('\\'), 93 | preg_quote('Zend\Expressive\ConfigManager\\') 94 | ); 95 | 96 | parent::__construct($projectRoot); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Injector/InjectorInterface.php: -------------------------------------------------------------------------------- 1 | [ 26 | 'pattern' => '/^(return\s+(?:array\s*\(|\[))\s*$/m', 27 | 'replacement' => "\$1\n '%s',", 28 | ], 29 | self::TYPE_MODULE => [ 30 | 'pattern' => "/(return\s+(?:array\s*\(|\[).*?)\n(\s*)(\)|\])/s", 31 | 'replacement' => "\$1\n\$2 '%s',\n\$2\$3", 32 | ], 33 | self::TYPE_DEPENDENCY => [ 34 | 'pattern' => '/^(return\s+(?:array\s*\(|\[)[^)\]]*\'%s\')/m', 35 | 'replacement' => "\$1,\n '%s'", 36 | ], 37 | self::TYPE_BEFORE_APPLICATION => [ 38 | 'pattern' => '/^(return\s+(?:array\s*\(|\[)[^)\]]*)(\'%s\')/m', 39 | 'replacement' => "\$1'%s',\n \$2", 40 | ], 41 | ]; 42 | 43 | /** 44 | * Pattern to use to determine if the code item is registered. 45 | * 46 | * @var string 47 | */ 48 | protected $isRegisteredPattern = '/return\s+(?:array\(|\[)[^)\]]*\'%s\'/s'; 49 | 50 | /** 51 | * Patterns and replacements to use when removing a code item. 52 | * 53 | * @var string[] 54 | */ 55 | protected $removalPatterns = [ 56 | 'pattern' => '/^\s+\'%s\',\s*$/m', 57 | 'replacement' => '', 58 | ]; 59 | } 60 | -------------------------------------------------------------------------------- /src/Injector/NoopInjector.php: -------------------------------------------------------------------------------- 1 |