Routes, Views & Settings
Routes
Place in routes/web.php or routes/api.php. Auto-loaded by the framework.
Web routes (with web middleware — auth, CSRF, session):
Route::middleware('auth')->group(function () {
Route::post('my-plugin/action', [MyController::class, 'handle'])
->name('my-plugin.action');
});
API routes (with api middleware — no CSRF, no session):
Route::post('my-plugin/webhook', [MyWebhookController::class, 'handle'])
->name('my-plugin.webhook');
Admin routes:
Route::group([
'prefix' => 'admin/my-plugin',
'as' => 'admin.my-plugin.',
'middleware' => ['auth:admin', 'setlang'],
], function () {
Route::get('/', [MyAdminController::class, 'index'])
->name('index')->permission('my-plugin-manage');
});
Views
Place Blade files in views/. Access as view('my-plugin::filename').
// From a slot renderer
$this->ctx->slots->register('page.banner', 'my-plugin.banner', function (array $ctx): string {
ob_start();
require __DIR__ . '/../views/banner.php';
return ob_get_clean();
});
Database Migrations
Place in database/migrations/. Auto-loaded by artisan migrate.
// database/migrations/2026_01_01_000001_create_my_tables.php
return new class extends Migration {
public function up(): void
{
Schema::create('my_plugin_data', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('value');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('my_plugin_data');
}
};
Rules:
- Prefix table names with plugin slug to avoid collisions
- Always provide
down()for rollback - Migrations run in the core database
Settings Storage
Option A: static_options Table (Recommended)
// Read
$value = get_static_option('my_plugin_api_key', 'default');
// Write
update_static_option('my_plugin_api_key', $newValue);
Option B: Config File
Place config/my-plugin.php, access via config('plugins.my-plugin.key').
Option C: Plugin-Owned Tables
Create your own tables via migrations. Use Eloquent models within your plugin namespace.
Admin Menu Registration
Via plugin.json (Recommended)
{
"xilancerMetaData": {
"admin_settings": {
"menu_item": [
{
"id": "my-plugin-menu",
"label": "My Plugin",
"route": "admin.my-plugin.index",
"icon": "fa-solid fa-star",
"permissions": "my-plugin-manage"
}
]
}
}
}
Via Registry (Programmatic)
$this->ctx->registry->register('admin.menu', 'my-plugin', [
'label' => 'My Plugin',
'route' => '/admin/my-plugin',
'icon' => 'fa-solid fa-star',
'permissions' => 'my-plugin-manage',
]);
Complete Example: Discount Coupon Plugin
plugins/coupons/
├── plugin.json
├── src/
│ ├── CouponsPlugin.php
│ ├── Coupon.php
│ ├── CouponRedemption.php
│ ├── CouponStore.php
│ ├── DbCouponStore.php
│ └── Http/
│ └── CouponApplyController.php
├── routes/web.php
├── database/migrations/
└── views/
Entry class:
final class CouponsPlugin implements PluginInterface
{
public function __construct(private PluginContext $ctx) {}
public function boot(): void
{
// Hook: modify checkout price when coupon applied
$this->ctx->hooks->addFilter('checkout.price.calculate',
[CouponApplyController::class, 'applyDiscount']
);
// Slot: inject coupon field into checkout
$this->ctx->slots->register('checkout.coupon_field', 'coupons.field',
fn(array $ctx): string => view('coupons::coupon-field', $ctx)->render()
);
// Admin menu
$this->ctx->registry->register('admin.menu', 'coupons', [
'label' => 'Coupons',
'route' => '/admin/coupons',
'icon' => 'fa-solid fa-ticket',
'permissions' => 'coupons-manage',
]);
}
}
Last updated: September 2026
Still stuck?
Our support team is ready to help you get set up.

