Contensio logo

The service provider

Your plugin's entry point - the $ns property, registering routes, views, migrations, hooks, and lifecycle callbacks.

Every Contensio plugin has one service provider. It's a standard Laravel service provider. Contensio calls its register() and boot() methods when the plugin is enabled.

The $ns property

Every provider declares a protected string $ns property. This single string drives all namespaces in the plugin - views, translations, config, route name prefixes, publish tags, and public asset paths.

The value is always {vendor}-{slug} where slug is the Composer package name without the plugin- prefix:

acme/plugin-awesome  ->  $ns = 'acme-awesome'
contensio/plugin-ads-manager  ->  $ns = 'contensio-ads-manager'

This is mandatory. See Plugin anatomy for the full convention.

Full skeleton

<?php

namespace Acme\Awesome;

use Contensio\Support\Hook;
use Illuminate\Support\ServiceProvider;

class AwesomeServiceProvider extends ServiceProvider
{
    /**
     * Namespace: {vendor}-{slug}, no 'plugin-' prefix.
     * acme/plugin-awesome  ->  'acme-awesome'
     */
    protected string $ns = 'acme-awesome';

    public function register(): void
    {
        // Service container bindings only.
        // Merge default config if the plugin ships one:
        $this->mergeConfigFrom(__DIR__ . '/../config/' . $this->ns . '.php', $this->ns);
    }

    public function boot(): void
    {
        // Views - referenced as acme-awesome::admin.settings
        $this->loadViewsFrom(__DIR__ . '/../resources/views', $this->ns);

        // Routes
        $this->loadRoutesFrom(__DIR__ . '/../routes/admin.php');

        // Public routes that reference runtime config (e.g. a URL prefix from settings)
        // must be registered after all providers have booted:
        $this->app->booted(function () {
            $this->loadRoutesFrom(__DIR__ . '/../routes/public.php');
        });

        // Migrations - run automatically on plugin enable
        $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');

        // Translations - referenced as acme-awesome::acme-awesome.key
        $this->loadTranslationsFrom(__DIR__ . '/../lang', $this->ns);

        // Publishable assets
        $this->publishes([
            __DIR__ . '/../public' => public_path('vendor/' . $this->ns),
        ], $this->ns . '-assets');

        // Publishable config (for developer-level overrides)
        $this->publishes([
            __DIR__ . '/../config/' . $this->ns . '.php' => config_path($this->ns . '.php'),
        ], $this->ns . '-config');

        // Settings hub card - appears in admin Settings grid
        Hook::add('contensio/admin/settings-cards', function () {
            return view($this->ns . '::partials.settings-hub-card')->render();
        });
    }

    // ── Lifecycle callbacks ──────────────────────────────────────

    /** Called once when the plugin is first enabled (after migrations run). */
    public function onInstall(): void
    {
        // Seed default data, register block types, etc.
    }

    /** Called every time the plugin is enabled. */
    public function onEnable(): void
    {
        // Start scheduled tasks, warm caches, etc.
    }

    /** Called every time the plugin is disabled. */
    public function onDisable(): void
    {
        // Stop scheduled tasks, clear plugin caches, etc.
    }

    /** Called before plugin files are deleted on uninstall. */
    public function onUninstall(): void
    {
        // Delete uploaded files, revoke external API tokens, etc.
        // DB table cleanup is handled separately via the "drop tables" checkbox.
    }
}

Only include what you need

A settings-only plugin needs just loadTranslationsFrom() and the settings card hook. Don't register empty migrations directories or assets that don't exist. The skeleton above shows every possible method - use only the ones your plugin actually needs.

Lifecycle callbacks in detail

Contensio calls these methods at specific moments in a plugin's life:

Method When called Typical use
onInstall() First enable only (after migrations) Seed defaults, register block types in DB
onEnable() Every enable (including first) Start scheduled tasks, warm caches
onDisable() Every disable Stop scheduled tasks, clear plugin caches
onUninstall() Before files are deleted Delete uploads, revoke API tokens

onInstall() is the right place to seed default data - not a cache guard in boot(), and not inside the migration itself. The migration creates the schema; onInstall() populates it.

onUninstall() is called before file deletion. Database table cleanup is handled separately - the admin sees a "Also remove database tables" checkbox that triggers rollback of the plugin's migrations. You don't need to drop tables manually in onUninstall().

Registering hooks

All three hook types are registered in boot():

public function boot(): void
{
    // Action - fire-and-forget when an event occurs
    Hook::addAction('contensio/content/published', function ($content) {
        // ping an index, send a notification, etc.
    });

    // Filter - transform a value before it's used
    Hook::addFilter('contensio/content/body', function (string $html, $content): string {
        // modify rendered post HTML
        return $html;
    });

    // Render hook - inject HTML into a template slot
    Hook::add('contensio/admin/login-after-form', function (): string {
        return view($this->ns . '::partials.login-buttons')->render();
    });
}

See Hook system for the full API.

Public routes that depend on settings

If your plugin has a configurable URL prefix (e.g., a podcast at /episodes or a knowledge base at /help), read that prefix from storage and register the route inside $this->app->booted(). This ensures all service providers have finished booting - including the settings table service - before your routes are registered:

$this->app->booted(function () {
    $prefix = MyConfig::get('prefix', 'episodes');

    Route::middleware('web')->prefix($prefix)->group(function () {
        Route::get('/', [FrontendController::class, 'index'])->name($this->ns . '.public.index');
    });
});

What NOT to do in register()

register() runs before boot() for all providers. Core services are not yet available.

  • Don't touch the database in register()
  • Don't register hooks in register()
  • Don't load views, routes, or migrations in register()

register() is limited to $this->app->singleton(...) and mergeConfigFrom().

When plugins are booted

  • Contensio's CmsServiceProvider::boot() runs on every request.
  • It calls PluginRegistry::discover() to find every plugin.
  • For each enabled plugin, it registers and boots its provider.
  • Disabled plugins stay on disk but are never booted.

Toggling a plugin off in the admin removes all its runtime effects on the next request.

Next

  • Hook system - full API for actions, filters, and render hooks
  • Data storage - choosing between PluginOptions, shared tables, and own migrations
  • Routes and views - routing patterns and view namespaces