Shortcodes
Shortcodes allow non-technical users to embed dynamic content in product descriptions, page builder content, and email templates.
Registering a Shortcode
$this->register_shortcode('loyalty_balance', function ($attrs, $content = null) {
$user = auth()->user();
if (!$user) return '';
$points = $user->loyalty_points ?? 0;
return '<span class="loyalty-points">' . $points . ' pts</span>';
});
Shortcode Syntax
<!-- Self-closing -->
[loyalty_balance /]
<!-- With attributes -->
[loyalty_balance color="gold" size="large" /]
<!-- With inner content -->
[my_tag class="highlight"]Your content here[/my_tag]
Manual Parsing
Use the global helper to parse shortcodes in arbitrary HTML strings:
$html = '<p>Your balance: [loyalty_balance /]</p>';
$rendered = parse_shortcodes($html);
Scope
Shortcodes are applied automatically in: product descriptions, page builder content blocks, and email templates.
Scheduled Tasks
Register recurring background tasks from within boot(). Callbacks are collected and injected into Laravel's scheduler, running as part of php artisan schedule:run.
Predefined Frequencies
everyMinute everyFiveMinutes hourly daily weekly monthly
public function boot(): void
{
// Using a named frequency
$this->schedule('daily', function () {
SyncLoyaltyPointsJob::dispatch();
});
// Using a cron expression — every Monday at 8 AM
$this->schedule('0 8 * * 1', function () {
WeeklyReportJob::dispatch();
});
}
💡 Ensure your server's crontab runs php artisan schedule:run every minute: * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
Routes: API & Web
Plugins have two route registration helpers: register_api_routes() for JSON/REST endpoints and register_web_routes() for standard web pages (admin, frontend, user dashboard). Both must be called from routes(), not boot(). The routes() method runs for all discovered plugins before tenancy middleware fires, ensuring routes are registered at the correct point in the request lifecycle.
API Routes
Endpoints are automatically mounted under /api/v1/plugins/{plugin-id}/ with the api middleware group (rate limiting, JSON formatting). Register from routes().
public function routes(): void
{
$this->registerSettings(); // always first
$this->register_api_routes(function ($router) {
// GET /api/v1/plugins/acme-seo/status
$router->get('/status', fn() => response()->json(['ok' => true]));
// POST /api/v1/plugins/acme-seo/sync
$router->post('/sync', [AcmeSeoController::class, 'sync']);
});
$this->register_web_routes(function () {
Route::middleware(['tenantUserMailVerify', 'package_expire', 'auth'])
->prefix('user-home/my-plugin')
->name('tenant.user.my-plugin.')
->group(function () {
Route::get('/', [MyPluginController::class, 'index'])->name('index');
Route::post('/save', [MyPluginController::class, 'save'])->name('save');
});
});
}
Web Routes
Use register_web_routes() for any standard Laravel web route — tenant admin pages, frontend pages, user dashboard pages, etc. Both API and web route helpers must be called from within your routes() method.
// Tenant admin routes example (inside routes())
$this->register_web_routes(function () {
Route::middleware(['auth:admin', 'tenantadmin'])
->prefix('admin/my-plugin')
->name('tenant.admin.my-plugin.')
->group(function () {
Route::get('/settings', [MyPluginController::class, 'settings'])->name('settings');
Route::post('/settings', [MyPluginController::class, 'saveSettings'])->name('settings.save');
});
});
⚠️ Do not use register_web_routes() for landlord admin routes. Admin routes for the landlord panel are registered by your Module's own ServiceProvider via the standard routes/web.php mechanism. register_web_routes() is intended for tenant-facing frontend and user dashboard pages.
User Dashboard Integration
After registering your web route, add an entry to the user dashboard sidebar and optionally a dashboard widget using the provided action hooks:
// In boot() — add sidebar link
$this->add_action('nazmart:user_dashboard_sidebar', function () {
$active = request()->routeIs('tenant.user.my-plugin.*') ? 'active' : '';
$url = route('tenant.user.my-plugin.index');
echo "<li class=\"list {$active}\"><a href=\"{$url}\"><i class=\"las la-star\"></i> My Plugin</a></li>";
});
// In boot() — add dashboard stat card (receives the authenticated user)
$this->add_action('nazmart:user_dashboard_home', function (mixed $user) {
if (!$user) return;
echo '<div class="col-xl-6 col-md-6 orders-child"> ... </div>';
});
ℹ️ Hook fire points: nazmart:user_dashboard_sidebar — fires inside the <ul> of the user dashboard sidebar nav, before the logout link. nazmart:user_dashboard_home — fires after the four core stat cards on the dashboard home page. Receives the authenticated $user object as the first argument.
Multi-Tenant Support
Nazmart's multi-tenancy model gives you fine-grained control over how your plugin behaves across different tenant shops.
Plugin Type Behaviour
| Type | Landlord boots? | Tenant boots? |
|---|---|---|
landlord | Yes | No |
tenant | No | Yes |
both | Yes | Yes |
Per-Tenant Settings
get_option() and update_option() automatically scope to the current tenant when running in tenant context. This means each tenant can have independent configuration.
Tenant Overrides
The landlord can control plugin availability per tenant via the plugin_tenant_overrides database table. Landlords can:
- Allow or deny specific plugins for specific tenants
- Allow tenants to self-manage (activate/deactivate) a plugin
- Set global defaults that apply to all tenants
Tenant Boot Timing
Plugins with "type": "tenant" (or "both") are not booted during the initial service provider boot phase, because tenancy middleware has not yet run at that point and tenant() returns null. Instead, the platform listens for the TenancyInitialized event — fired when the tenancy middleware resolves the current tenant from the request domain — and calls bootForTenant() at that moment.
What this means for your plugin:
- Web routes registered via
register_web_routes()are added to the router after tenancy is initialized, so they are available on every tenant request. - Hook callbacks registered via
add_action()/add_filter()insideboot()fire at the correct time — hooks themselves are still called during the HTTP request cycle. - CLI / queue / scheduler contexts do not trigger
TenancyInitializedvia HTTP. Tenant plugins may not boot automatically in those contexts. Use Stancl'stenancy()->initialize($tenant)explicitly when you need to run tenant plugin logic in queue jobs or scheduled commands.
⚠️ Never call tenant() directly inside boot() for tenant-type plugins. The method fires after tenancy initialization, but for safety (CLI contexts, test environments) always null-check: if (!function_exists('tenant') || !tenant()) return;
Safe Tenant Context Checks
public function boot(): void
{
// tenant() may be null during CLI commands — always null-check
$tenant = tenant();
if ($tenant === null) {
return;
}
// Safe to use tenant-scoped logic here
$this->add_action('nazmart:order_completed', function ($order) {
// runs for this tenant only
});
}
Data Export
Declare which of your custom tables should be included when a tenant exports their data:
$this->register_export_tables([
'acme_loyalty_points',
'acme_loyalty_transactions',
]);

