plugin-sdk / wp
Plugin SDK — WordPress runtime: declarative manifest loader, fluent Settings API, REST routes with JSON-schema validation, dbDelta migrations, plus admin-UI render helpers.
Requires
- php: >=7.4
- ext-json: *
This package is auto-updated.
Last update: 2026-05-25 22:22:34 UTC
README
Read-only mirror. This repository is auto-generated from the
packages/wp-composer/subdirectory of artificialpoets/plugin-sdk by agit subtree splitGitHub Action on every tagged release.File issues + PRs against the main repo, not here. The mirror is rebuilt on every release; any commits pushed directly here will be overwritten.
The WordPress runtime for Plugin SDK — a declarative manifest loader, fluent Settings/REST/Migration APIs, plus the admin-UI render helpers. Drop a plugin-sdk.json in your plugin root, call Plugin::fromManifest() once, and the SDK wires up:
- the settings page (with capability check + sanitisation per field),
- the REST API (with cap check + JSON-schema body validation),
- and the database tables (rendered to dbDelta SQL, run on activate),
…then gets out of your way for handlers and business logic.
Pair with @plugin-sdk/wp-core-css for the stylesheet.
Install
composer require plugin-sdk/wp
Requires PHP 7.4 or later.
Declarative quickstart
Add a plugin-sdk.json to your plugin root:
{
"$schema": "https://cdn.wp-admincss.com/wordpress/plugin-sdk.schema.json",
"platform": "wordpress",
"name": "Acme Forms",
"slug": "acme-forms",
"textDomain": "acme-forms",
"version": "0.1.0",
"namespace": "Acme\\Forms",
"settings": {
"page": {
"title": "Acme Forms",
"capability": "manage_options",
"sections": [{
"id": "general",
"title": "General",
"fields": [
{ "id": "api_key", "label": "API Key", "type": "password", "required": true }
]
}]
}
},
"rest": {
"namespace": "acme-forms/v1",
"routes": [{
"path": "/submissions",
"method": "POST",
"capability": "manage_options",
"handler": "Acme\\Forms\\REST\\Submissions::create",
"schema": {
"type": "object",
"required": ["email"],
"properties": { "email": { "type": "string", "format": "email" } }
}
}]
},
"database": {
"tables": [{
"name": "submissions",
"columns": [
{ "name": "id", "type": "BIGINT UNSIGNED", "primary": true, "autoIncrement": true },
{ "name": "email", "type": "VARCHAR(255)", "notNull": true },
{ "name": "created_at", "type": "DATETIME", "notNull": true, "default": "CURRENT_TIMESTAMP" }
]
}]
}
}
In your plugin's main PHP file:
<?php use PluginSDK\WP\Plugin; use Acme\Forms\REST\Submissions; add_action('plugins_loaded', function () { Plugin::fromManifest(__DIR__ . '/plugin-sdk.json', __FILE__) ->withRestHandler('Acme\\Forms\\REST\\Submissions::create', [Submissions::class, 'create']) ->boot(); });
That's the whole bootstrap. Errors in the manifest throw ConfigException with the full validation trace, so you fail fast at boot time instead of hitting a half-broken admin page later.
Fluent runtime (imperative layer)
The declarative loader compiles to the same fluent calls — you can use either layer or both.
use PluginSDK\WP\Settings; use PluginSDK\WP\Settings\Field; use PluginSDK\WP\REST; use PluginSDK\WP\Migration; use PluginSDK\WP\Migration\Table; (new Settings('acme-forms', 'Acme Forms')) ->capability('manage_options') ->section('general', 'General') ->field('api_key', 'API Key', Field::TYPE_PASSWORD, required: true) ->section('advanced', 'Advanced') ->field('endpoint', 'Endpoint', Field::TYPE_URL); (new REST('acme-forms/v1')) ->route('/submissions', 'POST') ->capability('manage_options') ->schema(['type' => 'object', 'required' => ['email'], 'properties' => ['email' => ['type' => 'string', 'format' => 'email']]]) ->setHandler([Submissions::class, 'create']); (new Migration('acme_forms_db_version', '0.1.0', 'acme_')) ->addTable(new Table('submissions', [ ['name' => 'id', 'type' => 'BIGINT UNSIGNED', 'primary' => true, 'autoIncrement' => true], ['name' => 'email', 'type' => 'VARCHAR(255)', 'notNull' => true], ]));
Admin UI render helpers
The package also includes the original render helpers — useful for hand-rolled admin pages that don't fit the settings model:
<?php use PluginSDK\WP\Components; use PluginSDK\WP\Assets; // Enqueue the stylesheet (from CDN, or pass a local URL) add_action('admin_enqueue_scripts', [Assets::class, 'enqueue_cdn']); // Render a settings page function my_plugin_render_settings_page(): void { echo Components::wrap( Components::page_header('My Plugin Settings', [ 'action' => ['label' => 'Add New', 'href' => '?page=add-new'], ]) . Components::notice_success('Settings saved.', ['dismissible' => true]) . Components::form_table( Components::form_row('API Key', Components::input(['name' => 'api_key', 'id' => 'api-key']), ['for' => 'api-key', 'description' => 'Find your key in the dashboard.'] ) . Components::form_row('Notifications', Components::toggle([ 'name' => 'notify', 'label' => 'Email me when events fire', 'checked' => true, ]) ) ) . Components::submit( Components::button('Save Changes', ['variant' => 'primary', 'type' => 'submit']) ) ); }
The rendered HTML uses real WordPress admin class names — .wrap, .notice notice-success is-dismissible, .form-table, .button button-primary, etc. — so plugin admin pages look indistinguishable from a native WP screen and inherit the user's color scheme automatically.
Available helpers
All methods live on \PluginSDK\WP\Components as static methods. Each underlying renderer is also available directly (e.g. \PluginSDK\WP\Components\Button::render()).
Actions
button(string $label, array $args = [])— variantsprimary/secondary/link/link-delete, sizessmall/large/hero. Passhtml_labelto use pre-rendered HTML (e.g. for icons + text).
Icons
dashicon(string $icon, array $args = [])— WordPress's native icon font. Pass any name from the Dashicons catalog, e.g.'admin-users'or'edit'. Sizes:'small'(16px), default (20px),'large'(28px).icon(string $name, array $args = [])— Inline SVG, the modern WP direction. 20 built-in names (plus / minus / close / check / chevrons / arrows / edit / trash / search / more-horizontal / more-vertical / info / warning / external-link). Passsvg_htmlto supply your own inline SVG content.
Notice
notice(string $variant, string $message_html, array $args)— pass pre-escaped HTMLnotice_success(string $message, array $args)— convenience wrapper that escapes a single-line messagenotice_error(),notice_warning(),notice_info()
Forms
input(array $args)— widthregular/large/small, type, name, value, etc.textarea(array $args)select(array $options, array $args)form_table(string $rows_html),form_row(string $label, string $control_html, array $args)description(string $text)—<p class="description">help_tip(string $tip)—?icon with hover tooltiptoggle(array $args)— Gutenberg-style switchsubmit(string $children_html)—<p class="submit">wrapper
Navigation
nav_tabs(array $tabs)—[['label' => …, 'href' => …, 'active' => true], …]subsubsub(array $items)— status filter row
Layout
wrap(string $inner_html),page_header(string $title, array $args)two_column(string $main_html, string $sidebar_html)screen_options(string $children_html, bool $open = false)help_tabs(array $tabs, ?string $active_id = null)
Data display
postbox(string $title, string $inside_html)welcome_panel(array $args)stat_card(array $args)—label,value,delta,trend(up/down)activity_item(array $args)
Tables
list_table(string $inner_html, array $args),row_actions(array $actions)bulk_actions(array $actions, array $args)search_box(array $args),pagination(array $args)empty_state(array $args)
Feedback
status_badge(string $label, string $variant)—active/error/warning/info/neutralspinner(bool $active = true)pointer(array $args),skeleton(string $variant)
Asset enqueue
Assets::enqueue_cdn()— loads the Plugin SDK bundle fromhttps://cdn.wp-admincss.com/css/latest.cssAssets::enqueue_local(string $url)— loads the Plugin SDK bundle from a URL you controlAssets::enqueue_dashicons()— callswp_enqueue_style('dashicons'). Prefer this over the bundled CSS when inside WordPress; avoids the external CDN request.
Example
See examples/settings-page.php for a complete plugin admin page demonstrating most components.
Security
The declarative runtime bakes in capability checks (via current_user_can), nonce verification (settings forms use WP's options.php round-trip), and per-field sanitisation. REST routes run JSON-schema validation before the handler executes — a malformed body is rejected with a rest_invalid_body 400 response, never reaching your code.
Every render helper accepting user-controllable strings (label, title, message, URL, attribute value) runs them through esc_html() / esc_attr() / esc_url() (with safe fallbacks when WordPress functions aren't loaded). Methods that take pre-rendered HTML (e.g. notice() with $message_html) are documented as such — escape inputs yourself with \PluginSDK\WP\Html::esc() if they're user-controllable.
Tests
Zero-dep PHP test harness — no PHPUnit needed:
composer test # or directly: php tests/run.php
Tests run against WP-function stubs that capture invocations, so the runtime can be unit-tested without WordPress installed.
License
Apache 2.0. See the LICENSE at the repo root.