• 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 Sage Roots modern environments extensions utilizing modern WordPress Settings API schemas

How to build custom Sage Roots modern environments extensions utilizing modern WordPress Settings API schemas

Leveraging WordPress Settings API Schemas with Sage Roots for Custom Environments

Modern WordPress development, particularly within frameworks like Sage Roots, demands robust and maintainable solutions for managing application settings. The WordPress Settings API, while powerful, can become unwieldy when dealing with complex configurations or when aiming for a structured, schema-driven approach. This post details how to build custom Sage Roots environment extensions by defining and utilizing modern WordPress Settings API schemas, ensuring cleaner code, better validation, and improved developer experience.

Defining a Schema for Environment-Specific Settings

Instead of directly registering settings fields, we’ll define a JSON schema that describes our desired settings. This schema will dictate the structure, data types, validation rules, and even UI hints for our environment-specific configurations. For this example, let’s consider settings for a custom API integration that might differ between development, staging, and production environments.

We’ll create a JSON file, for instance, config/schemas/api-settings.json within your Sage Roots theme or a dedicated plugin. This schema will serve as the single source of truth for our API settings.

Example config/schemas/api-settings.json:

{
  "title": "API Integration Settings",
  "description": "Settings for the external API integration.",
  "type": "object",
  "properties": {
    "api_base_url": {
      "title": "API Base URL",
      "description": "The base URL for the API endpoint.",
      "type": "string",
      "format": "url",
      "default": "https://api.example.com/v1"
    },
    "api_key": {
      "title": "API Key",
      "description": "Your secret API key.",
      "type": "string",
      "minLength": 32,
      "default": ""
    },
    "timeout_seconds": {
      "title": "Request Timeout (seconds)",
      "description": "Maximum time to wait for an API response.",
      "type": "integer",
      "minimum": 5,
      "maximum": 60,
      "default": 15
    },
    "enable_logging": {
      "title": "Enable API Logging",
      "description": "Log API requests and responses for debugging.",
      "type": "boolean",
      "default": false
    }
  },
  "required": [
    "api_base_url",
    "api_key"
  ]
}

Integrating Schema with Sage Roots and Settings API

Sage Roots utilizes a configuration system that can be extended. We’ll hook into the WordPress Settings API registration process, dynamically generating settings fields based on our JSON schema. This approach allows us to keep our schema definition separate and easily manageable.

First, let’s create a PHP class to handle the schema loading and Settings API registration. This could reside in a custom plugin or within your Sage Roots theme’s `app/` directory, perhaps in `app/SchemaSettings.php`.

<?php
namespace App\SchemaSettings;

use Illuminate\Support\Arr;
use WP_Error;

class SchemaSettings {

    protected $schema_file = 'config/schemas/api-settings.json';
    protected $option_group = 'api_settings_group';
    protected $option_name = 'api_settings';
    protected $page_slug = 'api-settings';

    public function __construct() {
        add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
        add_action( 'admin_init', [ $this, 'register_settings' ] );
        add_action( 'admin_notices', [ $this, 'display_admin_notices' ] );
    }

    public function add_admin_menu() {
        add_options_page(
            __( 'API Settings', 'sage' ),
            __( 'API Settings', 'sage' ),
            'manage_options',
            $this->page_slug,
            [ $this, 'render_settings_page' ]
        );
    }

