Building applications that can be extended through plugins or modules is a powerful architectural pattern. It allows you to create systems where functionality can be added, removed, or modified without changing the core application code. This guide will walk you through implementing a robust module system in Laravel using advanced service provider techniques and dependency injection.
Table Of Contents
- Understanding Module Architecture
- Core Module System Implementation
- Example Module Implementation
- Advanced Module Features
- Best Practices
- Common Pitfalls
- Conclusion
Understanding Module Architecture
A well-designed module system enables you to build applications that are maintainable, scalable, and extensible. Each module encapsulates specific functionality and can be independently developed, tested, and deployed. This approach pairs well with custom facades for creating clean module interfaces.
Core Module System Implementation
Step 1: Create the Module Interface
First, define what a module should provide:
<?php
namespace App\Modules\Contracts;
interface ModuleInterface
{
/**
* Get the module's unique identifier
*/
public function getId(): string;
/**
* Get the module's display name
*/
public function getName(): string;
/**
* Get the module's version
*/
public function getVersion(): string;
/**
* Get module dependencies
*/
public function getDependencies(): array;
/**
* Bootstrap the module
*/
public function boot(): void;
/**
* Register module services
*/
public function register(): void;
/**
* Check if module is enabled
*/
public function isEnabled(): bool;
}
Step 2: Implement the Base Module Class
Create an abstract base class that modules will extend:
<?php
namespace App\Modules;
use App\Modules\Contracts\ModuleInterface;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Route;
abstract class BaseModule implements ModuleInterface
{
protected array $config = [];
protected bool $enabled = true;
public function __construct()
{
$this->loadConfiguration();
}
/**
* Load module configuration
*/
protected function loadConfiguration(): void
{
$configPath = $this->getBasePath() . '/config/module.php';
if (file_exists($configPath)) {
$this->config = require $configPath;
$this->enabled = $this->config['enabled'] ?? true;
}
}
/**
* Get the module's base path
*/
public function getBasePath(): string
{
$reflection = new \ReflectionClass($this);
return dirname($reflection->getFileName());
}
/**
* Register module routes
*/
protected function registerRoutes(): void
{
$routesPath = $this->getBasePath() . '/routes';
if (file_exists($routesPath . '/web.php')) {
Route::middleware('web')
->prefix($this->getRoutePrefix())
->name($this->getId() . '.')
->group($routesPath . '/web.php');
}
if (file_exists($routesPath . '/api.php')) {
Route::middleware('api')
->prefix('api/' . $this->getRoutePrefix())
->name('api.' . $this->getId() . '.')
->group($routesPath . '/api.php');
}
}
/**
* Register module views
*/
protected function registerViews(): void
{
$viewsPath = $this->getBasePath() . '/resources/views';
if (is_dir($viewsPath)) {
View::addNamespace($this->getId(), $viewsPath);
}
}
/**
* Register module migrations
*/
protected function registerMigrations(): void
{
$migrationsPath = $this->getBasePath() . '/database/migrations';
if (is_dir($migrationsPath)) {
app('migrator')->path($migrationsPath);
}
}
/**
* Get route prefix for the module
*/
protected function getRoutePrefix(): string
{
return $this->config['route_prefix'] ?? $this->getId();
}
/**
* Default implementations
*/
public function getDependencies(): array
{
return $this->config['dependencies'] ?? [];
}
public function getVersion(): string
{
return $this->config['version'] ?? '1.0.0';
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function register(): void
{
// Override in child classes
}
public function boot(): void
{
if (!$this->isEnabled()) {
return;
}
$this->registerRoutes();
$this->registerViews();
$this->registerMigrations();
}
}
Step 3: Create the Module Manager
Build a manager to handle module discovery and lifecycle:
<?php
namespace App\Modules;
use App\Modules\Contracts\ModuleInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
class ModuleManager
{
protected Collection $modules;
protected array $moduleInstances = [];
public function __construct(
protected string $modulesPath,
protected string $modulesNamespace = 'Modules'
) {
$this->modules = collect();
}
/**
* Discover and load all modules
*/
public function discover(): void
{
$this->modules = Cache::remember('modules.discovered', 3600, function () {
return $this->scanForModules();
});
$this->loadModules();
}
/**
* Scan filesystem for modules
*/
protected function scanForModules(): Collection
{
$modules = collect();
if (!File::isDirectory($this->modulesPath)) {
return $modules;
}
foreach (File::directories($this->modulesPath) as $modulePath) {
$moduleName = basename($modulePath);
$moduleClass = $this->modulesNamespace . '\\' . $moduleName . '\\' . $moduleName . 'Module';
if (class_exists($moduleClass)) {
$modules->put($moduleName, [
'name' => $moduleName,
'path' => $modulePath,
'class' => $moduleClass,
]);
}
}
return $modules;
}
/**
* Load and instantiate modules
*/
protected function loadModules(): void
{
foreach ($this->modules as $moduleData) {
try {
$module = new $moduleData['class']();
if ($module instanceof ModuleInterface) {
$this->moduleInstances[$module->getId()] = $module;
}
} catch (\Exception $e) {
logger()->error('Failed to load module: ' . $moduleData['name'], [
'error' => $e->getMessage()
]);
}
}
// Sort modules by dependencies
$this->sortModulesByDependencies();
}
/**
* Sort modules based on their dependencies
*/
protected function sortModulesByDependencies(): void
{
$sorted = [];
$visited = [];
foreach ($this->moduleInstances as $module) {
$this->visitModule($module, $sorted, $visited);
}
$this->moduleInstances = $sorted;
}
/**
* Topological sort helper
*/
protected function visitModule(ModuleInterface $module, array &$sorted, array &$visited): void
{
$moduleId = $module->getId();
if (isset($visited[$moduleId])) {
return;
}
$visited[$moduleId] = true;
foreach ($module->getDependencies() as $dependency) {
if (isset($this->moduleInstances[$dependency])) {
$this->visitModule($this->moduleInstances[$dependency], $sorted, $visited);
}
}
$sorted[$moduleId] = $module;
}
/**
* Register all enabled modules
*/
public function register(): void
{
foreach ($this->getEnabledModules() as $module) {
$module->register();
}
}
/**
* Boot all enabled modules
*/
public function boot(): void
{
foreach ($this->getEnabledModules() as $module) {
$module->boot();
}
}
/**
* Get all enabled modules
*/
public function getEnabledModules(): array
{
return array_filter($this->moduleInstances, fn($module) => $module->isEnabled());
}
/**
* Get a specific module
*/
public function getModule(string $moduleId): ?ModuleInterface
{
return $this->moduleInstances[$moduleId] ?? null;
}
/**
* Enable a module
*/
public function enableModule(string $moduleId): bool
{
$module = $this->getModule($moduleId);
if (!$module) {
return false;
}
// Update configuration
$configPath = $module->getBasePath() . '/config/module.php';
$config = require $configPath;
$config['enabled'] = true;
File::put($configPath, '<?php return ' . var_export($config, true) . ';');
// Clear cache
Cache::forget('modules.discovered');
return true;
}
/**
* Disable a module
*/
public function disableModule(string $moduleId): bool
{
$module = $this->getModule($moduleId);
if (!$module) {
return false;
}
// Check if other modules depend on this one
$dependents = $this->findDependentModules($moduleId);
if (!empty($dependents)) {
throw new \Exception(
"Cannot disable module. Other modules depend on it: " .
implode(', ', array_map(fn($m) => $m->getName(), $dependents))
);
}
// Update configuration
$configPath = $module->getBasePath() . '/config/module.php';
$config = require $configPath;
$config['enabled'] = false;
File::put($configPath, '<?php return ' . var_export($config, true) . ';');
// Clear cache
Cache::forget('modules.discovered');
return true;
}
/**
* Find modules that depend on given module
*/
protected function findDependentModules(string $moduleId): array
{
$dependents = [];
foreach ($this->getEnabledModules() as $module) {
if (in_array($moduleId, $module->getDependencies())) {
$dependents[] = $module;
}
}
return $dependents;
}
}
Step 4: Create the Module Service Provider
Register the module system with Laravel using advanced service provider patterns:
<?php
namespace App\Providers;
use App\Modules\ModuleManager;
use Illuminate\Support\ServiceProvider;
class ModuleServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(ModuleManager::class, function ($app) {
return new ModuleManager(
modulesPath: base_path('modules'),
modulesNamespace: 'Modules'
);
});
// Discover and register modules
$moduleManager = $this->app->make(ModuleManager::class);
$moduleManager->discover();
$moduleManager->register();
}
public function boot(): void
{
$moduleManager = $this->app->make(ModuleManager::class);
$moduleManager->boot();
// Register module commands
if ($this->app->runningInConsole()) {
$this->commands([
\App\Console\Commands\ModuleListCommand::class,
\App\Console\Commands\ModuleEnableCommand::class,
\App\Console\Commands\ModuleDisableCommand::class,
\App\Console\Commands\ModuleMakeCommand::class,
]);
}
}
}
Example Module Implementation
Blog Module Example
Here's a complete example of a blog module:
<?php
namespace Modules\Blog;
use App\Modules\BaseModule;
use Modules\Blog\Providers\BlogServiceProvider;
class BlogModule extends BaseModule
{
public function getId(): string
{
return 'blog';
}
public function getName(): string
{
return 'Blog Module';
}
public function register(): void
{
app()->register(BlogServiceProvider::class);
}
public function boot(): void
{
parent::boot();
// Register module-specific middleware
$this->registerMiddleware();
// Register event listeners
$this->registerEventListeners();
// You can also use pipeline pattern for module processing
// Learn more: https://mycuriosity.blog/laravel-pipeline-pattern-beyond-middleware
// Register view composers
$this->registerViewComposers();
}
protected function registerMiddleware(): void
{
app('router')->aliasMiddleware(
'blog.author',
\Modules\Blog\Http\Middleware\AuthorMiddleware::class
);
}
protected function registerEventListeners(): void
{
\Event::listen(
\Modules\Blog\Events\PostPublished::class,
\Modules\Blog\Listeners\SendPostNotification::class
);
}
protected function registerViewComposers(): void
{
view()->composer('blog::*', function ($view) {
$view->with('blogSettings', app('blog.settings'));
});
}
}
Module Structure
modules/Blog/
├── BlogModule.php
├── config/
│ └── module.php
├── database/
│ ├── migrations/
│ └── seeders/
├── Http/
│ ├── Controllers/
│ ├── Middleware/
│ └── Requests/
├── Models/
├── Providers/
│ └── BlogServiceProvider.php
├── resources/
│ ├── views/
│ └── assets/
├── routes/
│ ├── web.php
│ └── api.php
└── Services/
Module Configuration
<?php
// modules/Blog/config/module.php
return [
'version' => '1.2.0',
'enabled' => true,
'dependencies' => ['media', 'comments'],
'route_prefix' => 'blog',
'permissions' => [
'blog.view' => 'View blog posts',
'blog.create' => 'Create blog posts',
'blog.edit' => 'Edit blog posts',
'blog.delete' => 'Delete blog posts',
],
'settings' => [
'posts_per_page' => 10,
'enable_comments' => true,
'moderation_required' => true,
],
];
Advanced Module Features
1. Module Hooks System
Implement a hooks system for inter-module communication:
<?php
namespace App\Modules;
class ModuleHooks
{
protected array $hooks = [];
/**
* Register a hook
*/
public function register(string $hook, callable $callback, int $priority = 10): void
{
if (!isset($this->hooks[$hook])) {
$this->hooks[$hook] = [];
}
$this->hooks[$hook][$priority][] = $callback;
}
/**
* Execute hooks
*/
public function execute(string $hook, ...$args): array
{
if (!isset($this->hooks[$hook])) {
return [];
}
$results = [];
$callbacks = $this->hooks[$hook];
ksort($callbacks);
foreach ($callbacks as $priority => $callbackList) {
foreach ($callbackList as $callback) {
$results[] = $callback(...$args);
}
}
return $results;
}
/**
* Filter through hooks
*/
public function filter(string $hook, $value, ...$args)
{
if (!isset($this->hooks[$hook])) {
return $value;
}
$callbacks = $this->hooks[$hook];
ksort($callbacks);
foreach ($callbacks as $priority => $callbackList) {
foreach ($callbackList as $callback) {
$value = $callback($value, ...$args);
}
}
return $value;
}
}
// Usage in modules
class ProductModule extends BaseModule
{
public function boot(): void
{
parent::boot();
// Register hook
app('module.hooks')->register('product.price.calculate', function ($product) {
// Apply discounts
return $product->price * 0.9;
}, 20);
}
}
2. Module Assets Management
Handle module assets efficiently. This pairs well with Laravel's service container for asset management:
<?php
namespace App\Modules\Services;
use Illuminate\Support\Facades\File;
class ModuleAssetManager
{
protected array $publishedAssets = [];
/**
* Publish module assets
*/
public function publish(string $moduleId, string $source, string $destination): void
{
if (!File::isDirectory($destination)) {
File::makeDirectory($destination, 0755, true);
}
File::copyDirectory($source, $destination);
$this->publishedAssets[$moduleId] = [
'source' => $source,
'destination' => $destination,
'published_at' => now(),
];
$this->saveManifest();
}
/**
* Get asset URL for module
*/
public function asset(string $moduleId, string $path): string
{
return asset("modules/{$moduleId}/{$path}");
}
/**
* Save published assets manifest
*/
protected function saveManifest(): void
{
File::put(
storage_path('app/modules/assets.json'),
json_encode($this->publishedAssets, JSON_PRETTY_PRINT)
);
}
}
3. Module Console Commands
Create a command to scaffold new modules:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class ModuleMakeCommand extends Command
{
protected $signature = 'module:make {name}';
protected $description = 'Create a new module';
public function handle(): void
{
$name = $this->argument('name');
$path = base_path("modules/{$name}");
if (File::exists($path)) {
$this->error("Module {$name} already exists!");
return;
}
// Create module structure
$this->createModuleStructure($name, $path);
$this->info("Module {$name} created successfully!");
$this->line("Don't forget to:");
$this->line("- Update the module configuration");
$this->line("- Run 'composer dump-autoload' to register the namespace");
$this->line("- Run 'php artisan module:enable {$name}' to enable it");
}
protected function createModuleStructure(string $name, string $path): void
{
// Create directories
$directories = [
'',
'config',
'database/migrations',
'database/seeders',
'Http/Controllers',
'Http/Middleware',
'Http/Requests',
'Models',
'Providers',
'resources/views',
'resources/assets',
'routes',
'Services',
];
foreach ($directories as $dir) {
File::makeDirectory("{$path}/{$dir}", 0755, true);
}
// Create module class
$stub = File::get(base_path('stubs/module.stub'));
$content = str_replace(
['{{name}}', '{{id}}'],
[$name, strtolower($name)],
$stub
);
File::put("{$path}/{$name}Module.php", $content);
// Create configuration
File::put("{$path}/config/module.php", $this->getConfigStub($name));
// Create routes
File::put("{$path}/routes/web.php", "<?php\n\n// Web routes for {$name} module\n");
File::put("{$path}/routes/api.php", "<?php\n\n// API routes for {$name} module\n");
}
protected function getConfigStub(string $name): string
{
return <<<PHP
<?php
return [
'version' => '1.0.0',
'enabled' => false,
'dependencies' => [],
'route_prefix' => '" . strtolower($name) . "',
'settings' => [],
];
PHP;
}
}
Best Practices
1. Module Isolation
Keep modules isolated and independent:
// Good: Module uses contracts
public function __construct(
PaymentGatewayInterface $gateway,
NotificationServiceInterface $notifier
) {}
// Avoid: Direct dependencies on other modules
public function __construct(
\Modules\Stripe\StripeService $stripe,
\Modules\Email\EmailService $email
) {}
2. Version Management
Implement proper version checking:
public function checkCompatibility(): bool
{
foreach ($this->getDependencies() as $dependency => $version) {
$module = app(ModuleManager::class)->getModule($dependency);
if (!$module || !version_compare($module->getVersion(), $version, '>=')) {
return false;
}
}
return true;
}
3. Error Handling
Gracefully handle module failures:
public function boot(): void
{
try {
parent::boot();
$this->initializeServices();
} catch (\Exception $e) {
logger()->error("Failed to boot module {$this->getId()}", [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// Disable module to prevent further issues
app(ModuleManager::class)->disableModule($this->getId());
}
}
Common Pitfalls
1. Circular Dependencies
Prevent circular dependencies between modules:
protected function detectCircularDependencies(): void
{
$visited = [];
$stack = [];
foreach ($this->moduleInstances as $module) {
if (!isset($visited[$module->getId()])) {
$this->checkCycles($module, $visited, $stack);
}
}
}
2. Performance Impact
Optimize module loading for production:
// Cache module discovery in production
if (app()->environment('production')) {
$modules = Cache::rememberForever('modules.production', function () {
return $this->scanForModules();
});
}
Conclusion
Building a dynamic module architecture in Laravel enables you to create highly extensible applications that can grow and adapt over time. By implementing proper module discovery, dependency management, and lifecycle hooks, you create a foundation for scalable, maintainable applications.
The key to successful module architecture is finding the right balance between flexibility and complexity. Start simple, focus on clear interfaces and contracts, and gradually add features as your application's needs evolve.
For more advanced patterns and architectural concepts, explore our guides on service providers, custom facades, service container patterns, and Laravel pipeline patterns to create even more sophisticated modular systems.
Add Comment
No comments yet. Be the first to comment!