Search by

adminbolt / plugin-sdk

bobicloudvision

PHP SDK for building AdminBolt panel plugins

Package info

github.com/AdminBolt/plugin-sdk

pkg:composer/adminbolt/plugin-sdk

Statistics

Installs: 32

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.7 2026-09-11 16:35 UTC

This package is auto-updated.

Last update: 2026-09-11 16:36:41 UTC


README

PHP SDK for building AdminBolt panel plugins.

A plugin is an ordinary PHP application. It does not extend a panel class, it is not loaded by the panel's autoloader, and it does not share a process with the panel. It speaks two contracts: it receives signed hook deliveries, and it calls the panel's REST APIs with a scoped key. That is the whole coupling.

Which means a plugin ships on its own schedule, with no panel release, and a bug in it cannot take the panel down.

composer require adminbolt/plugin-sdk

Requires PHP 8.2 and ext-curl. Nothing else: no framework, no HTTP library.

A plugin

<?php

use AdminBolt\Plugin\Hook\Hook;
use AdminBolt\Plugin\Hook\HookRequest;
use AdminBolt\Plugin\Hook\HookResponse;
use AdminBolt\Plugin\Plugin;

require __DIR__ . '/../vendor/autoload.php';

$plugin = Plugin::boot(__DIR__);

// Runs inside the operation and can stop it.
$plugin->on(Hook::DOMAIN_CREATING, function (HookRequest $hook) {
    if (str_ends_with((string) $hook->payload('domain'), '.test')) {
        return HookResponse::reject('.test domains cannot be hosted here.');
    }

    return HookResponse::ok();
});

// Runs after it succeeded, and calls back into the panel.
$plugin->on(Hook::DOMAIN_CREATED, function (HookRequest $hook) use ($plugin) {
    $plugin->clientFor($hook)->dnsRecords()->createRecord(
        domainId: (int) $hook->payload('id'),
        type: 'TXT',
        name: '_verify',
        content: 'provisioned',
    );
});

$plugin->run();

Plus a plugin.json naming the plugin, the entrypoint, the hooks it wants and the API scopes it needs. That is a complete plugin.

Start here

composer create-project adminbolt/plugin-starter my-plugin

Or scaffold one with the CLI:

bolt-plugin new my-plugin
bolt-plugin validate
bolt-plugin hook domain.creating --payload='{"domain":"example.test"}'

Documentation

Hooks in one paragraph

Hooks are <resource>.<verb>, and the tense tells you the phase. Blocking hooks are present participles like domain.creating: they run inside the operation and can veto it with a message the user sees, or adjust an allow-listed input. Notification hooks are past participles like domain.created: they run once it has succeeded, are queued and retried, and cannot change anything. Every delivery is HMAC signed over the raw body with the timestamp inside the signed string, so a captured delivery cannot be replayed. A veto is HTTP 200 with "status": "reject", never a 4xx, because a 4xx is indistinguishable from a broken listener.

The full catalogue is in docs/hooks.md.

Calling the panel

$plugin->admin()->hostingAccounts()->findByUsername('acme');
$plugin->client('acme')->domains()->all();
$plugin->client('acme')->sslCertificates()->issue($domainId);
$plugin->clientFor($hook)->files()->write('/public_html/robots.txt', "User-agent: *\n");

Typed resources cover accounts, plans, resellers, domains, DNS, email, databases, FTP, cron, IP blockers, TLS, files, WordPress and services. Anything else is $plugin->admin()->raw()->get('system-updates'), signed and retried identically.

The key is minted by the panel at install time and scoped to the endpoints the manifest declared. A call outside them comes back 403. Ask for the minimum: the scopes are shown to the administrator approving the install.

Running something in an account

A plugin can run php artisan migrate or composer install for the account whose page it is drawing. It cannot run a command: it declares the ones it needs, an administrator approves them by name and by argument, and the panel builds every command line from its own copy of what was approved.

"commands": [
    {
        "name": "artisan",
        "program": "php",
        "args": ["artisan", "{command}", "--no-interaction"],
        "params": { "command": { "type": "enum", "values": ["migrate:status", "optimize"] } },
        "cwd": "required"
    }
]
$result = $plugin->clientFor($request)->cli()->run('artisan', ['command' => 'optimize'], cwd: 'shop');

$result->ok();
$result->output();

A non-zero exit is a result, not an exception: a failed migration has something to tell the customer and it is on stdout. Anything that outlives a request is async and polled instead.

docs/commands.md has the rest, including what the panel checks before it spawns anything.

Testing

The runtime is a pure function of headers and a body, so the whole delivery path is testable with no panel and no web server.

use AdminBolt\Plugin\Testing\FakeHttpClient;

$plugin = Plugin::create($manifest, $config, http: new FakeHttpClient());
$result = $plugin->httpRuntime()->handle('POST', '/', $headers, $body);

self::assertSame('reject', $result->json()['status']);

FakeHttpClient is shipped, not a fixture to copy, and it stands in for both the panel and whatever third-party service the plugin talks to through $plugin->http().

composer test

Related

License

MIT.