• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to build custom Carbon Fields custom wrappers extensions utilizing modern WordPress Settings API schemas

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 is render_field_callback within 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 Field object 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_callback provided to register_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 internal sanitize_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 from get_option() before extracting individual field values.
  • The register_setting() call in register_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.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Designing audit logs for enterprise WordPress setups tracking internal user modifications to member profile directories
  • How to build custom Elementor custom widgets extensions utilizing modern WordPress Settings API schemas
  • Step-by-Step Guide to building a custom automated performance diagnostic log block for Gutenberg using Alpine.js lightweight states
  • Troubleshooting namespace class loading collisions in production when using modern Classic Core PHP wrappers
  • How to securely integrate GitHub API repositories endpoints into WordPress custom plugins using Transients API

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (636)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (841)
  • PHP (5)
  • PHP Development (36)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (613)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (237)
  • WordPress Theme Development (357)

Recent Posts

  • Designing audit logs for enterprise WordPress setups tracking internal user modifications to member profile directories
  • How to build custom Elementor custom widgets extensions utilizing modern WordPress Settings API schemas
  • Step-by-Step Guide to building a custom automated performance diagnostic log block for Gutenberg using Alpine.js lightweight states

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (841)
  • Debugging & Troubleshooting (636)
  • Security & Compliance (613)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala