View .md

Plugin settings

How FeatherPanel plugin settings work. The conf.yml config form, reading and writing values with PluginSettings, required settings, events and visibility rules.

On this page

Each plugin gets its own key and value store in the featherpanel_addons_settings table. Admins edit it through a form built from the config: block of conf.yml, and your code reads and writes it with App\Plugins\PluginSettings.

The settings form

The Settings tab of Admin > Plugins > your plugin renders one field per entry in config::

config:
  - name: api_url
    display_name: 'API URL'
    type: url
    description: 'Base URL of the remote service.'
    required: true
    validation: {}
    default: 'https://api.example.com'
  - name: api_key
    display_name: 'API key'
    type: password
    description: 'Stored as plain text in the panel database.'
    required: true
    validation: {}
    default: ''
  - name: show_banner
    display_name: 'Show banner'
    type: boolean
    description: 'Show the banner widget on the dashboard.'
    required: false
    validation: {}
    default: 'false'

How each key is used by the form:

KeyEffect
nameThe setting key your code reads.
display_nameField label.
typeboolean renders a checkbox that saves 'true' or 'false'. password renders a masked input and number a number input. Every other type, including text, email and url, renders a plain text input.
descriptionHelp text. For a boolean it is the checkbox label.
requiredShows a “required” badge. On v1.5.2 and earlier the form does not block saving an empty value.
defaultShown as the placeholder of an empty text field. On v1.5.2 and earlier it is never saved.
validationNot used on v1.5.2 and earlier.

Save posts every value the form holds, one request per key. A field the admin never touched and that has no stored value is not sent.

Read and write settings

use App\Plugins\PluginSettings;

// One value, or null when the key was never set or was removed.
$url = PluginSettings::getSetting('myplugin', 'api_url') ?? 'https://api.example.com';

// Booleans are the strings 'true' and 'false'.
$showBanner = PluginSettings::getSetting('myplugin', 'show_banner') === 'true';

// Create or update.
PluginSettings::setSetting('myplugin', 'last_sync', (string) time());

// Structured data: store JSON.
PluginSettings::setSetting('myplugin', 'servers', json_encode([12, 15]));
$servers = json_decode(PluginSettings::getSetting('myplugin', 'servers') ?? '[]', true);

// Remove a key.
PluginSettings::deleteSettings('myplugin', 'last_sync');

// All rows of a plugin: each row has key, value, locked, deleted and date.
$rows = PluginSettings::getSettings('myplugin');

Details from the implementation:

  • Identifier and key must be non-empty and at most 255 characters, or an InvalidArgumentException is thrown.
  • Values are cast to string and stored as they are, so JSON survives a round trip.
  • deleteSettings() marks the row as deleted instead of removing it. getSetting() and getSettings() skip deleted rows, and a later setSetting() on the same key brings the row back.
  • There is no cache in front of the table, so a value saved in the admin area is visible on the next read.

Seed defaults in pluginInstall(), and keep a fallback in code for installs where the hook did not run (a plugin copied into storage/addons by hand never runs it):

public static function pluginInstall(): void
{
    foreach (['api_url' => 'https://api.example.com', 'show_banner' => 'false'] as $key => $value) {
        if (PluginSettings::getSetting('myplugin', $key) === null) {
            PluginSettings::setSetting('myplugin', $key, $value);
        }
    }
}

On an update from the admin area, the panel saves the current settings before it replaces the files and restores them afterwards, so an update does not reset what the admin entered.

Required settings

List keys that must be present in plugin.requiredConfigs:

plugin:
  # ...
  requiredConfigs:
    - api_url
    - api_key

On v1.5.2 and earlier the check only looks for a stored, non-deleted row with that key, and an empty string counts as present. From v1.5.3 an empty value counts as missing, config: fields with required: true count too, and the admin API returns plugin.configured: false (fixed in v1.5.3, PR #365). When a key is missing:

  • the plugin still loads and its routes still answer,
  • the admin plugin page lists it, and GET /api/admin/plugins/{identifier}/config returns it in plugin.missingConfigs,
  • the developer validate endpoint reports required_configs_set: false,
  • the panel logs a Missing required config ... warning each time it checks.

Your code is responsible for refusing to work without the value, for example by returning an error from a route.

The admin settings API

The settings tab uses these routes. You can call them from your own admin page too.

Method and pathPermissionBody
GET /api/admin/plugins/{identifier}/configadmin.plugins.viewNone. Returns plugin (with loaded, unmetDependencies and missingConfigs), settings, configSchema and allowedOnlyOnSpells.
POST /api/admin/plugins/{identifier}/settings/setadmin.plugins.manage{"key": "...", "value": ...}. The value must be a string, number or boolean. From v1.5.3 (PR #365), a value that fails required or validation returns 400 PLUGIN_SETTING_INVALID.
POST /api/admin/plugins/{identifier}/settings/removeadmin.plugins.manage{"key": "..."}

Both write routes add an entry to the activity log.

React to changes

The write routes emit an event after the value is stored. The listener receives the payload values in order:

use App\Plugins\Events\Events\PluginsSettingsEvent;

public static function processEvents(PluginEvents $event): void
{
    $event->on(PluginsSettingsEvent::onPluginSettingUpdate(), function (string $identifier, string $key, $value): void {
        if ($identifier !== 'myplugin') {
            return; // The event fires for every plugin.
        }
        // Clear a cache, reconnect to the API and so on.
    });

    $event->on(PluginsSettingsEvent::onPluginSettingDelete(), function (string $identifier, string $key): void {
        // ...
    });
}
EventNamePayload
PluginsSettingsEvent::onPluginSettingUpdate()plugins:settings:updateidentifier, key, value
PluginsSettingsEvent::onPluginSettingDelete()plugins:settings:deleteidentifier, key

Only the admin routes emit these events. A call to PluginSettings::setSetting() from your own code does not. If a listener throws, the value is already saved but the admin sees “Failed to update setting”.

Show or hide frontend parts from a setting

Sidebar entries, widgets, public pages and UI pack items accept enabled and hidden rules. A rule can read one of your settings, so an admin can switch a page on or off without code:

{
  "client": {
    "/myplugin": {
      "name": "My Plugin",
      "component": "index.html",
      "enabled": { "type": "plugin_setting", "key": "show_page", "equals": "true" }
    }
  }
}

A plugin_setting rule is false when the setting is missing or empty, and equals defaults to 'true'. The full rule syntax is on Widget slots.

The panel stores two kinds of settings of its own under your identifier. Do not use these keys for anything else:

KeyWritten by
plugin-visibility-hidden-<scope>-<sha1 of the item id>The Visibility tab, when an admin hides a sidebar entry, widget, public page or UI pack item. The value is 'true' or 'false'.
plugin-sidebar-server-allowedOnlyOnSpellsThe Spells tab. A JSON list of spell ids; your server sidebar entries only show on servers that use one of them.

Frequently asked questions

Is the default in conf.yml saved when the plugin is installed?

Not on v1.5.2 and earlier. There the form shows default as the placeholder of an empty field and never saves it, so store real defaults in pluginInstall() with PluginSettings::setSetting(), or fall back in code when getSetting() returns null. Saving defaults for keys that are not stored yet is fixed in v1.5.3 (PR

Does a missing required setting stop the plugin from loading?

No. The plugin still loads. The admin plugin page lists the missing keys, and the validate endpoint reports required_configs_set as false. From v1.5.3 (PR

Can I store JSON in a setting?

Yes. Values are stored as raw strings, so json_encode() on write and json_decode() on read works. The panel itself stores the spell restriction list this way.