How to validate options with Settings API callbacks in PHP in WordPress

Contents

Overview: Validating Options with the WordPress Settings API

This tutorial explains, in full detail, how to validate and sanitize plugin or theme options using the WordPress Settings API in PHP. You will learn how to register settings, add sections and fields, render fields, and — critically — validate or sanitize user input with callbacks. The article covers simple single-value fields, grouped option arrays, common field types (text, email, URL, number, checkbox, select), error reporting with add_settings_error(), and best practices for secure and predictable option handling.

Why validation and sanitization matter

  • Security: Ensures malicious input (scripts, unexpected HTML) is neutralized before it is stored or output.
  • Data integrity: Keeps option values in correct types and ranges (numbers as ints, valid emails/URLs, hex colors, etc.).
  • User feedback: When input is invalid, the Settings API provides a way to show clear errors through add_settings_error().
  • Compatibility: Clean, validated options prevent broken templates, failed API calls, and unexpected behavior downstream.

Key concepts

  • register_setting() — registers a setting and accepts a sanitize_callback (or args array including sanitize_callback) that will receive the submitted input and must return the sanitized/validated value.
  • sanitize vs validate: Sanitization always transforms input into a safe form (e.g., sanitize_text_field, absint, esc_url_raw). Validation may check constraints (e.g., email must be valid, number must be between 1–100) and use add_settings_error() to report errors. The sanitize_callback can both sanitize and validate: sanitize what you can, reject or replace invalid values and add errors as needed.
  • Field callbacks: Callbacks passed to add_settings_field are responsible for printing the form controls and should echo values safely with esc_attr or esc_textarea where appropriate.
  • Storage: Options can be stored as a single scalar option (one option per setting name) or as an array (one option that contains multiple fields). Arrays are convenient for grouping related settings in a single option row.

Typical workflow

  1. Hook into admin_menu to add an options page (or use existing WP menu pages such as Settings).
  2. Hook admin_init to register your setting(s), add section(s), and add field(s).
  3. Provide field rendering callbacks that output input elements and fill current values using get_option().
  4. Provide a sanitize callback (passed to register_setting) that receives the submitted input and returns sanitized/validated output. Use add_settings_error() for invalid values and return a safe fallback.
  5. On the admin page, use settings_fields(), do_settings_sections() and submit_button() to render the form and handle save behavior.

Complete annotated example: a full plugin-style settings page

Below is a complete example that demonstrates:

  • Registering a grouped options array called my_plugin_options on option group my_plugin.
  • Rendering fields for text, email, url, number, checkbox, and select.
  • Sanitizing and validating each field in a sanitize callback and reporting errors via add_settings_error().
  • Using settings_fields() and do_settings_sections() to render a settings page that integrates with WP UI.
lt?php
/
  Example Settings API usage with validation amp sanitization.
  Put this in a plugin main file or include it in your plugin.
 /

// 1) Hook to add admin page and initialize settings
add_action( admin_menu, my_plugin_add_admin_menu )
add_action( admin_init,  my_plugin_settings_init )

/
  Add an options page under Settings.
 /
function my_plugin_add_admin_menu() {
    add_options_page(
        My Plugin Settings,    // page title
        My Plugin,             // menu title
        manage_options,        // capability
        my_plugin,             // menu slug (also used as settings page id)
        my_plugin_options_page // callback to render the page
    )
}

/
  Register settings, add sections and fields.
 /