    public function register_settings() {
        $schema = $this->load_schema();
        if ( ! $schema ) {
            return;
        }

        // Register the main option group
        register_setting(
            $this->option_group,
            $this->option_name,
            [ $this, 'sanitize_settings' ]
        );

        // Dynamically add settings sections and fields based on schema
        if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
            add_settings_section(
                'api_settings_section',
                __( 'API Configuration', 'sage' ),
                [ $this, 'render_section_description' ],
                $this->page_slug
            );

            foreach ( $schema['properties'] as $key => $field_config ) {
                $this->add_setting_field( $key, $field_config );
            }
        }
    }

    protected function load_schema() {
        $path = \Roots\asset_path( $this->schema_file ); // Assuming Sage's asset_path helper
        if ( ! file_exists( $path ) ) {
            error_log( "Schema file not found: " . $path );
            return false;
        }
        $schema_content = file_get_contents( $path );
        $schema = json_decode( $schema_content, true );

        if ( json_last_error() !== JSON_ERROR_NONE ) {
            error_log( "Error decoding schema JSON: " . json_last_error_msg() );
            return false;
        }
        return $schema;
    }

    protected function add_setting_field( $key, $config ) {
        add_settings_field(
            $key,
            isset( $config['title'] ) ? $config['title'] : ucwords( str_replace( '_', ' ', $key ) ),
            [ $this, 'render_field' ],
            $this->page_slug,
            'api_settings_section',
            [
                'label_for' => $key,
                'config'    => $config,
                'value'     => $this->get_setting( $key ),
            ]
        );
    }

    public function render_section_description() {
        $schema = $this->load_schema();
        if ( $schema && isset( $schema['description'] ) ) {
            echo '<p>' . esc_html( $schema['description'] ) . '</p>';
        }
    }

    public function render_field( $args ) {
        $key = $args['label_for'];
        $config = $args['config'];
        $value = $args['value'];

        $field_type = $config['type'] ?? 'text';
        $title = isset( $config['title'] ) ? $config['title'] : ucwords( str_replace( '_', ' ', $key ) );
        $description = isset( $config['description'] ) ? $config['description'] : '';
        $required = in_array( $key, $config['required'] ?? [] );

        // Basic type mapping to HTML input types
        $input_type = 'text';
        switch ( $field_type ) {
            case 'string':
                if ( isset( $config['format'] ) && $config['format'] === 'url' ) {
                    $input_type = 'url';
                } elseif ( isset( $config['format'] ) && $config['format'] === 'email' ) {
                    $input_type = 'email';
                }
                // Add more formats like 'password' if needed
                break;
            case 'integer':
            case 'number':
                $input_type = 'number';
                break;
            case 'boolean':
                $input_type = 'checkbox';
                break;
            default:
                $input_type = 'text';
        }

        // Handle specific types like checkboxes
        if ( $input_type === 'checkbox' ) {
            printf(
                '<input type="checkbox" id="%1$s" name="%2$s[%1$s]" value="1" %3$s /><label for="%1$s"> %4$s</label>',
                esc_attr( $key ),
                esc_attr( $this->option_name ),
                checked( 1, $value, false ),
                esc_html( $title )
            );
        } else {
            printf(
                '<input type="%1$s" id="%2$s" name="%3$s[%2$s]" value="%4$s" class="regular-text" %5$s />',
                esc_attr( $input_type ),
                esc_attr( $key ),
                esc_attr( $this->option_name ),
                esc_attr( $value ),
                isset( $config['required'] ) && in_array( $key, $config['required'] ) ? 'required' : ''
            );
            if ( $input_type === 'number' ) {
                if ( isset( $config['minimum'] ) ) {
                    printf( ' min="%s"', esc_attr( $config['minimum'] ) );
                }
                if ( isset( $config['maximum'] ) ) {
                    printf( ' max="%s"', esc_attr( $config['maximum'] ) );
                }
            }
        }

        if ( ! empty( $description ) ) {
            printf( '<p class="description">%s</p>', esc_html( $description ) );
        }
    }

    public function sanitize_settings( $input ) {
        $schema = $this->load_schema();
        if ( ! $schema || ! isset( $schema['properties'] ) ) {
            return $input;
        }

        $sanitized_input = [];
        $current_options = get_option( $this->option_name, [] );

        foreach ( $schema['properties'] as $key => $config ) {
            $value = Arr::get( $input, $key );
            $default_value = $config['default'] ?? null;

            // Handle boolean/checkbox specifically
            if ( $config['type'] === 'boolean' ) {
                $sanitized_input[$key] = (bool) ( $value ?? false );
                continue;
            }

            // If value is not provided, use default or keep existing if available
            if ( $value === null || $value === '' ) {
                $sanitized_input[$key] = $current_options[$key] ?? $default_value;
                continue;
            }

            // Basic sanitization based on type
            switch ( $config['type'] ) {
                case 'string':
                    $sanitized_input[$key] = sanitize_text_field( $value );
                    if ( isset( $config['format'] ) ) {
                        if ( $config['format'] === 'url' ) {
                            $sanitized_input[$key] = esc_url_raw( $value );
                        } elseif ( $config['format'] === 'email' ) {
                            $sanitized_input[$key] = sanitize_email( $value );
                        }
                    }
                    break;
                case 'integer':
                case 'number':
                    $sanitized_input[$key] = filter_var( $value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION );
                    if ( $config['type'] === 'integer' ) {
                        $sanitized_input[$key] = intval( $sanitized_input[$key] );
                    }
                    break;
                default:
                    $sanitized_input[$key] = sanitize_text_field( $value );
            }

            // Apply validation rules from schema (basic example)
            if ( isset( $config['minLength'] ) && strlen( $sanitized_input[$key] ) < $config['minLength'] ) {
                add_settings_error( $this->option_name, 'minLength_error', sprintf( __( 'Field "%s" must be at least %d characters long.', 'sage' ), $config['title'], $config['minLength'] ), 'error' );
            }
            if ( isset( $config['minimum'] ) && $sanitized_input[$key] < $config['minimum'] ) {
                add_settings_error( $this->option_name, 'minimum_error', sprintf( __( 'Field "%s" must be at least %d.', 'sage' ), $config['title'], $config['minimum'] ), 'error' );
            }
            if ( isset( $config['maximum'] ) && $sanitized_input[$key] > $config['maximum'] ) {
                add_settings_error( $this->option_name, 'maximum_error', sprintf( __( 'Field "%s" must be no more than %d.', 'sage' ), $config['title'], $config['maximum'] ), 'error' );
            }
        }

        // Check for required fields that were not provided
        if ( isset( $schema['required'] ) ) {
            foreach ( $schema['required'] as $required_key ) {
                if ( ! isset( $sanitized_input[$required_key] ) || $sanitized_input[$required_key] === '' ) {
                    $field_title = $schema['properties'][$required_key]['title'] ?? ucwords( str_replace( '_', ' ', $required_key ) );
                    add_settings_error( $this->option_name, 'required_field_error', sprintf( __( 'Field "%s" is required.', 'sage' ), $field_title ), 'error' );
                }
            }
        }

        // If errors occurred, return the input as is to show errors, otherwise return sanitized
        if ( ! empty( $GLOBALS['wp_settings_errors'] ) ) {
            // Filter for errors related to our option group
            $our_errors = array_filter( $GLOBALS['wp_settings_errors'], function( $error ) {
                return $error['setting'] === $this->option_name;
            } );
            if ( ! empty( $our_errors ) ) {
                return $input; // Return original input to preserve user data on error
            }
        }

        return $sanitized_input;
    }

    public function render_settings_page() {
        ?>
        

option_group ); do_settings_sections( $this->page_slug ); submit_button(); ?>
option_name, 'settings_updated', __( 'Settings saved.', 'sage' ), 'success' ); } settings_errors( $this->option_name ); } /** * Get a specific setting value. * * @param string $key The setting key. * @param mixed $default The default value if not found. * @return mixed The setting value. */ public function get_setting( string $key, $default = null ) { $options = get_option( $this->option_name, [] ); return Arr::get( $options, $key, $default ); } /** * Get all settings. * * @return array */ public function get_all_settings() { return get_option( $this->option_name, [] ); } } // Instantiate the class to register hooks new SchemaSettings();

