Moving to the Customizer API from the Settings API, using Options API

Theme developers using a custom implementation of Options API/Settings API, or who use one of the many excellent frameworks/libraries that use Options API/Settings API, to handle Theme options and create settings pages, will soon need to modify their Theme options to use of the Customizer API. This post will discuss some of the issues potentially encountered with moving from Options API/Settings API to Options API/Customizer API.

Note: there are other tutorials that describe basic Customizer API implementation. My focus with this post is specifically porting from Options API/Settings API to Options API/Customizer API.

What API?

First, a note regarding APIs.

The Options API and the Theme Mods API are used to store/retrieve options to/from the database. The Settings API and the Customizer API are used to expose settings to the user to configure options. Either of the storage/retrieval APIs can be used with either of the user-configuration APIs.

The new Theme Review requirement stipulates use of the Customizer API in lieu of the Settings API, but does not require use of either the Options API or the Theme Mods API in lieu of the other. So, both Theme Mods API/Customizer API implementations and Options API/Customizer API implementations remain acceptable.

Now, for new Themes, it is certainly recommended – and easier – to use a Theme Mods API/Customizer API implementation. However, existing Themes with existing users, it is not feasible to move from an Options API/Settings API implementation to a Theme Mods API/Customizer API implementation. Fortunately, the Options API/Customizer API implementation is a feasible alternative.

Options Configuration Array

For the purposes of this post, I’ll assume that your existing Options API/Settings API implementation is built around a configuration array that defines each of the Theme options, and the various parameters for each option. Each setting will have certain standard parameters, including ID, title, description, field type (checkbox, radio, text, textarea, etc.), a sanitization/data type (HTML text, no-HTML text, email, hex color, etc.), and default value. Additionally, each setting may have a defined settings page section, and, if applicable, settings page tab. Finally, for settings that have select-type fields (select, radio, etc.), the parameters will include an array of choices.

For the purposes of this post, we’ll assume that the configuration array looks like so:

$settings_parameters = array(
	'text_setting_1' => array(
		'id' => 'text_setting_1',
		'title' => __( 'Text Setting 1', 'theme-slug' ),
		'description' => __( 'Text setting 1 description', 'theme-slug' ),
		'field_type' => 'text',
		'sanitize' => 'html',
		'tab' => 'tab_1',
		'section' => 'section_1',
		'default' => __( 'Default Text Setting 1 text', 'theme-slug' )
	),
	'text_setting_2' => array(
		'id' => 'text_setting_2',
		'title' => __( 'Text Setting 2', 'theme-slug' ),
		'description' => __( 'Text setting 2 description', 'theme-slug' ),
		'field_type' => 'text',
		'sanitize' => 'email',
		'tab' => 'tab_1',
		'section' => 'section_2',
		'default' => __( 'noreply@example.com', 'theme-slug' )
	),
	'select_setting_1' => array(
		'id' => 'select_setting_1',
		'title' => __( 'Select Setting 1', 'theme-slug' ),
		'description' => __( 'Select setting 1 description', 'theme-slug' ),
		'field_type' => 'select',
		'sanitize' => 'select',
		'tab' => 'tab_2',
		'section' => 'section_1',
		'default' => 'blue',
		'choices' => array(
			'blue' => __( 'Blue', 'text-slug' ),
			'red' => __( 'Red', 'text-slug' ),
			'green' => __( 'Green', 'text-slug' ),
		)
	),
);

Assuming that your implementation has a tabbed settings page, with settings sections for each tab, you will have another array defined, perhaps like so:

