Xgenious/ docs
Products
Get support

Plugin Base Class API

Your main class must extend App\PluginSystem\PluginBase and implement id(): string. All other methods are optional overrides or inherited helpers.

namespace Plugins\AcmeSeo\Src;

use App\PluginSystem\PluginBase;

class AcmeSeoPlugin extends PluginBase
{
    public function id(): string
    {
        return 'acme-seo';
    }

    public function routes(): void
    {
        // Called for ALL discovered plugins — register settings + routes here
        $this->registerSettings();
        $this->registerRoutes();
    }

    public function boot(): void
    {
        $this->registerSettings(); // idempotent — must run before tenant guard

        if (!function_exists('tenant') || !tenant()) return;

        $this->registerMenus();
        $this->registerHooks();
    }
}

Lifecycle Methods

Override these methods to hook into the plugin lifecycle:

  • routes(): void — Called for every discovered plugin (active or inactive) on every request, before boot(). Always call register_settings() here first — this is what makes the Settings button visible on the plugin card, even before the plugin is activated. Also call register_web_routes() and register_api_routes() here. Do not call these in boot().
  • boot(): void — Called on every request where the plugin is active. Register hooks, menus, and assets here. Call register_settings() again here (it is idempotent). For tenant-type plugins, guard with if (!function_exists('tenant') || !tenant()) return; to avoid crashes in CLI and landlord contexts. Do not call register_web_routes() or register_api_routes() here — use routes() instead.
  • on_activate(): void — Called once when the plugin is first activated by an admin. Ideal for running migrations, seeding defaults, or sending a welcome notification.
  • on_deactivate(): void — Called when the plugin is deactivated. Clean up scheduled tasks or temporary data. Plugin data (DB tables, options) is preserved.
  • on_update(string $from_version): void — Called after an update ZIP is installed. The previous version string is passed as $from_version. Run migration diffs or data transformations here.

Hook System

  • add_action(string $hook, callable $cb, int $priority = 10): void — Subscribe to an action hook. Callback receives the hook's arguments. Actions do not return values.
  • add_filter(string $hook, callable $cb, int $priority = 10, int $args = 1): void — Subscribe to a filter hook. Callback must return the (potentially modified) first argument. Use $args to receive additional context arguments.

Asset Injection

All assets registered here are automatically rendered by @pluginFrontendStyles (inside <head>) and @pluginFrontendScripts (before </body>) — both are wired into every theme's master layout. No changes to theme files are ever required.

File-based — via PluginBase helpers (call in boot())

  • enqueue_admin_style(string $handle, string $src, array $deps = []): void — Inject a stylesheet into admin panel pages. Handle must be globally unique — prefix with your plugin ID.
  • enqueue_admin_script(string $handle, string $src, array $deps = [], bool $in_footer = true): void — Inject a JavaScript file into admin panel pages. Pass false for $in_footer to load in <head>.
  • enqueue_frontend_style(string $handle, string $src, array $deps = []): void — Inject a stylesheet into tenant storefront pages (all themes).
  • enqueue_frontend_script(string $handle, string $src, array $deps = [], bool $in_footer = true): void — Inject a JavaScript file into tenant storefront pages. Available deps: 'jquery', 'alpine', 'toastr', 'sweetalert2', 'tailwind'.

Inline snippets — for dynamic content that depends on plugin settings

  • enqueue_inline_style(string $handle, string $css, string $context = 'frontend'): void — Inject an inline <style> block. Use for CSS variables or rules that depend on plugin settings (e.g. brand colour). Context: 'frontend' or 'admin'.
  • enqueue_inline_script(string $handle, string $js, string $context = 'frontend', bool $in_footer = true): void — Inject an inline <script> block. Use for configuration objects that JS files need before they run (e.g. window.MyPluginConfig = {...}). Pass false for $in_footer to place in <head>.

