Laravel Modular is built on Strict Domain-Driven Design (DDD) principles, but implemented with the Laravel conventions you already know.
Modules are designed to be isolated.
module.json and composer.json).We do not invent new concepts.
Illuminate\Support\ServiceProvider.A typical modular application looks like this. We deviate slightly from the default app/ structure to keep things tighter for modules.
modules/
└── Shop/ <-- The Module Root (StudlyCase)
├── composer.json <-- PHP Dependencies (Isolated)
├── module.json <-- Module Configuration
├── package.json <-- JS/CSS Dependencies (Isolated)
├── vite.config.js <-- Asset Build Config
│
├── app/ <-- PSR-4 Source Root (Mapped to Modules\Shop\)
│ ├── Models/ <-- Eloquent Models
│ │ └── Product.php
│ ├── Http/
│ │ ├── Controllers/ <-- Http Classes
│ │ ├── Requests/
│ │ └── Middleware/
│ ├── Providers/ <-- derived from ServiceProvider
│ │ ├── ShopServiceProvider.php (Main)
│ │ └── EventServiceProvider.php
│ └── Services/ <-- Business Logic (Optional)
│
├── database/
│ ├── migrations/ <-- Database Schema
│ ├── seeders/ <-- Database Seeding
│ └── factories/ <-- Model Factories
│
├── resources/
│ ├── views/ <-- Blade Templates
│ ├── lang/ <-- Translations
│ └── assets/ <-- Raw CSS/JS
│
├── routes/
│ ├── web.php <-- Web Routes
│ └── api.php <-- API Routes
│
└── tests/
├── Unit/ <-- Isolated Unit Tests
└── Feature/ <-- Integration Tests
app/ directory: We use an app folder for PHP code, mirroring the standard Laravel application structure.composer.json: Each module has one. This allows you to extract a module into a standalone package easily in the future.Providers/: A module must have at least one ServiceProvider to boot itself.How does Laravel know your module exists?
ModuleRegistry)When the framework boots, the LaravelModularServiceProvider (in the core package) instantiates the ModuleRegistry.
modules/* (or your configured path).module.json for every folder.Unlike standard packages where you manually add them to config/app.php, Laravel Modular does this dynamically.
Modules\Shop\ is registered to modules/Shop/app/ via a runtime PSR-4 loader (or composer.json autoloading in production).module.json (provider key) is called.The ModularServiceProvider and ModuleRegistry automatically discover standard Laravel components without manual registration in your ServiceProvider.
| Component | Path (inside module) | Logic |
|---|---|---|
| Routes | routes/web.php, routes/api.php, routes/channels.php, routes/console.php |
Automatically loaded. web middleware applied to web.php, api prefix + middleware to api.php. |
| Config | config/*.php |
Merged into global config. Accessed via Module::file.key (or lowercase alias module::file.key). |
| Commands | app/Console/Commands/*.php |
Automatically registered if class is not abstract. |
| Policies | app/Policies/*.php |
Discovered if naming follows ModelPolicy convention (e.g., ProductPolicy for Product model). |
| Events | app/Listeners/*.php |
Discovered if the class has a subscribe method. |
| Views | resources/views/ |
Registered under namespace module-name:: (lowercase). |
| Migrations | database/migrations/ |
Registered and run by migrate command. |
| Translations | lang/ |
Registered under namespace module-name:: (lowercase). |
Scanning the filesystem is slow. In production, you run php artisan modular:cache.
bootstrap/cache/modular.php.ModuleRegistry loads this array instantly, bypassing all filesystem checks.The ShopServiceProvider.php (generated by make:module) is your entry point. It automatically handles:
web.php and api.php.resources/views under the namespace shop::.database/migrations.resources/lang.You can modify this file exactly like AppServiceProvider.
public function boot(): void
{
parent::boot();
// Your custom boot logic
Model::preventLazyLoading();
}
module.json FileThis is the brain of your module.
{
"name": "Shop",
"namespace": "Modules\\Shop\\",
"provider": "Modules\\Shop\\Providers\\ShopServiceProvider",
"route_prefix": "shop-api/v1",
"removable": true,
"disableable": true,
"requires": [],
"events": {
"Modules\\Shop\\Events\\OrderPlaced": [
"Modules\\Shop\\Listeners\\SendOrderInvoice",
"Modules\\Shop\\Listeners\\UpdateInventory"
]
}
}
route_prefix: (String, Optional) Automatically wraps all routes registered in routes/web.php and routes/api.php with this prefix and namespacing logic. By default, it is empty. If set to shop-api/v1, an API route will natively resolve to api/shop-api/v1/your-route entirely transparently!disableable: (Boolean) If false, the module cannot be disabled via CLI/UI.removable: (Boolean) If false, the module cannot be uninstalled via CLI/UI.events: (Object) Explicit mapping of Event classes to an array of Listener classes. These take precedence over subscriber auto-discovery.The architecture is designed to be highly verifiable. Running php artisan modular:doctor generates a comprehensive Health Score (0-100) for each module by analyzing 8 core architectural pillars:
module.json present and readable?README.md?requires currently enabled and present?tests/Feature or tests/Unit directory exist?provider namespace valid and resolvable?How can I help you explore Laravel packages today?