Skip to main content
Wintertrace
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:

Phaseregister()boot()
WhenBefore any provider is bootedAfter all providers are registered
Safe toBind into container, merge configResolve services, register into registries, load views/routes
Don’tResolve other services or registriesBind 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 typeStored 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.