Global helper functions (usable anywhere, not just inside PluginBase)

  • plugin_enqueue_style(string $handle, string $src, string $context = 'frontend', array $deps = []): void — Enqueue a CSS file from anywhere (middleware, service providers, controllers).
  • plugin_enqueue_script(string $handle, string $src, string $context = 'frontend', array $deps = [], bool $in_footer = true): void — Enqueue a JS file from anywhere.
  • plugin_enqueue_inline_style(string $handle, string $css, string $context = 'frontend'): void — Inject an inline CSS snippet from anywhere.
  • plugin_enqueue_inline_script(string $handle, string $js, string $context = 'frontend', bool $in_footer = true): void — Inject an inline JS snippet from anywhere.
Warning

💡 Dependency resolution: Platform handles (jquery, alpine, toastr, sweetalert2, tailwind) are always available and never re-enqueued. Your custom deps are sorted topologically before output. First registration wins — duplicate handles are silently ignored.

Dashboard Menus

  • add_menu(array $config): void — Register a top-level sidebar menu item. See §7 for config keys.
  • add_submenu(string $parent_id, array $config): void — Register a submenu item under an existing menu by its ID.

Settings

  • register_settings(array $fields): void — Declare your plugin's settings fields. The platform auto-generates a settings UI page. See §8 for field definitions.
  • get_option(string $key, mixed $default = null): mixed — Retrieve a stored setting value. Automatically scoped to the current tenant when in tenant context.
  • update_option(string $key, mixed $value): void — Persist a setting value. Scoped to tenant automatically.

Helpers

  • plugin_path(string $path = ''): string — Returns the absolute filesystem path to the plugin root, optionally joined with $path.
  • plugin_url(string $path = ''): string — Returns the public URL to the plugin's assets directory, suitable for use in enqueue_* calls.
  • is_licensed(): bool — Returns true if the plugin is pricing: "paid" and a valid license key has been entered and verified. Always returns true for free plugins.

Advanced

  • register_shortcode(string $tag, callable $handler): void — Register a shortcode tag. See §9.
  • schedule(string $frequency, callable $callback): void — Register a recurring task. See §10.
  • register_api_routes(callable $callback): void — Register REST API routes mounted under /api/v1/plugins/{id}/. Call from routes(), not boot(). See §12.
  • register_web_routes(callable $callback): void — Register standard Laravel web routes (admin pages, frontend, user dashboard, etc.). Call from routes(), not boot(). See §12.
  • register_export_tables(array $tables): void — Declare database tables to include in tenant data exports. Table names without prefix.

Lifecycle & Migrations

The plugin lifecycle gives you five precise moments to run code: route and settings registration for all plugins (routes), every request when active (boot), first activation (on_activate), deactivation (on_deactivate), and after an update ZIP is installed (on_update). Two built-in helpers — run_migrations() and run_rollback() — handle Laravel migration runs scoped to your plugin's directory.

Migration File Location

Place standard Laravel migration files under database/migrations/ inside your plugin directory. The helpers resolve this path automatically from plugin_path():

Modules/AcmeSeo/
└── database/
    └── migrations/
        ├── 2026_04_30_000001_create_acme_seo_meta_table.php
        └── 2026_04_30_000002_add_canonical_to_acme_seo_meta.php

Migration files follow the same format as any Laravel migration. The platform calls php artisan migrate --path=... --force internally — already-run migrations are skipped automatically (Laravel tracks them in the standard migrations table).

on_activate — First Activation

Called once when an admin enables the plugin. Use it to run migrations, write default settings, and seed any required data:

public function on_activate(): void
{
    // 1. Create / update database tables
    $this->run_migrations();

    // 2. Seed a required config row (insertOrIgnore = safe to call repeatedly)
    DB::table('acme_seo_config')->insertOrIgnore([
        ['key' => 'sitemap_limit',  'value' => '500'],
        ['key' => 'robots_default', 'value' => 'index,follow'],
    ]);

    // 3. Write default plugin options (stored in plugin_options table)
    $this->update_option('notify_email', '');
    $this->update_option('auto_generate_sitemap', '1');
}
Warning

