Search by

baxtian / wp_settings

baxtian

Class to be inherite to create settings

Package info

bitbucket.org/baxtian/wp_settings

pkg:composer/baxtian/wp_settings

Statistics

Installs: 103

Dependents: 0

Suggesters: 0

0.1.19 2026-09-12 02:15 UTC

This package is auto-updated.

Last update: 2026-09-12 02:22:35 UTC


README

Class to be inherited to create a WP Settings.

Mantainers

Juan SebastiΓ‘n Echeverry baxtian.echeverry@gmail.com

πŸš€ Usage Guide

This library requires a minimum of two configuration files: one to define the Page and one to define a Section within that page.

1. Defining the Settings Page

The page configuration file sets up the main menu item, its location, and basic properties within the WordPress dashboard.

PropertyDescriptionRequired?Example Value
slugThe unique identifier (slug) for this settings page in WordPress.Yesmy_plugin_options
page_titleThe title displayed at the top of the settings screen.YesMy Plugin Settings
sectionsAn array containing instances of the defined Section classes.YesSee examples below.
menu_titleThe text displayed for the menu/sub-menu item in the dashboard.YesPlugin Options
parent_slugThe slug of the existing parent menu this page belongs to.Yesoptions-general.php

Parent Slugs for Standard WordPress Menus:

  • Appearance: themes.php
  • Tools: tools.php
  • Settings: options-general.php
  • (Use a custom slug for a new top-level menu.)
<?php
namespace My_Plugin\Settings;

use My_Plugin\Settings\Section\Style;
use Baxtian\WP_Settings;

/**
 * Configuration page
 */
class My_Plugin extends WP_Settings
{
	use \Baxtian\SingletonTrait;

	protected function __construct()
	{
		$this->slug        = 'my_plugin';
		$this->parent_slug = 'options-general.php';
		$this->sections    = [
			new Style()
		];

		add_action('init', [$this, 'init']);

		parent::__construct();
	}

	public function init()
	{
		$this->page_title = __('My Plugin Settings', 'my_plugin');
		$this->menu_title = __('My Plugin', 'my_plugin');
	}

}

Filename: src/Settings/My_Plugin.php

2. Defining a Section

The section configuration defines a tab or logical grouping of settings within the Page.

PropertyDescriptionRequired?Example Value
slugThe unique identifier (slug) for this section within the page.Yesgeneral_settings_tab
titleThe title displayed for the section's tab.YesGeneral Settings
subsectionsAn array containing the definitions for each subsection.YesSee Subsection Properties below.

3. Defining Subsections and Fields

The subsections array holds definitions for the fields that will actually store data, grouped logically for display.

Subsection Properties

Each item in the subsections array defines a block of related fields:

PropertyDescriptionNotes
slugThe unique identifier (slug) for this subsection within the section.Used internally for grouping fields.
titleThe displayed title for this subsection block.Set to false to suppress the title display.
descriptionDescriptive text displayed below the title.Set to false to suppress the description.
fieldsAn array defining all input fields for this subsection.See Field Properties below.

Field Properties

Each item in the fields array defines a single input control:

PropertyDescriptionDefaultAvailable Types
nameThe unique option name used to retrieve this field's stored value.
labelThe descriptive label displayed next to the field.
typeThe type of input field to render.texttext, checkbox, dropdown, number, password
defaultThe fallback value used if the option has not yet been saved by the user.
descriptionHelp text displayed beneath the input field.Set to false to hide.
classCustom CSS classes to apply to the input element.
editable_ifA callable evaluated (with no arguments) on every save. When it returns false, the submitted value for this field is discarded and the previously stored value is restored β€” enforced server-side in the sanitize_callback, independent of whether the field is rendered readonly/disabled in the browser.null (always editable)
wp_optionBinds this field to a native WordPress option (e.g. admin_email, blogname) instead of storing it in this page's own serialized option. Reading (for display) always goes through get_option() on this option. Writing (on save) also goes through update_option() on this same option, unless wp_option_write overrides the write target.null (stored in this page's own option)
wp_option_writeOnly meaningful together with wp_option. Overrides where the value is written on save, while wp_option keeps controlling what's displayed. Needed when a native option's real update flow lives behind a different option β€” admin_email is the standing example: WordPress core never writes it directly from a form submission, it writes new_admin_email instead, whose own hook (update_option_new_admin_email() in wp-admin/includes/misc.php) sends a confirmation email and only updates admin_email once that link is followed. Binding straight to 'wp_option' => 'admin_email' with no wp_option_write skips that confirmation entirely β€” it changes the live admin email immediately, unconfirmed.null (writes to wp_option)

Reading a wp_option field from outside the owning plugin: call get_option('admin_email') directly (or whatever native option name you bound), the same as you would for any other native WordPress option β€” it's never written into this page's own serialized option, so reading it through Settings::get_instance()->get_option(...) from another plugin/theme still works, but going straight to get_option() is simpler and avoids depending on the page-owning class at all.

admin_email field example β€” read from one option, write to another, and surface WordPress's own pending-confirmation state:

[
    'name'            => 'notifications_email',
    'label'           => __('Notifications email', 'my_plugin'),
    'type'            => 'text',
    'default'         => get_option('admin_email'),
    'wp_option'       => 'admin_email',
    'wp_option_write' => 'new_admin_email',
],

The confirmation link WordPress sends goes to the new address, not the old one (wp_mail($value, ...) in update_option_new_admin_email()) β€” admin_email keeps showing the old value until that link is followed. To warn the user a change is pending, check get_option('new_admin_email') against get_option('admin_email') in your Section::render_field() override, the same data wp-admin/options-general.php reads for its own notice.

<?php
namespace My_Plugin\Settings\Section;

use Baxtian\WP_Settings\Section;

/**
 * Section of the configuration
 */
class Style extends Section
{
	public function __construct()
	{
		$this->slug = 'style';
		add_action('init', [$this, 'init']);

		parent::__construct();
	}

	public function init()
	{
		$this->title = __('Style', 'my_plugin');

		$this->subsections = [
			[
				'slug'        => 'colors',
				'title'       => __('Colors', 'my_plugin'),
				'description' => false,
				'fields'      => [
					[
						'name'        => 'text_color',
						'label'       => __('Text color', 'my_plugin'),
						'class'       => false,
						'description' => false,
						'default'     => 'black',
						'type'        => 'string',
					],
					[
						'name'        => 'text_background',
						'label'       => __('Text background color', 'my_plugin'),
						'class'       => false,
						'description' => false,
						'default'     => 'silver',
						'type'        => 'string',
					],
				],
			],
		];
	}
}

Filename: src/Settings/Section/Style.php

4. Extending Sections from Another Plugin or Theme

WP_Settings::__construct() applies the filter wp_settings_sections_{slug} (where {slug} is the transformed slug β€” for the page-owning class from section 1 above, $this->slug = 'my_plugin' yields 'config_my_plugin') right after computing $this->slug and before linking sections to it. This lets a separate plugin β€” or a theme β€” add its own Section to an existing settings page without modifying its source.

Build the Section in the extending class's own constructor, not inside the filter callback. The filter callback runs late (see "Load-order guarantee" below), while the class that registers the filter is itself constructed early, at plugin/theme load time β€” the same moment the page-owning class builds its own sections (section 1). Building the Section there, instead of lazily inside the callback, is what lets that Section use the same add_action('init', [$this, 'init']) declaration pattern from section 2/3 β€” see the callout below for why that matters.

From another plugin:

<?php
namespace Other_Plugin\Settings;

class Sections
{
    use \Baxtian\SingletonTrait;

    private Section\MySection $section;

    protected function __construct()
    {
        $this->section = new Section\MySection();

        add_filter('wp_settings_sections_config_my_plugin', [$this, 'add_section']);
    }

    public function add_section($sections)
    {
        $sections[] = $this->section;
        return $sections;
    }
}

From a theme β€” identical pattern, only the namespace changes:

