How to build custom Carbon Fields custom wrappers extensions utilizing modern WordPress Settings API schemas
Leveraging the WordPress Settings API for Advanced Carbon Fields Wrappers
Carbon Fields offers a powerful abstraction layer for managing meta boxes and options pages. However, for highly customized UI elements or integrations with complex WordPress functionalities, directly manipulating the underlying WordPress Settings API can provide unparalleled flexibility. This guide details how to build custom Carbon Fields wrapper extensions that interface directly with the Settings API, enabling advanced use cases beyond standard field types.
Understanding the WordPress Settings API Schema
The WordPress Settings API is the backbone of how options are registered, saved, and displayed in the WordPress admin. It revolves around three core functions:
register_setting(): Registers a setting group and a specific setting within that group. This function defines the setting’s name, sanitization callback, and whether to show its default value.add_settings_section(): Adds a new section to a settings page. Sections are logical groupings of settings.add_settings_field(): Adds a field to a specific settings section. This function defines the field’s title, callback function for rendering its HTML, callback for rendering its description, and arguments passed to the rendering callbacks.
The data for these settings is typically stored in the `wp_options` table, keyed by the setting name. When you use Carbon Fields, it often abstracts these calls. However, by understanding this underlying structure, we can create custom wrappers that hook into this system directly.
Designing a Custom Carbon Fields Wrapper
A custom wrapper in Carbon Fields essentially acts as a bridge. It defines the structure of a group of fields (the wrapper) and then registers these fields with the WordPress Settings API. We’ll create a simple example: a custom wrapper that groups fields under a single option key in the `wp_options` table, mimicking a JSON-encoded array.
1. The Custom Wrapper Class
We’ll extend Carbon Fields’ `Wrapper` class. The key is to override the `render()` method and, crucially, the `save()` method to handle our custom Settings API integration.
namespace MyPlugin\CarbonFields;
use Carbon_Fields\Wrapper;
use Carbon_Fields\Field;
class SettingsApiWrapper extends Wrapper {
protected $option_key;
protected $settings_api_args = [];
public function __construct( $option_key = 'my_plugin_options' ) {
$this->option_key = $option_key;
parent::__construct();
}
public function set_settings_api_args( array $args ) {
$this->settings_api_args = $args;
return $this;
}
public function render() {
// This method will be overridden by the Settings API hooks
// We don't render directly here; WordPress does it via add_settings_field
}
public function save() {
// This method will be overridden by the Settings API hooks
// We don't save directly here; WordPress does it via register_setting
}
/**
* Registers the wrapper and its fields with the Settings API.
*/
public function register_settings_api() {
$section_id = sanitize_key( $this->get_name() );
$page_slug = isset( $this->settings_api_args['page_slug'] ) ? $this->settings_api_args['page_slug'] : 'options-general.php'; // Default to General Settings
// Register the setting group if not already done
if ( ! isset( $GLOBALS['my_plugin_settings_api_registered_groups'][$this->option_key] ) ) {
register_setting(
$this->option_key, // Option group
$this->option_key, // Option name (will store a JSON encoded array)
$this->settings_api_args['sanitize_callback'] ?? [$this, 'sanitize_options_array'] // Sanitization callback
);
$GLOBALS['my_plugin_settings_api_registered_groups'][$this->option_key] = true;
}
// Add the settings section
add_settings_section(
$section_id, // ID
$this->get_title(), // Title
$this->settings_api_args['section_callback'] ?? null, // Callback
$page_slug // Page slug
);
// Add each field to the section
foreach ( $this->fields as $field ) {
/** @var Field $field */
$field_id = $field->get_name();
add_settings_field(
$field_id, // ID
$field->get_name_label(), // Title
[ $this, 'render_field_callback' ], // Callback
$page_slug, // Page slug
$section_id, // Section ID
[ 'field' => $field ] // Arguments
);
}
}
/**
* Callback to render individual fields.
*
* @param array $args Arguments passed from add_settings_field.
*/
public function render_field_callback( $args ) {
$field = $args['field'];
$option_value = get_option( $this->option_key, [] );
$field_value = isset( $option_value[$field->get_name()] ) ? $option_value[$field->get_name()] : $field->get_default_value();
// Set the value for Carbon Fields to render
$field->set_value( $field_value );
// Render the field using Carbon Fields' internal rendering
echo '<div class="carbon-fields-settings-api-wrapper">';
$field->render();
echo '</div>';
}
/**
* Sanitizes the entire options array before saving.
*
* @param array $input The raw input from the $_POST request.
* @return array The sanitized options array.
*/
public function sanitize_options_array( $input ) {
$sanitized_output = [];
$current_options = get_option( $this->option_key, [] );
// Ensure we have a valid array structure for the option key
if ( ! is_array( $input ) ) {
$input = [];
}
// Iterate through fields defined in this wrapper
foreach ( $this->fields as $field ) {
/** @var Field $field */
$field_name = $field->get_name();
$field_value = isset( $input[$field_name] ) ? $input[$field_name] : null;
// Apply Carbon Fields' internal sanitization for the specific field type
$sanitized_value = $field->sanitize_value( $field_value );
// If the field was not submitted (e.g., checkbox unchecked), use its default or keep existing
if ( $sanitized_value === null && $field->get_type() === 'checkbox' ) {
$sanitized_value = $field->get_default_value(); // Or 0 if default is not set and checkbox is off
} elseif ( $sanitized_value === null && $field->get_type() !== 'checkbox' ) {
// For other fields, if no value is submitted and no default, we might want to unset or keep old value
// For simplicity here, we'll just ensure it's not set if null.
// A more robust solution might check $field->get_default_value()
if ( $field->get_default_value() !== null ) {
$sanitized_value = $field->get_default_value();
} else {
// If no default and no input, remove from saved options if it exists
if ( isset( $current_options[$field_name] ) ) {
unset( $current_options[$field_name] );
}
continue; // Skip adding this to sanitized_output
}
}
$sanitized_output[$field_name] = $sanitized_value;
}
// Merge with existing options to preserve fields from other wrappers/sources
// This is crucial if multiple wrappers save to the same option_key or if other options exist.
// For this example, we assume one wrapper per option_key for simplicity.
// A more complex scenario would involve merging $current_options with $sanitized_output.
return $sanitized_output;
}
/**
* Hook into Carbon Fields' initialization to register the Settings API hooks.
*/
public static function carbon_fields_init() {
// This is a placeholder. The actual registration needs to happen
// when the Carbon Fields container is being built.
// We'll trigger this manually or via a filter.
}
}
2. Registering the Wrapper with Settings API Hooks
The custom wrapper needs to be registered with the WordPress Settings API. This typically happens within your plugin’s main file or an initialization class, hooked into `admin_init`.
add_action( 'admin_init', function() {
// Define your custom wrapper instance
$my_options_wrapper = new MyPlugin\CarbonFields\SettingsApiWrapper( 'my_plugin_options' );
$my_options_wrapper
->set_title( 'My Custom Plugin Settings' )
->set_page_slug( 'options-general.php' ) // Target the General Settings page
->set_settings_api_args( [
'page_slug' => 'options-general.php',
// Optional: Custom section callback for the settings section
// 'section_callback' => function( $args ) {
// echo '<p>Configure your plugin settings here.</p>';
// },
// Optional: Custom sanitize callback for the entire option group
// 'sanitize_callback' => 'my_plugin_custom_sanitize_function'
] );
// Add fields to the wrapper
$my_options_wrapper->add_fields( [
Field::make( 'text', 'api_key', 'API Key' )
->set_attribute( 'placeholder', 'Enter your API key' ),
Field::make( 'textarea', 'api_secret', 'API Secret' )
->set_attribute( 'rows', 4 ),
Field::make( 'checkbox', 'enable_feature', 'Enable Feature' )
->set_option_value( 'yes' ) // Important for checkboxes
->set_default_value( 'no' ), // Explicitly set default
] );
// Manually trigger the registration with Settings API
$my_options_wrapper->register_settings_api();
// --- Example for another wrapper on a different page ---
$another_wrapper = new MyPlugin\CarbonFields\SettingsApiWrapper( 'my_plugin_advanced_settings' );
$another_wrapper
->set_title( 'Advanced Settings' )
->set_page_slug( 'edit.php?post_type=page' ) // Target a custom admin page (e.g., a CPT list table)
->set_settings_api_args( [
'page_slug' => 'edit.php?post_type=page',
'section_callback' => function( $args ) {
echo '<p>Advanced configurations.</p>';
},
] );
$another_wrapper->add_fields( [
Field::make( 'select', 'log_level', 'Log Level' )
->add_options( [
'debug' => 'Debug',
'info' => 'Info',
'warning' => 'Warning',
'error' => 'Error',
] )
->set_default_value( 'info' ),
] );
$another_wrapper->register_settings_api();
} );
// If you need a custom sanitize callback function
function my_plugin_custom_sanitize_function( $input ) {
// This function would be called by register_setting if specified.
// The SettingsApiWrapper::sanitize_options_array is generally preferred
// as it leverages Carbon Fields' field-specific sanitization.
// This is just an example if you need global sanitization logic.
return $input;
}
How it Works
When `register_settings_api()` is called:
- It calls
register_setting(). The first argument is the ‘option group’ (used in the form’s `name` attribute), and the second is the ‘option name’ (the actual key in `wp_options`). We store a single array under'my_plugin_options'. - It calls
add_settings_section()to create a visual grouping on the target admin page. - For each field defined in the wrapper, it calls
add_settings_field(). The callback for rendering the field isrender_field_callbackwithin our custom wrapper.
Rendering Fields
The render_field_callback() method is crucial:
- It retrieves the current saved options array using
get_option( $this->option_key, [] ). - It extracts the specific field’s value from this array. If the field doesn’t exist, it falls back to the field’s default value.
- It sets this retrieved value onto the Carbon Fields
Fieldobject using$field->set_value(). This is vital for Carbon Fields to render the field with its current saved state. - Finally, it calls the Carbon Fields field’s own
render()method, which outputs the HTML for that specific field.
Saving and Sanitization
When a user submits the form:
- WordPress’s Settings API handles the initial data capture. The form’s `action` attribute will point to `admin-post.php` or similar, and the `option_page` parameter will match the group name passed to
register_setting(). - The
sanitize_callbackprovided toregister_setting()is invoked. In our case, this is$this->sanitize_options_array(). - Our
sanitize_options_array()method iterates through all fields defined in the wrapper. For each field, it calls the field’s internalsanitize_value()method, which leverages Carbon Fields’ built-in sanitization logic (e.g., for text, number, email). - It reconstructs an array containing only the sanitized values for the fields belonging to this wrapper.
- This sanitized array is then saved as a single entry in the `wp_options` table under the specified
$option_key.
Handling Complex Data Structures (JSON Encoding)
The approach above stores an array of fields under a single option key. For more complex scenarios, like storing a repeatable field group or a set of related settings as a single JSON object, you can modify the sanitization and retrieval logic.
Modified Sanitization for JSON
class SettingsApiWrapper {
// ... (previous code)
/**
* Sanitizes the entire options array and JSON encodes it.
*
* @param array $input The raw input from the $_POST request.
* @return string JSON encoded string of sanitized options.
*/
public function sanitize_options_array_json( $input ) {
$sanitized_output = [];
// ... (field sanitization logic as before) ...
foreach ( $this->fields as $field ) {
$field_name = $field->get_name();
$field_value = isset( $input[$field_name] ) ? $input[$field_name] : null;
$sanitized_value = $field->sanitize_value( $field_value );
// Handle nulls and defaults as before
if ( $sanitized_value === null ) {
if ( $field->get_default_value() !== null ) {
$sanitized_value = $field->get_default_value();
} else {
continue; // Skip if no input and no default
}
}
$sanitized_output[$field_name] = $sanitized_value;
}
// JSON encode the entire array
return json_encode( $sanitized_output );
}
/**
* Callback to render individual fields, decoding JSON.
*
* @param array $args Arguments passed from add_settings_field.
*/
public function render_field_callback( $args ) {
$field = $args['field'];
$option_json = get_option( $this->option_key, '{}' ); // Default to empty JSON object
$option_value = json_decode( $option_json, true );
if ( ! is_array( $option_value ) ) {
$option_value = []; // Ensure it's an array if decoding failed
}
$field_value = isset( $option_value[$field->get_name()] ) ? $option_value[$field->get_name()] : $field->get_default_value();
$field->set_value( $field_value );
echo '<div class="carbon-fields-settings-api-wrapper">';
$field->render();
echo '</div>';
}
// Update register_settings_api to use the JSON sanitizer
public function register_settings_api() {
// ... (section and field registration) ...
$sanitize_callback = $this->settings_api_args['sanitize_callback'] ?? [$this, 'sanitize_options_array_json']; // Use JSON sanitizer
register_setting(
$this->option_key,
$this->option_key,
$sanitize_callback
);
// ... (rest of the method) ...
}
}
In this modified version:
sanitize_options_array_json()now JSON encodes the final array before returning it.render_field_callback()decodes the JSON string fromget_option()before extracting individual field values.- The
register_setting()call inregister_settings_api()should explicitly use this JSON sanitization callback if you want this behavior.
Integrating with Carbon Fields Containers
While the above demonstrates direct Settings API integration, you can also create a custom field type that *uses* this wrapper internally. This allows you to place your Settings API-managed fields within standard Carbon Fields containers (like meta boxes or options pages) more seamlessly.
Custom Field Type Example
namespace MyPlugin\CarbonFields\Fields;
use Carbon_Fields\Field;
class SettingsApiGroupField extends Field {
protected $type = 'settings_api_group';
protected $wrapper_instance;
public static function make( $name, $label = null ) {
$field = parent::make( $name, $label );
// Initialize a temporary wrapper instance to manage fields within this group
$field->wrapper_instance = new \MyPlugin\CarbonFields\SettingsApiWrapper( $name ); // Use field name as option key
$field->wrapper_instance->set_title( $label ); // Use field label as wrapper title
return $field;
}
public function add_fields( $fields ) {
$this->wrapper_instance->add_fields( $fields );
return $this;
}
// This method is called by Carbon Fields when rendering the field within a container
public function render() {
// Render the fields managed by the internal wrapper
// We need to ensure the Settings API hooks are registered for this wrapper
// This is a bit tricky as Carbon Fields renders fields individually.
// A better approach might be to have the wrapper register itself globally
// and then have this field type simply act as a placeholder or a way to group.
// For a true integration, the wrapper's register_settings_api() should be called
// during the Carbon Fields container setup, not here.
// This field type would then just be a marker.
// A simpler approach: Render the fields directly if not using Settings API hooks
// This defeats the purpose of Settings API integration for this specific field type.
// Let's assume the wrapper is registered elsewhere via admin_init.
// This field type's render() method might not be directly used for output.
// Its primary role is to group fields and ensure the wrapper is configured.
echo '<div class="settings-api-group-field">';
echo '<h4>' . esc_html( $this->get_name_label() ) . '</h4>';
// The actual fields will be rendered by WordPress via add_settings_field
// when the wrapper's register_settings_api() is called.
echo '</div>';
}
// This method is called by Carbon Fields when saving fields within a container
public function save() {
// The actual saving is handled by the Settings API via register_setting.
// This method might not be needed if the wrapper is registered independently.
}
// Expose wrapper methods
public function set_settings_api_args( array $args ) {
$this->wrapper_instance->set_settings_api_args( $args );
return $this;
}
public function register_settings_api() {
$this->wrapper_instance->register_settings_api();
return $this;
}
}
To use this custom field type:
use MyPlugin\CarbonFields\Fields\SettingsApiGroupField;
add_action( 'carbon_fields_register_fields', function() {
// Register the custom field type with Carbon Fields
\Carbon_Fields\Carbon_Fields::init();
\Carbon_Fields\Field::register( 'settings_api_group', SettingsApiGroupField::class );
} );
add_action( 'admin_init', function() {
// Instantiate and configure the wrapper, then register it
$my_settings_group = SettingsApiGroupField::make( 'my_plugin_options', 'Plugin Settings Group' )
->set_settings_api_args( [
'page_slug' => 'options-general.php',
'section_callback' => function() { echo '<p>Settings managed via Settings API.</p>'; }
] )
->add_fields( [
Field::make( 'text', 'api_key', 'API Key' ),
Field::make( 'checkbox', 'enable_feature', 'Enable Feature' )
->set_option_value( 'yes' )
->set_default_value( 'no' ),
] );
// Crucially, register the underlying Settings API hooks
$my_settings_group->register_settings_api();
} );
This approach allows you to define fields using Carbon Fields’ syntax but have them managed by the WordPress Settings API. The SettingsApiGroupField acts as a configuration object for our SettingsApiWrapper, ensuring that the necessary register_setting, add_settings_section, and add_settings_field calls are made.
Conclusion
By extending Carbon Fields and understanding the intricacies of the WordPress Settings API, you can build highly customized and robust administrative interfaces. This method provides a clean way to manage settings that require specific validation, complex data structures, or integration with core WordPress settings pages, all while leveraging the developer-friendly syntax of Carbon Fields.