💡 Always use insertOrIgnore() or updateOrInsert() for seed data. on_activate() can be called again if a user deactivates and re-activates the plugin — plain insert() would throw a duplicate-key error.

on_deactivate — Plugin Disabled

Called when an admin disables the plugin. The plugin's data is preserved — deactivation is not uninstallation. Use this hook to stop active processes, clear caches, and release locks:

public function on_deactivate(): void
{
    // Clear plugin-specific cache keys
    Cache::forget('acme_seo.sitemap');
    Cache::forget('acme_seo.robots');

    // Remove any queued jobs this plugin dispatched
    DB::table('jobs')
        ->where('queue', 'acme-seo')
        ->delete();

    // DO NOT drop tables — data must survive deactivation
    // DO NOT call run_rollback() here unless this is an explicit uninstall flow
}
Warning

⚠️ Never call run_rollback() in on_deactivate(). If an admin disables and re-enables a plugin, all stored data would be permanently lost. Only call run_rollback() in an explicit "uninstall / delete plugin data" action that the admin consciously triggers.

on_update — After ZIP Update

Called automatically after a new version's ZIP is extracted. The previous version string is passed so you can run version-specific data transforms:

public function on_update(string $from_version): void
{
    // run_migrations() is idempotent — new migration files run, old ones are skipped
    $this->run_migrations();

    // Version-gated data transforms
    if (version_compare($from_version, '1.2.0', '<')) {
        // Backfill a column added in 1.2.0
        DB::table('acme_seo_meta')
            ->whereNull('canonical_url')
            ->update(['canonical_url' => '']);
    }

    if (version_compare($from_version, '2.0.0', '<')) {
        // Rename a config key changed in 2.0.0
        DB::table('acme_seo_config')
            ->where('key', 'robots')
            ->update(['key' => 'robots_default']);
    }
}
Warning

💡 Always guard transforms with version_compare(). If a customer skips several versions (e.g. 1.0 → 2.1 directly), every applicable block will run in sequence, making the update path safe regardless of the starting version.

run_rollback — Full Data Removal

run_rollback() calls php artisan migrate:rollback --path=... scoped to your plugin's migrations, dropping your tables. Only use this for an explicit admin-triggered "Delete all plugin data" action — never in on_deactivate().

// Example: a dedicated "uninstall" admin action in your plugin controller
public function uninstall(): JsonResponse
{
    // 1. Deactivate first
    app(PluginManager::class)->deactivate($this->id());

    // 2. Drop all plugin tables
    $this->run_rollback();

    // 3. Remove stored options
    DB::table('plugin_options')
        ->where('plugin_id', $this->id())
        ->delete();

    return response()->json(['status' => 'uninstalled']);
}

Helper Reference

  • run_migrations(): void — Runs php artisan migrate --path=Modules/{YourPlugin}/database/migrations --force. Safe to call multiple times — already-run migrations are skipped. Call in on_activate() and on_update().
  • run_rollback(): void — Runs php artisan migrate:rollback --path=... scoped to your plugin's migrations. Drops your tables and destroys all plugin data. Never call from on_deactivate().

Lifecycle Summary

MethodWhen calledTypical useSafe to call run_migrations()?
routes()Every request — ALL plugins (active or not)Call register_settings() first, then register_web_routes() / register_api_routes()No
boot()Every request — active plugins onlyRegister hooks, menus, assets; call register_settings() again (idempotent)No — too slow for per-request
on_activate()Once on admin enableMigrate, seed defaults, write options✅ Yes
on_deactivate()Once on admin disableClear caches, cancel jobsNo — data must survive
on_update(string $v)After update ZIP installedMigrate new tables, transform data✅ Yes
Still stuck?
Our support team is ready to help you get set up.
Get support