<?php
namespace My_Theme\Settings;

class Sections
{
    use \Baxtian\SingletonTrait;

    private Section\MySection $section;

    protected function __construct()
    {
        $this->section = new Section\MySection();

        add_filter('wp_settings_sections_config_my_plugin', [$this, 'add_section']);
    }

    public function add_section($sections)
    {
        $sections[] = $this->section;
        return $sections;
    }
}

Computing the filter name: the slug passed to the filter is str_replace('-', '_', sanitize_title('Config-' . $original_slug)). For $this->slug = 'my_plugin' this yields config_my_plugin.

Load-order guarantee: the wp_settings_sections_{slug} filter is applied on the init action (priority 20) β€” not in the constructor, and not lazily on first render. This means the page-owning class can be constructed eagerly, at plugin load time, even by a theme that extends it: themes finish loading after every plugin, so a filter added from functions.php would otherwise always be registered too late for a filter applied in the constructor. init runs after every plugin/theme's normal loading has registered its add_filter() call, but β€” importantly β€” before admin_init, which is when each Section registers its fields via add_settings_section()/add_settings_field(). A section added by the filter still needs to exist before admin_init fires for its fields to register; resolving any later (e.g. on first render) would miss that window entirely.

⚠️ Don't build the Section inside the filter callback. It's tempting to write $sections[] = new Section\MySection(); directly in add_section(), since it's shorter β€” but the filter callback only runs when resolve_sections() applies it, on init at priority 20. If Section\MySection's own constructor follows the standard pattern from section 2/3 (add_action('init', [$this, 'init'])), that registration happens while init is already executing β€” WordPress has moved past priority 10 for this pass, so init() never runs, and the section renders with no title and no fields (silently β€” no error, no exception). Building the Section eagerly in the extending Sections class's own constructor (as shown above) avoids this entirely: the Section exists well before init fires, so its own add_action('init', ...) registers on time, exactly like it does for a section owned directly by the settings page (section 1).

Reading values from outside the owning plugin: a Section added via this filter still stores its values in the page-owning class's option (e.g. config_my_plugin, not one scoped to the extending plugin/theme). If you're that extending plugin/theme and need to read a value elsewhere in your own code (not from your Section, which already gets option_name for free β€” see below), read the option directly with WordPress's native get_option('config_my_plugin', [])['field_name'] ?? $default, not by instantiating the page-owning class (My_Plugin\Settings\My_Plugin::get_instance()->get_option(...), the class from section 1 that declared $this->slug = 'my_plugin'). Two reasons:

  • Decoupling. The option name is the real, stable contract between the two β€” it's what your own Section already writes to and what the page's installer/activation code already reads/writes. The class that owns the page is an internal implementation detail of that plugin; depending on its name/namespace couples you to something it's free to restructure without warning.
  • No exception path. WP_Settings::get_option() throws if the field isn't registered in the resolved sections or has no default β€” an unnecessary failure mode for code (e.g. a front-end template) that should just fall back to a default instead of fataling.

Code that lives inside the page-owning plugin (alongside the WP_Settings subclass itself) doesn't need this β€” calling its own class's get_option() there is a normal in-package dependency, not a cross-boundary one.

5. Retrieving an Option Value

To retrieve a field's value, call the instance of the settings class and use the get_option() method. If the field has not been configured by the user, the system automatically returns the defined default value. If the field is not registered or does not have a default value, the system generates an exception.

use My_Plugin\Settings\My_Plugin as Settings;
.
.
.
$settings = Settings::get_instance();
$text_color = $settings->get_option('text_color');
$text_background = $settings->get_option('text_background');

6. Writing Values Outside a Field (from other code)

Other code β€” an installer, a REST endpoint, another plugin β€” can write directly into this page's option via update_option($slug, [...]) without ever defining a field for that key. Saving the settings page afterwards won't erase it: sanitize() merges the submitted input into what's already stored, so any key not backed by a registered field survives across saves. Submitted values still take priority over stored ones for the same key.

Changelog

See CHANGELOG.md.