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 theoption_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 fromadd_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 eitheradd_settings_section( $id, $title, $callback, $page )
oradd_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