Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laminas Modulemanager Laravel Package

laminas/laminas-modulemanager

Laminas ModuleManager provides the infrastructure for building modular Laminas applications: it discovers and loads modules, resolves dependencies, merges configuration, and triggers lifecycle events for extensible bootstrap and application setup.

Deep Wiki
Context7

Getting Started

Install it directly only if you are not already on Laminas MVC (which pulls it in transitively):

composer require laminas/laminas-modulemanager

Look first at three files in an existing Laminas MVC app:

  1. config/application.config.php — the modules list and module_listener_options drive everything.
  2. module/<Name>/src/Module.php (legacy: module/<Name>/Module.php) — the class the manager instantiates for each enabled module.
  3. module/<Name>/config/module.config.php — the array returned by Module::getConfig() and merged into the application config.

A minimal application.config.php:

<?php
declare(strict_types=1);

return [
    'modules' => [
        'Laminas\Router',
        'Laminas\Validator',
        'Application',
    ],
    'module_listener_options' => [
        'module_paths'             => ['./module', './vendor'],
        'config_glob_paths'        => ['./config/autoload/{,*.}{global,local}.php'],
        'config_cache_enabled'     => false,          // true in production
        'config_cache_key'         => 'app_config',
        'module_map_cache_enabled' => true,
        'module_map_cache_key'     => 'app_modules',
        'cache_dir'                => './data/cache',
        'use_module_autoloaders'   => true,
        'use_module_configs'       => true,
    ],
];

First use case — enable a module. Create or install the module, ensure its namespace is autoloadable (Composer PSR-4 is the normal path), then add the module name to modules. If it is a local module under module/, the namespace must match the module name and resolve to a Module class (e.g. Reporting → Reporting\Module). Nothing else is required: the manager resolves the class, fires lifecycle events, and merges its config.

Second use case — change a config value. Either edit the owning module's module.config.php or add an override in config/autoload/*.local.php. Local files are merged last, so they win. In development, keep config_cache_enabled false; otherwise your edits silently do nothing until the cache is cleared.

If you are wiring module loading outside MVC, the manager is just a small event-driven object. Create it, attach a ConfigListener, and read the merged array:

use Laminas\EventManager\EventManager;
use Laminas\ModuleManager\ModuleManager;
use Laminas\ModuleManager\Listener\ConfigListener;
use Laminas\ModuleManager\Listener\ListenerOptions;

$options = new ListenerOptions([
    'module_paths'         => [__DIR__ . '/module'],
    'config_glob_paths'    => [__DIR__ . '/config/autoload/{,*.}{global,local}.php'],
    'config_cache_enabled' => false,
]);

$manager = new ModuleManager(['Application', 'Reporting']);
$manager->setEventManager(new EventManager());

$configListener = new ConfigListener($options);
$manager->getEventManager()->attach($configListener);

$manager->loadModules();
$config = $configListener->getMergedConfig(); // flat array, ready to use

Implementation Patterns

Config-first modules. Most day-to-day work never touches the manager API. A module is a namespace plus a Module.php that implements ConfigProviderInterface:

<?php
declare(strict_types=1);

namespace Reporting;

use Laminas\ModuleManager\Feature\ConfigProviderInterface;
use Laminas\ModuleManager\Feature\ServiceProviderInterface;

final class Module implements ConfigProviderInterface, ServiceProviderInterface
{
    public function getConfig(): array
    {
        return include __DIR__ . '/config/module.config.php';
    }

    public function getServiceConfig(): array
    {
        return [
            'factories' => [
                Service\ReportService::class => Service\ReportServiceFactory::class,
            ],
        ];
    }
}

Keep module.config.php returning plain arrays only. Avoid closures, new objects, and environment reads inside it — those break config caching and make merge order hard to reason about.

Use feature interfaces deliberately.

Interface Method Typical use
ConfigProviderInterface getConfig(): array Module configuration
ServiceProviderInterface getServiceConfig(): array Register factories/aliases (MVC ServiceListener)
RouteProviderInterface getRouteConfig(): array Router config
InitProviderInterface init(ModuleManagerInterface $manager) Cross-module setup, early hooks
BootstrapListenerInterface onBootstrap(EventInterface $e) Work that needs merged config / container
DependencyIndicatorInterface getModuleDependencies(): array Force ordering against other modules
AutoloaderProviderInterface getAutoloaderConfig(): array Legacy custom autoloaders

Choose init() for wiring that must happen while modules load, and onBootstrap() for anything that needs the fully merged config service. onBootstrap fires after config merging, so it is the safe default.

Dependency-aware ordering. If module B's config must land after module A's, declare it:

public function getModuleDependencies(): array
{
    return ['Laminas\Router', 'Laminas\Validator'];
}