function my_plugin_settings_init() {
    // Register setting: option group = my_plugin, option name = my_plugin_options
    // Pass sanitize callback in args array (WP 4.7  style)
    register_setting(
        my_plugin,               // option_group (used by settings_fields())
        my_plugin_options,       // option_name (stored in wp_options)
        array(
            sanitize_callback => my_plugin_sanitize_callback,
            default             => array(
                text    =gt ,
                email   =gt ,
                siteurl =gt ,
                count   =gt 5,
                enable  =gt 0,
                color   =gt #ffffff,
                choice  =gt one,
            ),
        )
    )

    // Add a section
    add_settings_section(
        my_plugin_section,           // id
        General Settings,            // title
        my_plugin_section_cb,        // callback
        my_plugin                    // page (matches add_options_page slug)
    )

    // Add fields (label_for helps accessibility)
    add_settings_field(
        my_plugin_text,
        Text,
        my_plugin_text_cb,
        my_plugin,
        my_plugin_section,
        array( label_for =gt my_plugin_text )
    )

    add_settings_field(
        my_plugin_email,
        Email,
        my_plugin_email_cb,
        my_plugin,
        my_plugin_section,
        array( label_for =gt my_plugin_email )
    )

    add_settings_field(
        my_plugin_siteurl,
        URL,
        my_plugin_siteurl_cb,
        my_plugin,
        my_plugin_section,
        array( label_for =gt my_plugin_siteurl )
    )

    add_settings_field(
        my_plugin_count,
        Number (1–100),
        my_plugin_count_cb,
        my_plugin,
        my_plugin_section,
        array( label_for =gt my_plugin_count )
    )

    add_settings_field(
        my_plugin_enable,
        Enable feature,
        my_plugin_enable_cb,
        my_plugin,
        my_plugin_section,
        array( label_for =gt my_plugin_enable )
    )

    add_settings_field(
        my_plugin_color,
        Hex Color,
        my_plugin_color_cb,
        my_plugin,
        my_plugin_section,
        array( label_for =gt my_plugin_color )
    )

    add_settings_field(
        my_plugin_choice,
        Choice,
        my_plugin_choice_cb,
        my_plugin,
        my_plugin_section,
        array( label_for =gt my_plugin_choice )
    )
}

/
  Section description callback.
 /
function my_plugin_section_cb() {
    echo ltpgtSet your plugin options below. Invalid values will be rejected with appropriate error messages.lt/pgt
}

/
  Field callbacks: render form elements and default values.
  Use esc_attr() / checked() / selected() when outputting values.
 /
function my_plugin_text_cb() {
    opts = get_option( my_plugin_options, array() )
    val  = isset( opts[text] ) ? opts[text] : 
    printf(
        ltinput type=text id=my_plugin_text name=my_plugin_options[text] value=%s class=regular-text /gt,
        esc_attr( val )
    )
}

function my_plugin_email_cb() {
    opts = get_option( my_plugin_options, array() )
    val  = isset( opts[email] ) ? opts[email] : 
    printf(
        ltinput type=email id=my_plugin_email name=my_plugin_options[email] value=%s class=regular-text /gt,
        esc_attr( val )
    )
}

function my_plugin_siteurl_cb() {
    opts = get_option( my_plugin_options, array() )
    val  = isset( opts[siteurl] ) ? opts[siteurl] : 
    printf(
        ltinput type=url id=my_plugin_siteurl name=my_plugin_options[siteurl] value=%s class=regular-text /gt,
        esc_attr( val )
    )
}

function my_plugin_count_cb() {
    opts = get_option( my_plugin_options, array() )
    val  = isset( opts[count] ) ? intval( opts[count] ) : 5
    printf(
        ltinput type=number id=my_plugin_count name=my_plugin_options[count] value=%d min=1 max=100 /gt,
        val
    )
}

function my_plugin_enable_cb() {
    opts = get_option( my_plugin_options, array() )
    val  = isset( opts[enable] ) ? intval( opts[enable] ) : 0
    printf(
        ltinput type=checkbox id=my_plugin_enable name=my_plugin_options[enable] value=1 %s /gt ltlabel for=my_plugin_enablegtEnabledlt/labelgt,
        checked( 1, val, false )
    )
}

function my_plugin_color_cb() {
    opts = get_option( my_plugin_options, array() )
    val  = isset( opts[color] ) ? opts[color] : #ffffff
    printf(
        ltinput type=text id=my_plugin_color name=my_plugin_options[color] value=%s class=regular-text /gt ltspan class=descriptiongtUse hex like #ff0000lt/spangt,
        esc_attr( val )
    )
}

function my_plugin_choice_cb() {
    opts = get_option( my_plugin_options, array() )
    val  = isset( opts[choice] ) ? opts[choice] : one
    ?>
    ltselect id=my_plugin_choice name=my_plugin_options[choice]gt
        ltoption value=one 
    ltdiv class=wrapgt
        lth1gtMy Plugin Settingslt/h1gt
        ltform action=options.php method=postgt
            
        lt/formgt
    lt/divgt
    lt?php
}
?gt

