Plugin Development Guide
Gäld supports a plugin system for extending functionality without modifying the core application. Plugins are auto-discovered from the plugins/ directory.
Plugin Structure
plugins/
└── my-plugin/
├── plugin.json # Required manifest
├── composer.json # Optional (for external deps)
├── src/
│ └── MyPluginServiceProvider.php
├── routes/
│ └── web.php
├── resources/
│ └── views/
└── migrations/
plugin.json (Manifest)
Every plugin must have a plugin.json at its root. All fields shown below are supported:
{
"name": "My Plugin",
"slug": "my-plugin",
"version": "1.0.0",
"description": "Description of what this plugin does.",
"author": "Your Name",
"provider": "Plugins\\MyPlugin\\MyPluginServiceProvider",
"enabled": true,
"requires": []
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable plugin name |
slug | string | Yes | Unique identifier (kebab-case, must match directory name) |
version | string | No | Semantic version |
description | string | No | Short description |
author | string | No | Author name |
provider | string | Yes | Fully qualified class name of the service provider |
enabled | boolean | No | Set to false to disable without removing (default: true) |
requires | string[] | No | Array of plugin slugs this plugin depends on |
The provider class must be under the Plugins\\ root namespace (e.g. Plugins\\MyPlugin\\...). The plugin loader enforces this — any other namespace will be rejected.
Service Provider
The service provider is a standard Laravel ServiceProvider. Use it to register routes, views, migrations, and bindings:
<?php
namespace Plugins\MyPlugin;
use Illuminate\Support\ServiceProvider;
class MyPluginServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind services into the container
}
public function boot(): void
{
// Load routes
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
// Load views with a namespace
$this->loadViewsFrom(
__DIR__.'/../resources/views',
'my-plugin'
);
// Load migrations
$this->loadMigrationsFrom(__DIR__.'/../migrations');
}
}
Auto-Discovery
Plugins are automatically discovered at application boot. The PluginServiceProvider scans the plugins/ directory for subdirectories containing a plugin.json. No manual registration is needed.
The process:
- Read
plugin.json— validateproviderandslugare present - Skip if
enabledisfalse - Resolve dependencies listed in
requires(loaded first) - Register a PSR-4 autoloader mapping
Plugins\PluginName\→plugins/plugin-name/src/ - Register the service provider
Configuration
Plugin system settings are in config/plugins.php:
return [
'enabled' => env('PLUGINS_ENABLED', true), // Master switch
'path' => base_path('plugins'), // Scan path
'namespace' => 'Plugins', // Root namespace
];
Set PLUGINS_ENABLED=false in .env to disable all plugins.
Dependencies
Plugins can depend on other plugins via the requires field:
{
"slug": "advanced-reports",
"requires": ["my-base-plugin"]
}
The loader resolves the dependency graph automatically and boots plugins in the correct order. Circular dependencies are detected and rejected — both plugins will fail to load with an error logged.
Composer Dependencies
If your plugin needs external packages, add a composer.json to your plugin directory and run composer install inside it. The plugin loader automatically loads vendor/autoload.php from each plugin directory if present.
plugins/my-plugin/
├── composer.json
├── vendor/ ← Created by composer install
├── plugin.json
└── src/
Accessing Core Services
Plugins can inject and use any core Gäld service:
use App\Domains\Accounting\Services\LedgerService;
use App\Domains\Invoicing\Models\Invoice;
class MyPluginServiceProvider extends ServiceProvider
{
public function boot(LedgerService $ledger): void
{
// Use core services
}
}
Listening to Domain Events
Plugins can listen to Laravel events dispatched by the core application. Register listeners in your service provider's boot() method:
use Illuminate\Support\Facades\Event;
use App\Domains\Invoicing\Events\InvoiceFinalized;
public function boot(): void
{
Event::listen(InvoiceFinalized::class, function ($event) {
// React to invoice finalization
$invoice = $event->invoice;
});
}
Best Practices
- Namespace: Always use
Plugins\YourPlugin\as your root namespace - Slug: Use kebab-case matching your directory name (
my-plugin/) - Routes: Prefix your routes to avoid conflicts (e.g.
/my-plugin/...) - Views: Use the view namespace (
'my-plugin') soview('my-plugin::dashboard')is unambiguous - Migrations: Prefix table names to avoid collisions (e.g.
mp_settings) - Organisation scoping: Use the
BelongsToOrganizationtrait on your models to respect multi-tenancy