Skip to content

Typed settings

In this page you will learn how to declare a plugin’s settings with Woodev_Abstract_Settings, which setting and control types exist, what each argument does, and how to read and write values from PHP.

Every field on the settings page takes two calls inside register_settings():

CallDeclares
register_setting( $id, $type, $args )The value: its id, type, default, validation rules and visibility.
register_control( $id, $type, $args )The control: what the merchant sees and types into.
<?php
defined( 'ABSPATH' ) || exit;
class My_Carrier_Settings extends \Woodev_Abstract_Settings {
protected function register_settings(): void {
$this->register_setting( 'mode', \Woodev_Setting::TYPE_STRING, [ 'name' => __( 'Mode', 'my-carrier' ) ] );
$this->register_control( 'mode', \Woodev_Control::TYPE_SELECT );
}
}

Register a control for every setting you want to show. A setting only appears if a section lists its id (see Tabs, sections & cards), and without a control the page infers a plain one from the type: a toggle for a boolean, a select when there are options, otherwise a text input. State the control you mean and the intent is clear.

register_setting() takes one of the Woodev_Setting::TYPE_* constants.

ConstantValueStored and read back as
Woodev_Setting::TYPE_STRINGstringa string
Woodev_Setting::TYPE_URLurla string that must be a valid http:// or https:// URL
Woodev_Setting::TYPE_EMAILemaila string that must be a valid e-mail address
Woodev_Setting::TYPE_INTEGERintegeran int; numeric strings from the form are converted
Woodev_Setting::TYPE_FLOATfloata float; numeric strings from the form are converted
Woodev_Setting::TYPE_BOOLEANbooleana bool, stored as yes or no

All arguments are optional.

ArgumentMeaning
nameThe label shown above the field. A control can override it.
descriptionHelp text shown under the field. A control can override it.
defaultThe value used while nothing is stored. Defaults to null.
optionsAllowed values for a choice, as key => label. A flat list such as [ 'red', 'green' ] is also accepted for strings. Explicit [ 'value' => ..., 'label' => ... ] pairs preserve numeric values starting at zero.
is_multitrue when the setting holds a list of values. Each element is validated on its own.
requiredtrue to reject an empty value. Has no effect on a toggle, a checkbox or a range, which always carry a value.
validateA callable fn( $value ): bool, see Custom validation.
validate_messageThe message shown when validate returns anything but true.
show_ifConditions that decide when the field is shown, see Showing a field conditionally.
disabled_ifConditions that decide when the field is disabled. Same format as show_if.
sensitivetrue for a secret. The saved value is never sent to the browser.
constant_nameThe name of a PHP constant that supplies the value, see Secrets.

For an enum whose numeric keys start at zero, PHP makes a key => label array indistinguishable from a flat list. Use explicit pairs to keep submitted values separate from their labels:

'options' => [
[ 'value' => '0', 'label' => __( 'No', 'my-plugin' ) ],
[ 'value' => '1', 'label' => __( 'Yes', 'my-plugin' ) ],
],

Pairs work with every setting type; labels must be strings. Malformed or duplicate pairs report _doing_it_wrong() and reject every submitted value, even with WP_DEBUG off. An unchanged get_options() → set_options() round trip preserves the pair declaration.

Declare options on the setting. A control may narrow the list but cannot add keys the setting does not have.

register_control() takes one of the Woodev_Control::TYPE_* constants. Pick the one that fits the setting’s type.

ConstantValueUse it for
TYPE_TEXTtextA short string.
TYPE_TEXTAREAtextareaA longer string. Accepts rows.
TYPE_NUMBERnumberAn integer or float. Honours min, max and step.
TYPE_EMAILemailAn e-mail address.
TYPE_TELtelA phone number.
TYPE_URLurlA URL.
TYPE_PASSWORDpasswordA secret string, shown masked.
TYPE_DATEdateA date, stored as a string.
TYPE_CHECKBOXcheckboxA boolean.
TYPE_TOGGLEtoggleA boolean, drawn as a switch. Accepts description_on.
TYPE_RADIOradioOne choice from a short list.
TYPE_SELECTselectOne choice from a list.
TYPE_MULTISELECTmultiselectSeveral choices; the setting needs is_multi.
TYPE_SEARCH_SELECTsearch-selectOne choice found by searching on the server, for lists too long to show.
TYPE_FILEfileAccepted, but the settings page has no file picker and draws a text input. Do not use it yet.
TYPE_COLORcolorA colour, stored as a string such as #06aedd.
TYPE_RANGErangeA number chosen with a slider. Honours min, max and step.
TYPE_RICHTEXTrichtextFormatted text. The value is passed through wp_kses_post() before it is saved.
TYPE_BOXES_TABLEboxes-tableThe table of boxes used by shipping plugins. See Packaging and box packing.
TYPE_LOCATION_PICKERlocation-pickerThe default-locality search used by shipping plugins. See Location and checkout fields.

