Settings that belong to the site
Post meta belongs to one post. A setting belongs to the whole site: a plugin's API key, the number of items to show, a colour. WordPress stores such values as options in the wp_options table. WordPress's own Settings screens (General, Reading, Permalinks) are built on the same options you can read from code.
The Options API
<?php
// Read: the second argument is what you get if the option does not exist.
echo get_option( 'blogname' ), "\n";
var_dump( get_option( 'bakery_items', 5 ) );
// Create and change.
add_option( 'bakery_items', 8 ); // only adds if missing
update_option( 'bakery_items', 10 ); // adds or changes
var_dump( get_option( 'bakery_items' ) );
// Several values together: store one array option instead of many.
update_option( 'bakery_settings', [ 'items' => 10, 'show_prices' => true ] );
print_r( get_option( 'bakery_settings' ) );
delete_option( 'bakery_items' );
var_dump( get_option( 'bakery_items', 'gone' ) );My WordPress Site
int(5)
int(10)
Array
(
[items] => 10
[show_prices] => 1
)
string(4) "gone"Guidelines:
- Prefix option names with your plugin name.
- Options are loaded into memory on every request when "autoloaded" (the default). Keep autoloaded options small; pass
falseas the third argument ofadd_option()for big or rarely used values. - Use one array option for related settings of a plugin, instead of ten separate rows.
- Options are not for per-post data (use post meta) or for temporary cached values (use transients).
From the command line, wp option get, wp option update and wp option list do the same.
The Settings API
You could build a settings form by hand, but then you must handle the nonce, the capability, saving, and the "Settings saved" notice. The Settings API does that for you. You describe the setting, a section and the fields, and WordPress draws and saves them.
<?php
/**
* Plugin Name: Bakery Settings
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// 1. Register the setting with a sanitise callback.
add_action( 'admin_init', function () {
register_setting( 'bakery_group', 'bakery_settings', [
'type' => 'array',
'sanitize_callback' => 'bakery_sanitize_settings',
'default' => [ 'items' => 5, 'shop_name' => '' ],
] );
// 2. A section and its fields.
add_settings_section( 'bakery_main', 'Shop display', '__return_false', 'bakery-settings' );
add_settings_field( 'items', 'Items to show', 'bakery_field_items', 'bakery-settings', 'bakery_main' );
add_settings_field( 'shop_name', 'Shop name', 'bakery_field_name', 'bakery-settings', 'bakery_main' );
} );
function bakery_sanitize_settings( $input ): array {
$input = is_array( $input ) ? $input : [];
$items = isset( $input['items'] ) ? absint( $input['items'] ) : 5;
return [
'items' => min( max( $items, 1 ), 50 ), // keep it between 1 and 50
'shop_name' => sanitize_text_field( $input['shop_name'] ?? '' ), // plain text only
];
}
function bakery_field_items() {
$options = get_option( 'bakery_settings' );
printf( '<input type="number" name="bakery_settings[items]" value="%d" min="1" max="50">', (int) ( $options['items'] ?? 5 ) );
}
function bakery_field_name() {
$options = get_option( 'bakery_settings' );
printf( '<input type="text" name="bakery_settings[shop_name]" value="%s" class="regular-text">', esc_attr( $options['shop_name'] ?? '' ) );
}
// 3. The menu entry and the page.
add_action( 'admin_menu', function () {
add_options_page( 'Bakery Settings', 'Bakery', 'manage_options', 'bakery-settings', 'bakery_settings_page' );
} );
function bakery_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( 'bakery_group' ); // nonce and hidden fields
do_settings_sections( 'bakery-settings' ); // our section and fields
submit_button();
?>
</form>
</div>
<?php
}wp plugin activate bakery-settingsPlugin 'bakery-settings' activated.
Success: Activated 1 of 1 plugins.Three parts:
register_setting()names the option, its type, a default and, most importantly, the sanitise callback. WordPress calls it every time the option is saved through the form.- Sections and fields are described with
add_settings_section()andadd_settings_field(); each field has a function that prints its input. Name inputsoption_name[key]so everything arrives as one array. - The page is added to the Settings menu with
add_options_page(), restricted tomanage_options. The form posts tooptions.php, andsettings_fields()adds the nonce and hidden fields that make saving secure. You do not write a save handler.
Sign in and see the page
The page is in the dashboard, so log in first. WordPress sets a test cookie when it shows the login form, then the form posts the username and password and answers with a redirect to the dashboard:
GET /wp-login.phpPOST /wp-login.php
Content-Type: application/x-www-form-urlencoded
log=admin&pwd=password&wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1HTTP/1.1 302 Found
Location: /wp-admin/The cookies WordPress sends back identify you on later requests, and now the settings page opens:
GET /wp-admin/options-general.php?page=bakery-settingsBakery SettingsGET /wp-admin/options-general.php?page=bakery-settings<input type="number" name="bakery_settings[items]" value="10" min="1" max="50">
<input type="text" name="bakery_settings[shop_name]" value="" class="regular-text">What the sanitise callback does
When the form is submitted, options.php passes the posted values through the callback before saving. You can see what it does by calling it with messy input:
<?php
print_r( bakery_sanitize_settings( [ 'items' => '500', 'shop_name' => ' <b>Daily</b> Bread ' ] ) );
print_r( bakery_sanitize_settings( [ 'items' => 'abc', 'shop_name' => "Line one\nLine two" ] ) );
print_r( bakery_sanitize_settings( 'not even an array' ) );Array
(
[items] => 50
[shop_name] => Daily Bread
)
Array
(
[items] => 1
[shop_name] => Line one Line two
)
Array
(
[items] => 5
[shop_name] =>
)500 became 50, the HTML tags were removed from the name and surplus whitespace trimmed, a non-number fell back to the minimum, and a wrong kind of input became a valid array. Always validate in the callback, because anyone who can post to options.php can send any values. (WordPress attaches the callback when the dashboard loads, which is why code run from the command line must call it directly or use update_option( 'bakery_settings', bakery_sanitize_settings( $values ) ).)
Showing your settings on the site
<?php
update_option( 'bakery_settings', bakery_sanitize_settings( [ 'items' => '12', 'shop_name' => 'Daily Bread' ] ) );
function bakery_get_setting( string $key ) {
$options = wp_parse_args( get_option( 'bakery_settings', [] ), [ 'items' => 5, 'shop_name' => '' ] );
return $options[ $key ] ?? null;
}
echo esc_html( bakery_get_setting( 'shop_name' ) ), ' shows ', (int) bakery_get_setting( 'items' ), " items\n";Daily Bread shows 12 itemswp_parse_args() fills in defaults for missing keys. Escape on output (esc_html(), esc_attr()), even for values you sanitised on the way in.
Other places for settings
- Customizer and Site Editor: theme options belong in
theme.jsonand the Site Editor for block themes. - Block attributes: options for a single block belong in the block.
wp-config.phpconstants: for secrets and environment settings you do not want editable in the admin.
Common mistakes
- No
sanitize_callback, so any value can be stored. - Printing option values without escaping.
- Many separate options for one plugin, all autoloaded.
- Building your own save handler without nonce and capability checks. Use the Settings API.
- Showing the settings page to everyone (missing capability check).
Exercise
Add a checkbox setting show_prices to the form. Sanitise it as a boolean, so unchecked boxes (which are not submitted at all) save as false.
Show one possible answer
// in bakery_sanitize_settings():
'show_prices' => ! empty( $input['show_prices'] ),
// the field:
function bakery_field_prices() {
$options = get_option( 'bakery_settings' );
printf( '<input type="checkbox" name="bakery_settings[show_prices]" value="1" %s>', checked( ! empty( $options['show_prices'] ), true, false ) );
}
add_settings_field( 'show_prices', 'Show prices', 'bakery_field_prices', 'bakery-settings', 'bakery_main' );An unchecked checkbox sends nothing, so ! empty( $input['show_prices'] ) gives false.
Key points
- Options are site-wide key/value settings stored in the wp_options table; use get_option(), add_option(), update_option() and delete_option().
- The Settings API (register_setting, add_settings_section, add_settings_field) builds a settings page and saves it for you with a nonce and capability check.
- Give every setting a sanitize_callback so only valid values are stored, and escape values when you print them.
- Add the page with add_options_page() or add_menu_page() and require a capability such as manage_options.