joshbrw/laravel-module-installer
Composer installer that places Laravel modules in a custom Modules directory instead of vendor, so module code stays editable and autoload-friendly. A lightweight drop-in for modular Laravel projects, commonly used alongside nwidart/laravel-modules.
joshbrw/laravel-module-installer is a Composer plugin that registers a custom installer for packages whose composer.json declares "type": "laravel-module". Instead of landing in vendor/, those packages are unpacked into your nwidart Modules/ directory — so composer require acme/blog gives you Modules/Blog with a real, editable module tree.
Step 1 — install the plugin in the project root. It must go in require, not require-dev (see Gotchas).
composer require joshbrw/laravel-module-installer
Step 2 — allow the plugin (Composer ≥ 2.2) and set your target directory in the root composer.json:
{
"require": {
"joshbrw/laravel-module-installer": "^1.0",
"nwidart/laravel-modules": "^10.0"
},
"extra": {
"module-dir": "Modules"
},
"config": {
"allow-plugins": {
"joshbrw/laravel-module-installer": true
}
}
}
extra.module-dir defaults to Modules; set it explicitly anyway so the intent is documented. It is resolved relative to the project root, so "Modules" → <project>/Modules. Keep it in sync with config/modules.php → 'paths.modules'.
Step 3 — first use case: pull in a module.
composer require acme/blog
# - Installing acme/blog (1.2.0): Downloading...
# - Installing acme/blog (1.2.0) into Modules/Blog
php artisan module:enable Blog
The folder name is derived from the package name (normally the segment after the slash, StudlyCased), so pick package names that map cleanly to your namespace — acme/blog → Modules/Blog → Modules\Blog\. Verify with ls Modules on your very first install rather than assuming.
Where to look first if it misbehaves:
composer show acme/blog # confirm "type : laravel-module"
grep -A2 'acme/blog' vendor/composer/installed.json # look for "install-path": "../Modules/Blog"
composer install -vvv | grep -i install
install-path in vendor/composer/installed.json is the ground truth for where Composer thinks the package went.
For a module to be picked up by the installer, its own composer.json must look like:
{
"name": "acme/blog",
"type": "laravel-module",
"require": {
"nwidart/laravel-modules": "^10.0"
},
"autoload": {
"psr-4": {
"Modules\\Blog\\": ""
}
},
"extra": {
"laravel": {
"providers": ["Modules\\Blog\\Providers\\BlogServiceProvider"]
}
}
}
The type is the single switch that activates the plugin. Keep autoload.psr-4 accurate — Composer still generates autoload rules from this file even though the code lives outside vendor/, and a bad mapping produces class-not-found errors that look like an installer bug. Ship a module.json alongside it so nwidart can read providers, aliases, and files.
Don't develop modules as published packages. Use a path repository with symlinking:
{
"repositories": [
{
"type": "path",
"url": "../packages/blog",
"options": { "symlink": true }
}
]
}
Now composer require acme/blog:dev-main symlinks ../packages/blog into Modules/Blog. Edit in the source repo, changes appear instantly, and composer update can't clobber your work. This is the setup that makes the plugin genuinely useful for a team.
For VCS-hosted modules add a vcs repository (or point at a private Packagist/Satis). Tag releases properly — without tags you'll be requiring dev-main and fighting minimum-stability.
After every composer require/install, the module is on disk but the app doesn't know about it yet:
php artisan module:enable Blog
php artisan module:list
php artisan optimize:clear
If the module registers routes/views, confirm modules_statuses.json now contains "Blog": true and that it's committed if you want consistent environments.
The plugin works fine in CI/CD as long as it's a production dependency:
composer install --no-dev --no-interaction --prefer-dist
php artisan module:migrate --force
Add Modules/ to .gitignore when modules are exclusively Composer-managed — but do commit composer.lock and modules_statuses.json. If any module is instead created via php artisan module:make and committed, exempt it explicitly (!Modules/Local/). Decide per-directory who owns the files; mixing ownership is the #1 source of "my changes vanished" incidents.
allow-plugins is mandatory on Composer 2.2+. Without it Composer prints a warning and silently skips the plugin — your module ends up in vendor/ and Modules/ stays empty. This is the most common failure by far.require-dev. On composer install --no-dev the plugin isn't activated, so modules install into vendor/ on production while working locally. Nightmare to diagnose.--no-plugins disables it too. If someone added that flag to a deploy script, modules silently land in vendor/.composer require invocation, the plugin may not be active for that transaction. Two commands: plugin first, then modules.type typos fail silently. "laravel-modules", "laravel_module", or a missing type all fall back to the default library installer. composer show <pkg> reveals the resolved type.composer remove acme/blog deletes Modules/Blog outright, including any uploads, migrations you edited, or local commits living only there. Same for composer update — packages are re-extracted from dist and local edits are lost. Only edit modules you own via a path repo.Modules/Blog with module:make) will overwrite it after a confirmation prompt. Rename one or the other.extra.module-dir and config/modules.php aligned. A mismatch (Modules/ vs modules/) works on macOS and explodes on a case-sensitive Linux CI box.composer.lock and installed.json even though it isn't in vendor/. Don't hand-copy modules around — you'll desync the lock.composer install -vvv shows the resolved install path; vendor/composer/installed.json shows "install-path" for every package. Between those two you can diagnose any placement issue.LibraryInstaller, and it still works with Composer 2.x, but pin ^1.0, run composer diagnose after Composer upgrades, and be prepared to fork — the extension point is literally "subclass the installer and register your own supports()" if you need per-package paths or an alias type.oomphinc/composer-installers-extender + composer/installers gives you installer-paths mapping for arbitrary package types, at the cost of more config.laravel-module dependencies also land in Modules/. That's usually desirable, but audit transitive module installs — you can end up with a Modules/ directory you never explicitly asked for.How can I help you explore Laravel packages today?