A toggle or checkbox only fits a single boolean. Registering one for a boolean setting with is_multi is refused.

ArgumentMeaning
name, descriptionOverride the setting’s label and help text for this control.
tooltipA short explanation shown in an info bubble next to the label.
placeholderGrey hint text inside an empty input.
optionsNarrow the setting’s options to a subset.
min, max, stepBounds for number and range. They are also enforced on the server.
native_boundstrue to also pass min, max and step to the browser’s own input. Off by default.
rowsThe height of a textarea, in rows.
disabledtrue to show the control read-only. A disabled control’s stored value is left untouched on save.
disabled_reasonThe reason shown to the merchant while the control is disabled. Always give one.
description_onA second line shown under a toggle or checkbox only while it is switched on.
search_callback, label_callbackRequired for search-select, see below.

A search-select needs a scalar string or integer setting and two callables. search_callback( string $term ) returns [ [ 'value' => 12, 'label' => 'Warehouse A' ], … ]. label_callback( $value ) returns the label of a saved value, or '' if it is unknown.

This handler uses most of the arguments above.

<?php
defined( 'ABSPATH' ) || exit;
/**
* The settings of the My Carrier plugin.
*/
class My_Carrier_Settings extends \Woodev_Abstract_Settings {
/**
* Declares the settings and their controls.
*
* @return void
*/
protected function register_settings(): void {
// A choice with a default.
$this->register_setting(
'mode',
\Woodev_Setting::TYPE_STRING,
[
'name' => __( 'Mode', 'my-carrier' ),
'options' => [
'test' => __( 'Test', 'my-carrier' ),
'live' => __( 'Live', 'my-carrier' ),
],
'default' => 'test',
]
);
$this->register_control(
'mode',
\Woodev_Control::TYPE_SELECT,
[ 'tooltip' => __( 'In test mode no real shipments are created.', 'my-carrier' ) ]
);
// A required secret, shown only in live mode, that may come from wp-config.php.
$this->register_setting(
'api_key',
\Woodev_Setting::TYPE_STRING,
[
'name' => __( 'API key', 'my-carrier' ),
'required' => true,
'sensitive' => true,
'constant_name' => 'MY_CARRIER_API_KEY',
'show_if' => [
'setting' => 'mode',
'value' => 'live',
],
]
);
$this->register_control(
'api_key',
\Woodev_Control::TYPE_PASSWORD,
[
'tooltip' => __( 'Find the key in your carrier account.', 'my-carrier' ),
'description' => __( 'The key is stored hidden and never shown again.', 'my-carrier' ),
]
);
// A bounded number.
$this->register_setting(
'markup',
\Woodev_Setting::TYPE_INTEGER,
[
'name' => __( 'Markup, %', 'my-carrier' ),
'default' => 10,
]
);
$this->register_control(
'markup',
\Woodev_Control::TYPE_NUMBER,
[
'min' => 0,
'max' => 100,
'native_bounds' => true,
]
);
// A switch with a line that appears only when it is on.
$this->register_setting(
'notify',
\Woodev_Setting::TYPE_BOOLEAN,
[
'name' => __( 'Notify customers', 'my-carrier' ),
'default' => true,
]
);
$this->register_control(
'notify',
\Woodev_Control::TYPE_TOGGLE,
[
'description_on' => __( 'Customers get an e-mail when the parcel is handed to the carrier.', 'my-carrier' ),
]
);
// A custom rule.
$this->register_setting(
'support_phone',
\Woodev_Setting::TYPE_STRING,
[
'name' => __( 'Support phone', 'my-carrier' ),
'validate' => static function ( $value ): bool {
return 11 === strlen( (string) preg_replace( '/\D/', '', (string) $value ) );
},
'validate_message' => __( 'Enter a phone number with 11 digits.', 'my-carrier' ),
]
);
$this->register_control( 'support_phone', \Woodev_Control::TYPE_TEL, [ 'placeholder' => '+7 999 123-45-67' ] );
// Several choices.
$this->register_setting(
'methods',
\Woodev_Setting::TYPE_STRING,
[
'name' => __( 'Delivery methods', 'my-carrier' ),
'is_multi' => true,
'options' => [
'pickup' => __( 'Pickup point', 'my-carrier' ),
'courier' => __( 'Courier', 'my-carrier' ),
],
'default' => [ 'pickup' ],
]
);
$this->register_control( 'methods', \Woodev_Control::TYPE_MULTISELECT );
// A longer text.
$this->register_setting( 'comment', \Woodev_Setting::TYPE_STRING, [ 'name' => __( 'Order comment', 'my-carrier' ) ] );
$this->register_control( 'comment', \Woodev_Control::TYPE_TEXTAREA, [ 'rows' => 4 ] );
}
}

