- Introduction
- Module System Architecture
- Module Structure
- Creating a Custom Module
- Module Configuration
- Module Lifecycle Hooks
- Module Dependencies
- Routes and Controllers
- Database Migrations
- Views and Assets
- Module Services
- Filament 5 Integration
- Custom Theme Support
- Testing Modules
- Best Practices
This Laravel boilerplate now uses the internachi/modular pattern for module management, providing a modern, composer-based approach to modularizing your Laravel application. This system integrates seamlessly with Filament 5 for admin panel functionality and supports custom themes.
- Composer-based autoloading: Each module is treated as a composer package
- Laravel package discovery: Automatic service provider registration
- Filament 5 integration: Auto-discovery of Filament resources, pages, and widgets
- Custom theme support: Modules can provide their own themes
- Backward compatibility: Supports both old and new module structures
- IDE-friendly: Full autocomplete and code navigation support
The module system supports two directory structures:
- New modular structure (recommended):
app-modules/- follows internachi/modular pattern - Legacy structure:
app/Modules/- for backward compatibility
Both structures are supported, but new modules should use the modular pattern in app-modules/.
A typical module following the internachi/modular pattern has this structure:
app-modules/YourModule/
├── composer.json # Module composer configuration
├── src/
│ ├── YourModuleModule.php # Main module class
│ ├── Providers/
│ │ └── YourModuleServiceProvider.php
│ ├── Http/
│ │ ├── Controllers/
│ │ └── Middleware/
│ ├── Models/
│ ├── Services/
│ ├── Filament/ # Filament 5 resources (auto-discovered)
│ │ ├── Resources/
│ │ ├── Pages/
│ │ └── Widgets/
├── routes/
│ ├── web.php
│ ├── api.php
│ └── admin.php (optional)
├── database/
│ ├── migrations/
│ ├── factories/
│ └── seeders/
├── resources/
│ ├── views/
│ ├── lang/
│ └── assets/
│ ├── css/
│ └── js/
├── config/
│ └── YourModule.php
└── tests/
├── Unit/
└── Feature/
The easiest way to create a new module is using the built-in Artisan command:
php artisan make:module YourModule
# or
php artisan module create YourModuleThis will create a complete module structure in app-modules/YourModule following the internachi/modular pattern.
The command will generate:
- Complete directory structure
- Composer.json for autoloading
- Service provider with Laravel package discovery
- Main module class
- Example controller, model, and migration
- Route files (web, api)
- Basic view template
- Configuration file
- Test scaffolding
If you prefer to create a module manually, follow these steps:
- Create the module directory:
app/Modules/YourModule - Create the main module class
- Create the module.json file
- Create the service provider
- Add routes, controllers, and other components as needed
Every module must have a module.json file that contains metadata:
{
"name": "YourModule",
"version": "1.0.0",
"description": "Description of your module",
"dependencies": ["AnotherModule"],
"config": {
"enabled": false,
"auto_enable": false
}
}Create your main module class by extending BaseModule:
<?php
namespace App\Modules\YourModule;
use App\Modules\BaseModule;
class YourModuleModule extends BaseModule
{
protected function onEnable(): void
{
// Called when module is enabled
// Initialize services, register event listeners, etc.
}
protected function onDisable(): void
{
// Called when module is disabled
// Clean up resources, unregister listeners, etc.
}
protected function onInstall(): void
{
// Called when module is installed
// Seed initial data, create default records, etc.
}
protected function onUninstall(): void
{
// Called when module is uninstalled
// Clean up data, remove files, etc.
}
}Modules support both lifecycle methods and a flexible hook system:
onEnable()- Called when the module is enabledonDisable()- Called when the module is disabledonInstall()- Called when the module is installedonUninstall()- Called when the module is uninstalled
The hook system allows you to extend module functionality at various points:
// Register a hook
$this->registerHook('before_enable', function($module) {
// Code to execute before module is enabled
}, priority: 10);
// Execute hooks
$this->executeHook('custom_action', $param1, $param2);
// Available built-in hooks:
// - before_enable
// - after_enable
// - before_disable
// - after_disable
// - before_install
// - after_install
// - before_uninstall
// - after_uninstallModules can use the Configurable trait for easy configuration management:
// Get a config value
$value = $this->config('key', 'default');
// Set a config value (runtime only)
$this->setConfig('key', 'value');
// Check if config exists
if ($this->hasConfig('key')) {
// ...
}
// Get all configuration
$allConfig = $this->getAllConfig();
// Merge configuration
$this->mergeConfig(['key1' => 'value1', 'key2' => 'value2']);Modules can declare dependencies on other modules:
{
"dependencies": ["CoreModule", "AuthModule"]
}The system will:
- Prevent enabling a module if its dependencies are not enabled
- Prevent disabling a module if other enabled modules depend on it
- Validate dependencies during installation
<?php
use Illuminate\Support\Facades\Route;
use App\Modules\YourModule\Http\Controllers\YourController;
Route::prefix('yourmodule')->group(function () {
Route::get('/', [YourController::class, 'index'])->name('yourmodule.index');
Route::get('/{id}', [YourController::class, 'show'])->name('yourmodule.show');
});<?php
use Illuminate\Support\Facades\Route;
use App\Modules\YourModule\Http\Controllers\Api\YourApiController;
Route::prefix('yourmodule')->group(function () {
Route::get('/', [YourApiController::class, 'index']);
});<?php
namespace App\Modules\YourModule\Http\Controllers;
use App\Http\Controllers\Controller;
class YourController extends Controller
{
public function index()
{
return view('yourmodule::index');
}
}Place migration files in database/migrations/:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('yourmodule_items', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('yourmodule_items');
}
};Migrations are automatically run when the module is installed.
Views are stored in resources/views/ and can be accessed using the module name:
{{-- resources/views/index.blade.php --}}
@extends('layouts.app')
@section('content')
<h1>Your Module</h1>
@endsectionAccess in controller:
return view('yourmodule::index');Assets in resources/assets/ are published to public/modules/YourModule/ when the module is installed.
Reference in views:
<link href="{{ asset('modules/YourModule/css/style.css') }}" rel="stylesheet">
<script src="{{ asset('modules/YourModule/js/script.js') }}"></script>Create reusable services in the Services/ directory:
<?php
namespace App\Modules\YourModule\Services;
class YourService
{
public function doSomething()
{
// Service logic
}
}Register in service provider:
public function register(): void
{
$this->app->singleton(YourService::class, function ($app) {
return new YourService();
});
}Create tests in the tests/ directory:
<?php
namespace Tests\Modules\YourModule;
use Tests\TestCase;
use App\Modules\YourModule\YourModuleModule;
class YourModuleTest extends TestCase
{
public function test_module_can_be_enabled()
{
$module = new YourModuleModule();
$module->enable();
$this->assertTrue($module->isEnabled());
}
}- Minimize dependencies on other modules
- Use events for inter-module communication
- Keep coupling loose
- Follow PSR-4 autoloading standards
- Use consistent naming conventions
- Organize code logically
- Add clear descriptions in module.json
- Document public APIs
- Include examples in comments
- Use try-catch blocks in lifecycle hooks
- Log errors appropriately
- Provide meaningful error messages
- Write unit tests for services
- Test lifecycle hooks
- Test with and without dependencies
- Follow semantic versioning
- Document breaking changes
- Provide migration guides
- Use config files for module settings
- Provide sensible defaults
- Allow runtime configuration where appropriate
- Validate all inputs
- Use Laravel's security features
- Follow OWASP guidelines
- Don't expose sensitive data
- Cache when appropriate
- Optimize database queries
- Lazy load resources
- Use queues for heavy operations
- Test with different PHP/Laravel versions
- Check for breaking changes in dependencies
- Maintain backwards compatibility when possible
Modules can integrate seamlessly with Filament 5 by placing Filament resources, pages, and widgets in the appropriate directories. The module system will automatically discover and register them.
Place Filament resources in src/Filament/Resources/:
<?php
namespace Modules\YourModule\Filament\Resources;
use Filament\Resources\Resource;
use Filament\Tables\Table;
use Filament\Forms\Form;
use Modules\YourModule\Models\YourModel;
class YourModelResource extends Resource
{
protected static ?string $model = YourModel::class;
protected static ?string $navigationIcon = 'heroicon-o-collection';
protected static ?string $navigationGroup = 'Your Module';
public static function form(Form $form): Form
{
return $form
->schema([
// Your form fields
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
// Your table columns
]);
}
}Create custom Filament pages in src/Filament/Pages/:
<?php
namespace Modules\YourModule\Filament\Pages;
use Filament\Pages\Page;
class CustomPage extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static string $view = 'yourmodule::filament.pages.custom-page';
}Add widgets in src/Filament/Widgets/:
<?php
namespace Modules\YourModule\Filament\Widgets;
use Filament\Widgets\Widget;
class CustomWidget extends Widget
{
protected static string $view = 'yourmodule::filament.widgets.custom-widget';
}The ModularServiceProvider automatically discovers Filament components when:
config('modular.filament.enabled')istrue- Components are in the correct namespace (
Modules\YourModule\Filament\*) - The module is properly autoloaded via composer
Modules can provide custom themes that integrate with the application's theme system.
app-modules/YourModule/
└── resources/
└── themes/
└── your-theme/
├── theme.json
├── views/
│ └── layouts/
│ └── app.blade.php
├── css/
│ └── app.css
└── js/
└── app.js
Create a theme.json file:
{
"name": "Your Theme",
"version": "1.0.0",
"description": "Custom theme for YourModule",
"author": "Your Name",
"extends": "default"
}In your module's service provider:
public function boot(): void
{
// Register theme
$themePath = __DIR__ . '/../../resources/themes/your-theme';
if (is_dir($themePath)) {
$this->publishes([
$themePath => public_path('themes/your-theme'),
], 'yourmodule-themes');
}
}@extends(theme_layout('app'))
@section('content')
@themeCss
<h1>Your Module Content</h1>
@themeJs
@endsectionThe application provides several helpers for working with themes:
// Set active theme
set_theme('your-theme');
// Get active theme
$theme = active_theme();
// Get theme asset path
$asset = theme_asset('images/logo.png');
// Get theme layout
$layout = theme_layout('app');php artisan module listphp artisan module enable Blogphp artisan module disable Blogphp artisan module install Blogphp artisan module uninstall Blogphp artisan module info Blog- Ensure the module directory exists in
app/Modules/ - Check that the module class name matches the directory name
- Verify module.json exists and is valid JSON
- Enable required modules first
- Check dependency names match exactly
- Verify dependencies are installed
- Check route files are in
routes/directory - Ensure routes are properly registered
- Clear route cache:
php artisan route:clear
- Use the correct view namespace:
yourmodule::viewname - Check view files exist in
resources/views/ - Clear view cache:
php artisan view:clear
This modular architecture provides a flexible and maintainable way to extend your Laravel application. By following these guidelines and best practices, you can create robust, reusable modules that integrate seamlessly with the rest of your application.
For more examples, refer to the included BlogModule in app/Modules/BlogModule/.