I’ve been getting back to WordPress development lately for my side project, a custom plugin to manage outgoing emails in WordPress. One of the things that I want to do is to provide a basic settings page where the user can customize the behavior of the plugin. For developers, Settings API and Options API provided by WordPress core out of the box is powerful enough for us to create a settings page on our own.

Although there are no direct actions or filters that we can hook into to validate the options, there’s a useful sanitize_callback option when we use register_setting to add custom settings in our plugin. It’s commonly used to sanitize the values before being saved into the database but we can use the same filter to validate the options and show the validation errors accordingly.

The general idea is to validate the options, use add_settings_error to add any validation error, then switch the options with the original one to prevent WordPress from saving the updated options. I’ll show a simplified example of this.

Let’s create a basic example of a custom settings page. For testing purposes, let’s assume that we need to get a name and email from the user in the plugin. Following the Custom Settings Page in the handbook, we’ll end up with something like this:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
<?php
/**
 * Plugin Name:         Test plugin
 * Plugin URI:          https://example.com/test-plugin
 * Description:         Test
 * Author:              Test
 * Author URI:          https://example.com
 * License:             GPL v2 or later
 * License URI:         https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:         fs-email-helper
 * Domain Path:         /languages
 * Version:             0.1.0
 * Requires at least:   5.5
 * Requires PHP:        7.3
 */

function prefix_settings_init() {
    register_setting('prefix', 'prefix_options');

    add_settings_section(
        'prefix_section_info',
        __('Basic Information.', 'prefix'), 'prefix_section_info_callback',
        'prefix'
   );

    add_settings_field(
        'prefix_field_name',
        __('Name', 'prefix'),
        'prefix_field_name_cb',
        'prefix',
        'prefix_section_info',
        array(
            'label_for' => 'prefix_field_name',
            'class'     => 'prefix_row',
       )
   );

    add_settings_field(
        'prefix_field_email',
        __('Email Address', 'prefix'),
        'prefix_field_email_cb',
        'prefix',
        'prefix_section_info',
        array(
            'label_for'         => 'prefix_field_email',
            'class'             => 'prefix_row',
       )
   );
}
add_action('admin_init', 'prefix_settings_init');

function prefix_section_info_callback($args) {
    ?>
    <p id="<?php echo esc_attr($args['id']); ?>"><?php esc_html_e('Please provide your name and email', 'prefix'); ?></p>
    <?php
}

function prefix_field_name_cb($args) {
    $options = get_option('prefix_options');
    ?>
        <input class="regular-text" placeholder="John Doe" type="text" id="<?php echo esc_attr($args['label_for']); ?>" name="prefix_options[<?php echo esc_attr($args['label_for']); ?>]" value="<?php echo esc_attr($options[$args['label_for']]); ?>">
        <p class="description">
            <?php esc_html_e('Please enter your name.', 'prefix'); ?>
        </p>
    <?php
}

function prefix_field_email_cb($args) {
    $options = get_option('prefix_options');
    ?>
        <input class="regular-text" placeholder="[email protected]" type="text" id="<?php echo esc_attr($args['label_for']); ?>" name="prefix_options[<?php echo esc_attr($args['label_for']); ?>]" value="<?php echo esc_attr($options[$args['label_for']]); ?>">
        <p class="description">
            <?php esc_html_e('Please enter your email address.', 'prefix'); ?>
        </p>
    <?php
}

function prefix_options_page() {
    add_menu_page(
        'Prefix',
        'Prefix Options',
        'manage_options',
        'prefix',
        'prefix_options_page_html'
   );
}
add_action('admin_menu', 'prefix_options_page');

function prefix_options_page_html() {
    if (!current_user_can('manage_options')) {
        return;
    }

    if (isset($_GET['settings-updated'])) {
        add_settings_error('prefix_messages', 'prefix_message', __('Settings Saved', 'prefix'), 'updated');
    }

    settings_errors('prefix_messages');
    ?>
        <div class="wrap">
            <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
            <form action="options.php" method="post">
                <?php
                settings_fields('prefix');
                do_settings_sections('prefix');
                submit_button('Save Settings');
                ?>
            </form>
        </div>
    <?php
}

This should give us a fairly basic custom settings page that displays two fields, name and email. As of now, the field will accept any values, even an empty string. Do note that I use prefix as a general prefix to all the variables and function calls so please change this accordingly.

To implement the validation, add an array with the key sanitize_callback as the third argument to the register_setting call that we have. Instead of

1
register_setting('prefix', 'prefix_options');

we will now have

1
2
3
4
register_setting('prefix', 'prefix_options',  [
    'type'              => 'array',
    'sanitize_callback' => 'prefix_sanitize_options',
]);

We also add a type of array to set the type of data associated with this option. Next, we’ll implement our custom validation inside the prefix_sanitize_options. This function should receive a single argument, the data that will be passed to the update_option. This is how the prefix_sanitize_options looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function prefix_sanitize_options($data)
{
    $old_options = get_option('prefix_options');
    $has_errors = false;

    if (empty($data['prefix_field_name'])) {
        add_settings_error('prefix_messages', 'prefix_message', __('Name is required', 'prefix'), 'error');

        $has_errors = true;
    }

    if (empty($data['prefix_field_email'])) {
        add_settings_error('prefix_messages', 'prefix_message', __('Email address is required', 'prefix'), 'error');

        $has_errors = true;
    }

    if (!is_email($data['prefix_field_email'])) {
        add_settings_error('prefix_messages', 'prefix_message', __('Email address is invalid', 'prefix'), 'error');

        $has_errors = true;
    }

    if ($has_errors) {
        $data = $old_options;
    }

    return $data;
}

As we can see from line 3, we retrieve the original options that we have in the database and assign it to the $old_options variable. This variable will be used to swap the $data with existing options if our validation fails. We also create another variable $has_errors to act as a flag whenever the validation fails. The rest is pretty-self explanatory and we’re free to implement any validations we need to ensure the data integrity. Lastly, once we’ve done all validations, we will swap the value of $data with $old_options as we can see from lines 24 to 26.

One last change we need to make is to the prefix_options_page_html function that is used to display the settings page. We will add another check before we can reliably display the successful message. This can be done by using checking the value returned from get_settings_errors. Instead of

1
2
3
if (isset($_GET['settings-updated'])) {
    add_settings_error('prefix_messages', 'prefix_message', __('Settings Saved', 'prefix'), 'updated');
}

we will now change it to:

1
2
3
if (isset($_GET['settings-updated']) && empty(get_settings_errors('prefix_messages'))) {
    add_settings_error('prefix_messages', 'prefix_message', __('Settings Saved', 'prefix'), 'updated');
}

And you should be good to go!

This serves as a basic example of how to validate for our custom settings page and until we have a better filter or action to hook into for validation, this can be used as an alternative in our plugin.