├── .laminas-ci.json ├── .laminas-ci └── pre-install.sh ├── COPYRIGHT.md ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── renovate.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 │ ├── ConfigCannotBeCachedException.php │ ├── 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 /.laminas-ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_php_platform_requirements": { 3 | "8.5": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.laminas-ci/pre-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Temporary workaround for cyclic dependencies - can be removed once 3.0 has been released 4 | 5 | echo "Branch as 2.99.x in Pre-Install" 6 | 7 | git checkout -b 2.99.x 8 | -------------------------------------------------------------------------------- /COPYRIGHT.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/) 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | - Neither the name of Laminas Foundation nor the names of its contributors may 14 | be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laminas-modulemanager 2 | 3 | [![Build Status](https://github.com/laminas/laminas-modulemanager/workflows/Continuous%20Integration/badge.svg)](https://github.com/laminas/laminas-modulemanager/actions?query=workflow%3A"Continuous+Integration") 4 | 5 | > ## 🇷🇺 Русским гражданам 6 | > 7 | > Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм. 8 | > 9 | > У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую. 10 | > 11 | > Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!" 12 | > 13 | > ## 🇺🇸 To Citizens of Russia 14 | > 15 | > We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism. 16 | > 17 | > One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences. 18 | > 19 | > You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!" 20 | 21 | `Laminas\ModuleManager` introduces a new and powerful approach to modules. This new 22 | module system is designed with flexibility, simplicity, and re-usability in mind. 23 | A module may contain just about anything: PHP code, including MVC functionality; 24 | library code; view scripts; and/or public assets such as images, CSS, and 25 | JavaScript. The possibilities are endless. 26 | 27 | `Laminas\ModuleManager` is the component that enables the design of a module 28 | architecture for PHP applications. 29 | 30 | ## Installation 31 | 32 | Run the following to install this library: 33 | 34 | ```bash 35 | $ composer require laminas/laminas-modulemanager 36 | ``` 37 | 38 | ## Documentation 39 | 40 | Browse the documentation online at https://docs.laminas.dev/laminas-modulemanager/ 41 | 42 | ## Support 43 | 44 | * [Issues](https://github.com/laminas/laminas-modulemanager/issues/) 45 | * [Chat](https://laminas.dev/chat/) 46 | * [Forum](https://discourse.laminas.dev/) 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laminas/laminas-modulemanager", 3 | "description": "Modular application system for laminas-mvc applications", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "laminas", 7 | "modulemanager" 8 | ], 9 | "homepage": "https://laminas.dev", 10 | "support": { 11 | "docs": "https://docs.laminas.dev/laminas-modulemanager/", 12 | "issues": "https://github.com/laminas/laminas-modulemanager/issues", 13 | "source": "https://github.com/laminas/laminas-modulemanager", 14 | "rss": "https://github.com/laminas/laminas-modulemanager/releases.atom", 15 | "chat": "https://laminas.dev/chat", 16 | "forum": "https://discourse.laminas.dev" 17 | }, 18 | "config": { 19 | "allow-plugins": { 20 | "dealerdirect/phpcodesniffer-composer-installer": true 21 | }, 22 | "sort-packages": true, 23 | "platform": { 24 | "php": "8.2.99" 25 | } 26 | }, 27 | "extra": { 28 | }, 29 | "require": { 30 | "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", 31 | "brick/varexporter": "^0.6", 32 | "laminas/laminas-config": "^3.10.1", 33 | "laminas/laminas-eventmanager": "^3.14.0", 34 | "laminas/laminas-stdlib": "^3.21.0", 35 | "webimpress/safe-writer": "^2.2.0" 36 | }, 37 | "require-dev": { 38 | "laminas/laminas-coding-standard": "~3.1.0", 39 | "laminas/laminas-loader": "^2.11", 40 | "laminas/laminas-mvc": "^3.8.0", 41 | "laminas/laminas-servicemanager": "^3.23.0", 42 | "phpunit/phpunit": "^11.5.42", 43 | "psalm/plugin-phpunit": "^0.19.5", 44 | "vimeo/psalm": "^6.13.1", 45 | "amphp/dns": "^1.24 || ^2.1.2", 46 | "amphp/socket": "^1.2.1 || ^2.3.1" 47 | }, 48 | "suggest": { 49 | "laminas/laminas-console": "Laminas\\Console component", 50 | "laminas/laminas-loader": "Laminas\\Loader component if you are not using Composer autoloading for your modules", 51 | "laminas/laminas-mvc": "Laminas\\Mvc component", 52 | "laminas/laminas-servicemanager": "Laminas\\ServiceManager component" 53 | }, 54 | "autoload": { 55 | "psr-4": { 56 | "Laminas\\ModuleManager\\": "src/" 57 | } 58 | }, 59 | "autoload-dev": { 60 | "psr-4": { 61 | "ListenerTestModule\\": "test/TestAsset/ListenerTestModule/", 62 | "ModuleAsClass\\": "test/TestAsset/ModuleAsClass/", 63 | "LaminasTest\\ModuleManager\\": "test/" 64 | } 65 | }, 66 | "scripts": { 67 | "check": [ 68 | "@cs-check", 69 | "@test" 70 | ], 71 | "cs-check": "phpcs", 72 | "cs-fix": "phpcbf", 73 | "test": "phpunit --colors=always", 74 | "static-analysis": "psalm --shepherd --stats", 75 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 76 | }, 77 | "conflict": { 78 | "zendframework/zend-modulemanager": "*", 79 | "amphp/amp":"<2.6.4" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>laminas/.github:renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 'A short description of that parameter', 25 | * '-another-parameter' => 'A short description of another parameter', 26 | * ... 27 | * ) 28 | * 29 | * @return array|string|null 30 | */ 31 | public function getConsoleUsage(AdapterInterface $console); 32 | } 33 | -------------------------------------------------------------------------------- /src/Feature/ControllerPluginProviderInterface.php: -------------------------------------------------------------------------------- 1 | setOptions($options); 21 | } 22 | 23 | /** @return ListenerOptions */ 24 | public function getOptions() 25 | { 26 | return $this->options; 27 | } 28 | 29 | /** 30 | * @param ListenerOptions $options the value to be set 31 | * @return AbstractListener 32 | */ 33 | public function setOptions(ListenerOptions $options) 34 | { 35 | $this->options = $options; 36 | return $this; 37 | } 38 | 39 | /** 40 | * Write a simple array of scalars to a file 41 | * 42 | * @param string $filePath 43 | * @param array $array 44 | * @return AbstractListener 45 | */ 46 | protected function writeArrayToFile($filePath, $array) 47 | { 48 | try { 49 | $content = "getModule(); 19 | if ( 20 | ! $module instanceof AutoloaderProviderInterface 21 | && ! method_exists($module, 'getAutoloaderConfig') 22 | ) { 23 | return; 24 | } 25 | $autoloaderConfig = $module->getAutoloaderConfig(); 26 | AutoloaderFactory::factory($autoloaderConfig); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Listener/ConfigListener.php: -------------------------------------------------------------------------------- 1 | hasCachedConfig()) { 54 | $this->skipConfig = true; 55 | $this->setMergedConfig($this->getCachedConfig()); 56 | } else { 57 | $this->addConfigGlobPaths($this->getOptions()->getConfigGlobPaths()); 58 | $this->addConfigStaticPaths($this->getOptions()->getConfigStaticPaths()); 59 | } 60 | } 61 | 62 | /** {@inheritDoc} */ 63 | #[Override] 64 | public function attach(EventManagerInterface $events, $priority = 1) 65 | { 66 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULES, [$this, 'onloadModulesPre'], 1000); 67 | 68 | if ($this->skipConfig) { 69 | // We already have the config from cache, no need to collect or merge. 70 | return; 71 | } 72 | 73 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE, [$this, 'onLoadModule']); 74 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULES, [$this, 'onLoadModules'], -1000); 75 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_MERGE_CONFIG, [$this, 'onMergeConfig'], 1000); 76 | } 77 | 78 | /** 79 | * Pass self to the ModuleEvent object early so everyone has access. 80 | * 81 | * @return ConfigListener 82 | */ 83 | public function onloadModulesPre(ModuleEvent $e) 84 | { 85 | $e->setConfigListener($this); 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Merge the config for each module 92 | * 93 | * @return ConfigListener 94 | */ 95 | public function onLoadModule(ModuleEvent $e) 96 | { 97 | $module = $e->getModule(); 98 | 99 | if ( 100 | ! $module instanceof ConfigProviderInterface 101 | && ! is_callable([$module, 'getConfig']) 102 | ) { 103 | return $this; 104 | } 105 | 106 | $config = $module->getConfig(); 107 | $this->addConfig($e->getModuleName(), $config); 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Merge all config files matched by the given glob()s 114 | * 115 | * This is only attached if config is not cached. 116 | * 117 | * @return ConfigListener 118 | */ 119 | public function onMergeConfig(ModuleEvent $e) 120 | { 121 | // Load the config files 122 | foreach ($this->paths as $path) { 123 | $this->addConfigByPath($path['path'], $path['type']); 124 | } 125 | 126 | // Merge all of the collected configs 127 | $this->mergedConfig = $this->getOptions()->getExtraConfig() ?: []; 128 | foreach ($this->configs as $config) { 129 | $this->mergedConfig = ArrayUtils::merge($this->mergedConfig, $config); 130 | } 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Optionally cache merged config 137 | * 138 | * This is only attached if config is not cached. 139 | * 140 | * @return ConfigListener 141 | */ 142 | public function onLoadModules(ModuleEvent $e) 143 | { 144 | // Trigger MERGE_CONFIG event. This is a hook to allow the merged application config to be 145 | // modified before it is cached (In particular, allows the removal of config keys) 146 | $originalEventName = $e->getName(); 147 | $e->setName(ModuleEvent::EVENT_MERGE_CONFIG); 148 | $e->getTarget()->getEventManager()->triggerEvent($e); 149 | 150 | // Reset event name 151 | $e->setName($originalEventName); 152 | 153 | // If enabled, update the config cache 154 | if ( 155 | $this->getOptions()->getConfigCacheEnabled() 156 | && false === $this->skipConfig 157 | ) { 158 | $configFile = $this->getOptions()->getConfigCacheFile(); 159 | $this->writeArrayToFile($configFile, $this->getMergedConfig(false)); 160 | } 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * @param bool $returnConfigAsObject 167 | * @return mixed 168 | */ 169 | #[Override] 170 | public function getMergedConfig($returnConfigAsObject = true) 171 | { 172 | if ($returnConfigAsObject === true) { 173 | if ($this->mergedConfigObject === null) { 174 | $this->mergedConfigObject = new Config($this->mergedConfig); 175 | } 176 | return $this->mergedConfigObject; 177 | } 178 | 179 | return $this->mergedConfig; 180 | } 181 | 182 | /** 183 | * @return ConfigListener 184 | */ 185 | #[Override] 186 | public function setMergedConfig(array $config) 187 | { 188 | $this->mergedConfig = $config; 189 | $this->mergedConfigObject = null; 190 | return $this; 191 | } 192 | 193 | /** 194 | * Add an array of glob paths of config files to merge after loading modules 195 | * 196 | * @param array|Traversable $globPaths 197 | * @return ConfigListener 198 | */ 199 | public function addConfigGlobPaths($globPaths) 200 | { 201 | $this->addConfigPaths($globPaths, self::GLOB_PATH); 202 | return $this; 203 | } 204 | 205 | /** 206 | * Add a glob path of config files to merge after loading modules 207 | * 208 | * @param string $globPath 209 | * @return ConfigListener 210 | */ 211 | public function addConfigGlobPath($globPath) 212 | { 213 | $this->addConfigPath($globPath, self::GLOB_PATH); 214 | return $this; 215 | } 216 | 217 | /** 218 | * Add an array of static paths of config files to merge after loading modules 219 | * 220 | * @param array|Traversable $staticPaths 221 | * @return ConfigListener 222 | */ 223 | public function addConfigStaticPaths($staticPaths) 224 | { 225 | $this->addConfigPaths($staticPaths, self::STATIC_PATH); 226 | return $this; 227 | } 228 | 229 | /** 230 | * Add a static path of config files to merge after loading modules 231 | * 232 | * @param string $staticPath 233 | * @return ConfigListener 234 | */ 235 | public function addConfigStaticPath($staticPath) 236 | { 237 | $this->addConfigPath($staticPath, self::STATIC_PATH); 238 | return $this; 239 | } 240 | 241 | /** 242 | * Add an array of paths of config files to merge after loading modules 243 | * 244 | * @param Traversable|array $paths 245 | * @param string $type 246 | * @throws Exception\InvalidArgumentException 247 | */ 248 | protected function addConfigPaths($paths, $type) 249 | { 250 | if ($paths instanceof Traversable) { 251 | $paths = ArrayUtils::iteratorToArray($paths); 252 | } 253 | 254 | if (! is_array($paths)) { 255 | throw new Exception\InvalidArgumentException( 256 | sprintf( 257 | 'Argument passed to %s::%s() must be an array, ' 258 | . 'implement the Traversable interface, or be an ' 259 | . 'instance of Laminas\Config\Config. %s given.', 260 | self::class, 261 | __METHOD__, 262 | gettype($paths) 263 | ) 264 | ); 265 | } 266 | 267 | foreach ($paths as $path) { 268 | $this->addConfigPath($path, $type); 269 | } 270 | } 271 | 272 | /** 273 | * Add a path of config files to load and merge after loading modules 274 | * 275 | * @param string $path 276 | * @param string $type 277 | * @throws Exception\InvalidArgumentException 278 | * @return ConfigListener 279 | */ 280 | protected function addConfigPath($path, $type) 281 | { 282 | if (! is_string($path)) { 283 | throw new Exception\InvalidArgumentException( 284 | sprintf( 285 | 'Parameter to %s::%s() must be a string; %s given.', 286 | self::class, 287 | __METHOD__, 288 | gettype($path) 289 | ) 290 | ); 291 | } 292 | $this->paths[] = ['type' => $type, 'path' => $path]; 293 | return $this; 294 | } 295 | 296 | /** 297 | * @param string $key 298 | * @param array|Traversable $config 299 | * @throws Exception\InvalidArgumentException 300 | * @return ConfigListener 301 | */ 302 | protected function addConfig($key, $config) 303 | { 304 | if ($config instanceof Traversable) { 305 | $config = ArrayUtils::iteratorToArray($config); 306 | } 307 | 308 | if (! is_array($config)) { 309 | throw new Exception\InvalidArgumentException( 310 | sprintf( 311 | 'Config being merged must be an array, ' 312 | . 'implement the Traversable interface, or be an ' 313 | . 'instance of Laminas\Config\Config. %s given.', 314 | gettype($config) 315 | ) 316 | ); 317 | } 318 | 319 | $this->configs[$key] = $config; 320 | 321 | return $this; 322 | } 323 | 324 | /** 325 | * Given a path (glob or static), fetch the config and add it to the array 326 | * of configs to merge. 327 | * 328 | * @param string $path 329 | * @param string $type 330 | * @return ConfigListener 331 | */ 332 | protected function addConfigByPath($path, $type) 333 | { 334 | switch ($type) { 335 | case self::STATIC_PATH: 336 | $this->addConfig($path, ConfigFactory::fromFile($path)); 337 | break; 338 | 339 | case self::GLOB_PATH: 340 | // We want to keep track of where each value came from so we don't 341 | // use ConfigFactory::fromFiles() since it does merging internally. 342 | foreach (Glob::glob($path, Glob::GLOB_BRACE, true) as $file) { 343 | $this->addConfig($file, ConfigFactory::fromFile($file)); 344 | } 345 | break; 346 | } 347 | 348 | return $this; 349 | } 350 | 351 | /** @return bool */ 352 | protected function hasCachedConfig() 353 | { 354 | if ( 355 | ($this->getOptions()->getConfigCacheEnabled()) 356 | && (file_exists($this->getOptions()->getConfigCacheFile())) 357 | ) { 358 | return true; 359 | } 360 | return false; 361 | } 362 | 363 | /** @return mixed */ 364 | protected function getCachedConfig() 365 | { 366 | return include $this->getOptions()->getConfigCacheFile(); 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/Listener/ConfigMergerInterface.php: -------------------------------------------------------------------------------- 1 | getOptions(); 31 | $configListener = $this->getConfigListener(); 32 | $locatorRegistrationListener = new LocatorRegistrationListener($options); 33 | 34 | // High priority, we assume module autoloading (for FooNamespace\Module 35 | // classes) should be available before anything else. 36 | // Register it only if use_laminas_loader config is true, however. 37 | if ($options->useLaminasLoader()) { 38 | $moduleLoaderListener = new ModuleLoaderListener($options); 39 | $moduleLoaderListener->attach($events); 40 | $this->listeners[] = $moduleLoaderListener; 41 | } 42 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE_RESOLVE, new ModuleResolverListener()); 43 | 44 | if ($options->useLaminasLoader()) { 45 | // High priority, because most other loadModule listeners will assume 46 | // the module's classes are available via autoloading 47 | // Register it only if use_laminas_loader config is true, however. 48 | $this->listeners[] = $events->attach( 49 | ModuleEvent::EVENT_LOAD_MODULE, 50 | new AutoloaderListener($options), 51 | 9000 52 | ); 53 | } 54 | 55 | if ($options->getCheckDependencies()) { 56 | $this->listeners[] = $events->attach( 57 | ModuleEvent::EVENT_LOAD_MODULE, 58 | new ModuleDependencyCheckerListener(), 59 | 8000 60 | ); 61 | } 62 | 63 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE, new InitTrigger($options)); 64 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE, new OnBootstrapListener($options)); 65 | 66 | $locatorRegistrationListener->attach($events); 67 | $configListener->attach($events); 68 | $this->listeners[] = $locatorRegistrationListener; 69 | $this->listeners[] = $configListener; 70 | return $this; 71 | } 72 | 73 | /** 74 | * Detach all previously attached listeners 75 | * 76 | * @return void 77 | */ 78 | #[Override] 79 | public function detach(EventManagerInterface $events) 80 | { 81 | foreach ($this->listeners as $key => $listener) { 82 | if ($listener instanceof ListenerAggregateInterface) { 83 | $listener->detach($events); 84 | unset($this->listeners[$key]); 85 | continue; 86 | } 87 | 88 | $events->detach($listener); 89 | unset($this->listeners[$key]); 90 | } 91 | } 92 | 93 | /** 94 | * Get the config merger. 95 | * 96 | * @return ConfigMergerInterface 97 | */ 98 | public function getConfigListener() 99 | { 100 | if (! $this->configListener instanceof ConfigMergerInterface) { 101 | $this->setConfigListener(new ConfigListener($this->getOptions())); 102 | } 103 | return $this->configListener; 104 | } 105 | 106 | /** 107 | * Set the config merger to use. 108 | * 109 | * @return DefaultListenerAggregate 110 | */ 111 | public function setConfigListener(ConfigMergerInterface $configListener) 112 | { 113 | $this->configListener = $configListener; 114 | return $this; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Listener/Exception/ConfigCannotBeCachedException.php: -------------------------------------------------------------------------------- 1 | getMessage() 27 | ), 28 | $exportException->getCode(), 29 | $exportException 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Listener/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | getModule(); 18 | if ( 19 | ! $module instanceof InitProviderInterface 20 | && ! method_exists($module, 'init') 21 | ) { 22 | return; 23 | } 24 | 25 | $module->init($e->getTarget()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Listener/ListenerOptions.php: -------------------------------------------------------------------------------- 1 | modulePaths; 59 | } 60 | 61 | /** 62 | * Set an array of paths where modules reside 63 | * 64 | * @param array|Traversable $modulePaths 65 | * @throws Exception\InvalidArgumentException 66 | * @return ListenerOptions Provides fluent interface 67 | */ 68 | public function setModulePaths($modulePaths) 69 | { 70 | if (! is_array($modulePaths) && ! $modulePaths instanceof Traversable) { 71 | throw new Exception\InvalidArgumentException( 72 | sprintf( 73 | 'Argument passed to %s::%s() must be an array, ' 74 | . 'implement the Traversable interface, or be an ' 75 | . 'instance of Laminas\Config\Config. %s given.', 76 | self::class, 77 | __METHOD__, 78 | gettype($modulePaths) 79 | ) 80 | ); 81 | } 82 | 83 | $this->modulePaths = $modulePaths; 84 | return $this; 85 | } 86 | 87 | /** 88 | * Get the glob patterns to load additional config files 89 | * 90 | * @return array 91 | */ 92 | public function getConfigGlobPaths() 93 | { 94 | return $this->configGlobPaths; 95 | } 96 | 97 | /** 98 | * Get the static paths to load additional config files 99 | * 100 | * @return array 101 | */ 102 | public function getConfigStaticPaths() 103 | { 104 | return $this->configStaticPaths; 105 | } 106 | 107 | /** 108 | * Set the glob patterns to use for loading additional config files 109 | * 110 | * @param array|Traversable $configGlobPaths 111 | * @throws Exception\InvalidArgumentException 112 | * @return ListenerOptions Provides fluent interface 113 | */ 114 | public function setConfigGlobPaths($configGlobPaths) 115 | { 116 | if (! is_array($configGlobPaths) && ! $configGlobPaths instanceof Traversable) { 117 | throw new Exception\InvalidArgumentException( 118 | sprintf( 119 | 'Argument passed to %s::%s() must be an array, ' 120 | . 'implement the Traversable interface, or be an ' 121 | . 'instance of Laminas\Config\Config. %s given.', 122 | self::class, 123 | __METHOD__, 124 | gettype($configGlobPaths) 125 | ) 126 | ); 127 | } 128 | 129 | $this->configGlobPaths = $configGlobPaths; 130 | return $this; 131 | } 132 | 133 | /** 134 | * Set the static paths to use for loading additional config files 135 | * 136 | * @param array|Traversable $configStaticPaths 137 | * @throws Exception\InvalidArgumentException 138 | * @return ListenerOptions Provides fluent interface 139 | */ 140 | public function setConfigStaticPaths($configStaticPaths) 141 | { 142 | if (! is_array($configStaticPaths) && ! $configStaticPaths instanceof Traversable) { 143 | throw new Exception\InvalidArgumentException( 144 | sprintf( 145 | 'Argument passed to %s::%s() must be an array, ' 146 | . 'implement the Traversable interface, or be an ' 147 | . 'instance of Laminas\Config\Config. %s given.', 148 | self::class, 149 | __METHOD__, 150 | gettype($configStaticPaths) 151 | ) 152 | ); 153 | } 154 | 155 | $this->configStaticPaths = $configStaticPaths; 156 | return $this; 157 | } 158 | 159 | /** 160 | * Get any extra config to merge in. 161 | * 162 | * @return array|Traversable 163 | */ 164 | public function getExtraConfig() 165 | { 166 | return $this->extraConfig; 167 | } 168 | 169 | /** 170 | * Add some extra config array to the main config. This is mainly useful 171 | * for unit testing purposes. 172 | * 173 | * @param array|Traversable $extraConfig 174 | * @throws Exception\InvalidArgumentException 175 | * @return ListenerOptions Provides fluent interface 176 | */ 177 | public function setExtraConfig($extraConfig) 178 | { 179 | if (! is_array($extraConfig) && ! $extraConfig instanceof Traversable) { 180 | throw new Exception\InvalidArgumentException( 181 | sprintf( 182 | 'Argument passed to %s::%s() must be an array, ' 183 | . 'implement the Traversable interface, or be an ' 184 | . 'instance of Laminas\Config\Config. %s given.', 185 | self::class, 186 | __METHOD__, 187 | gettype($extraConfig) 188 | ) 189 | ); 190 | } 191 | 192 | $this->extraConfig = $extraConfig; 193 | return $this; 194 | } 195 | 196 | /** 197 | * Check if the config cache is enabled 198 | * 199 | * @return bool 200 | */ 201 | public function getConfigCacheEnabled() 202 | { 203 | return $this->configCacheEnabled; 204 | } 205 | 206 | /** 207 | * Set if the config cache should be enabled or not 208 | * 209 | * @param bool $enabled 210 | * @return ListenerOptions 211 | */ 212 | public function setConfigCacheEnabled($enabled) 213 | { 214 | $this->configCacheEnabled = (bool) $enabled; 215 | return $this; 216 | } 217 | 218 | /** 219 | * Get key used to create the cache file name 220 | * 221 | * @return string 222 | */ 223 | public function getConfigCacheKey() 224 | { 225 | return (string) $this->configCacheKey; 226 | } 227 | 228 | /** 229 | * Set key used to create the cache file name 230 | * 231 | * @param string $configCacheKey the value to be set 232 | * @return ListenerOptions 233 | */ 234 | public function setConfigCacheKey($configCacheKey) 235 | { 236 | $this->configCacheKey = $configCacheKey; 237 | return $this; 238 | } 239 | 240 | /** 241 | * Get the path to the config cache 242 | * 243 | * Should this be an option, or should the dir option include the 244 | * filename, or should it simply remain hard-coded? Thoughts? 245 | * 246 | * @return string 247 | */ 248 | public function getConfigCacheFile() 249 | { 250 | if ($this->getConfigCacheKey()) { 251 | return $this->getCacheDir() . '/module-config-cache.' . $this->getConfigCacheKey() . '.php'; 252 | } 253 | 254 | return $this->getCacheDir() . '/module-config-cache.php'; 255 | } 256 | 257 | /** 258 | * Get the path where cache file(s) are stored 259 | * 260 | * @return string|null 261 | */ 262 | public function getCacheDir() 263 | { 264 | return $this->cacheDir; 265 | } 266 | 267 | /** 268 | * Set the path where cache files can be stored 269 | * 270 | * @param string|null $cacheDir the value to be set 271 | * @return ListenerOptions 272 | */ 273 | public function setCacheDir($cacheDir) 274 | { 275 | $this->cacheDir = $cacheDir ? static::normalizePath($cacheDir) : null; 276 | 277 | return $this; 278 | } 279 | 280 | /** 281 | * Check if the module class map cache is enabled 282 | * 283 | * @return bool 284 | */ 285 | public function getModuleMapCacheEnabled() 286 | { 287 | return $this->moduleMapCacheEnabled; 288 | } 289 | 290 | /** 291 | * Set if the module class map cache should be enabled or not 292 | * 293 | * @param bool $enabled 294 | * @return ListenerOptions 295 | */ 296 | public function setModuleMapCacheEnabled($enabled) 297 | { 298 | $this->moduleMapCacheEnabled = (bool) $enabled; 299 | return $this; 300 | } 301 | 302 | /** 303 | * Get key used to create the cache file name 304 | * 305 | * @return string 306 | */ 307 | public function getModuleMapCacheKey() 308 | { 309 | return (string) $this->moduleMapCacheKey; 310 | } 311 | 312 | /** 313 | * Set key used to create the cache file name 314 | * 315 | * @param string $moduleMapCacheKey the value to be set 316 | * @return ListenerOptions 317 | */ 318 | public function setModuleMapCacheKey($moduleMapCacheKey) 319 | { 320 | $this->moduleMapCacheKey = $moduleMapCacheKey; 321 | return $this; 322 | } 323 | 324 | /** 325 | * Get the path to the module class map cache 326 | * 327 | * @return string 328 | */ 329 | public function getModuleMapCacheFile() 330 | { 331 | if ($this->getModuleMapCacheKey()) { 332 | return $this->getCacheDir() . '/module-classmap-cache.' . $this->getModuleMapCacheKey() . '.php'; 333 | } 334 | 335 | return $this->getCacheDir() . '/module-classmap-cache.php'; 336 | } 337 | 338 | /** 339 | * Set whether to check dependencies during module loading or not 340 | * 341 | * @return bool 342 | */ 343 | public function getCheckDependencies() 344 | { 345 | return $this->checkDependencies; 346 | } 347 | 348 | /** 349 | * Set whether to check dependencies during module loading or not 350 | * 351 | * @param bool $checkDependencies the value to be set 352 | * @return ListenerOptions 353 | */ 354 | public function setCheckDependencies($checkDependencies) 355 | { 356 | $this->checkDependencies = (bool) $checkDependencies; 357 | 358 | return $this; 359 | } 360 | 361 | /** 362 | * Whether or not to use laminas-loader to autoload modules. 363 | * 364 | * @return bool 365 | */ 366 | public function useLaminasLoader() 367 | { 368 | return $this->useLaminasLoader; 369 | } 370 | 371 | /** 372 | * Set a flag indicating if the module manager should use laminas-loader 373 | * 374 | * Setting this option to false will disable ModuleAutoloader, requiring 375 | * other means of autoloading to be used (e.g., Composer). 376 | * 377 | * If disabled, the AutoloaderProvider feature will be disabled as well 378 | * 379 | * @param bool $flag 380 | * @return ListenerOptions 381 | */ 382 | public function setUseLaminasLoader($flag) 383 | { 384 | $this->useLaminasLoader = (bool) $flag; 385 | return $this; 386 | } 387 | 388 | /** 389 | * Normalize a path for insertion in the stack 390 | * 391 | * @param string $path 392 | * @return string 393 | */ 394 | public static function normalizePath($path) 395 | { 396 | $path = rtrim($path, '/'); 397 | $path = rtrim($path, '\\'); 398 | return $path; 399 | } 400 | 401 | /** @deprecated Use self::useLaminasLoader instead */ 402 | public function useZendLoader(): bool 403 | { 404 | return $this->useLaminasLoader(...func_get_args()); 405 | } 406 | 407 | /** @deprecated Use self::setUseLaminasLoader instead */ 408 | public function setUseZendLoader(bool $flag): ListenerOptions 409 | { 410 | return $this->setUseLaminasLoader(...func_get_args()); 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /src/Listener/LocatorRegistrationListener.php: -------------------------------------------------------------------------------- 1 | getModule() instanceof LocatorRegisteredInterface) { 38 | return; 39 | } 40 | $this->modules[] = $e->getModule(); 41 | } 42 | 43 | /** 44 | * Once all the modules are loaded, loop 45 | * 46 | * @return void 47 | */ 48 | public function onLoadModules(ModuleEvent $e) 49 | { 50 | $moduleManager = $e->getTarget(); 51 | $events = $moduleManager->getEventManager()->getSharedManager(); 52 | 53 | if (! $events) { 54 | return; 55 | } 56 | 57 | // Shared instance for module manager 58 | $events->attach( 59 | Application::class, 60 | ModuleManager::EVENT_BOOTSTRAP, 61 | static function (MvcEvent $e) use ($moduleManager): void { 62 | $moduleClassName = $moduleManager::class; 63 | $moduleClassNameArray = explode('\\', $moduleClassName); 64 | $moduleClassNameAlias = end($moduleClassNameArray); 65 | $application = $e->getApplication(); 66 | /** @var ServiceManager $services */ 67 | $services = $application->getServiceManager(); 68 | if (! $services->has($moduleClassName)) { 69 | $services->setAlias($moduleClassName, $moduleClassNameAlias); 70 | } 71 | }, 72 | 1000 73 | ); 74 | 75 | if (! $this->modules) { 76 | return; 77 | } 78 | 79 | // Attach to the bootstrap event if there are modules we need to process 80 | $events->attach(Application::class, ModuleManager::EVENT_BOOTSTRAP, [$this, 'onBootstrap'], 1000); 81 | } 82 | 83 | /** 84 | * This is ran during the MVC bootstrap event because it requires access to 85 | * the DI container. 86 | * 87 | * @TODO: Check the application / locator / etc a bit better to make sure 88 | * the env looks how we're expecting it to? 89 | * @return void 90 | */ 91 | public function onBootstrap(MvcEvent $e) 92 | { 93 | $application = $e->getApplication(); 94 | /** @var ServiceManager $services */ 95 | $services = $application->getServiceManager(); 96 | 97 | foreach ($this->modules as $module) { 98 | $moduleClassName = $module::class; 99 | if (! $services->has($moduleClassName)) { 100 | $services->setService($moduleClassName, $module); 101 | } 102 | } 103 | } 104 | 105 | /** {@inheritDoc} */ 106 | #[Override] 107 | public function attach(EventManagerInterface $events, $priority = 1) 108 | { 109 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE, [$this, 'onLoadModule']); 110 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULES, [$this, 'onLoadModules'], -1000); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Listener/ModuleDependencyCheckerListener.php: -------------------------------------------------------------------------------- 1 | getModule(); 23 | 24 | if ($module instanceof DependencyIndicatorInterface || method_exists($module, 'getModuleDependencies')) { 25 | $dependencies = $module->getModuleDependencies(); 26 | 27 | foreach ($dependencies as $dependencyModule) { 28 | if (! isset($this->loaded[$dependencyModule])) { 29 | throw new Exception\MissingDependencyModuleException( 30 | sprintf( 31 | 'Module "%s" depends on module "%s", which was not initialized before it', 32 | $e->getModuleName(), 33 | $dependencyModule 34 | ) 35 | ); 36 | } 37 | } 38 | } 39 | 40 | $this->loaded[$e->getModuleName()] = true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Listener/ModuleLoaderListener.php: -------------------------------------------------------------------------------- 1 | generateCache = $this->options->getModuleMapCacheEnabled(); 35 | $this->moduleLoader = new ModuleAutoloader($this->options->getModulePaths()); 36 | 37 | if ($this->hasCachedClassMap()) { 38 | $this->generateCache = false; 39 | $this->moduleLoader->setModuleClassMap($this->getCachedConfig()); 40 | } 41 | } 42 | 43 | /** {@inheritDoc} */ 44 | #[Override] 45 | public function attach(EventManagerInterface $events, $priority = 1) 46 | { 47 | $this->callbacks[] = $events->attach( 48 | ModuleEvent::EVENT_LOAD_MODULES, 49 | [$this->moduleLoader, 'register'], 50 | 9000 51 | ); 52 | 53 | if ($this->generateCache) { 54 | $this->callbacks[] = $events->attach( 55 | ModuleEvent::EVENT_LOAD_MODULES_POST, 56 | [$this, 'onLoadModulesPost'] 57 | ); 58 | } 59 | } 60 | 61 | /** {@inheritDoc} */ 62 | #[Override] 63 | public function detach(EventManagerInterface $events) 64 | { 65 | foreach ($this->callbacks as $index => $callback) { 66 | if ($events->detach($callback)) { 67 | unset($this->callbacks[$index]); 68 | } 69 | } 70 | } 71 | 72 | /** @return bool */ 73 | protected function hasCachedClassMap() 74 | { 75 | if ( 76 | $this->options->getModuleMapCacheEnabled() 77 | && file_exists($this->options->getModuleMapCacheFile()) 78 | ) { 79 | return true; 80 | } 81 | 82 | return false; 83 | } 84 | 85 | /** @return array */ 86 | protected function getCachedConfig() 87 | { 88 | return include $this->options->getModuleMapCacheFile(); 89 | } 90 | 91 | /** 92 | * Unregisters the ModuleLoader and generates the module class map cache. 93 | */ 94 | public function onLoadModulesPost(ModuleEvent $event) 95 | { 96 | $this->moduleLoader->unregister(); 97 | $this->writeArrayToFile( 98 | $this->options->getModuleMapCacheFile(), 99 | $this->moduleLoader->getModuleClassMap() 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Listener/ModuleResolverListener.php: -------------------------------------------------------------------------------- 1 | getModuleName(); 31 | 32 | $class = sprintf('%s\Module', $moduleName); 33 | if (class_exists($class)) { 34 | return new $class(); 35 | } 36 | 37 | if ( 38 | class_exists($moduleName) 39 | && ! in_array($moduleName, $this->invalidClassNames, true) 40 | ) { 41 | return new $moduleName(); 42 | } 43 | 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Listener/OnBootstrapListener.php: -------------------------------------------------------------------------------- 1 | getModule(); 20 | if ( 21 | ! $module instanceof BootstrapListenerInterface 22 | && ! method_exists($module, 'onBootstrap') 23 | ) { 24 | return; 25 | } 26 | 27 | $moduleManager = $e->getTarget(); 28 | $events = $moduleManager->getEventManager(); 29 | $sharedEvents = $events->getSharedManager(); 30 | $sharedEvents->attach(Application::class, ModuleManager::EVENT_BOOTSTRAP, [$module, 'onBootstrap']); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Listener/ServiceListener.php: -------------------------------------------------------------------------------- 1 | setDefaultServiceConfig($configuration); 58 | } 59 | } 60 | 61 | /** 62 | * @param array $configuration 63 | * @return ServiceListener 64 | */ 65 | #[Override] 66 | public function setDefaultServiceConfig($configuration) 67 | { 68 | $this->defaultServiceConfig = $configuration; 69 | return $this; 70 | } 71 | 72 | /** {@inheritDoc} */ 73 | #[Override] 74 | public function addServiceManager($serviceManager, $key, $moduleInterface, $method) 75 | { 76 | if (is_string($serviceManager)) { 77 | $smKey = $serviceManager; 78 | } elseif ($serviceManager instanceof ServiceManager) { 79 | $smKey = spl_object_hash($serviceManager); 80 | } else { 81 | throw new Exception\RuntimeException(sprintf( 82 | 'Invalid service manager provided, expected ServiceManager or string, %s provided', 83 | is_object($serviceManager) ? $serviceManager::class : gettype($serviceManager) 84 | )); 85 | } 86 | 87 | $this->serviceManagers[$smKey] = [ 88 | 'service_manager' => $serviceManager, 89 | 'config_key' => $key, 90 | 'module_class_interface' => $moduleInterface, 91 | 'module_class_method' => $method, 92 | 'configuration' => [], 93 | ]; 94 | 95 | if ($key === 'service_manager' && $this->defaultServiceConfig) { 96 | $this->serviceManagers[$smKey]['configuration']['default_config'] = $this->defaultServiceConfig; 97 | } 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * @param int $priority 104 | * @return ServiceListener 105 | */ 106 | #[Override] 107 | public function attach(EventManagerInterface $events, $priority = 1) 108 | { 109 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULE, [$this, 'onLoadModule']); 110 | $this->listeners[] = $events->attach(ModuleEvent::EVENT_LOAD_MODULES_POST, [$this, 'onLoadModulesPost']); 111 | return $this; 112 | } 113 | 114 | /** @return void */ 115 | #[Override] 116 | public function detach(EventManagerInterface $events) 117 | { 118 | foreach ($this->listeners as $key => $listener) { 119 | if ($events->detach($listener)) { 120 | unset($this->listeners[$key]); 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Retrieve service manager configuration from module, and 127 | * configure the service manager. 128 | * 129 | * If the module does not implement a specific interface and does not 130 | * implement a specific method, does nothing. Also, if the return value 131 | * of that method is not a ServiceConfig object, or not an array or 132 | * Traversable that can seed one, does nothing. 133 | * 134 | * The interface and method name can be set by adding a new service manager 135 | * via the addServiceManager() method. 136 | * 137 | * @return void 138 | */ 139 | public function onLoadModule(ModuleEvent $e) 140 | { 141 | $module = $e->getModule(); 142 | 143 | foreach ($this->serviceManagers as $key => $sm) { 144 | if ( 145 | ! $module instanceof $sm['module_class_interface'] 146 | && ! method_exists($module, $sm['module_class_method']) 147 | ) { 148 | continue; 149 | } 150 | 151 | $config = $module->{$sm['module_class_method']}(); 152 | 153 | if ($config instanceof ServiceConfigInterface) { 154 | $config = $this->serviceConfigToArray($config); 155 | } 156 | 157 | if ($config instanceof Traversable) { 158 | $config = ArrayUtils::iteratorToArray($config); 159 | } 160 | 161 | if (! is_array($config)) { 162 | // If we do not have an array by this point, nothing left to do. 163 | continue; 164 | } 165 | 166 | // We are keeping track of which modules provided which configuration to which service managers. 167 | // The actual merging takes place later. Doing it this way will enable us to provide more powerful 168 | // debugging tools for showing which modules overrode what. 169 | $fullname = $e->getModuleName() . '::' . $sm['module_class_method'] . '()'; /** @codingStandardsIgnoreLine */ 170 | $this->serviceManagers[$key]['configuration'][$fullname] = $config; 171 | } 172 | } 173 | 174 | /** 175 | * Use merged configuration to configure service manager 176 | * 177 | * If the merged configuration has a non-empty, array 'service_manager' 178 | * key, it will be passed to a ServiceManager Config object, and 179 | * used to configure the service manager. 180 | * 181 | * @throws Exception\RuntimeException 182 | * @return void 183 | */ 184 | public function onLoadModulesPost(ModuleEvent $e) 185 | { 186 | $configListener = $e->getConfigListener(); 187 | $config = $configListener->getMergedConfig(false); 188 | 189 | foreach ($this->serviceManagers as $key => $sm) { 190 | $smConfig = $this->mergeServiceConfiguration($key, $sm, $config); 191 | 192 | if (! $sm['service_manager'] instanceof ServiceManager) { 193 | if (! $this->defaultServiceManager->has($sm['service_manager'])) { 194 | // No plugin manager registered by that name; nothing to configure. 195 | continue; 196 | } 197 | 198 | $instance = $this->defaultServiceManager->get($sm['service_manager']); 199 | if (! $instance instanceof ServiceManager) { 200 | throw new Exception\RuntimeException(sprintf( 201 | 'Could not find a valid ServiceManager for %s', 202 | $sm['service_manager'] 203 | )); 204 | } 205 | 206 | $sm['service_manager'] = $instance; 207 | } 208 | 209 | $serviceConfig = new ServiceConfig($smConfig); 210 | 211 | // The service listener is meant to operate during bootstrap, and, as such, 212 | // needs to be able to override existing configuration. 213 | $allowOverride = $sm['service_manager']->getAllowOverride(); 214 | $sm['service_manager']->setAllowOverride(true); 215 | 216 | $serviceConfig->configureServiceManager($sm['service_manager']); 217 | 218 | $sm['service_manager']->setAllowOverride($allowOverride); 219 | } 220 | } 221 | 222 | /** 223 | * Merge a service configuration container 224 | * 225 | * Extracts the various service configuration arrays. 226 | * 227 | * @param ServiceConfigInterface|string $config ServiceConfigInterface or 228 | * class name resolving to one. 229 | * @return array 230 | * @throws Exception\RuntimeException If resolved class name is not a 231 | * ServiceConfigInterface implementation. 232 | * @throws Exception\RuntimeException Under laminas-servicemanager v2 if the 233 | * configuration instance is not specifically a ServiceConfig, as there 234 | * is no way to extract service configuration in that case. 235 | */ 236 | protected function serviceConfigToArray($config) 237 | { 238 | if (is_string($config) && class_exists($config)) { 239 | $class = $config; 240 | $config = new $class(); 241 | } 242 | 243 | if (! $config instanceof ServiceConfigInterface) { 244 | throw new Exception\RuntimeException(sprintf( 245 | 'Invalid service manager configuration class provided; received "%s", expected an instance of %s', 246 | is_object($config) ? $config::class : (is_scalar($config) ? $config : gettype($config)), 247 | ServiceConfigInterface::class 248 | )); 249 | } 250 | 251 | if (method_exists($config, 'toArray')) { 252 | // laminas-servicemanager v3 interface 253 | return $config->toArray(); 254 | } 255 | 256 | // For laminas-servicemanager v2, we need a Laminas\ServiceManager\Config 257 | // instance specifically. 258 | if (! $config instanceof ServiceConfig) { 259 | throw new Exception\RuntimeException(sprintf( 260 | 'Invalid service manager configuration class provided; received "%s", expected an instance of %s', 261 | is_object($config) ? $config::class : (is_scalar($config) ? $config : gettype($config)), 262 | ServiceConfig::class 263 | )); 264 | } 265 | 266 | // Pull service configuration from discrete methods. 267 | return [ 268 | 'abstract_factories' => $config->getAbstractFactories(), 269 | 'aliases' => $config->getAliases(), 270 | 'delegators' => $config->getDelegators(), 271 | 'factories' => $config->getFactories(), 272 | 'initializers' => $config->getInitializers(), 273 | 'invokables' => $config->getInvokables(), 274 | 'services' => $config->getServices(), 275 | 'shared' => $config->getShared(), 276 | ]; 277 | } 278 | 279 | /** 280 | * Merge all configuration for a given service manager to a single array. 281 | * 282 | * @param string $key Named service manager 283 | * @param array $metadata Service manager metadata 284 | * @param array $config Merged configuration 285 | * @return array Service manager-specific configuration 286 | */ 287 | private function mergeServiceConfiguration($key, array $metadata, array $config) 288 | { 289 | if ( 290 | isset($config[$metadata['config_key']]) 291 | && is_array($config[$metadata['config_key']]) 292 | && ! empty($config[$metadata['config_key']]) 293 | ) { 294 | $this->serviceManagers[$key]['configuration']['merged_config'] = $config[$metadata['config_key']]; 295 | } 296 | 297 | // Merge all of the things! 298 | $serviceConfig = []; 299 | foreach ($this->serviceManagers[$key]['configuration'] as $name => $configs) { 300 | if (isset($configs['configuration_classes'])) { 301 | foreach ($configs['configuration_classes'] as $class) { 302 | $configs = ArrayUtils::merge($configs, $this->serviceConfigToArray($class)); 303 | } 304 | } 305 | $serviceConfig = ArrayUtils::merge($serviceConfig, $configs); 306 | } 307 | 308 | return $serviceConfig; 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/Listener/ServiceListenerInterface.php: -------------------------------------------------------------------------------- 1 | moduleName; 46 | } 47 | 48 | /** 49 | * Set the name of a given module 50 | * 51 | * @param string $moduleName 52 | * @throws Exception\InvalidArgumentException 53 | * @return ModuleEvent 54 | */ 55 | public function setModuleName($moduleName) 56 | { 57 | if (! is_string($moduleName)) { 58 | throw new Exception\InvalidArgumentException( 59 | sprintf( 60 | '%s expects a string as an argument; %s provided', 61 | __METHOD__, 62 | gettype($moduleName) 63 | ) 64 | ); 65 | } 66 | // Performance tweak, don't add it as param. 67 | $this->moduleName = $moduleName; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Get module object 74 | * 75 | * @return null|object 76 | */ 77 | public function getModule() 78 | { 79 | return $this->module; 80 | } 81 | 82 | /** 83 | * Set module object to compose in this event 84 | * 85 | * @param object $module 86 | * @throws Exception\InvalidArgumentException 87 | * @return ModuleEvent 88 | */ 89 | public function setModule($module) 90 | { 91 | if (! is_object($module)) { 92 | throw new Exception\InvalidArgumentException( 93 | sprintf( 94 | '%s expects a module object as an argument; %s provided', 95 | __METHOD__, 96 | gettype($module) 97 | ) 98 | ); 99 | } 100 | // Performance tweak, don't add it as param. 101 | $this->module = $module; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Get the config listener 108 | * 109 | * @return null|Listener\ConfigMergerInterface 110 | */ 111 | public function getConfigListener() 112 | { 113 | return $this->configListener; 114 | } 115 | 116 | /** 117 | * Set module object to compose in this event 118 | * 119 | * @return ModuleEvent 120 | */ 121 | public function setConfigListener(Listener\ConfigMergerInterface $configListener) 122 | { 123 | $this->setParam('configListener', $configListener); 124 | $this->configListener = $configListener; 125 | 126 | return $this; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/ModuleManager.php: -------------------------------------------------------------------------------- 1 | setModules($modules); 50 | if ($eventManager instanceof EventManagerInterface) { 51 | $this->setEventManager($eventManager); 52 | } 53 | } 54 | 55 | /** 56 | * Handle the loadModules event 57 | * 58 | * @return void 59 | */ 60 | public function onLoadModules() 61 | { 62 | if (true === $this->modulesAreLoaded) { 63 | return; 64 | } 65 | 66 | foreach ($this->getModules() as $moduleName => $module) { 67 | if (is_object($module)) { 68 | if (! is_string($moduleName)) { 69 | throw new Exception\RuntimeException(sprintf( 70 | 'Module (%s) must have a key identifier.', 71 | $module::class 72 | )); 73 | } 74 | $module = [$moduleName => $module]; 75 | } 76 | 77 | $this->loadModule($module); 78 | } 79 | 80 | $this->modulesAreLoaded = true; 81 | } 82 | 83 | /** 84 | * Load the provided modules. 85 | * 86 | * @triggers loadModules 87 | * @triggers loadModules.post 88 | * @return ModuleManager 89 | */ 90 | #[Override] 91 | public function loadModules() 92 | { 93 | if (true === $this->modulesAreLoaded) { 94 | return $this; 95 | } 96 | 97 | $events = $this->getEventManager(); 98 | $event = $this->getEvent(); 99 | $event->setName(ModuleEvent::EVENT_LOAD_MODULES); 100 | 101 | $events->triggerEvent($event); 102 | 103 | /** 104 | * Having a dedicated .post event abstracts the complexity of priorities from the user. 105 | * Users can attach to the .post event and be sure that important 106 | * things like config merging are complete without having to worry if 107 | * they set a low enough priority. 108 | */ 109 | $event->setName(ModuleEvent::EVENT_LOAD_MODULES_POST); 110 | $events->triggerEvent($event); 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Load a specific module by name. 117 | * 118 | * @param string|array $module 119 | * @throws Exception\RuntimeException 120 | * @triggers loadModule.resolve 121 | * @triggers loadModule 122 | * @return mixed Module's Module class 123 | */ 124 | #[Override] 125 | public function loadModule($module) 126 | { 127 | $moduleName = $module; 128 | if (is_array($module)) { 129 | $moduleName = key($module); 130 | $module = current($module); 131 | } 132 | 133 | if (isset($this->loadedModules[$moduleName])) { 134 | return $this->loadedModules[$moduleName]; 135 | } 136 | 137 | /* 138 | * Keep track of nested module loading using the $loadFinished 139 | * property. 140 | * 141 | * Increment the value for each loadModule() call and then decrement 142 | * once the loading process is complete. 143 | * 144 | * To load a module, we clone the event if we are inside a nested 145 | * loadModule() call, and use the original event otherwise. 146 | */ 147 | if (! isset($this->loadFinished)) { 148 | $this->loadFinished = 0; 149 | } 150 | 151 | $event = $this->loadFinished > 0 ? clone $this->getEvent() : $this->getEvent(); 152 | $event->setModuleName($moduleName); 153 | 154 | $this->loadFinished++; 155 | 156 | if (! is_object($module)) { 157 | $module = $this->loadModuleByName($event); 158 | } 159 | $event->setModule($module); 160 | $event->setName(ModuleEvent::EVENT_LOAD_MODULE); 161 | 162 | $this->loadedModules[$moduleName] = $module; 163 | $this->getEventManager()->triggerEvent($event); 164 | 165 | $this->loadFinished--; 166 | 167 | return $module; 168 | } 169 | 170 | /** 171 | * Load a module with the name 172 | * 173 | * @return mixed module instance 174 | * @throws Exception\RuntimeException 175 | */ 176 | protected function loadModuleByName(ModuleEvent $event) 177 | { 178 | $event->setName(ModuleEvent::EVENT_LOAD_MODULE_RESOLVE); 179 | $result = $this->getEventManager()->triggerEventUntil(static fn($r): bool => is_object($r), $event); 180 | 181 | $module = $result->last(); 182 | if (! is_object($module)) { 183 | throw new Exception\RuntimeException(sprintf( 184 | 'Module (%s) could not be initialized.', 185 | $event->getModuleName() 186 | )); 187 | } 188 | 189 | return $module; 190 | } 191 | 192 | /** 193 | * Get an array of the loaded modules. 194 | * 195 | * @param bool $loadModules If true, load modules if they're not already 196 | * @return array An array of Module objects, keyed by module name 197 | */ 198 | #[Override] 199 | public function getLoadedModules($loadModules = false) 200 | { 201 | if (true === $loadModules) { 202 | $this->loadModules(); 203 | } 204 | 205 | return $this->loadedModules; 206 | } 207 | 208 | /** 209 | * Get an instance of a module class by the module name 210 | * 211 | * @param string $moduleName 212 | * @return mixed 213 | */ 214 | public function getModule($moduleName) 215 | { 216 | if (! isset($this->loadedModules[$moduleName])) { 217 | return; 218 | } 219 | return $this->loadedModules[$moduleName]; 220 | } 221 | 222 | /** 223 | * Get the array of module names that this manager should load. 224 | * 225 | * @return array 226 | */ 227 | #[Override] 228 | public function getModules() 229 | { 230 | return $this->modules; 231 | } 232 | 233 | /** 234 | * Set an array or Traversable of module names that this module manager should load. 235 | * 236 | * @param mixed $modules array or Traversable of module names 237 | * @throws Exception\InvalidArgumentException 238 | * @return ModuleManager 239 | */ 240 | #[Override] 241 | public function setModules($modules) 242 | { 243 | if (is_array($modules) || $modules instanceof Traversable) { 244 | $this->modules = $modules; 245 | } else { 246 | throw new Exception\InvalidArgumentException( 247 | sprintf( 248 | 'Parameter to %s\'s %s method must be an array or implement the Traversable interface', 249 | self::class, 250 | __METHOD__ 251 | ) 252 | ); 253 | } 254 | return $this; 255 | } 256 | 257 | /** 258 | * Get the module event 259 | * 260 | * @return ModuleEvent 261 | */ 262 | public function getEvent() 263 | { 264 | if (! $this->event instanceof ModuleEvent) { 265 | $this->setEvent(new ModuleEvent()); 266 | } 267 | return $this->event; 268 | } 269 | 270 | /** 271 | * Set the module event 272 | * 273 | * @return ModuleManager 274 | */ 275 | public function setEvent(ModuleEvent $event) 276 | { 277 | $event->setTarget($this); 278 | $this->event = $event; 279 | return $this; 280 | } 281 | 282 | /** 283 | * Set the event manager instance used by this module manager. 284 | * 285 | * @return ModuleManager 286 | */ 287 | #[Override] 288 | public function setEventManager(EventManagerInterface $events) 289 | { 290 | $events->setIdentifiers([ 291 | self::class, 292 | static::class, 293 | 'module_manager', 294 | ]); 295 | $this->events = $events; 296 | $this->attachDefaultListeners($events); 297 | return $this; 298 | } 299 | 300 | /** 301 | * Retrieve the event manager 302 | * 303 | * Lazy-loads an EventManager instance if none registered. 304 | * 305 | * @return EventManagerInterface 306 | */ 307 | #[Override] 308 | public function getEventManager() 309 | { 310 | if (! $this->events instanceof EventManagerInterface) { 311 | $this->setEventManager(new EventManager()); 312 | } 313 | return $this->events; 314 | } 315 | 316 | /** 317 | * Register the default event listeners 318 | * 319 | * @param EventManagerInterface $events 320 | */ 321 | protected function attachDefaultListeners($events) 322 | { 323 | $events->attach(ModuleEvent::EVENT_LOAD_MODULES, [$this, 'onLoadModules']); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/ModuleManagerInterface.php: -------------------------------------------------------------------------------- 1 |