Explanation:

  • `$schema_file`: Path to our JSON schema definition relative to Sage’s asset path.
  • `$option_group`, `$option_name`, `$page_slug`: Standard WordPress Settings API identifiers.
  • `add_admin_menu()`: Registers a new submenu page under ‘Settings’ for our API settings.
  • `register_settings()`: This is the core. It loads the schema, registers the main option group, and then iterates through the schema’s properties to dynamically add settings sections and fields using add_settings_section and add_settings_field.
  • `load_schema()`: Safely loads and decodes the JSON schema file. It uses Sage’s asset_path() helper to resolve the path correctly.
  • `add_setting_field()`: A helper to call add_settings_field for each property in the schema, passing configuration and current values.
  • `render_section_description()`: Displays the schema’s top-level description.
  • `render_field()`: Dynamically renders the HTML input for each setting based on the schema’s type and format. It handles basic types like text, number, and checkbox.
  • `sanitize_settings()`: This crucial method receives the raw input from the form. It iterates through the schema again, applying sanitization based on the defined types and formats. It also includes basic validation checks (minLength, minimum, maximum, required) and adds errors using add_settings_error. If validation errors occur, it returns the original input to prevent data loss and display the errors.
  • `render_settings_page()`: Renders the standard WordPress settings page structure, including form, settings fields, and submit button.
  • `display_admin_notices()`: Shows success or error messages after form submission.
  • `get_setting()` / `get_all_settings()`: Utility methods to easily retrieve saved settings elsewhere in your application.

Environment-Specific Overrides

The real power comes when we combine this schema-driven approach with environment-specific configurations. Sage Roots typically uses environment variables (e.g., via `.env` files) to manage settings across different deployment stages. We can leverage these variables to override default or saved settings.

Modify the SchemaSettings::get_setting() and SchemaSettings::get_all_settings() methods to check for environment variables. We’ll assume environment variables are prefixed, e.g., API_KEY, API_BASE_URL.

// Inside App\SchemaSettings\SchemaSettings class

// ... (previous methods)

/**
 * Get a specific setting value, checking environment variables first.
 *
 * @param string $key The setting key.
 * @param mixed $default The default value if not found.
 * @return mixed The setting value.
 */
