Browse the documentation
Building modules
The ServiceProvider pattern
The ServiceProvider is a module's single entry point. The register vs. boot phases, loading views and translations, and registering settings — with examples.
Every module has exactly one ServiceProvider that extends Illuminate\Support\ServiceProvider. It is the single entry point where the module registers all its capabilities. Everything a module can do starts here.
Anatomy of a module ServiceProvider
<?php
namespace Schneespur\Module\MyModule;
use Illuminate\Support\ServiceProvider;
class MyModuleServiceProvider extends ServiceProvider
{
public function register(): void
{
// Phase 1: Bind services into the container.
// Other modules/providers may not be booted yet.
// Do NOT resolve other services here.
}
public function boot(): void
{
// Phase 2: All providers are registered.
// Safe to resolve registries and register into them.
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'my-module');
$this->registerNavigation();
$this->registerWidget();
$this->registerRoutes();
// ... other registrations
}
}
Register vs boot
The distinction between the two phases is not cosmetic — it determines whether your module loads reliably:
| Phase | register() | boot() |
|---|---|---|
| When | Before any provider is booted | After all providers are registered |
| Safe to | Bind into container, merge config | Resolve services, register into registries, load views/routes |
| Don’t | Resolve other services or registries | Bind into container (it works but violates convention) |
The reason: during register() there is no guarantee that every other provider has been registered yet. Resolving a registry there risks finding it does not exist. In boot() that guarantee holds — which is why all registry registrations belong there.
Loading views
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'my-module');
Views are then referenced as my-module::view-name:
return view('my-module::settings', ['data' => $data]);
Loading translations
Translations are auto-loaded by ModuleManager from modules/{slug}/lang/. They are namespaced as {slug}::file.key:
// In lang/de/messages.php: return ['hello' => 'Hallo'];
__('example::messages.hello'); // → 'Hallo'
You do not need to call $this->loadTranslationsFrom() — the ModuleManager handles this.
Registering settings
Use ModuleManager::registerSettings() to define default settings. They are stored in the settings table with the module slug as prefix:
protected function registerSettings(): void
{
app(ModuleManager::class)->registerSettings('my-module', [
'api_key' => '',
'enabled' => true,
'max_retries' => 3,
]);
}
Settings are stored as my-module.api_key, my-module.enabled, etc. Types are auto-detected:
| PHP type | Stored as |
|---|---|
string | 'string' |
bool | 'bool' |
int | 'int' |
array | 'json' |
Read settings with:
use App\Models\Setting;
$apiKey = Setting::get('my-module.api_key');
$enabled = Setting::get('my-module.enabled', false);
Write settings with:
Setting::set('my-module.api_key', 'new-value');
Setting::set('my-module.enabled', true, 'bool');
The slug prefix is also the reason settings can be cleaned up reliably when a module is removed — the ModuleManager simply deletes all keys beginning with {slug}..
A complete example
A full working ServiceProvider — with navigation, a dashboard widget, routes, settings, and translations — is built step by step in the Quick start. The bundled reference module under modules/example/ is also a fully annotated example of every extension point.
Common pattern: conditional booting
The reference module uses a shouldBoot() guard so it only loads during development:
protected function shouldBoot(): bool
{
return (bool) env('EXAMPLE_MODULE_ENABLED', false);
}
Production modules should not use this pattern — enable/disable is managed through the module management UI. This pattern is only used for the bundled development reference module.
ServiceProvider checklist
A module ServiceProvider typically registers some or all of the following (in boot()):
- Views:
$this->loadViewsFrom() - Settings:
ModuleManager::registerSettings() - Navigation items:
NavigationRegistry::addItem() - Dashboard widgets:
DashboardWidgetRegistry::registerWidget() - Filters:
FilterRegistry::register() - Template slots:
SlotRegistry::append()/SlotRegistry::replace() - Event listeners:
$this->app['events']->listen() - Notification channels:
NotificationChannelRegistry::register() - 2FA methods:
TwoFactorMethodRegistry::registerMethod() - Dispatch strategies:
DispatchStrategyRegistry::register() - Web routes:
Route::middleware(...)->group() - API routes:
ModuleApiRegistrar::routes() - Permissions:
PermissionRegistry::registerPermission() - Role templates:
RoleTemplateRegistry::registerTemplate() - Scheduled tasks:
ScheduledTaskRegistry::register() - Backup targets:
BackupTargetRegistry::register() - Storage backends:
StorageBackendRegistry::register() - Diagnostic reporters:
DiagnosticReporterRegistry::register()
The exact API for each of these registries is in the Registries reference.