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 callregister_settings()here first — this is what makes the Settings button visible on the plugin card, even before the plugin is activated. Also callregister_web_routes()andregister_api_routes()here. Do not call these inboot(). - 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 withif (!function_exists('tenant') || !tenant()) return;to avoid crashes in CLI and landlord contexts. Do not callregister_web_routes()orregister_api_routes()here — useroutes()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
$argsto 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
falsefor$in_footerto 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 = {...}). Passfalsefor$in_footerto 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.
💡 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
trueif the plugin ispricing: "paid"and a valid license key has been entered and verified. Always returnstruefor 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 fromroutes(), notboot(). See §12. - register_web_routes(callable $callback): void — Register standard Laravel web routes (admin pages, frontend, user dashboard, etc.). Call from
routes(), notboot(). 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');
}
💡 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
}
⚠️ 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']);
}
}
💡 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 inon_activate()andon_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 fromon_deactivate().
Lifecycle Summary
| Method | When called | Typical use | Safe 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 only | Register hooks, menus, assets; call register_settings() again (idempotent) | No — too slow for per-request |
on_activate() | Once on admin enable | Migrate, seed defaults, write options | ✅ Yes |
on_deactivate() | Once on admin disable | Clear caches, cancel jobs | No — data must survive |
on_update(string $v) | After update ZIP installed | Migrate new tables, transform data | ✅ Yes |