Notes about the example

  • The option group used in register_setting is my_plugin. This must match the string passed to settings_fields(my_plugin).
  • The option name is my_plugin_options and stores an array of fields. This single option row simplifies saving multiple fields together.
  • All field outputs are escaped using esc_attr() and printed through safe helpers like printf().
  • The sanitize callback both sanitizes and validates. It uses add_settings_error() to show messages. It returns a safe array to store as the option value.
  • When a field is invalid, the code either preserves the previous value (from defaults) or sets a safe fallback, then adds an error so the user understands what happened.

Common validation / sanitization helpers and patterns

  • sanitize_text_field() — basic stripping of tags and trimming suitable for simple text.
  • sanitize_email() and is_email() — sanitize and validate email addresses.
  • esc_url_raw() and filter_var(url, FILTER_VALIDATE_URL) — use esc_url_raw for storing URLs combine with FILTER_VALIDATE_URL for stricter checks.
  • absint(), intval() — convert numeric input to integers. Then enforce min/max range.
  • wp_kses() — allow a safe whitelist of HTML if you must accept markup carefully restrict allowed tags and attributes.
  • preg_match() — useful for pattern validation like hex colors or custom formats.
  • in_array() whitelist — validate select/radio values by comparing against an allowed array of values.
  • add_settings_error() — add error/notice messages during validation so they will be shown on the settings page (use settings_errors() to display them).

Handling arrays vs single options

Options may be stored as an array (recommended when fields are related) or each field may be its own option. If you use an array, sanitize_callback receives the entire array on submit and should return the whole array. Merge submitted values with defaults (to avoid losing previously stored keys when a field is omitted) before returning. In the example above we used get_option() as a source of defaults.

Displaying validation messages

Use add_settings_error( setting, code, message, type ) inside your sanitize callback to present errors or notices after the user clicks Save. The setting parameter is used by settings_errors() to fetch and display only messages relevant to your setting commonly you use the option name or the option group. On the admin page, call settings_errors( your_setting_or_option_name ) before do_settings_sections(), or wherever you want messages to appear.

Best practices and pitfalls

  • Always escape output: When printing values into HTML attributes, use esc_attr() into textareas, use esc_textarea() into URLs, use esc_url().
  • Always sanitize input: Even if you only expect trusted admins to use the UI, sanitize and validate everything. Never store raw user input directly.
  • Prefer whitelisting: When validating values like select options or radio choices, use a whitelist of allowed values. For HTML, prefer a whitelist of tags via wp_kses().
  • Keep UX friendly: If you auto-correct a value (e.g., clamp a number to range), leave a notice explaining the correction. Use add_settings_error with type notice for non-critical adjustments.
  • Test edge cases: Test empty submissions, invalid formats, missing keys, and malicious payloads such as script tags to confirm sanitization works.
  • Use capability checks: Only show the page to users with manage_options (or another appropriate capability).
  • Consider data types in register_setting: WordPress register_setting accepts a type argument (string, boolean, integer, array). Use it when appropriate to communicate the expected type.

Quick reference examples (small snippets)

Simple register_setting with callback

register_setting( my_group, my_option_name, my_sanitize_cb )

Sanitize numeric range

function my_num_cb( val ){
    n = intval( val )
    if ( n lt 0 ) n = 0
    if ( n gt 200 ) n = 200
    return n
}

Checkbox handling

// In sanitize:
output[flag] = isset( input[flag] ) ? 1 : 0

Email validation

email = sanitize_email( input[email] )
if ( ! is_email( email ) ){
    add_settings_error( my_options, bad_email, Invalid email. )
    output[email] = 
} else {
    output[email] = email
}

Summary

Validating options with the Settings API centers around providing a robust sanitize/validate callback to register_setting(). That callback should perform sanitization (strip tags, normalize, cast types), enforce business rules (ranges, formats), provide helpful user feedback via add_settings_error(), and always return a safe value to store. Pair this with correctly escaped field rendering callbacks and use the built-in helpers (sanitize_text_field, sanitize_email, esc_url_raw, absint, wp_kses, etc.) to keep your options secure and predictable.

Further reading (official docs)



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *