Skip to main content

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": []
}
FieldTypeRequiredDescription
namestringYesHuman-readable plugin name
slugstringYesUnique identifier (kebab-case, must match directory name)
versionstringNoSemantic version
descriptionstringNoShort description
authorstringNoAuthor name
providerstringYesFully qualified class name of the service provider
enabledbooleanNoSet to false to disable without removing (default: true)
requiresstring[]NoArray of plugin slugs this plugin depends on
Namespace requirement

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:

  1. Read plugin.json — validate provider and slug are present
  2. Skip if enabled is false
  3. Resolve dependencies listed in requires (loaded first)
  4. Register a PSR-4 autoloader mapping Plugins\PluginName\plugins/plugin-name/src/
  5. 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') so view('my-plugin::dashboard') is unambiguous
  • Migrations: Prefix table names to avoid collisions (e.g. mp_settings)
  • Organisation scoping: Use the BelongsToOrganization trait on your models to respect multi-tenancy