├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Exception ├── ExceptionInterface.php ├── InvalidArgumentException.php ├── MissingDependencyModuleException.php └── RuntimeException.php ├── Feature ├── AutoloaderProviderInterface.php ├── BootstrapListenerInterface.php ├── ConfigProviderInterface.php ├── ConsoleBannerProviderInterface.php ├── ConsoleUsageProviderInterface.php ├── ControllerPluginProviderInterface.php ├── ControllerProviderInterface.php ├── DependencyIndicatorInterface.php ├── FilterProviderInterface.php ├── FormElementProviderInterface.php ├── HydratorProviderInterface.php ├── InitProviderInterface.php ├── InputFilterProviderInterface.php ├── LocatorRegisteredInterface.php ├── LogProcessorProviderInterface.php ├── LogWriterProviderInterface.php ├── RouteProviderInterface.php ├── SerializerProviderInterface.php ├── ServiceProviderInterface.php ├── TranslatorPluginProviderInterface.php ├── ValidatorProviderInterface.php └── ViewHelperProviderInterface.php ├── Listener ├── AbstractListener.php ├── AutoloaderListener.php ├── ConfigListener.php ├── ConfigMergerInterface.php ├── DefaultListenerAggregate.php ├── Exception │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ └── RuntimeException.php ├── InitTrigger.php ├── ListenerOptions.php ├── LocatorRegistrationListener.php ├── ModuleDependencyCheckerListener.php ├── ModuleLoaderListener.php ├── ModuleResolverListener.php ├── OnBootstrapListener.php ├── ServiceListener.php └── ServiceListenerInterface.php ├── ModuleEvent.php ├── ModuleManager.php └── ModuleManagerInterface.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.8.5 - 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.8.4 - 2019-10-28 28 | 29 | ### Added 30 | 31 | - Nothing. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - [#91](https://github.com/zendframework/zend-modulemanager/pull/91) fixes permission on cache file. 48 | The permission denied issue occurs on reading the cache file, when cache has been build using different user account. 49 | 50 | ## 2.8.3 - 2019-10-18 51 | 52 | ### Added 53 | 54 | - [#85](https://github.com/zendframework/zend-modulemanager/pull/85) adds support for PHP 7.3. 55 | 56 | ### Changed 57 | 58 | - Nothing. 59 | 60 | ### Deprecated 61 | 62 | - Nothing. 63 | 64 | ### Removed 65 | 66 | - Nothing. 67 | 68 | ### Fixed 69 | 70 | - [#88](https://github.com/zendframework/zend-modulemanager/pull/88) fixes 71 | how cache files are created. Temporary file is created first and when 72 | completed it is renamed to target file. It prevents from loading uncompleted 73 | files. 74 | 75 | ## 2.8.2 - 2017-12-02 76 | 77 | ### Added 78 | 79 | - Nothing. 80 | 81 | ### Changed 82 | 83 | - Nothing. 84 | 85 | ### Deprecated 86 | 87 | - Nothing. 88 | 89 | ### Removed 90 | 91 | - Nothing. 92 | 93 | ### Fixed 94 | 95 | - [#74](https://github.com/zendframework/zend-modulemanager/pull/74) fixes 96 | exception message in ConfigListener 97 | 98 | ## 2.8.1 - 2017-11-01 99 | 100 | ### Added 101 | 102 | - Nothing. 103 | 104 | ### Changed 105 | 106 | - [#73](https://github.com/zendframework/zend-modulemanager/pull/73) modifies 107 | the `ModuleResolverListener` slightly. In 108 | [#5](https://github.com/zendframework/zend-modulemanager/pull/5), 109 | released in 2.8.0, we added the ability to use classes named after the module 110 | itself as a module class. However, in some specific cases, primarily when the 111 | module is a top-level namespace, this can lead to conflicts with 112 | globally-scoped classes. The patch in this release modifies the logic to first 113 | check if a `Module` class exists under the module namespace, and will use 114 | that; otherwise, it will then check if a class named after the namespace 115 | exists. Additionally, the class now implements a blacklist of specific classes 116 | known to be non-instantiable, including the `Generator` class shipped with the 117 | PHP language itself. 118 | 119 | ### Deprecated 120 | 121 | - Nothing. 122 | 123 | ### Removed 124 | 125 | - Nothing. 126 | 127 | ### Fixed 128 | 129 | - Nothing. 130 | 131 | ## 2.8.0 - 2017-07-11 132 | 133 | ### Added 134 | 135 | - [#4](https://github.com/zendframework/zend-modulemanager/pull/4) adds a new 136 | `ListenerOptions` option, `use_zend_loader`. The option defaults to `true`, 137 | which keeps the current behavior of registering the `ModuleAutoloader` and 138 | `AutoloaderProvider`. If you disable it, these features will no longer be 139 | loaded, allowing `ModuleManager` to be used without zend-loader. 140 | - [#5](https://github.com/zendframework/zend-modulemanager/pull/5) adds the 141 | ability to use a class of any name for a module, so long as you provide the 142 | fully qualified class name when registering the module with the module 143 | manager. 144 | 145 | ### Deprecated 146 | 147 | - Nothing. 148 | 149 | ### Removed 150 | 151 | - [#62](https://github.com/zendframework/zend-modulemanager/pull/62) removes 152 | support for PHP 5.5 and HHVM. 153 | 154 | ### Fixed 155 | 156 | - [#53](https://github.com/zendframework/zend-modulemanager/pull/53) preventing race conditions 157 | when writing cache files (merged configuration) 158 | 159 | ## 2.7.3 - 2017-07-11 160 | 161 | ### Added 162 | 163 | - Nothing. 164 | 165 | ### Deprecated 166 | 167 | - Nothing. 168 | 169 | ### Removed 170 | 171 | - Nothing. 172 | 173 | ### Fixed 174 | 175 | - [#39](https://github.com/zendframework/zend-modulemanager/pull/39) and 176 | [#53](https://github.com/zendframework/zend-modulemanager/pull/53) prevent 177 | race conditions when writing cache files (merged configuration). 178 | - [#36](https://github.com/zendframework/zend-modulemanager/pull/36) removes a 179 | throw from `ServiceListener::onLoadModulesPost()` that was previously emitted 180 | when a named plugin manager did not have an associated service present yet. 181 | Doing so allows plugin managers to be registered after configuration is fully 182 | merged, instead of requiring they be defined early. This change allows 183 | components to define their plugin managers via their `Module` classes. 184 | - [#58](https://github.com/zendframework/zend-modulemanager/pull/58) corrects 185 | the typehint for the `ServiceListener::$listeners` property. 186 | 187 | ## 2.7.2 - 2016-05-16 188 | 189 | ### Added 190 | 191 | - [#38](https://github.com/zendframework/zend-modulemanager/pull/38) prepares 192 | and publishes the documentation to https://zendframework.github.io/zend-modulemanager/ 193 | - [#40](https://github.com/zendframework/zend-modulemanager/pull/40) adds a 194 | requirement on zend-config. Since the default use case centers around config 195 | merging and requires the component, it should be required by 196 | zend-modulemanager. 197 | 198 | ### Deprecated 199 | 200 | - Nothing. 201 | 202 | ### Removed 203 | 204 | - Nothing. 205 | 206 | ### Fixed 207 | 208 | - Nothing. 209 | 210 | ## 2.7.1 - 2016-02-27 211 | 212 | ### Added 213 | 214 | - Nothing. 215 | 216 | ### Deprecated 217 | 218 | - Nothing. 219 | 220 | ### Removed 221 | 222 | - Nothing. 223 | 224 | ### Fixed 225 | 226 | - [#31](https://github.com/zendframework/zend-modulemanager/pull/31) updates the 227 | `ServiceListener:onLoadModulesPost()` workflow to override existing services 228 | on a given service/plugin manager instance when configuring it. Since the 229 | listener operates as part of bootstrapping, this is a requirement. 230 | 231 | ## 2.7.0 - 2016-02-25 232 | 233 | ### Added 234 | 235 | - Nothing. 236 | 237 | ### Deprecated 238 | 239 | - Nothing. 240 | 241 | ### Removed 242 | 243 | - Nothing. 244 | 245 | ### Fixed 246 | 247 | - [#13](https://github.com/zendframework/zend-modulemanager/pull/13) and 248 | [#28](https://github.com/zendframework/zend-modulemanager/pull/28) update the 249 | component to be forwards-compatible with zend-servicemanager v3. This 250 | primarily affects how configuration is aggregated within the 251 | `ServiceListener` (as v3 has a dedicated method in the 252 | `Zend\ServiceManager\ConfigInterface` for retrieving it). 253 | 254 | - [#12](https://github.com/zendframework/zend-modulemanager/pull/12), 255 | [#28](https://github.com/zendframework/zend-modulemanager/pull/28), and 256 | [#29](https://github.com/zendframework/zend-modulemanager/pull/29) update the 257 | component to be forwards-compatible with zend-eventmanager v3. Primarily, this 258 | involves: 259 | - Changing trigger calls to `triggerEvent()` and/or `triggerEventUntil()`, and 260 | ensuring the event instance is injected with the new event name prior. 261 | - Ensuring aggregates are attached using the `$aggregate->attach($events)` 262 | signature instead of the `$events->attachAggregate($aggregate)` signature. 263 | - Using zend-eventmanager's `EventListenerIntrospectionTrait` to test that 264 | listeners are attached at expected priorities. 265 | 266 | ## 2.6.1 - 2015-09-22 267 | 268 | ### Added 269 | 270 | - Nothing. 271 | 272 | ### Deprecated 273 | 274 | - Nothing. 275 | 276 | ### Removed 277 | 278 | - Nothing. 279 | 280 | ### Fixed 281 | 282 | - Fixed a condition where the `ModuleEvent` target was not properly populated 283 | with the `ModuleManager` as the target. 284 | 285 | ## 2.6.0 - 2015-09-22 286 | 287 | ### Added 288 | 289 | - Nothing. 290 | 291 | ### Deprecated 292 | 293 | - Nothing. 294 | 295 | ### Removed 296 | 297 | - Nothing. 298 | 299 | ### Fixed 300 | 301 | - [#10](https://github.com/zendframework/zend-modulemanager/pull/10) pins the 302 | zend-stdlib version to `~2.7`, allowing it to use that version forward, and 303 | ensuring compatibility with consumers of the new zend-hydrator library. 304 | 305 | ## 2.5.3 - 2015-09-22 306 | 307 | ### Added 308 | 309 | - Nothing. 310 | 311 | ### Deprecated 312 | 313 | - Nothing. 314 | 315 | ### Removed 316 | 317 | - Nothing. 318 | 319 | ### Fixed 320 | 321 | - Fixed a condition where the `ModuleEvent` target was not properly populated 322 | with the `ModuleManager` as the target. 323 | 324 | ## 2.5.2 - 2015-09-22 325 | 326 | ### Added 327 | 328 | - Nothing. 329 | 330 | ### Deprecated 331 | 332 | - Nothing. 333 | 334 | ### Removed 335 | 336 | - Nothing. 337 | 338 | ### Fixed 339 | 340 | - [#9](https://github.com/zendframework/zend-modulemanager/pull/9) pins the 341 | zend-stdlib version to `>=2.5.0,<2.7.0`, as 2.7.0 deprecates the hydrators (in 342 | favor of the new zend-hydrator library). 343 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-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 | # zend-modulemanager 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-modulemanager](https://github.com/laminas/laminas-modulemanager). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-modulemanager.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-modulemanager) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-modulemanager/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-modulemanager?branch=master) 9 | 10 | Zend Framework 2.0 introduces a new and powerful approach to modules. This new 11 | module system is designed with flexibility, simplicity, and re-usability in mind. 12 | A module may contain just about anything: PHP code, including MVC functionality; 13 | library code; view scripts; and/or public assets such as images, CSS, and 14 | JavaScript. The possibilities are endless. 15 | 16 | `Zend\ModuleManager` is the component that enables the design of a module 17 | architecture for PHP applications. 18 | 19 | ## Installation 20 | 21 | Run the following to install this library: 22 | 23 | ```bash 24 | $ composer require zendframework/zend-modulemanager 25 | ``` 26 | 27 | ## Documentation 28 | 29 | Browse the documentation online at https://docs.zendframework.com/zend-modulemanager/ 30 | 31 | ## Support 32 | 33 | * [Issues](https://github.com/zendframework/zend-modulemanager/issues/) 34 | * [Chat](https://zendframework-slack.herokuapp.com/) 35 | * [Forum](https://discourse.zendframework.com/) 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-modulemanager", 3 | "description": "Modular application system for zend-mvc applications", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zendframework", 7 | "zf", 8 | "modulemanager" 9 | ], 10 | "support": { 11 | "docs": "https://docs.zendframework.com/zend-modulemanager/", 12 | "issues": "https://github.com/zendframework/zend-modulemanager/issues", 13 | "source": "https://github.com/zendframework/zend-modulemanager", 14 | "rss": "https://github.com/zendframework/zend-modulemanager/releases.atom", 15 | "chat": "https://zendframework-slack.herokuapp.com", 16 | "forum": "https://discourse.zendframework.com/c/questions/components" 17 | }, 18 | "require": { 19 | "php": "^5.6 || ^7.0", 20 | "zendframework/zend-config": "^3.1 || ^2.6", 21 | "zendframework/zend-eventmanager": "^3.2 || ^2.6.3", 22 | "zendframework/zend-stdlib": "^3.1 || ^2.7" 23 | }, 24 | "require-dev": { 25 | "zendframework/zend-console": "^2.6", 26 | "zendframework/zend-di": "^2.6", 27 | "zendframework/zend-loader": "^2.5", 28 | "zendframework/zend-mvc": "^3.0 || ^2.7", 29 | "zendframework/zend-servicemanager": "^3.0.3 || ^2.7.5", 30 | "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", 31 | "zendframework/zend-coding-standard": "~1.0.0" 32 | }, 33 | "suggest": { 34 | "zendframework/zend-console": "Zend\\Console component", 35 | "zendframework/zend-loader": "Zend\\Loader component if you are not using Composer autoloading for your modules", 36 | "zendframework/zend-mvc": "Zend\\Mvc component", 37 | "zendframework/zend-servicemanager": "Zend\\ServiceManager component" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Zend\\ModuleManager\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "files": [ 46 | "test/autoload.php", 47 | "test/TestAsset/ModuleAsClass.php" 48 | ], 49 | "psr-4": { 50 | "ListenerTestModule\\": "test/TestAsset/ListenerTestModule/", 51 | "ModuleAsClass\\": "test/TestAsset/ModuleAsClass/", 52 | "ZendTest\\ModuleManager\\": "test/" 53 | } 54 | }, 55 | "config": { 56 | "sort-packages": true 57 | }, 58 | "extra": { 59 | "branch-alias": { 60 | "dev-master": "2.8.x-dev", 61 | "dev-develop": "2.9.x-dev" 62 | } 63 | }, 64 | "scripts": { 65 | "check": [ 66 | "@cs-check", 67 | "@test" 68 | ], 69 | "cs-check": "phpcs", 70 | "cs-fix": "phpcbf", 71 | "test": "phpunit --colors=always", 72 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 'A short description of that parameter', 28 | * '-another-parameter' => 'A short description of another parameter', 29 | * ... 30 | * ) 31 | * 32 | * @param AdapterInterface $console 33 | * @return array|string|null 34 | */ 35 | public function getConsoleUsage(AdapterInterface $console); 36 | } 37 | -------------------------------------------------------------------------------- /src/Feature/ControllerPluginProviderInterface.php: -------------------------------------------------------------------------------- 1 | setOptions($options); 29 | } 30 | 31 | /** 32 | * Get options. 33 | * 34 | * @return ListenerOptions 35 | */ 36 | public function getOptions() 37 | { 38 | return $this->options; 39 | } 40 | 41 | /** 42 | * Set options. 43 | * 44 | * @param ListenerOptions $options the value to be set 45 | * @return AbstractListener 46 | */ 47 | public function setOptions(ListenerOptions $options) 48 | { 49 | $this->options = $options; 50 | return $this; 51 | } 52 | 53 | /** 54 | * Write a simple array of scalars to a file 55 | * 56 | * @param string $filePath 57 | * @param array $array 58 | * @return AbstractListener 59 | */ 60 | protected function writeArrayToFile($filePath, $array) 61 | { 62 | // Write cache file to temporary file first and then rename it. 63 | // We don't want cache file to be read when it is not written completely. 64 | // include/require functions require additional lock, see: 65 | // https://bugs.php.net/bug.php?id=52895 66 | $tmp = tempnam(sys_get_temp_dir(), md5($filePath)); 67 | 68 | $content = "getModule(); 26 | if (! $module instanceof AutoloaderProviderInterface 27 | && ! method_exists($module, 'getAutoloaderConfig') 28 | ) { 29 | return; 30 | } 31 | $autoloaderConfig = $module->getAutoloaderConfig(); 32 | AutoloaderFactory::factory($autoloaderConfig); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Listener/ConfigListener.php: -------------------------------------------------------------------------------- 1 | hasCachedConfig()) { 67 | $this->skipConfig = true; 68 | $this->setMergedConfig($this->getCachedConfig()); 69 | } else { 70 | $this->addConfigGlobPaths($this->getOptions()->getConfigGlobPaths()); 71 | $this->addConfigStaticPaths($this->getOptions()->getConfigStaticPaths()); 72 | } 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function attach(EventManagerInterface $events, $priority = 1) 79 | { 80 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULES, [$this, 'onloadModulesPre'], 1000); 81 | 82 | if ($this->skipConfig) { 83 | // We already have the config from cache, no need to collect or merge. 84 | return; 85 | } 86 | 87 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE, [$this, 'onLoadModule']); 88 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULES, [$this, 'onLoadModules'], -1000); 89 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_MERGE_CONFIG, [$this, 'onMergeConfig'], 1000); 90 | } 91 | 92 | /** 93 | * Pass self to the ModuleEvent object early so everyone has access. 94 | * 95 | * @param ModuleEvent $e 96 | * @return ConfigListener 97 | */ 98 | public function onloadModulesPre(ModuleEvent $e) 99 | { 100 | $e->setConfigListener($this); 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * Merge the config for each module 107 | * 108 | * @param ModuleEvent $e 109 | * @return ConfigListener 110 | */ 111 | public function onLoadModule(ModuleEvent $e) 112 | { 113 | $module = $e->getModule(); 114 | 115 | if (! $module instanceof ConfigProviderInterface 116 | && ! is_callable([$module, 'getConfig']) 117 | ) { 118 | return $this; 119 | } 120 | 121 | $config = $module->getConfig(); 122 | $this->addConfig($e->getModuleName(), $config); 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * Merge all config files matched by the given glob()s 129 | * 130 | * This is only attached if config is not cached. 131 | * 132 | * @param ModuleEvent $e 133 | * @return ConfigListener 134 | */ 135 | public function onMergeConfig(ModuleEvent $e) 136 | { 137 | // Load the config files 138 | foreach ($this->paths as $path) { 139 | $this->addConfigByPath($path['path'], $path['type']); 140 | } 141 | 142 | // Merge all of the collected configs 143 | $this->mergedConfig = $this->getOptions()->getExtraConfig() ?: []; 144 | foreach ($this->configs as $config) { 145 | $this->mergedConfig = ArrayUtils::merge($this->mergedConfig, $config); 146 | } 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * Optionally cache merged config 153 | * 154 | * This is only attached if config is not cached. 155 | * 156 | * @param ModuleEvent $e 157 | * @return ConfigListener 158 | */ 159 | public function onLoadModules(ModuleEvent $e) 160 | { 161 | // Trigger MERGE_CONFIG event. This is a hook to allow the merged application config to be 162 | // modified before it is cached (In particular, allows the removal of config keys) 163 | $originalEventName = $e->getName(); 164 | $e->setName(ModuleEvent::EVENT_MERGE_CONFIG); 165 | $e->getTarget()->getEventManager()->triggerEvent($e); 166 | 167 | // Reset event name 168 | $e->setName($originalEventName); 169 | 170 | // If enabled, update the config cache 171 | if ($this->getOptions()->getConfigCacheEnabled() 172 | && false === $this->skipConfig 173 | ) { 174 | $configFile = $this->getOptions()->getConfigCacheFile(); 175 | $this->writeArrayToFile($configFile, $this->getMergedConfig(false)); 176 | } 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * getMergedConfig 183 | * 184 | * @param bool $returnConfigAsObject 185 | * @return mixed 186 | */ 187 | public function getMergedConfig($returnConfigAsObject = true) 188 | { 189 | if ($returnConfigAsObject === true) { 190 | if ($this->mergedConfigObject === null) { 191 | $this->mergedConfigObject = new Config($this->mergedConfig); 192 | } 193 | return $this->mergedConfigObject; 194 | } 195 | 196 | return $this->mergedConfig; 197 | } 198 | 199 | /** 200 | * setMergedConfig 201 | * 202 | * @param array $config 203 | * @return ConfigListener 204 | */ 205 | public function setMergedConfig(array $config) 206 | { 207 | $this->mergedConfig = $config; 208 | $this->mergedConfigObject = null; 209 | return $this; 210 | } 211 | 212 | /** 213 | * Add an array of glob paths of config files to merge after loading modules 214 | * 215 | * @param array|Traversable $globPaths 216 | * @return ConfigListener 217 | */ 218 | public function addConfigGlobPaths($globPaths) 219 | { 220 | $this->addConfigPaths($globPaths, self::GLOB_PATH); 221 | return $this; 222 | } 223 | 224 | /** 225 | * Add a glob path of config files to merge after loading modules 226 | * 227 | * @param string $globPath 228 | * @return ConfigListener 229 | */ 230 | public function addConfigGlobPath($globPath) 231 | { 232 | $this->addConfigPath($globPath, self::GLOB_PATH); 233 | return $this; 234 | } 235 | 236 | /** 237 | * Add an array of static paths of config files to merge after loading modules 238 | * 239 | * @param array|Traversable $staticPaths 240 | * @return ConfigListener 241 | */ 242 | public function addConfigStaticPaths($staticPaths) 243 | { 244 | $this->addConfigPaths($staticPaths, self::STATIC_PATH); 245 | return $this; 246 | } 247 | 248 | /** 249 | * Add a static path of config files to merge after loading modules 250 | * 251 | * @param string $staticPath 252 | * @return ConfigListener 253 | */ 254 | public function addConfigStaticPath($staticPath) 255 | { 256 | $this->addConfigPath($staticPath, self::STATIC_PATH); 257 | return $this; 258 | } 259 | 260 | /** 261 | * Add an array of paths of config files to merge after loading modules 262 | * 263 | * @param Traversable|array $paths 264 | * @param string $type 265 | * @throws Exception\InvalidArgumentException 266 | * @return ConfigListener 267 | */ 268 | protected function addConfigPaths($paths, $type) 269 | { 270 | if ($paths instanceof Traversable) { 271 | $paths = ArrayUtils::iteratorToArray($paths); 272 | } 273 | 274 | if (! is_array($paths)) { 275 | throw new Exception\InvalidArgumentException( 276 | sprintf( 277 | 'Argument passed to %s::%s() must be an array, ' 278 | . 'implement the Traversable interface, or be an ' 279 | . 'instance of Zend\Config\Config. %s given.', 280 | __CLASS__, 281 | __METHOD__, 282 | gettype($paths) 283 | ) 284 | ); 285 | } 286 | 287 | foreach ($paths as $path) { 288 | $this->addConfigPath($path, $type); 289 | } 290 | } 291 | 292 | /** 293 | * Add a path of config files to load and merge after loading modules 294 | * 295 | * @param string $path 296 | * @param string $type 297 | * @throws Exception\InvalidArgumentException 298 | * @return ConfigListener 299 | */ 300 | protected function addConfigPath($path, $type) 301 | { 302 | if (! is_string($path)) { 303 | throw new Exception\InvalidArgumentException( 304 | sprintf( 305 | 'Parameter to %s::%s() must be a string; %s given.', 306 | __CLASS__, 307 | __METHOD__, 308 | gettype($path) 309 | ) 310 | ); 311 | } 312 | $this->paths[] = ['type' => $type, 'path' => $path]; 313 | return $this; 314 | } 315 | 316 | /** 317 | * @param string $key 318 | * @param array|Traversable $config 319 | * @throws Exception\InvalidArgumentException 320 | * @return ConfigListener 321 | */ 322 | protected function addConfig($key, $config) 323 | { 324 | if ($config instanceof Traversable) { 325 | $config = ArrayUtils::iteratorToArray($config); 326 | } 327 | 328 | if (! is_array($config)) { 329 | throw new Exception\InvalidArgumentException( 330 | sprintf( 331 | 'Config being merged must be an array, ' 332 | . 'implement the Traversable interface, or be an ' 333 | . 'instance of Zend\Config\Config. %s given.', 334 | gettype($config) 335 | ) 336 | ); 337 | } 338 | 339 | $this->configs[$key] = $config; 340 | 341 | return $this; 342 | } 343 | 344 | /** 345 | * Given a path (glob or static), fetch the config and add it to the array 346 | * of configs to merge. 347 | * 348 | * @param string $path 349 | * @param string $type 350 | * @return ConfigListener 351 | */ 352 | protected function addConfigByPath($path, $type) 353 | { 354 | switch ($type) { 355 | case self::STATIC_PATH: 356 | $this->addConfig($path, ConfigFactory::fromFile($path)); 357 | break; 358 | 359 | case self::GLOB_PATH: 360 | // We want to keep track of where each value came from so we don't 361 | // use ConfigFactory::fromFiles() since it does merging internally. 362 | foreach (Glob::glob($path, Glob::GLOB_BRACE) as $file) { 363 | $this->addConfig($file, ConfigFactory::fromFile($file)); 364 | } 365 | break; 366 | } 367 | 368 | return $this; 369 | } 370 | 371 | /** 372 | * @return bool 373 | */ 374 | protected function hasCachedConfig() 375 | { 376 | if (($this->getOptions()->getConfigCacheEnabled()) 377 | && (file_exists($this->getOptions()->getConfigCacheFile())) 378 | ) { 379 | return true; 380 | } 381 | return false; 382 | } 383 | 384 | /** 385 | * @return mixed 386 | */ 387 | protected function getCachedConfig() 388 | { 389 | return include $this->getOptions()->getConfigCacheFile(); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/Listener/ConfigMergerInterface.php: -------------------------------------------------------------------------------- 1 | getOptions(); 40 | $configListener = $this->getConfigListener(); 41 | $locatorRegistrationListener = new LocatorRegistrationListener($options); 42 | 43 | // High priority, we assume module autoloading (for FooNamespace\Module 44 | // classes) should be available before anything else. 45 | // Register it only if use_zend_loader config is true, however. 46 | if ($options->useZendLoader()) { 47 | $moduleLoaderListener = new ModuleLoaderListener($options); 48 | $moduleLoaderListener->attach($events); 49 | $this->listeners[] = $moduleLoaderListener; 50 | } 51 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE_RESOLVE, new ModuleResolverListener); 52 | 53 | if ($options->useZendLoader()) { 54 | // High priority, because most other loadModule listeners will assume 55 | // the module's classes are available via autoloading 56 | // Register it only if use_zend_loader config is true, however. 57 | $this->listeners[] = $events->attach( 58 | ModuleEvent::EVENT_LOAD_MODULE, 59 | new AutoloaderListener($options), 60 | 9000 61 | ); 62 | } 63 | 64 | if ($options->getCheckDependencies()) { 65 | $this->listeners[] = $events->attach( 66 | ModuleEvent::EVENT_LOAD_MODULE, 67 | new ModuleDependencyCheckerListener, 68 | 8000 69 | ); 70 | } 71 | 72 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE, new InitTrigger($options)); 73 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE, new OnBootstrapListener($options)); 74 | 75 | $locatorRegistrationListener->attach($events); 76 | $configListener->attach($events); 77 | $this->listeners[] = $locatorRegistrationListener; 78 | $this->listeners[] = $configListener; 79 | return $this; 80 | } 81 | 82 | /** 83 | * Detach all previously attached listeners 84 | * 85 | * @param EventManagerInterface $events 86 | * @return void 87 | */ 88 | public function detach(EventManagerInterface $events) 89 | { 90 | foreach ($this->listeners as $key => $listener) { 91 | if ($listener instanceof ListenerAggregateInterface) { 92 | $listener->detach($events); 93 | unset($this->listeners[$key]); 94 | continue; 95 | } 96 | 97 | $events->detach($listener); 98 | unset($this->listeners[$key]); 99 | } 100 | } 101 | 102 | /** 103 | * Get the config merger. 104 | * 105 | * @return ConfigMergerInterface 106 | */ 107 | public function getConfigListener() 108 | { 109 | if (! $this->configListener instanceof ConfigMergerInterface) { 110 | $this->setConfigListener(new ConfigListener($this->getOptions())); 111 | } 112 | return $this->configListener; 113 | } 114 | 115 | /** 116 | * Set the config merger to use. 117 | * 118 | * @param ConfigMergerInterface $configListener 119 | * @return DefaultListenerAggregate 120 | */ 121 | public function setConfigListener(ConfigMergerInterface $configListener) 122 | { 123 | $this->configListener = $configListener; 124 | return $this; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Listener/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | getModule(); 25 | if (! $module instanceof InitProviderInterface 26 | && ! method_exists($module, 'init') 27 | ) { 28 | return; 29 | } 30 | 31 | $module->init($e->getTarget()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Listener/ListenerOptions.php: -------------------------------------------------------------------------------- 1 | modulePaths; 81 | } 82 | 83 | /** 84 | * Set an array of paths where modules reside 85 | * 86 | * @param array|Traversable $modulePaths 87 | * @throws Exception\InvalidArgumentException 88 | * @return ListenerOptions Provides fluent interface 89 | */ 90 | public function setModulePaths($modulePaths) 91 | { 92 | if (! is_array($modulePaths) && ! $modulePaths instanceof Traversable) { 93 | throw new Exception\InvalidArgumentException( 94 | sprintf( 95 | 'Argument passed to %s::%s() must be an array, ' 96 | . 'implement the Traversable interface, or be an ' 97 | . 'instance of Zend\Config\Config. %s given.', 98 | __CLASS__, 99 | __METHOD__, 100 | gettype($modulePaths) 101 | ) 102 | ); 103 | } 104 | 105 | $this->modulePaths = $modulePaths; 106 | return $this; 107 | } 108 | 109 | /** 110 | * Get the glob patterns to load additional config files 111 | * 112 | * @return array 113 | */ 114 | public function getConfigGlobPaths() 115 | { 116 | return $this->configGlobPaths; 117 | } 118 | 119 | /** 120 | * Get the static paths to load additional config files 121 | * 122 | * @return array 123 | */ 124 | public function getConfigStaticPaths() 125 | { 126 | return $this->configStaticPaths; 127 | } 128 | 129 | /** 130 | * Set the glob patterns to use for loading additional config files 131 | * 132 | * @param array|Traversable $configGlobPaths 133 | * @throws Exception\InvalidArgumentException 134 | * @return ListenerOptions Provides fluent interface 135 | */ 136 | public function setConfigGlobPaths($configGlobPaths) 137 | { 138 | if (! is_array($configGlobPaths) && ! $configGlobPaths instanceof Traversable) { 139 | throw new Exception\InvalidArgumentException( 140 | sprintf( 141 | 'Argument passed to %s::%s() must be an array, ' 142 | . 'implement the Traversable interface, or be an ' 143 | . 'instance of Zend\Config\Config. %s given.', 144 | __CLASS__, 145 | __METHOD__, 146 | gettype($configGlobPaths) 147 | ) 148 | ); 149 | } 150 | 151 | $this->configGlobPaths = $configGlobPaths; 152 | return $this; 153 | } 154 | 155 | /** 156 | * Set the static paths to use for loading additional config files 157 | * 158 | * @param array|Traversable $configStaticPaths 159 | * @throws Exception\InvalidArgumentException 160 | * @return ListenerOptions Provides fluent interface 161 | */ 162 | public function setConfigStaticPaths($configStaticPaths) 163 | { 164 | if (! is_array($configStaticPaths) && ! $configStaticPaths instanceof Traversable) { 165 | throw new Exception\InvalidArgumentException( 166 | sprintf( 167 | 'Argument passed to %s::%s() must be an array, ' 168 | . 'implement the Traversable interface, or be an ' 169 | . 'instance of Zend\Config\Config. %s given.', 170 | __CLASS__, 171 | __METHOD__, 172 | gettype($configStaticPaths) 173 | ) 174 | ); 175 | } 176 | 177 | $this->configStaticPaths = $configStaticPaths; 178 | return $this; 179 | } 180 | 181 | /** 182 | * Get any extra config to merge in. 183 | * 184 | * @return array|Traversable 185 | */ 186 | public function getExtraConfig() 187 | { 188 | return $this->extraConfig; 189 | } 190 | 191 | /** 192 | * Add some extra config array to the main config. This is mainly useful 193 | * for unit testing purposes. 194 | * 195 | * @param array|Traversable $extraConfig 196 | * @throws Exception\InvalidArgumentException 197 | * @return ListenerOptions Provides fluent interface 198 | */ 199 | public function setExtraConfig($extraConfig) 200 | { 201 | if (! is_array($extraConfig) && ! $extraConfig instanceof Traversable) { 202 | throw new Exception\InvalidArgumentException( 203 | sprintf( 204 | 'Argument passed to %s::%s() must be an array, ' 205 | . 'implement the Traversable interface, or be an ' 206 | . 'instance of Zend\Config\Config. %s given.', 207 | __CLASS__, 208 | __METHOD__, 209 | gettype($extraConfig) 210 | ) 211 | ); 212 | } 213 | 214 | $this->extraConfig = $extraConfig; 215 | return $this; 216 | } 217 | 218 | /** 219 | * Check if the config cache is enabled 220 | * 221 | * @return bool 222 | */ 223 | public function getConfigCacheEnabled() 224 | { 225 | return $this->configCacheEnabled; 226 | } 227 | 228 | /** 229 | * Set if the config cache should be enabled or not 230 | * 231 | * @param bool $enabled 232 | * @return ListenerOptions 233 | */ 234 | public function setConfigCacheEnabled($enabled) 235 | { 236 | $this->configCacheEnabled = (bool) $enabled; 237 | return $this; 238 | } 239 | 240 | /** 241 | * Get key used to create the cache file name 242 | * 243 | * @return string 244 | */ 245 | public function getConfigCacheKey() 246 | { 247 | return (string) $this->configCacheKey; 248 | } 249 | 250 | /** 251 | * Set key used to create the cache file name 252 | * 253 | * @param string $configCacheKey the value to be set 254 | * @return ListenerOptions 255 | */ 256 | public function setConfigCacheKey($configCacheKey) 257 | { 258 | $this->configCacheKey = $configCacheKey; 259 | return $this; 260 | } 261 | 262 | /** 263 | * Get the path to the config cache 264 | * 265 | * Should this be an option, or should the dir option include the 266 | * filename, or should it simply remain hard-coded? Thoughts? 267 | * 268 | * @return string 269 | */ 270 | public function getConfigCacheFile() 271 | { 272 | if ($this->getConfigCacheKey()) { 273 | return $this->getCacheDir() . '/module-config-cache.' . $this->getConfigCacheKey().'.php'; 274 | } 275 | 276 | return $this->getCacheDir() . '/module-config-cache.php'; 277 | } 278 | 279 | /** 280 | * Get the path where cache file(s) are stored 281 | * 282 | * @return string|null 283 | */ 284 | public function getCacheDir() 285 | { 286 | return $this->cacheDir; 287 | } 288 | 289 | /** 290 | * Set the path where cache files can be stored 291 | * 292 | * @param string|null $cacheDir the value to be set 293 | * @return ListenerOptions 294 | */ 295 | public function setCacheDir($cacheDir) 296 | { 297 | $this->cacheDir = $cacheDir ? static::normalizePath($cacheDir) : null; 298 | 299 | return $this; 300 | } 301 | 302 | /** 303 | * Check if the module class map cache is enabled 304 | * 305 | * @return bool 306 | */ 307 | public function getModuleMapCacheEnabled() 308 | { 309 | return $this->moduleMapCacheEnabled; 310 | } 311 | 312 | /** 313 | * Set if the module class map cache should be enabled or not 314 | * 315 | * @param bool $enabled 316 | * @return ListenerOptions 317 | */ 318 | public function setModuleMapCacheEnabled($enabled) 319 | { 320 | $this->moduleMapCacheEnabled = (bool) $enabled; 321 | return $this; 322 | } 323 | 324 | /** 325 | * Get key used to create the cache file name 326 | * 327 | * @return string 328 | */ 329 | public function getModuleMapCacheKey() 330 | { 331 | return (string) $this->moduleMapCacheKey; 332 | } 333 | 334 | /** 335 | * Set key used to create the cache file name 336 | * 337 | * @param string $moduleMapCacheKey the value to be set 338 | * @return ListenerOptions 339 | */ 340 | public function setModuleMapCacheKey($moduleMapCacheKey) 341 | { 342 | $this->moduleMapCacheKey = $moduleMapCacheKey; 343 | return $this; 344 | } 345 | 346 | /** 347 | * Get the path to the module class map cache 348 | * 349 | * @return string 350 | */ 351 | public function getModuleMapCacheFile() 352 | { 353 | if ($this->getModuleMapCacheKey()) { 354 | return $this->getCacheDir() . '/module-classmap-cache.' . $this->getModuleMapCacheKey() . '.php'; 355 | } 356 | 357 | return $this->getCacheDir() . '/module-classmap-cache.php'; 358 | } 359 | 360 | /** 361 | * Set whether to check dependencies during module loading or not 362 | * 363 | * @return bool 364 | */ 365 | public function getCheckDependencies() 366 | { 367 | return $this->checkDependencies; 368 | } 369 | 370 | /** 371 | * Set whether to check dependencies during module loading or not 372 | * 373 | * @param bool $checkDependencies the value to be set 374 | * 375 | * @return ListenerOptions 376 | */ 377 | public function setCheckDependencies($checkDependencies) 378 | { 379 | $this->checkDependencies = (bool) $checkDependencies; 380 | 381 | return $this; 382 | } 383 | 384 | /** 385 | * Whether or not to use zend-loader to autoload modules. 386 | * 387 | * @return bool 388 | */ 389 | public function useZendLoader() 390 | { 391 | return $this->useZendLoader; 392 | } 393 | 394 | /** 395 | * Set a flag indicating if the module manager should use zend-loader 396 | * 397 | * Setting this option to false will disable ModuleAutoloader, requiring 398 | * other means of autoloading to be used (e.g., Composer). 399 | * 400 | * If disabled, the AutoloaderProvider feature will be disabled as well 401 | * 402 | * @param bool $flag 403 | * @return ListenerOptions 404 | */ 405 | public function setUseZendLoader($flag) 406 | { 407 | $this->useZendLoader = (bool) $flag; 408 | return $this; 409 | } 410 | 411 | /** 412 | * Normalize a path for insertion in the stack 413 | * 414 | * @param string $path 415 | * @return string 416 | */ 417 | public static function normalizePath($path) 418 | { 419 | $path = rtrim($path, '/'); 420 | $path = rtrim($path, '\\'); 421 | return $path; 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/Listener/LocatorRegistrationListener.php: -------------------------------------------------------------------------------- 1 | getModule() instanceof LocatorRegisteredInterface) { 44 | return; 45 | } 46 | $this->modules[] = $e->getModule(); 47 | } 48 | 49 | /** 50 | * loadModules 51 | * 52 | * Once all the modules are loaded, loop 53 | * 54 | * @param ModuleEvent $e 55 | * @return void 56 | */ 57 | public function onLoadModules(ModuleEvent $e) 58 | { 59 | $moduleManager = $e->getTarget(); 60 | $events = $moduleManager->getEventManager()->getSharedManager(); 61 | 62 | if (! $events) { 63 | return; 64 | } 65 | 66 | // Shared instance for module manager 67 | $events->attach( 68 | 'Zend\Mvc\Application', 69 | ModuleManager::EVENT_BOOTSTRAP, 70 | function (MvcEvent $e) use ($moduleManager) { 71 | $moduleClassName = get_class($moduleManager); 72 | $moduleClassNameArray = explode('\\', $moduleClassName); 73 | $moduleClassNameAlias = end($moduleClassNameArray); 74 | $application = $e->getApplication(); 75 | /* @var $services ServiceManager */ 76 | $services = $application->getServiceManager(); 77 | if (! $services->has($moduleClassName)) { 78 | $services->setAlias($moduleClassName, $moduleClassNameAlias); 79 | } 80 | }, 81 | 1000 82 | ); 83 | 84 | if (! $this->modules) { 85 | return; 86 | } 87 | 88 | // Attach to the bootstrap event if there are modules we need to process 89 | $events->attach('Zend\Mvc\Application', ModuleManager::EVENT_BOOTSTRAP, [$this, 'onBootstrap'], 1000); 90 | } 91 | 92 | /** 93 | * Bootstrap listener 94 | * 95 | * This is ran during the MVC bootstrap event because it requires access to 96 | * the DI container. 97 | * 98 | * @TODO: Check the application / locator / etc a bit better to make sure 99 | * the env looks how we're expecting it to? 100 | * @param MvcEvent $e 101 | * @return void 102 | */ 103 | public function onBootstrap(MvcEvent $e) 104 | { 105 | $application = $e->getApplication(); 106 | /* @var $services ServiceManager */ 107 | $services = $application->getServiceManager(); 108 | 109 | foreach ($this->modules as $module) { 110 | $moduleClassName = get_class($module); 111 | if (! $services->has($moduleClassName)) { 112 | $services->setService($moduleClassName, $module); 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * {@inheritDoc} 119 | */ 120 | public function attach(EventManagerInterface $events, $priority = 1) 121 | { 122 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE, [$this, 'onLoadModule']); 123 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULES, [$this, 'onLoadModules'], -1000); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Listener/ModuleDependencyCheckerListener.php: -------------------------------------------------------------------------------- 1 | getModule(); 32 | 33 | if ($module instanceof DependencyIndicatorInterface || method_exists($module, 'getModuleDependencies')) { 34 | $dependencies = $module->getModuleDependencies(); 35 | 36 | foreach ($dependencies as $dependencyModule) { 37 | if (! isset($this->loaded[$dependencyModule])) { 38 | throw new Exception\MissingDependencyModuleException( 39 | sprintf( 40 | 'Module "%s" depends on module "%s", which was not initialized before it', 41 | $e->getModuleName(), 42 | $dependencyModule 43 | ) 44 | ); 45 | } 46 | } 47 | } 48 | 49 | $this->loaded[$e->getModuleName()] = true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Listener/ModuleLoaderListener.php: -------------------------------------------------------------------------------- 1 | generateCache = $this->options->getModuleMapCacheEnabled(); 48 | $this->moduleLoader = new ModuleAutoloader($this->options->getModulePaths()); 49 | 50 | if ($this->hasCachedClassMap()) { 51 | $this->generateCache = false; 52 | $this->moduleLoader->setModuleClassMap($this->getCachedConfig()); 53 | } 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function attach(EventManagerInterface $events, $priority = 1) 60 | { 61 | $this->callbacks[] = $events->attach( 62 | ModuleEvent::EVENT_LOAD_MODULES, 63 | [$this->moduleLoader, 'register'], 64 | 9000 65 | ); 66 | 67 | if ($this->generateCache) { 68 | $this->callbacks[] = $events->attach( 69 | ModuleEvent::EVENT_LOAD_MODULES_POST, 70 | [$this, 'onLoadModulesPost'] 71 | ); 72 | } 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function detach(EventManagerInterface $events) 79 | { 80 | foreach ($this->callbacks as $index => $callback) { 81 | if ($events->detach($callback)) { 82 | unset($this->callbacks[$index]); 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * @return bool 89 | */ 90 | protected function hasCachedClassMap() 91 | { 92 | if ($this->options->getModuleMapCacheEnabled() 93 | && file_exists($this->options->getModuleMapCacheFile()) 94 | ) { 95 | return true; 96 | } 97 | 98 | return false; 99 | } 100 | 101 | /** 102 | * @return array 103 | */ 104 | protected function getCachedConfig() 105 | { 106 | return include $this->options->getModuleMapCacheFile(); 107 | } 108 | 109 | /** 110 | * loadModulesPost 111 | * 112 | * Unregisters the ModuleLoader and generates the module class map cache. 113 | * 114 | * @param ModuleEvent $event 115 | */ 116 | public function onLoadModulesPost(ModuleEvent $event) 117 | { 118 | $this->moduleLoader->unregister(); 119 | $this->writeArrayToFile( 120 | $this->options->getModuleMapCacheFile(), 121 | $this->moduleLoader->getModuleClassMap() 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Listener/ModuleResolverListener.php: -------------------------------------------------------------------------------- 1 | getModuleName(); 34 | 35 | $class = sprintf('%s\Module', $moduleName); 36 | if (class_exists($class)) { 37 | return new $class; 38 | } 39 | 40 | if (class_exists($moduleName) 41 | && ! in_array($moduleName, $this->invalidClassNames, true) 42 | ) { 43 | return new $moduleName; 44 | } 45 | 46 | return false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Listener/OnBootstrapListener.php: -------------------------------------------------------------------------------- 1 | getModule(); 26 | if (! $module instanceof BootstrapListenerInterface 27 | && ! method_exists($module, 'onBootstrap') 28 | ) { 29 | return; 30 | } 31 | 32 | $moduleManager = $e->getTarget(); 33 | $events = $moduleManager->getEventManager(); 34 | $sharedEvents = $events->getSharedManager(); 35 | $sharedEvents->attach('Zend\Mvc\Application', ModuleManager::EVENT_BOOTSTRAP, [$module, 'onBootstrap']); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Listener/ServiceListener.php: -------------------------------------------------------------------------------- 1 | defaultServiceManager = $serviceManager; 59 | 60 | if ($configuration !== null) { 61 | $this->setDefaultServiceConfig($configuration); 62 | } 63 | } 64 | 65 | /** 66 | * @param array $configuration 67 | * @return ServiceListener 68 | */ 69 | public function setDefaultServiceConfig($configuration) 70 | { 71 | $this->defaultServiceConfig = $configuration; 72 | return $this; 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function addServiceManager($serviceManager, $key, $moduleInterface, $method) 79 | { 80 | if (is_string($serviceManager)) { 81 | $smKey = $serviceManager; 82 | } elseif ($serviceManager instanceof ServiceManager) { 83 | $smKey = spl_object_hash($serviceManager); 84 | } else { 85 | throw new Exception\RuntimeException(sprintf( 86 | 'Invalid service manager provided, expected ServiceManager or string, %s provided', 87 | (is_object($serviceManager) ? get_class($serviceManager) : gettype($serviceManager)) 88 | )); 89 | } 90 | 91 | $this->serviceManagers[$smKey] = [ 92 | 'service_manager' => $serviceManager, 93 | 'config_key' => $key, 94 | 'module_class_interface' => $moduleInterface, 95 | 'module_class_method' => $method, 96 | 'configuration' => [], 97 | ]; 98 | 99 | if ($key === 'service_manager' && $this->defaultServiceConfig) { 100 | $this->serviceManagers[$smKey]['configuration']['default_config'] = $this->defaultServiceConfig; 101 | } 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * @param EventManagerInterface $events 108 | * @param int $priority 109 | * @return ServiceListener 110 | */ 111 | public function attach(EventManagerInterface $events, $priority = 1) 112 | { 113 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE, [$this, 'onLoadModule']); 114 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULES_POST, [$this, 'onLoadModulesPost']); 115 | return $this; 116 | } 117 | 118 | /** 119 | * @param EventManagerInterface $events 120 | * @return void 121 | */ 122 | public function detach(EventManagerInterface $events) 123 | { 124 | foreach ($this->listeners as $key => $listener) { 125 | if ($events->detach($listener)) { 126 | unset($this->listeners[$key]); 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * Retrieve service manager configuration from module, and 133 | * configure the service manager. 134 | * 135 | * If the module does not implement a specific interface and does not 136 | * implement a specific method, does nothing. Also, if the return value 137 | * of that method is not a ServiceConfig object, or not an array or 138 | * Traversable that can seed one, does nothing. 139 | * 140 | * The interface and method name can be set by adding a new service manager 141 | * via the addServiceManager() method. 142 | * 143 | * @param ModuleEvent $e 144 | * @return void 145 | */ 146 | public function onLoadModule(ModuleEvent $e) 147 | { 148 | $module = $e->getModule(); 149 | 150 | foreach ($this->serviceManagers as $key => $sm) { 151 | if (! $module instanceof $sm['module_class_interface'] 152 | && ! method_exists($module, $sm['module_class_method']) 153 | ) { 154 | continue; 155 | } 156 | 157 | $config = $module->{$sm['module_class_method']}(); 158 | 159 | if ($config instanceof ServiceConfigInterface) { 160 | $config = $this->serviceConfigToArray($config); 161 | } 162 | 163 | if ($config instanceof Traversable) { 164 | $config = ArrayUtils::iteratorToArray($config); 165 | } 166 | 167 | if (! is_array($config)) { 168 | // If we do not have an array by this point, nothing left to do. 169 | continue; 170 | } 171 | 172 | // We are keeping track of which modules provided which configuration to which service managers. 173 | // The actual merging takes place later. Doing it this way will enable us to provide more powerful 174 | // debugging tools for showing which modules overrode what. 175 | $fullname = $e->getModuleName() . '::' . $sm['module_class_method'] . '()'; 176 | $this->serviceManagers[$key]['configuration'][$fullname] = $config; 177 | } 178 | } 179 | 180 | /** 181 | * Use merged configuration to configure service manager 182 | * 183 | * If the merged configuration has a non-empty, array 'service_manager' 184 | * key, it will be passed to a ServiceManager Config object, and 185 | * used to configure the service manager. 186 | * 187 | * @param ModuleEvent $e 188 | * @throws Exception\RuntimeException 189 | * @return void 190 | */ 191 | public function onLoadModulesPost(ModuleEvent $e) 192 | { 193 | $configListener = $e->getConfigListener(); 194 | $config = $configListener->getMergedConfig(false); 195 | 196 | foreach ($this->serviceManagers as $key => $sm) { 197 | $smConfig = $this->mergeServiceConfiguration($key, $sm, $config); 198 | 199 | if (! $sm['service_manager'] instanceof ServiceManager) { 200 | if (! $this->defaultServiceManager->has($sm['service_manager'])) { 201 | // No plugin manager registered by that name; nothing to configure. 202 | continue; 203 | } 204 | 205 | $instance = $this->defaultServiceManager->get($sm['service_manager']); 206 | if (! $instance instanceof ServiceManager) { 207 | throw new Exception\RuntimeException(sprintf( 208 | 'Could not find a valid ServiceManager for %s', 209 | $sm['service_manager'] 210 | )); 211 | } 212 | 213 | $sm['service_manager'] = $instance; 214 | } 215 | 216 | $serviceConfig = new ServiceConfig($smConfig); 217 | 218 | // The service listener is meant to operate during bootstrap, and, as such, 219 | // needs to be able to override existing configuration. 220 | $allowOverride = $sm['service_manager']->getAllowOverride(); 221 | $sm['service_manager']->setAllowOverride(true); 222 | 223 | $serviceConfig->configureServiceManager($sm['service_manager']); 224 | 225 | $sm['service_manager']->setAllowOverride($allowOverride); 226 | } 227 | } 228 | 229 | /** 230 | * Merge a service configuration container 231 | * 232 | * Extracts the various service configuration arrays. 233 | * 234 | * @param ServiceConfigInterface|string $config ServiceConfigInterface or 235 | * class name resolving to one. 236 | * @return array 237 | * @throws Exception\RuntimeException if resolved class name is not a 238 | * ServiceConfigInterface implementation. 239 | * @throws Exception\RuntimeException under zend-servicemanager v2 if the 240 | * configuration instance is not specifically a ServiceConfig, as there 241 | * is no way to extract service configuration in that case. 242 | */ 243 | protected function serviceConfigToArray($config) 244 | { 245 | if (is_string($config) && class_exists($config)) { 246 | $class = $config; 247 | $config = new $class; 248 | } 249 | 250 | if (! $config instanceof ServiceConfigInterface) { 251 | throw new Exception\RuntimeException(sprintf( 252 | 'Invalid service manager configuration class provided; received "%s", expected an instance of %s', 253 | (is_object($config) ? get_class($config) : (is_scalar($config) ? $config : gettype($config))), 254 | ServiceConfigInterface::class 255 | )); 256 | } 257 | 258 | if (method_exists($config, 'toArray')) { 259 | // zend-servicemanager v3 interface 260 | return $config->toArray(); 261 | } 262 | 263 | // For zend-servicemanager v2, we need a Zend\ServiceManager\Config 264 | // instance specifically. 265 | if (! $config instanceof ServiceConfig) { 266 | throw new Exception\RuntimeException(sprintf( 267 | 'Invalid service manager configuration class provided; received "%s", expected an instance of %s', 268 | (is_object($config) ? get_class($config) : (is_scalar($config) ? $config : gettype($config))), 269 | ServiceConfig::class 270 | )); 271 | } 272 | 273 | // Pull service configuration from discrete methods. 274 | return [ 275 | 'abstract_factories' => $config->getAbstractFactories(), 276 | 'aliases' => $config->getAliases(), 277 | 'delegators' => $config->getDelegators(), 278 | 'factories' => $config->getFactories(), 279 | 'initializers' => $config->getInitializers(), 280 | 'invokables' => $config->getInvokables(), 281 | 'services' => $config->getServices(), 282 | 'shared' => $config->getShared(), 283 | ]; 284 | } 285 | 286 | /** 287 | * Merge all configuration for a given service manager to a single array. 288 | * 289 | * @param string $key Named service manager 290 | * @param array $metadata Service manager metadata 291 | * @param array $config Merged configuration 292 | * @return array Service manager-specific configuration 293 | */ 294 | private function mergeServiceConfiguration($key, array $metadata, array $config) 295 | { 296 | if (isset($config[$metadata['config_key']]) 297 | && is_array($config[$metadata['config_key']]) 298 | && ! empty($config[$metadata['config_key']]) 299 | ) { 300 | $this->serviceManagers[$key]['configuration']['merged_config'] = $config[$metadata['config_key']]; 301 | } 302 | 303 | // Merge all of the things! 304 | $serviceConfig = []; 305 | foreach ($this->serviceManagers[$key]['configuration'] as $name => $configs) { 306 | if (isset($configs['configuration_classes'])) { 307 | foreach ($configs['configuration_classes'] as $class) { 308 | $configs = ArrayUtils::merge($configs, $this->serviceConfigToArray($class)); 309 | } 310 | } 311 | $serviceConfig = ArrayUtils::merge($serviceConfig, $configs); 312 | } 313 | 314 | return $serviceConfig; 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/Listener/ServiceListenerInterface.php: -------------------------------------------------------------------------------- 1 | moduleName; 52 | } 53 | 54 | /** 55 | * Set the name of a given module 56 | * 57 | * @param string $moduleName 58 | * @throws Exception\InvalidArgumentException 59 | * @return ModuleEvent 60 | */ 61 | public function setModuleName($moduleName) 62 | { 63 | if (! is_string($moduleName)) { 64 | throw new Exception\InvalidArgumentException( 65 | sprintf( 66 | '%s expects a string as an argument; %s provided', 67 | __METHOD__, 68 | gettype($moduleName) 69 | ) 70 | ); 71 | } 72 | // Performance tweak, don't add it as param. 73 | $this->moduleName = $moduleName; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Get module object 80 | * 81 | * @return null|object 82 | */ 83 | public function getModule() 84 | { 85 | return $this->module; 86 | } 87 | 88 | /** 89 | * Set module object to compose in this event 90 | * 91 | * @param object $module 92 | * @throws Exception\InvalidArgumentException 93 | * @return ModuleEvent 94 | */ 95 | public function setModule($module) 96 | { 97 | if (! is_object($module)) { 98 | throw new Exception\InvalidArgumentException( 99 | sprintf( 100 | '%s expects a module object as an argument; %s provided', 101 | __METHOD__, 102 | gettype($module) 103 | ) 104 | ); 105 | } 106 | // Performance tweak, don't add it as param. 107 | $this->module = $module; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Get the config listener 114 | * 115 | * @return null|Listener\ConfigMergerInterface 116 | */ 117 | public function getConfigListener() 118 | { 119 | return $this->configListener; 120 | } 121 | 122 | /** 123 | * Set module object to compose in this event 124 | * 125 | * @param Listener\ConfigMergerInterface $configListener 126 | * @return ModuleEvent 127 | */ 128 | public function setConfigListener(Listener\ConfigMergerInterface $configListener) 129 | { 130 | $this->setParam('configListener', $configListener); 131 | $this->configListener = $configListener; 132 | 133 | return $this; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/ModuleManager.php: -------------------------------------------------------------------------------- 1 | setModules($modules); 68 | if ($eventManager instanceof EventManagerInterface) { 69 | $this->setEventManager($eventManager); 70 | } 71 | } 72 | 73 | /** 74 | * Handle the loadModules event 75 | * 76 | * @return void 77 | */ 78 | public function onLoadModules() 79 | { 80 | if (true === $this->modulesAreLoaded) { 81 | return; 82 | } 83 | 84 | foreach ($this->getModules() as $moduleName => $module) { 85 | if (is_object($module)) { 86 | if (! is_string($moduleName)) { 87 | throw new Exception\RuntimeException(sprintf( 88 | 'Module (%s) must have a key identifier.', 89 | get_class($module) 90 | )); 91 | } 92 | $module = [$moduleName => $module]; 93 | } 94 | 95 | $this->loadModule($module); 96 | } 97 | 98 | $this->modulesAreLoaded = true; 99 | } 100 | 101 | /** 102 | * Load the provided modules. 103 | * 104 | * @triggers loadModules 105 | * @triggers loadModules.post 106 | * @return ModuleManager 107 | */ 108 | public function loadModules() 109 | { 110 | if (true === $this->modulesAreLoaded) { 111 | return $this; 112 | } 113 | 114 | $events = $this->getEventManager(); 115 | $event = $this->getEvent(); 116 | $event->setName(ModuleEvent::EVENT_LOAD_MODULES); 117 | 118 | $events->triggerEvent($event); 119 | 120 | /** 121 | * Having a dedicated .post event abstracts the complexity of priorities from the user. 122 | * Users can attach to the .post event and be sure that important 123 | * things like config merging are complete without having to worry if 124 | * they set a low enough priority. 125 | */ 126 | $event->setName(ModuleEvent::EVENT_LOAD_MODULES_POST); 127 | $events->triggerEvent($event); 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Load a specific module by name. 134 | * 135 | * @param string|array $module 136 | * @throws Exception\RuntimeException 137 | * @triggers loadModule.resolve 138 | * @triggers loadModule 139 | * @return mixed Module's Module class 140 | */ 141 | public function loadModule($module) 142 | { 143 | $moduleName = $module; 144 | if (is_array($module)) { 145 | $moduleName = key($module); 146 | $module = current($module); 147 | } 148 | 149 | if (isset($this->loadedModules[$moduleName])) { 150 | return $this->loadedModules[$moduleName]; 151 | } 152 | 153 | /* 154 | * Keep track of nested module loading using the $loadFinished 155 | * property. 156 | * 157 | * Increment the value for each loadModule() call and then decrement 158 | * once the loading process is complete. 159 | * 160 | * To load a module, we clone the event if we are inside a nested 161 | * loadModule() call, and use the original event otherwise. 162 | */ 163 | if (! isset($this->loadFinished)) { 164 | $this->loadFinished = 0; 165 | } 166 | 167 | $event = ($this->loadFinished > 0) ? clone $this->getEvent() : $this->getEvent(); 168 | $event->setModuleName($moduleName); 169 | 170 | $this->loadFinished++; 171 | 172 | if (! is_object($module)) { 173 | $module = $this->loadModuleByName($event); 174 | } 175 | $event->setModule($module); 176 | $event->setName(ModuleEvent::EVENT_LOAD_MODULE); 177 | 178 | $this->loadedModules[$moduleName] = $module; 179 | $this->getEventManager()->triggerEvent($event); 180 | 181 | $this->loadFinished--; 182 | 183 | return $module; 184 | } 185 | 186 | /** 187 | * Load a module with the name 188 | * @param ModuleEvent $event 189 | * @return mixed module instance 190 | * @throws Exception\RuntimeException 191 | */ 192 | protected function loadModuleByName(ModuleEvent $event) 193 | { 194 | $event->setName(ModuleEvent::EVENT_LOAD_MODULE_RESOLVE); 195 | $result = $this->getEventManager()->triggerEventUntil(function ($r) { 196 | return (is_object($r)); 197 | }, $event); 198 | 199 | $module = $result->last(); 200 | if (! is_object($module)) { 201 | throw new Exception\RuntimeException(sprintf( 202 | 'Module (%s) could not be initialized.', 203 | $event->getModuleName() 204 | )); 205 | } 206 | 207 | return $module; 208 | } 209 | 210 | /** 211 | * Get an array of the loaded modules. 212 | * 213 | * @param bool $loadModules If true, load modules if they're not already 214 | * @return array An array of Module objects, keyed by module name 215 | */ 216 | public function getLoadedModules($loadModules = false) 217 | { 218 | if (true === $loadModules) { 219 | $this->loadModules(); 220 | } 221 | 222 | return $this->loadedModules; 223 | } 224 | 225 | /** 226 | * Get an instance of a module class by the module name 227 | * 228 | * @param string $moduleName 229 | * @return mixed 230 | */ 231 | public function getModule($moduleName) 232 | { 233 | if (! isset($this->loadedModules[$moduleName])) { 234 | return; 235 | } 236 | return $this->loadedModules[$moduleName]; 237 | } 238 | 239 | /** 240 | * Get the array of module names that this manager should load. 241 | * 242 | * @return array 243 | */ 244 | public function getModules() 245 | { 246 | return $this->modules; 247 | } 248 | 249 | /** 250 | * Set an array or Traversable of module names that this module manager should load. 251 | * 252 | * @param mixed $modules array or Traversable of module names 253 | * @throws Exception\InvalidArgumentException 254 | * @return ModuleManager 255 | */ 256 | public function setModules($modules) 257 | { 258 | if (is_array($modules) || $modules instanceof Traversable) { 259 | $this->modules = $modules; 260 | } else { 261 | throw new Exception\InvalidArgumentException( 262 | sprintf( 263 | 'Parameter to %s\'s %s method must be an array or implement the Traversable interface', 264 | __CLASS__, 265 | __METHOD__ 266 | ) 267 | ); 268 | } 269 | return $this; 270 | } 271 | 272 | /** 273 | * Get the module event 274 | * 275 | * @return ModuleEvent 276 | */ 277 | public function getEvent() 278 | { 279 | if (! $this->event instanceof ModuleEvent) { 280 | $this->setEvent(new ModuleEvent()); 281 | } 282 | return $this->event; 283 | } 284 | 285 | /** 286 | * Set the module event 287 | * 288 | * @param ModuleEvent $event 289 | * @return ModuleManager 290 | */ 291 | public function setEvent(ModuleEvent $event) 292 | { 293 | $event->setTarget($this); 294 | $this->event = $event; 295 | return $this; 296 | } 297 | 298 | /** 299 | * Set the event manager instance used by this module manager. 300 | * 301 | * @param EventManagerInterface $events 302 | * @return ModuleManager 303 | */ 304 | public function setEventManager(EventManagerInterface $events) 305 | { 306 | $events->setIdentifiers([ 307 | __CLASS__, 308 | get_class($this), 309 | 'module_manager', 310 | ]); 311 | $this->events = $events; 312 | $this->attachDefaultListeners($events); 313 | return $this; 314 | } 315 | 316 | /** 317 | * Retrieve the event manager 318 | * 319 | * Lazy-loads an EventManager instance if none registered. 320 | * 321 | * @return EventManagerInterface 322 | */ 323 | public function getEventManager() 324 | { 325 | if (! $this->events instanceof EventManagerInterface) { 326 | $this->setEventManager(new EventManager()); 327 | } 328 | return $this->events; 329 | } 330 | 331 | /** 332 | * Register the default event listeners 333 | * 334 | * @param EventManagerInterface $events 335 | * @return ModuleManager 336 | */ 337 | protected function attachDefaultListeners($events) 338 | { 339 | $events->attach(ModuleEvent::EVENT_LOAD_MODULES, [$this, 'onLoadModules']); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/ModuleManagerInterface.php: -------------------------------------------------------------------------------- 1 |