# Plugin settings

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

Source: https://featherpanel.com/docs/plugins/settings/

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:`:

```yaml
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:

| Key | Effect |
| --- | --- |
| `name` | The setting key your code reads. |
| `display_name` | Field label. |
| `type` | `boolean` 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. |
| `description` | Help text. For a boolean it is the checkbox label. |
| `required` | Shows a "required" badge. On v1.5.2 and earlier the form does not block saving an empty value. |
| `default` | Shown as the placeholder of an empty text field. On v1.5.2 and earlier it is never saved. |
| `validation` | Not used on v1.5.2 and earlier. |

> [!NOTE]
> Enforcing `required`, saving `default` and checking `type` and `validation` on save are fixed in v1.5.3 ([PR #365](https://github.com/MythicalLTD/FeatherPanel/pull/365)). The new
> behavior and the `validation` format (`regex`, `min`, `max`, `type`, `message`) are described in
> [conf.yml: validation rules](https://featherpanel.com/docs/plugins/manifest/#validation-rules). A rejected value returns status 400 with
> `error_code: PLUGIN_SETTING_INVALID`.

**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.

> [!WARNING]
> Values are stored as plain text and the admin config endpoint returns them to anyone with `admin.plugins.view`.
> `password` only masks the input on screen.

## Read and write settings

```php
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):

```php
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`:

```yaml
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](https://github.com/MythicalLTD/FeatherPanel/pull/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 path | Permission | Body |
| --- | --- | --- |
| `GET /api/admin/plugins/{identifier}/config` | `admin.plugins.view` | None. Returns `plugin` (with `loaded`, `unmetDependencies` and `missingConfigs`), `settings`, `configSchema` and `allowedOnlyOnSpells`. |
| `POST /api/admin/plugins/{identifier}/settings/set` | `admin.plugins.manage` | `{"key": "...", "value": ...}`. The value must be a string, number or boolean. From v1.5.3 ([PR #365](https://github.com/MythicalLTD/FeatherPanel/pull/365)), a value that fails `required` or `validation` returns 400 `PLUGIN_SETTING_INVALID`. |
| `POST /api/admin/plugins/{identifier}/settings/remove` | `admin.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:

```php
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 {
        // ...
    });
}
```

| Event | Name | Payload |
| --- | --- | --- |
| `PluginsSettingsEvent::onPluginSettingUpdate()` | `plugins:settings:update` | `identifier`, `key`, `value` |
| `PluginsSettingsEvent::onPluginSettingDelete()` | `plugins:settings:delete` | `identifier`, `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:

```json
{
  "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](https://featherpanel.com/docs/plugins/widgets/#visibility).

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

| Key | Written 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-allowedOnlyOnSpells` | The **Spells** tab. A JSON list of spell ids; your server sidebar entries only show on servers that use one of them. |

## Related

- [Manifest (conf.yml)](https://featherpanel.com/docs/plugins/manifest/)
- [Frontend and Power SDK](https://featherpanel.com/docs/plugins/frontend/)
- [Debugging plugins](https://featherpanel.com/docs/plugins/debugging/)

## 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.
