Plugin Service Provider: register() and boot()

The provider named in module.json is the one place the platform calls into your plugin. Knowing when each method runs tells you where every registration belongs.

What the base class gives you

Extend Nwidart\Modules\Support\ModuleServiceProvider. You must declare two properties (a LogicException is thrown otherwise):

class TestimonialsServiceProvider extends ModuleServiceProvider
{
    protected string $name = 'Testimonials';      // folder / StudlyCase name
    protected string $nameLower = 'testimonials'; // alias: config, view and lang namespace

    protected array $commands = [];               // Artisan command classes to register
    protected array $providers = [RouteServiceProvider::class]; // child providers, registered in register()
}

parent::boot() then performs, in this order:

  1. registerCommands() — the classes in $commands.
  2. registerCommandSchedules() — calls your configureSchedules(Schedule $schedule) after the app has booted.
  3. registerTranslations() — override it (see below).
  4. registerConfig() — every PHP file in config/ is merged into config('alias') (config.php) or config('alias.file'), unless the config is cached.
  5. registerViews() — the resources/views folder under the namespace alias:: (publishable overrides in resources/views/modules/alias), and the Blade class-component namespace Modules\{Name}\View\Components as <x-alias::component-name>.
  6. loadMigrationsFrom() — database/migrations, so php artisan migrate picks them up.

register() only registers the child providers listed in $providers.

Where does each registration belong?

MethodRunsPut here
register()Every request, as providers registerparent::register(), mergeConfigFrom(), container bindings and singletons. Do not resolve other services here: they may not be registered yet
boot()Every request, after all providers registeredparent::boot(), translations override, breadcrumbs, dashboard rows, Shortcode::..., Widgets::register(), BindingSourceRegistry sources, AppPages::register(), Blade components, model/event listeners, view composers, observers, cache-invalidation hooks, macros
Event::listen(AdminBooting::class, ...)Only on admin requests, after the core menu and settings are registeredMenuManager::make(), SettingsManager::add(), MenuBuilder::register() — anything that fills an admin-only registry or depends on the signed-in user
configureSchedules()When the scheduler or schedule:list boots$schedule->command('alias:task')->hourly()->withoutOverlapping();
Child RouteServiceProvider::map()Route registration (not when routes are cached)Route groups with their middleware
Child EventServiceProviderBoot$listen maps for your own and third-party events (see Booking's)

Why some registrations go on AdminBooting, others in boot()

Rules for an AdminBooting listener

  1. It runs on every admin request, including POSTs. Keep it fast; no queries unless essential.
  2. The signed-in user is available (Auth::user()) — check permissions and register only what the user may use.
  3. Pass settings pages as a closure so definitions are built only when the screen is opened.
  4. Do not echo or return a response; just mutate the registries.
  5. Put the code in helper classes (TestimonialsMenu::register(), TestimonialsSettings::register()) so the provider stays readable — the bundled modules do exactly this.

Overriding registerTranslations()

The base implementation calls loadTranslationsFrom($path) without a namespace, so lang/en/testimonials.php would only be reachable as __('testimonials.menu') and could clash with core keys. Every bundled module overrides it:

protected function registerTranslations(): void
{
    $langPath = lang_path('modules/' . $this->nameLower);   // site-level overrides win

    if (is_dir($langPath)) {
        $this->loadTranslationsFrom($langPath, $this->nameLower);
        $this->loadJsonTranslationsFrom($langPath);

        return;
    }

    $moduleLangPath = module_path($this->name, 'lang');
    $this->loadTranslationsFrom($moduleLangPath, $this->nameLower);
    $this->loadJsonTranslationsFrom($moduleLangPath);
}

This also lets a site owner override your strings by copying them to lang/modules/testimonials/… without editing the plugin.

Patterns used by the bundled modules

1. Defer work until everything has booted

$this->app->booted(function (): void {
    // runs once after every provider (including other plugins) has booted
});

2. Register a binding source and an app page in boot()

php (condensed from BookingServiceProvider)

app(BindingSourceRegistry::class)->register(new ServiceBindingSource);

AppPages::registerMany([
    PageDefinition::make('booking.services')
        ->label(static fn (): string => __('booking::booking.services'))
        ->selectedBySetting('booking_page_services')
        ->pageSetup(__('booking::booking.app_page_services'), __('booking::booking.app_page_services_help'))
        ->pageSetupGroup(__('booking::booking.booking'))
        ->route('booking.services.index'),
]);

3. Flush the response cache when your data changes

ResponseCacheInvalidator::flushWhenChanged([Testimonial::class]);   // saved / deleted / restored

// or, for models that extend App\Models\BaseModel, nothing at all: ClearCache already does it.

4. Version a reference-data cache

Booking and TruCommerce keep heavy reference data (services, taxonomies, countries) in a versioned cache service (BookingReferenceCache, StorefrontReferenceCache) that every relevant model saved/deleted event invalidates, together with the response cache. Register the configured media model (config('media-library.media_model')), not the base Spatie class, because Eloquent events fire per concrete class.

Anatomy of the sample plugin's provider

The complete, tested provider is shown in the tutorial. Its shape:

TestimonialsServiceProvider
  register()  parent::register(); mergeConfigFrom('config/config.php' -> config('testimonials'))
  boot()      parent::boot();
              registerTranslations();    // override, namespaced
              registerBreadcrumbs();     // require routes/breadcrumbs.php
              registerDashboard();       // Dashboard::admin()->row(...)->widget(...)
              registerPageBuilderParts();// Blade::component + Shortcode::category/registerIfMissing
              Widgets::register('testimonials-list', ...);
              Event::listen(AdminBooting::class, fn () => [menu, settings]);
  RouteServiceProvider::map()  admin routes with web, auth, verified, 2fa, AdminTheme

Common mistakes