The manager uses this information for ordering, but the modules array remains the fallback. Keep both consistent; do not rely on filesystem discovery order.

Event-driven customization. The manager exposes three useful points on its event manager: ModuleEvent::EVENT_LOAD_MODULE_RESOLVE, EVENT_LOAD_MODULE, and EVENT_LOAD_MODULES_POST. A common pattern is an audit/listener aggregate:

use Laminas\EventManager\AbstractListenerAggregate;
use Laminas\EventManager\EventManagerInterface;
use Laminas\ModuleManager\ModuleEvent;

final class ModuleAuditListener extends AbstractListenerAggregate
{
    public function attach(EventManagerInterface $events, $priority = 1)
    {
        $this->listeners[] = $events->attach(
            ModuleEvent::EVENT_LOAD_MODULES_POST,
            [$this, 'onLoaded'],
            $priority
        );
    }

    public function onLoaded(ModuleEvent $event): void
    {
        foreach ($event->getTarget()->getLoadedModules() as $name => $module) {
            // log, assert, or inspect $name / get_class($module)
        }
    }
}

Attach listeners before calling loadModules(). A listener on EVENT_LOAD_MODULES_POST with a lower priority than the ConfigListener can read $event->getConfig() to see the merged result.

Environment-specific module lists. Because application.config.php is plain PHP, you can compute the modules array from environment variables to enable development or debug modules only locally. This is the idiomatic ZF2/Laminas way and avoids editing production config.

Caching. In production, switch on config_cache_enabled and module_map_cache_enabled, and point cache_dir at a writable directory. The cache removes repeated include of every module.config.php and class-map resolution. In development, leave it off — the number one source of "my config change didn't apply" is a stale cache.

Standalone and Mezzio integration. For a CLI tool or a non-MVC app, the manual ModuleManager + ConfigListener snippet above is enough. Under Mezzio, prefer laminas/laminas-config-aggregator and add laminas/laminas-config-aggregator-modulemanager, whose ModuleManagerProvider bridges module-based config into the aggregator pipeline. Do not bolt ModuleManager directly into a Mezzio bootstrap unless you also handle its lifecycle events yourself.

Testing a module. You do not need a full MVC application to test configuration aggregation:

$manager = new ModuleManager(['Application']);
$listener = new ConfigListener(new ListenerOptions(['config_cache_enabled' => false]));
$manager->getEventManager()->attach($listener);
$manager->loadModules();

$config = $listener->getMergedConfig();
self::assertArrayHasKey('router', $config);

This is also a good way to catch config that depends on a specific module order before you ship.

Gotchas and Tips

  • Config cache is sticky. config_cache_enabled => true means Module::getConfig() is not called at all on subsequent requests. Clear data/cache (or change config_cache_key) on every deploy and every local config change.
  • Merge order determines winners. Config is merged module-by-module in load order, then glob files. Later arrays override earlier scalar keys. If two modules define the same key, the one loaded last wins — ordering bugs look like "my setting is ignored".
  • Numeric keys are appended, not replaced. The merge uses recursive array merging, so numerically indexed arrays (lists) concatenate. This can duplicate list entries across modules. Prefer associative keys for anything that must be overridden.
  • config_glob_paths order matters. The conventional {,*.}{global,local}.php pattern loads *.global.php before *.local.php, so local overrides global. If you add new glob patterns, put them in the intended precedence order.
  • init() is too early for merged config. init() runs while modules load; config aggregation completes at the post-load phase. Use onBootstrap() (or a low-priority post-load listener) when you need the merged config array.
  • Autoloading failures look like missing modules. ModuleResolverListener instantiates <ModuleName>\Module. If the namespace is not in Composer's autoloader (or module_paths/use_module_autoloaders are misconfigured), you get a class-not-found rather than a helpful "module failed to load". Verify the PSR-4 mapping first.
  • Never put closures or objects in config. They cannot be cached (serialization/export will fail or degrade), and they defeat the package's main performance benefit.
  • Override vendor config in your own module, placed last. Do not edit files under vendor/. Create a project module (often Application) and declare the override there so load order guarantees it wins.
  • Debug with the event manager, not var_dump in Module.php. Dump getLoadedModules() keys to confirm ordering, and inspect ConfigListener::getMergedConfig() to see the final array. For a stuck value, check the cache first, then list the modules and their load order.
  • Custom event managers are supported and useful in tests. Call setEventManager() before loading if you want isolated listeners, and type-hint ModuleManagerInterface in module code rather than the concrete class.
  • Treat this as infrastructure, not application code. The package's day-to-day surface is small: a module list, a Module class, config files, and two lifecycle hooks. If you find yourself writing elaborate listeners, check whether Mezzio's ConfigProvider/ConfigAggregator model would be simpler — for new non-MVC projects it usually is.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules