baxtian / wp_settings
Class to be inherite to create settings
Requires
- php: >=8.1
Requires (Dev)
- baxtian/merak: ^0.5.30
- baxtian/merak-env: ^0.7.6
- timber/timber: ^2.0
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.
| Property | Description | Required? | Example Value |
|---|---|---|---|
slug | The unique identifier (slug) for this settings page in WordPress. | Yes | my_plugin_options |
page_title | The title displayed at the top of the settings screen. | Yes | My Plugin Settings |
sections | An array containing instances of the defined Section classes. | Yes | See examples below. |
menu_title | The text displayed for the menu/sub-menu item in the dashboard. | Yes | Plugin Options |
parent_slug | The slug of the existing parent menu this page belongs to. | Yes | options-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.
| Property | Description | Required? | Example Value |
|---|---|---|---|
slug | The unique identifier (slug) for this section within the page. | Yes | general_settings_tab |
title | The title displayed for the section's tab. | Yes | General Settings |
subsections | An array containing the definitions for each subsection. | Yes | See 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:
| Property | Description | Notes |
|---|---|---|
slug | The unique identifier (slug) for this subsection within the section. | Used internally for grouping fields. |
title | The displayed title for this subsection block. | Set to false to suppress the title display. |
description | Descriptive text displayed below the title. | Set to false to suppress the description. |
fields | An 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:
| Property | Description | Default | Available Types |
|---|---|---|---|
name | The unique option name used to retrieve this field's stored value. | ||
label | The descriptive label displayed next to the field. | ||
type | The type of input field to render. | text | text, checkbox, dropdown, number, password |
default | The fallback value used if the option has not yet been saved by the user. | ||
description | Help text displayed beneath the input field. | Set to false to hide. | |
class | Custom CSS classes to apply to the input element. | ||
editable_if | A 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) |
<?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
Sectioninside the filter callback. It's tempting to write$sections[] = new Section\MySection();directly inadd_section(), since it's shorter β but the filter callback only runs whenresolve_sections()applies it, oninitat priority 20. IfSection\MySection's own constructor follows the standard pattern from section 2/3 (add_action('init', [$this, 'init'])), that registration happens whileinitis already executing β WordPress has moved past priority 10 for this pass, soinit()never runs, and the section renders with no title and no fields (silently β no error, no exception). Building theSectioneagerly in the extendingSectionsclass's own constructor (as shown above) avoids this entirely: theSectionexists well beforeinitfires, so its ownadd_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
Sectionalready 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');
Changelog
See CHANGELOG.md.