$settings_page_tabs = array(
	'tab_1' => array(
		'id' => 'tab_1',
		'title' => __( 'Page Tab 1', 'theme-slug' ),
		'description' => __( 'Page tab 1 description', 'theme-slug' ),
		'sections' => array(
			'section_1' => array(
				'id' => 'section_1',
				'title' => __( 'Section 1 Title', 'theme-slug' ),
				'description' => __( 'Section 1 description', 'theme-slug' )
			),
			'section_2' => array(
				'id' => 'section_2',
				'title' => __( 'Section 2 Title', 'theme-slug' ),
				'description' => __( 'Section 2 description', 'theme-slug' )
			),
		),
	'tab_2' => array(
		'id' => 'tab_2',
		'title' => __( 'Page Tab 2', 'theme-slug' ),
		'description' => __( 'Page tab 2 description', 'theme-slug' ),
		'sections' => array(
			'section_1' => array(
				'id' => 'section_1',
				'title' => __( 'Section 1 Title', 'theme-slug' ),
				'description' => __( 'Section 1 description', 'theme-slug' )
			),
			'section_2' => array(
				'id' => 'section_2',
				'title' => __( 'Section 2 Title', 'theme-slug' ),
				'description' => __( 'Section 2 description', 'theme-slug' )
			),
		)
	)
);

Moving from Settings API to Customizer API

Settings Page Tabs => Customizer Panels

With the Settings API, you have to write your own code to implement settings page tabs. With the Customizer, tabs are built in, and they are called Panels. Using our settings page configuration above, setting up Panels is simple:

foreach ( $settings_page_tabs as $panel ) {
	$wp_customize->add_panel(
		'theme_slug_' . $panel['id'], 
		array(
			'priority' 	=> 10,
			'capability' 	=> 'edit_theme_options',
			'title' 	=> $panel['title'],
			'description' 	=> $panel['description'],
		) 
	);
}

Note here that you can sort your Theme’s panels, using the 'priority' parameter. Also, passing 'capability' => 'edit_theme_options', you’re taking care of the capability check that you would do elsewhere, via add_theme_page(), with the Settings API.

Settings Page Sections => Customizer Panel Sections

With the Settings API, you add a settings page section using add_settings_section():

foreach ( $settings_page_tabs as $tab ) {
	// Loop through tabs for sections
	foreach ( $tab['sections'] as $section ) {
		add_settings_section(
			// $id
			'theme_slug_' . $section['id'] . '_section',
			// $title
			$section['title'],
			// $callback
			'theme_slug_section_callback',
			// $page (menu slug)
			'theme-slug'
		);
	}
}

You would then have to define the callback for each section (normally used for the descriptive text for the section), and you would also have to take care to pass the correct string for $page. Then, in your settings page, you would have to call do_settings_section( $id ) for each settings section.

With the Customizer API, again, this process is simplified and the code is similar:

foreach ( $settings_page_tabs as $panel ) {
	// Loop through tabs for sections
	foreach ( $panel['sections'] as $section ) {
		$wp_customize->add_section(
			// $id
			'theme_slug_' . $section['id'],
			// parameters
			array(
				'title'		=> $section['title'],
				'description'	=> $section['description'],
				'panel'		=> 'theme_slug_' . $panel['id']
			)
		);
	}
}

Settings Page Settings => Customizer Settings

The next step is to register each of the settings fields. With the Settings API, you use add_settings_field() to define the setting, and you pass as a parameter a callback that defines the form field markup for that setting. Using the settings parameters array, you might be doing something dynamic, like so:

foreach ( $settings_parameters as $option ) {
	add_settings_field(
		// $settingid
		'them_slug_setting_' . $option['id'],
		// $title
		$option['title'],
		// $callback
		'theme_slug_setting_callback',
		// $pageid
		'theme_slug_' . $option['tab'] . '_tab',
		// $sectionid
		'theme_slug_' . $option['section'] . '_section',
		// $args
		$option
	);

The code for the Customizer API is similar:

foreach ( $settings_parameters as $option ) {
	$wp_customize->add_setting(
		// $id
		'theme_slug_options[' . $option['id'] . ']',
		// parameters array
		array(
			'default'		=> $option['default'],
			'type'			=> 'option',
			'sanitize_callback'	=> 'theme_slug_sanitize_' . $option['sanitize']

		)
	);

A couple things to note here:

First, 'type' => 'option' tells the Customizer to use the Options API, rather than the Theme Mods API, to retrieve/store settings.

Second, the 'sanitize_callback' is required. We will address why later.

Settings API Settings Page Setting Field Callback => Customizer Control

With the Settings API, when you call add_settings_field(), you might have defined a single setting callback that switches through output based on field type, or you may have separate callbacks for each field type. Either way, with the Customizer API, this process is much simpler for field types defined by the API (text, checkbox, radio, select, dropdown_pages, textarea):

foreach ( $settings_parameters as $option ) {
	// Add setting control
	$wp_customize->add_control(
		// $id
		'theme_slug_options[' . $option['id'] . ']',
		// parameters array
		array(
			'label'		=> $option['title'],
			'section'	=> 'theme_slug_' . $option['section'],
			'settings'	=> 'theme_slug_options['. $option['id'] . ']',
			'type'		=> $option['field_type'],
			'label'		=> $option['title'],
			'description'   => $option['description'], // If applicable (select, radio, etc.) 'choices' => $option['choices'] ) ); }

Something very important to note here: the value you pass for the 'settings' parameter must be the same as the option ID you define in register_setting(). Also, assuming that you are properly using a single options array, the structure 'theme_slug_options[id]' is important, since that is what gets passed to the Settings API settings page form fields, so you want to ensure that you pass the same structure to the Customizer. That way, the Customizer will properly retrieve and save the user’s current settings.

For simplicity, I use the same structure 'theme_slug_options['. $option['id'] . ']' for the ID of the setting, the ID of the control, and the value of the 'settings' key passed to the control.

Also, if you need to add custom controls, you can loop through the option types separately. For example, if you want to add a core color control:

foreach ( $settings_parameters as $option ) {
	// Add controls for built-in control types		
	if ( in_array( $option['field_type'], array( 'text', 'checkbox', 'radio', 'select', 'dropdown_pages', 'textarea' ) ) ) {
		// Add setting control
		$wp_customize->add_control(
			// $id
			'theme_slug_options[' . $option['id'] . ']',
			// parameters array
			array()
		);
	}
	// Add color control
	else if ( 'color' == $option['field_type'] ) {
		$wp_customize->add_control( 
			new WP_Customize_Color_Control( 
				$wp_customize, 
				'link_color', 
				array() 
			) 
		);
	}
}

You can use the same method for any of the core controls, or for custom controls.

Sanitization/Validation

All-Settings Callback via register_setting()

With the Options API, you register your Theme options array using register_setting(), the third parameter of which is the sanitization/validation callback:

register_setting(
	// $optiongroup
	'theme_slug_options',
	// $option
	'theme_slug_options',
	// $sanitize_callback
	'theme_slug_options_validate'
);

When the Settings API settings page form is submitted, the Theme’s $option object is passed as $_POST data through the $sanitize_callback before being saved to the database.

With the Customizer API, the same thing happens. If you have called register_setting(), when the Customizer form is submitted, the Theme’s $option object is passed as $_POST data through the $sanitize_callback before being saved to the database.

Customizer API: All-Settings Callback via sanitize_options_$option Hook

With the Customizer API, you don’t necessarily have to retain the register_setting() call. You can also hook the $sanitize_callback into the 'sanitize_option_{$option}' hook, which would allow you to remove the register_setting() call altogether:

add_action( 'sanitize_option_theme_slug_options', 'theme_slug_options_validate' );

Customizer API: Per-Setting Sanitization

However, the Customizer is different from a settings page, in a way that makes the Customizer much more powerful: the live preview. By configuring settings in the Customizer, the user can see those settings changes applied immediately, via the live preview. But real-time configured settings are not passed through the 'sanitize_option_{$option}' hook until the settings are saved (i.e. when the Customizer form is submitted). That means that, without proper sanitization of the Customizer settings, the potential exists for XSS or other exploits.

The input will be passed through 'sanitize_callback' passed to $wp_customize->add_setting() not only when the settings are saved, but also when passed to the live previewer. Thus, per-setting sanitization, via the 'sanitize_callback' parameter passed to $wp_customize->add_setting(), is critical.

Dynamic Sanitization Callbacks

Let’s look at $wp_customize->add_setting() again:

$wp_customize->add_setting(
	// $id
	'theme_slug_options[' . $option['id'] . ']',
	// parameters array
	array(
		'default'		=> $option['default'],
		'type'			=> 'option',
		'sanitize_callback'	=> 'theme_slug_sanitize_' . $option['sanitize']
	)
);

Using this method, you need only define one callback for each sanitization type (HTML text, no-HTML text, email, checkbox true/false, etc. For example:

function theme_slug_sanitize_checkbox( $input ) {
	return ( ( isset( $input ) && true == $input ) ? true : false );
}
function theme_slug_sanitize_html( $input ) {
	return wp_filter_kses( $input );
}

function theme_slug_sanitize_nohtml( $input ) {
	return wp_filter_nohtml_kses( $input );
}

Dynamic Sanitization Callbacks for Select-Type Options

The above callbacks, that receive $input, manipulate it, and then return it, are fine for data that only requires sanitization. But what if the data requires validation, such as verifying that $input matches one of the valid choices defined for a select dropdown or radio list? An arbitrary callback such as the ones above will not have the list of valid choices. Fortunately, in addition to $input, the Customizer API passes a second parameter to 'sanitize_callback': the $setting object. We can use this object to grab the list of choices from the control:

function theme_slug_sanitize_select( $input, $setting ) {
	// Ensure input is a slug
	$input = sanitize_key( $input );
	// Get list of choices from the control
	// associated with the setting
	$choices = $setting->manager->get_control( $setting->id )->choices;
	// If the input is a valid key, return it;
	// otherwise, return the default
	return ( array_key_exists( $input, $choices ) ? $input : $setting->default );
}

Caveat: for this little bit of magic to work, it is imperative that the setting ID is identical to the control ID; otherwise, the callback won’t be able to retrieve the control:

// Setting
$wp_customize->add_setting(
	// $id
	'theme_slug_options[' . $option['id'] . ']',
	// parameters array
	array()
);

// Control
$wp_customize->add_control(
	// $id
	'theme_slug_options[' . $option['id'] . ']',
	// parameters array
	array()
);

Notice that in both calls, the $id is 'theme_slug_options[' . $option['id'] . ']'.

Eliminating Unused Options API/Settings API Code

At this point, your Theme options should be retrieved from the database and exposed in the Customizer, configurable with live preview in the Customizer, and saved properly to the database from the Customizer. Everything in the Settings API settings page should now be redundant with the customizer. The next step is to remove unneeded code.

Settings API Settings Page

Now that the Settings API settings page is redundant with the Customizer, it can be removed. The call to add_theme_page() and its callback, calls to add_settings_section() and their callbacks, calls to add_settings_field and their callbacks, as well as any Contextual Help, can all be safely removed.

register_setting() and All-Settings Sanitization Callback

As mentioned above, with per-setting sanitization in the Customizer, the all-settings sanitization callback is no longer needed. Also, because the customizer implementation explicitly adds settings and controls for all options in the Theme options array, the call to register_setting() is also no longer required. Both can be removed safely.

Conclusion

That’s it! If you have other questions or issues regarding moving from Options API/Settings API to Options API/Customizer API, please post them in the comments below.