Plugins
A plugin is how a Composer package adds pages, scripts, stylesheets, texts and routes to a panel, without the application copying anything. The application decides which panels it is enabled on. Available since 1.2.
Panel::make('admin')
->plugins([BillingPlugin::make()])The same plugin cannot be registered twice on one panel: Arrel throws an exception.
Writing a plugin
A plugin extends Arrel\Plugins\Plugin. Only id() is required; every other method returns an empty list unless you override it.
use Arrel\Pages\CustomPage;
use Arrel\Plugins\Plugin;
final class BillingPlugin extends Plugin
{
public static function make(): self
{
return new self;
}
public function id(): string
{
return 'billing';
}
public function customPages(): array
{
return [
CustomPage::make('billing', __('billing::billing.navigation_label'))
->icon('credit-card')
->visible(fn (): bool => Gate::allows('billing.manage')),
];
}
public function scripts(): array
{
return [asset('vendor/billing/billing.js')];
}
public function stylesheets(): array
{
return [asset('vendor/billing/billing.css')];
}
public function translations(): array
{
return ['title' => __('billing::billing.title')];
}
}| Method | What it contributes |
|---|---|
id() | The plugin's identifier. It prefixes its texts and its routes. |
customPages() | Custom pages, with their navigation entry. |
scripts() | Scripts the panel loads after its own. |
stylesheets() | Stylesheets the panel loads after its own. |
translations() | Texts for the frontend. |
The panel reads these methods every time it serves a page, not when the plugin is registered. That is why the order of plugins() and customPages() in the panel definition does not matter, and why __() already returns the text in the user's language.
The page
The plugin's script registers its page the same way the application would, usually with registerMountablePage and its own Vue instance. If the package uses Arrel's component kit, it bundles it into its own build.
If you build the script with Vite in library mode, define process.env.NODE_ENV. Vite does not replace it in that mode and Vue reads it: without the define, the script fails as it loads and the page is never registered.
// vite.config.js
export default defineConfig({
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
build: {
lib: { entry: 'resources/js/main.ts', formats: ['es'], fileName: () => 'billing.js' },
},
})Texts
The texts from translations() reach window.Arrel.translations prefixed with the plugin's identifier. So title from a billing plugin becomes billing.title, and it never replaces a panel text.
const title = window.Arrel.translations?.['billing.title']PHP texts (validation messages, the navigation) are loaded by the package with loadTranslationsFrom(), like any Laravel package. Arrel is not involved.
Routes
ArrelRoutes::plugin() registers the package's routes behind the same middleware stack as Arrel's authenticated API, including arrel.middleware and arrel.authenticated_middleware. If your application identifies the tenant in that middleware, the plugin's routes already have it.
use Arrel\Http\ArrelRoutes;
ArrelRoutes::plugin('billing', function (): void {
Route::get('/', [BillingController::class, 'context'])->name('context');
Route::post('checkout', [BillingController::class, 'checkout'])->name('checkout');
});The routes live under /arrel-plugins/{id} and are named arrel.plugins.{id}.*: the example serves GET /arrel-plugins/billing named arrel.plugins.billing.context.
The prefix sits outside /arrel/api and the panel prefix on purpose. Arrel's routes under them catch any path, and the order in which Laravel boots packages is not guaranteed: a route a package registered afterwards would be shadowed.
As with any endpoint of your own, authorize every request. The fact that visible() hides the navigation entry does not stop someone from calling the route by hand.