Keep a field’s label short: the essence in one line, usually two words, three at most. A toggle already says “on or off”, so write «Notify customers», not «Enable customer notifications».

Put the explanation in tooltip, which is the default home for it, and keep description for text that must be impossible to miss, easy to copy or that carries a link. The two work together, so a field may have both.

show_if hides a field until another field has a given value. The simplest form is one condition:

<?php
$this->register_setting(
'sandbox_url',
\Woodev_Setting::TYPE_URL,
[
'name' => __( 'Sandbox URL', 'my-carrier' ),
'show_if' => [
'setting' => 'mode',
'operator' => '=',
'value' => 'test',
],
]
);

Several conditions go in a list, joined by 'relation' => 'AND' (the default) or 'OR':

<?php
$this->register_setting(
'tracking_prefix',
\Woodev_Setting::TYPE_STRING,
[
'name' => __( 'Tracking prefix', 'my-carrier' ),
'show_if' => [
'relation' => 'AND',
[
'setting' => 'notify',
'value' => true,
],
[
'setting' => 'mode',
'operator' => 'in',
'value' => [ 'test', 'live' ],
],
],
]
);
OperatorMatches when
= (default)the controlling value equals value
!=it does not equal value
init is one of the values in the value list
not_init is none of the values in the value list

Rules worth knowing:

  • Values are compared as strings. For a select or radio, compare with the option key. For a toggle, use true or false.
  • An unknown operator hides the field.
  • Say “show when the mode is live” as = 'live', not != 'test': the second form also shows the field while the mode has never been set.
  • A hidden field is neither validated nor saved. The form and the server evaluate the same rule, so they agree.
  • show_if and disabled_if may also be a callable that takes the field id and returns the condition list. It runs on the server and cannot see values the merchant has just typed.

The server checks a submitted value in this order:

  1. If required is set and the value is empty, it fails with «Обязательное поле» (Required field).
  2. An empty optional value is valid. Nothing else runs.
  3. A validate callable, when set, decides. It runs only on a non-empty value and must return exactly true to pass; any other return is a failure and shows validate_message.
  4. Otherwise the built-in checks run: the format of an email, url, tel, number or range control and its min and max, then the setting’s type, then the allowed options.

Note step 3: a validate callable replaces the built-in format, type and options checks for that field. If the setting has options, your callable has to check them itself.

Mark a secret with sensitive => true. The saved value is never sent to the browser. The field shows that a value exists, and the merchant can replace it.

constant_name lets a site owner define the value in wp-config.php instead:

<?php
define( 'MY_CARRIER_API_KEY', 'abc123' );

While the constant is defined, get_value() returns it, the field is read-only with a note that explains why, and the value is never written to the database. A field with constant_name is treated as a secret even while the constant is not defined.

The handler is the one place that reads and writes values.

<?php
$settings = My_Carrier_Plugin::instance()->get_settings_handler();
// The stored value, or the default when nothing is stored.
$mode = $settings->get_value( 'mode' );
// The stored value only: null when nothing is stored.
$raw = $settings->get_value( 'mode', false );
// Validate, store and save one value. Throws Woodev_Plugin_Exception when it is invalid.
try {
$settings->update_value( 'markup', 15 );
} catch ( \Woodev_Plugin_Exception $e ) {
// $e->getMessage() is a message you can show.
}
// Remove the stored value, so the default applies again.
$settings->delete_value( 'markup' );

get_value() throws Woodev_Plugin_Exception for an id that was never registered, so keep ids in constants if you read them from several files.

update_value() runs the same checks as the settings page, so it throws on an invalid value. It does nothing for a setting backed by a defined constant.

Three filters change what register_setting() and register_control() accept. {id} is the id you gave the handler.

FilterReceivesEffect
woodev_{id}_settings_api_setting_typesthe list of setting types, the handlerThe list of types register_setting() accepts.
woodev_{id}_settings_api_control_typesthe list of control types, the handlerThe list of control types register_control() accepts.
woodev_{id}_settings_api_setting_control_types[], the setting type, the setting, the handlerThe control types allowed for one setting. An empty list means no restriction.

A filter must return an array; anything else is ignored. The third filter is the useful one. It lets a plugin keep a type of setting on controls that make sense, for instance:

<?php
add_filter(
'woodev_my-carrier_settings_api_setting_control_types',
static function ( array $control_types, string $setting_type ): array {
if ( \Woodev_Setting::TYPE_EMAIL === $setting_type ) {
return [ \Woodev_Control::TYPE_EMAIL, \Woodev_Control::TYPE_TEXT ];
}
return $control_types;
},
10,
2
);

A control type that the page does not know is drawn as a plain text input, so adding a name to the first two filters does not give you a new kind of control.