Wordpress - "Error: Options Page Not Found" on Settings Page Submission for an OOP Plugin

"Error: Options Page Not Found" Bug

This is a known issue in the WP Settings API. There was a ticket opened years ago, and it was marked as solved -- but the bug persists in the latest versions of WordPress. This is what the (now removed) Codex page said about this:

The "Error: options page not found." problem (including a solution and explanation):

The problem then is, that the 'whitelist_options' filter hasn't got the right index for your data. It gets applied on options.php#98 (WP 3.4).

register_settings() adds your data to the global $new_whitelist_options. This then gets merged with the global $whitelist_options inside the option_update_filter() (resp. add_option_whitelist()) callback(s). Those callbacks add your data to the global $new_whitelist_options with the $option_group as index. When you encounter "Error: options page not found." it means your index hasn't been recognized. The misleading thing is that the first argument is used as index and named $options_group, when the actual check in options.php#112 happens against $options_page, which is the $hook_suffix, which you get as @return value from add_submenu_page().

In short, an easy solution is to make $option_group match $option_name. Another cause for this error is having an invalid value for $page parameter when calling either add_settings_section( $id, $title, $callback, $page ) or add_settings_field( $id, $title, $callback, $page, $section, $args ).

Hint: $page should match $menu_slug from Function Reference/add theme page.

Simple Fix

Using the custom page name (in your case: $this->plugin_slug) as your section id would get around the issue. However, all your options would have to be contained in a single section.

Solution

For a more robust solution, make these changes to your Plugin_Name_Admin class:

Add to constructor:

// Tracks new sections for whitelist_custom_options_page()
$this->page_sections = array();
// Must run after wp's `option_update_filter()`, so priority > 10
add_action( 'whitelist_options', array( $this, 'whitelist_custom_options_page' ),11 );

Add these methods:

// White-lists options on custom pages.
// Workaround for second issue: http://j.mp/Pk3UCF
public function whitelist_custom_options_page( $whitelist_options ){
    // Custom options are mapped by section id; Re-map by page slug.
    foreach($this->page_sections as $page => $sections ){
        $whitelist_options[$page] = array();
        foreach( $sections as $section )
            if( !empty( $whitelist_options[$section] ) )
                foreach( $whitelist_options[$section] as $option )
                    $whitelist_options[$page][] = $option;
            }
    return $whitelist_options;
}

// Wrapper for wp's `add_settings_section()` that tracks custom sections
private function add_settings_section( $id, $title, $cb, $page ){
    add_settings_section( $id, $title, $cb, $page );
    if( $id != $page ){
        if( !isset($this->page_sections[$page]))
            $this->page_sections[$page] = array();
        $this->page_sections[$page][$id] = $id;
    }
}

And change add_settings_section() calls to: $this->add_settings_section().


Other notes on your code

  • Your form code is correct. Your form has to submit to options.php, as pointed out to me by @Chris_O, and as indicated in the WP Settings API documentation.
  • Namespacing has it's advantages, but it can make it more complex to debug, and lowers the compatibility of your code (requires PHP>=5.3, other plugins/themes that use autoloaders, etc). So if there is no good reason to namespace your file, don't. You are already avoiding naming conflicts by wrapping your code in a class. Make your class names more specific, and bring your validate() callbacks into the class as public methods.
  • Comparing your cited plugin boilerplate with your code, it looks like your code is actually based off a fork or an old version of the boilerplate. Even the filenames and paths are different. You could migrate your plugin to the latest version, but note that this plugin boilerplate may not be right for your needs. It makes use of singletons, which are generally discouraged. There are cases where the singleton pattern is sensible, but this should be conscious decision, not the goto solution.

I just found this post while looking for the same issue. The solution is much simpler than it looks because the documentation is misleading : in register_setting() the first argument named $option_group is your page slug, not the section in which you want to display the setting.

In the code above you should use

    // Update Settings
    add_settings_section(
        'maintenance', // section slug
        'Maintenance', // section title
        array( $this, 'maintenance_section' ), // section display callback
        $this->plugin_slug // page slug
    );

    // Check Updates Option
    register_setting( 
        $this->plugin_slug, // page slug, not the section slug
        'plugin-name_check_updates', // setting slug
        'wp_plugin_name\validate_bool' // invalid, should be an array of options, see doc for more info
    );

    add_settings_field(
        'plugin-name_check_updates', // setting slug
        'Should ' . $this->friendly_name . ' Check For Updates?', // setting title
        array( $this, 'check_updates_field' ), //setting display callback
        $this->plugin_slug, // page slug
        'maintenance' // section slug
    );

While registering options page with:

add_submenu_page( string $parent_slug, string $page_title, string $menu_title, string $capability, string $menu_slug, callable $function = '' )

And registering settings with

register_setting( string $option_group, string $option_name );

$option_group should be as same as $menu_slug