A simple structure for modularizing a Laravel project.
use App\Providers\ModuleServiceProvider;
return [
//other
ModuleServiceProvider::class,
];
Pure Laravel modular system (no third-party packages). Modules live in modules/ and are auto-discovered by ModuleServiceProvider.
| File | Purpose |
|---|---|
app/Providers/ModuleServiceProvider.php |
Auto-discovers, registers, and boots all modules |
app/Console/Commands/MakeModuleCommand.php |
make:module Artisan command |
bootstrap/providers.php |
Registers ModuleServiceProvider |
modules/ |
Root directory for all modules |
Every module follows this directory layout:
modules/{ModuleName}/
├── Console/
│ └── Commands/ # Artisan commands
├── Enums/ # PHP 8.1 backed enums
├── Helpers/ # Module helper functions
├── Http/
│ ├── Controllers/ # Traditional controllers
│ ├── Middleware/ # HTTP middleware
│ └── Requests/ # Form request validation
├── Jobs/ # Queued jobs
├── Livewire/ # Livewire components (auto-registered)
├── Models/ # Eloquent models
├── Observers/ # Model observers
├── Providers/ # Module service provider
├── Services/ # Business logic services
├── config/
│ └── {module}.php # Module config (merged into app config)
├── database/
│ └── migrations/ # Module migrations (auto-loaded)
├── lang/
│ ├── fa/
│ │ └── {module}.php # Persian translations (group file)
│ └── en/
│ └── {module}.php # English translations (group file)
├── resources/
│ └── views/ # Blade views
└── routes/
├── web.php # Admin panel routes (auto-loaded)
├── api.php # API routes (auto-loaded)
└── frontend.php # Public frontend routes (auto-loaded)
PSR-4 namespaces are registered directly via Composer's autoloader during ModuleServiceProvider::register():
| Namespace | Maps To |
|---|---|
Modules\{Name}\ |
modules/{Name}/ |
Modules\{Name}\Console\Commands\ |
modules/{Name}/Console/Commands/ |
Modules\{Name}\Enums\ |
modules/{Name}/Enums/ |
Modules\{Name}\Helpers\ |
modules/{Name}/Helpers/ |
Modules\{Name}\Http\Controllers\ |
modules/{Name}/Http/Controllers/ |
Modules\{Name}\Http\Middleware\ |
modules/{Name}/Http/Middleware/ |
Modules\{Name}\Http\Requests\ |
modules/{Name}/Http/Requests/ |
Modules\{Name}\Jobs\ |
modules/{Name}/Jobs/ |
Modules\{Name}\Livewire\ |
modules/{Name}/Livewire/ |
Modules\{Name}\Models\ |
modules/{Name}/Models/ |
Modules\{Name}\Observers\ |
modules/{Name}/Observers/ |
Modules\{Name}\Providers\ |
modules/{Name}/Providers/ |
Modules\{Name}\Services\ |
modules/{Name}/Services/ |
ModuleServiceProvider handles all registration automatically:
- PSR-4 autoloading - Module namespaces registered via Composer autoloader
- Config - Merges
config/{module}.phpinto app config (key = lowercase module name) - Migrations - Loads
database/migrations/vialoadMigrationsFrom() - Lang - Loads
lang/translations (namespace = lowercase module name) - Views - Loads
resources/views/(namespace = lowercase module name)
- Routes - Loads
routes/web.php,routes/api.php,routes/frontend.php - Livewire - Scans
Livewire/recursively and auto-registers all components - Module Provider - Boots
{Name}ServiceProviderif it exists
| Route File | Middleware Applied |
|---|---|
routes/web.php |
web |
routes/api.php |
api, auth:sanctum + prefix api |
routes/frontend.php |
web |
Components are auto-registered with kebab-case aliases:
| Class | Alias |
|---|---|
Modules\Shop\Livewire\ProductList |
shop.product-list |
Modules\Shop\Livewire\Cart\CartWidget |
shop.cart.cart-widget |
# Full module with everything
php artisan make:module Blog --all --provider
# Only directory structure
php artisan make:module Blog
# Selective generation
php artisan make:module Shop --model --livewire --migration --config --route --provider| Flag | Generates |
|---|---|
--all |
All sub-components below |
--model |
Eloquent model |
--livewire |
Livewire component (interactive: asks for name) |
--migration |
Migration file (interactive: asks for table name) |
--config |
Config file |
--route |
Route files (web, api, frontend) |
--lang |
Language files (fa, en) |
--view |
View directory with layout |
--service |
Service class |
--controller |
Controller class |
--enum |
Enum class (interactive: asks for name) |
--provider |
Module service provider |
$ php artisan make:module Shop --all --provider
Livewire component name: ProductList
Table name for migration: products
Enum name (e.g., Status): Status
Model [Shop] created.
Livewire component [ProductList] created.
Migration [products] created.
Config [shop.php] created.
Route files created.
Language files created.
View files created.
Service [ShopService] created.
Controller [ShopController] created.
Enum [Status] created.
Provider [ShopServiceProvider] created.
Module [Shop] created successfully!Create the directory structure manually, then add a composer.json if you want custom autoloading (optional -- the ModuleServiceProvider handles it).
Each module can have its own provider at Providers/{Name}ServiceProvider.php. It is auto-instantiated and both register() and boot() are called by ModuleServiceProvider.
<?php
namespace Modules\Shop\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class ShopServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind singletons, register services
$this->app->singleton(ShopService::class, function ($app) {
return new ShopService;
});
}
public function boot(): void
{
// Register observers, event listeners, middleware, etc.
Product::observe(ProductObserver::class);
}
}use Modules\Shop\Models\Product;
$products = Product::where('is_active', true)->get();In Blade templates (view namespace = lowercase module name):
{{-- Using Livewire tag --}}
<livewire:shop.product-list />
{{-- Or using @livewire directive --}}
@livewire('shop.product-list')In routes:
use Modules\Shop\Livewire\ProductList;
Route::get('/products', ProductList::class)->name('shop.products');// Access via module config key (lowercase module name)
$value = config('shop.enabled');// Namespace = lowercase module name, group = module name file
// Lang files: lang/{locale}/{module}.php
echo __('shop::shop.name'); // 'Shop'
echo __('shop::shop.welcome', ['name' => $name]);{{-- Namespace = lowercase module name --}}
@include('shop::components.layout')
{{-- Or in Livewire components --}}
return view('shop::product-list');- Static cache:
ModuleServiceProvider::$moduleCachestores discovered modules in memory (resets per request) - Config cached: If
php artisan config:cacheis used, the entireModuleServiceProvideris skipped (modules must be registered viaconfig/app.phpproviders instead) - No filesystem scan on boot: Modules are discovered once in
register(), reused inboot() - Composer autoloader: PSR-4 registration via Composer is O(1) lookups at runtime
- Copy the module folder into
modules/ - Run
php artisan migrate(new migrations are auto-loaded) - That's it --
ModuleServiceProviderauto-discovers it on next request
Modules can have model observers in Observers/ that are auto-registered. Convention: ProductObserver observes Product model from the same module's Models/ directory.
modules/Shop/
├── Models/
│ └── Product.php
└── Observers/
└── ProductObserver.php ← auto-registered on Product
File: modules/Shop/Observers/ProductObserver.php
<?php
namespace Modules\Shop\Observers;
use Modules\Shop\Models\Product;
class ProductObserver
{
public function created(Product $product): void
{
// Fires after Product is created
}
public function updated(Product $product): void
{
// Fires after Product is updated
}
}Rules:
- Observer file must be in
modules/{Name}/Observers/ - Observer class name must match
{ModelName}Observer - Model must exist in
modules/{Name}/Models/{ModelName}.php - Both classes must be autoloaded (PSR-4)
Disable modules without deleting folders via config/modules.php or Artisan commands.
Config: config/modules.php
return [
'disabled' => [
'Blog',
// 'Shop',
],
];Commands:
php artisan module:disable Shop
php artisan module:enable Shop
php artisan config:clearEffects of disabling:
- PSR-4 autoloading skipped
- Config not merged
- Migrations not loaded
- Translations not loaded
- Views not registered
- Routes not loaded
- Livewire components not registered
- Observers not registered
- Module ServiceProvider not booted
| Action | Command / Method |
|---|---|
| Create module | php artisan make:module {Name} --all --provider |
| Enable module | php artisan module:enable {Name} |
| Disable module | php artisan module:disable {Name} |
| Clear module cache | ModuleServiceProvider::clearCache() |
| List discovered modules | ModuleServiceProvider::getAllModules() |
| Access module config | config('{module}.key') |
| Use module translation | __('module::module.key') |
| Reference module view | view('module::view-name') |