public function get_setting( string $key, $default = null ) {
    $env_var_name = strtoupper( $key ); // Simple mapping, adjust as needed
    if ( getenv( $env_var_name ) !== false ) {
        return getenv( $env_var_name );
    }

    $options = get_option( $this->option_name, [] );
    return Arr::get( $options, $key, $default );
}

/**
 * Get all settings, checking environment variables first.
 *
 * @return array
 */
public function get_all_settings() {
    $saved_settings = get_option( $this->option_name, [] );
    $schema = $this->load_schema();
    $all_settings = [];

    if ( ! $schema || ! isset( $schema['properties'] ) ) {
        return $saved_settings; // Fallback if schema is missing
    }

    foreach ( $schema['properties'] as $key => $config ) {
        $env_var_name = strtoupper( $key );
        if ( getenv( $env_var_name ) !== false ) {
            $all_settings[$key] = getenv( $env_var_name );
        } else {
            $all_settings[$key] = $saved_settings[$key] ?? $config['default'] ?? null;
        }
    }
    return $all_settings;
}

// ... (rest of the class)

With these modifications, when the application runs in a specific environment (e.g., production), environment variables like API_KEY will take precedence over saved options in the database. This is crucial for security (e.g., not storing production API keys in the database) and for managing different configurations seamlessly.

Accessing Settings in Your Theme/Plugin

You can now access your API settings anywhere in your Sage Roots theme or plugin. It’s recommended to create a helper function or a service class to abstract this access.

// Example: In a helper file or service provider

use App\SchemaSettings\SchemaSettings;
use Illuminate\Support\Arr;

/**
 * Get a specific API setting.
 *
 * @param string $key The setting key (e.g., 'api_base_url').
 * @param mixed $default Default value if not found.
 * @return mixed
 */
function get_api_setting( string $key, $default = null ) {
    static $settings = null;
    if ( $settings === null ) {
        // Instantiate the class to get access to its methods
        $schema_settings = new SchemaSettings();
        $settings = $schema_settings->get_all_settings();
    }
    return Arr::get( $settings, $key, $default );
}

// Usage example:
$api_key = get_api_setting('api_key');
$base_url = get_api_setting('api_base_url');
$timeout = get_api_setting('timeout_seconds', 30); // Provide a fallback timeout

if ( ! empty( $api_key ) ) {
    // Use the API key and base URL
    // e.g., wp_remote_post( $base_url . '/resource', ['headers' => ['Authorization' => 'Bearer ' . $api_key]] );
}

Advanced Considerations and Next Steps

  • Schema Validation Libraries: For more robust validation, consider integrating a JSON Schema validation library (like `justinrainbow/json-schema` for PHP) within the sanitize_settings method to strictly enforce schema rules beyond basic type checks.
  • Complex Field Types: The render_field method can be extended to handle more complex field types like textareas, select dropdowns, color pickers, or even repeatable fields, by adding more logic based on schema properties (e.g., an `enum` property for select boxes).
  • UI/UX Enhancements: Use schema properties like description for tooltips or help text. You could also add custom attributes to the schema (e.g., "ui": {"component": "colorpicker"}) and use JavaScript in the admin to render richer UI elements.
  • Internationalization (i18n): Ensure all user-facing strings in the schema (titles, descriptions) and in the PHP code are translatable using WordPress’s internationalization functions (e.g., __(), _e()).
  • Security: Always sanitize and validate input thoroughly. For sensitive fields like API keys, ensure they are never exposed client-side and are preferably handled via environment variables. The sanitize_text_field and esc_url_raw are good starting points, but tailor them to your specific needs.
  • Sage Roots Configuration Integration: For deeper integration, explore Sage’s configuration system. You might store the schema definition path within Sage’s config/app.php or similar, making it more configurable.

By adopting a schema-driven approach for your WordPress settings, especially within a framework like Sage Roots, you create a more organized, maintainable, and extensible system for managing configurations across different environments. This pattern promotes best practices in application development and significantly enhances the developer experience.

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

  • WordPress Development Recipe: High-efficiency server-side rendering for Gutenberg blocks using Named Arguments
  • Debugging Guide: Diagnosing nonce validation collisions in multi-site network environments with modern tools
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in online course lessons
  • WordPress Development Recipe: Secure token-based API authentication for Twilio SMS Gateway in custom plugins
  • Troubleshooting PHP-FPM child process pool exhaustion in production when using modern Carbon Fields custom wrappers wrappers

Categories

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

Recent Posts

  • WordPress Development Recipe: High-efficiency server-side rendering for Gutenberg blocks using Named Arguments
  • Debugging Guide: Diagnosing nonce validation collisions in multi-site network environments with modern tools
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in online course lessons

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (869)
  • Debugging & Troubleshooting (653)
  • Security & Compliance (638)
  • 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