How to add a custom settings panel to the LA24Core Settings page.
Overview
The `la24_register_setting` filter adds panels to the LA24Core **Settings** page. Each panel has a priority (display order), a display name, and a callback class that renders the panel HTML including its form.

Hook Signature
php
apply_filters( 'la24_register_setting', array $settings );**Type:** Filter
**Parameters:** `$settings` (array) — the current settings panel registry
**Returns:** array — the modified registry
Registering a Settings Panel
php
add_filter( 'la24_register_setting', function ( $settings ) {
$settings['my_crm_settings'] = array(
'priority' => 50,
'name' => __( 'My CRM Settings', 'my-crm-ext' ),
'call_back' => '\MyPlugin\Settings\MyCrmSettings',
);
return $settings;
} );Settings Panel Fields
| Field | Type | Description |
|---|---|---|
| Key (array key) | string | Unique settings panel identifier |
| `priority` | int | Tab/panel display order (ascending) |
| `name` | string | Panel label shown in the Settings UI |
| `call_back` | string | Fully-qualified settings class name |
The Settings Class
The class referenced by `call_back` must **implement the `\LoyalAxis\interfaces\Settings` interface** (`includes/interfaces/Settings.php`):
php
namespace LoyalAxis\interfaces;
interface Settings {
public static function render_view();
public static function request_process();
}Both methods are **static** and take no arguments:
| Method | Called from | Responsibility |
|---|---|---|
| `request_process()` | `GeneralSettings::request_processor()` — once per page load, **before** any panel is rendered | Read `$_POST`, sanitize, persist the submitted values, optionally echo a success/error notice |
| `render_view()` | `GeneralSettings::tab_content_settings()` — after every registered panel's `request_process()` has already run | Echo the panel's HTML/form, pre-filled with the *current* option values |
Because every `request_process()` call runs before any `render_view()` call, a value saved during the current request is already up to date by the time `render_view()` reads it back — you never need to reconcile `$_POST` against stored options inside the render method.
Here is a minimal class that follows this contract:
php
<?php
namespace MyPlugin\Settings;
use LoyalAxis\interfaces\Settings;
defined( 'ABSPATH' ) || exit;
class MyCrmSettings implements Settings {
public static function render_view() {
$api_key = la24_get_option( 'api_key', 'my_crm_options' );
?>
<form method="post">
<table class="form-table">
<tr>
<th><?php esc_html_e( 'API Key', 'my-crm-ext' ); ?></th>
<td>
<input type="text" name="my_crm_api_key"
value="<?php echo esc_attr( $api_key ); ?>"
class="regular-text" />
</td>
</tr>
</table>
<button type="submit" name="my_crm_submit" class="button button-primary">
<?php esc_html_e( 'Save Settings', 'my-crm-ext' ); ?>
</button>
</form>
<?php
}
public static function request_process() {
if ( isset( $_POST['my_crm_submit'] ) ) {
la24_update_options( array(
'api_key' => sanitize_text_field( $_POST['my_crm_api_key'] ?? '' ),
), 'my_crm_options' );
}
}
}This mirrors the pattern LA24Core itself uses for every bundled module. The reference implementation is `LA24License_Settings.php` (`includes/plugins/license/includes/classes/LA24License_Settings.php`):
php
class LA24License_Settings implements Settings {
use Notification;
public static function render_view() {
echo Form::start_form_tag( "post", "", "" );
echo Form::start_form( 0, "la24-section-box" );
echo Form::header( __( "Software License Management", 'la24core' ), 12, "la24-titr" );
$enable_sos = la24_get_option( 'enable_sos', 'la24license_options' );
echo Form::checkbox( __( "Emergency Conditions (New Sales Suspension)", 'la24core' ), 6, "enable_sos", "enable_sos", "", $enable_sos, 0, $enable_sos, '' );
// ...more fields, each read back with la24_get_option()...
echo Form::submit_btn( __( "Save Settings", 'la24core' ), 12, "licenseplugin_submit", "la24_licenseplugin_submit" );
echo Form::close_form();
echo Form::close_form_tag();
}
public static function request_process() {
if ( isset( $_POST['la24_licenseplugin_submit'] ) ) {
la24_update_options(
array(
'global_keys' => /* ... */,
'interval_check_expire_license' => $_POST['interval_check_expire_license'] ?? '',
'enable_sos' => $_POST['enable_sos'] ?? '',
),
'la24license_options'
);
la24_update_options( array( 'active_setting_tab' => 'plugin_license' ) );
self::alert( __( 'License management settings saved successfully.', 'la24core' ), true, 'success' );
}
}
}Notice the same shape as `MyCrmSettings` above: `implements Settings`, static `render_view()` that reads values with `la24_get_option()`, and static `request_process()` that gates the save behind `isset( $_POST[''] )` and persists everything with a single `la24_update_options()` call.
> **Tip:** `LA24License_Settings::request_process()` also calls `la24_update_options( array( 'active_setting_tab' => 'plugin_license' ) )` after saving, so the Settings page reopens on the same tab the user just submitted. Do the same with your own panel's key if you want that behavior.
> **Note:** LA24Core's own panels don't add a nonce field — access to the Settings page is already gated by a capability check (`la24_generalsettings_access`) before `request_process()` ever runs. If your extension needs CSRF protection beyond that, you're free to add `wp_nonce_field()` / `wp_verify_nonce()` around your own save logic.
`Form::*` (used above) is LA24Core's internal form-rendering utility (`\LoyalAxis\classes\utilities\Form`) and isn't required — plain HTML, as in the `MyCrmSettings` example, works just as well.
Saving and Reading Options
There are two ways to persist the values your settings panel collects.
Option A — Standard WordPress functions
Use `update_option()` / `get_option()` directly. This works anywhere in WordPress and is the right choice if a value also needs to be read by code outside LA24Core (another plugin, a theme, etc.):
php
// Save
update_option( 'my_crm_api_key', sanitize_text_field( $_POST['api_key'] ?? '' ) );
// Read with a default
$api_key = get_option( 'my_crm_api_key', '' );Every value saved this way occupies its own row in `wp_options`. If your panel has several fields, group them into one array to keep the table tidy:
php
// Save
update_option( 'my_crm_options', array(
'api_key' => sanitize_text_field( $_POST['api_key'] ?? '' ),
'enabled' => (bool) ( $_POST['enabled'] ?? false ),
) );
// Read with defaults
$opts = wp_parse_args( get_option( 'my_crm_options', array() ), array(
'api_key' => '',
'enabled' => false,
) );Option B — LA24 option helpers
`la24_get_option()` and `la24_update_options()` do this grouping for you automatically, and are what every bundled LA24Core module — including `LA24License_Settings.php` above — actually uses:
php
// Save
la24_update_options( array(
'api_key' => sanitize_text_field( $_POST['api_key'] ?? '' ),
'enabled' => $_POST['enabled'] ?? '',
), 'my_crm_options' );
// Read
$api_key = la24_get_option( 'api_key', 'my_crm_options' );
$enabled = la24_get_option( 'enabled', 'my_crm_options' );See the [next section](#tutorial-la24_get_option-and-la24_update_options) for the full walkthrough of how these two functions behave.
Either approach is acceptable inside a settings class — pick whichever fits the rest of your extension. Just don't mix both for the *same* key: `update_option()` and `la24_update_options()` don't know about each other's data, since the latter stores everything as one serialized array under a group name.
Tutorial: la24_get_option() and la24_update_options()
Both functions are declared in `includes/helpers/la24-core-functions.php` and are thin wrappers around the internal `\LoyalAxis\classes\utilities\Options` class:
php
function la24_get_option( $key, $option = 'la24_options' ) {
return Options::get_option( $key, $option );
}
function la24_update_options( array $data, $option = 'la24_options' ) {
return Options::update_options( $data, $option );
}A related helper in the same file, `la24_delete_option( $key, $option = 'la24_options' )`, removes a single key from a group.
The "group" concept
Unlike `update_option()`, which stores one value per WordPress option row, the LA24 helpers store **many keys inside a single serialized WordPress option** — the `$option` argument (also referred to as the "group"). `LA24License_Settings.php` keeps every one of its settings (`global_keys`, `interval_check_expire_license`, `enable_sos`, `product_id_support`, `days_support`, …) inside one group, `'la24license_options'`, saved with a single call:
php
la24_update_options(
array(
'global_keys' => $temp_global_key,
'interval_check_expire_license' => $_POST['interval_check_expire_license'] ?? '',
'enable_sos' => $_POST['enable_sos'] ?? '',
'product_id_support' => $_POST['product_id_support'],
'days_support' => $_POST['days_support'],
),
'la24license_options'
);...and reads each one back individually:
php
$enable_sos = la24_get_option( 'enable_sos', 'la24license_options' );If you omit the group argument, it defaults to `'la24_options'` — the group LA24Core's own core settings (login, style, general, …) live in. **Always pass an explicit, unique group name for your extension** (e.g. `'my_crm_options'`) so your keys can't collide with LA24Core's own options or another extension's.
`la24_get_option( $key, $group = 'la24_options' )`
Reads `$key` out of the group's stored array:
- If the group already has a **non-empty** value for `$key`, that value is returned as-is (arrays included).
- Otherwise, LA24Core looks up a `default` registered for `$key` (see below) and returns that instead, or `null` if none is registered.
`la24_update_options( array $data, $group = 'la24_options' )`
Merges `$data` into the group's existing stored array and persists it with a single `update_option()` call:
- Every key present in `$data` overwrites the corresponding key already stored in the group.
- **An empty value (`''`, `null`, `false`, `0`, `[]`) removes that key from the group instead of storing it** — so, for example, an unchecked checkbox correctly falls back to its default the next time `la24_get_option()` is called, rather than being stored as an empty string.
- Keys not present in `$data` are left untouched.
So a typical `request_process()` only needs to pass the fields that were actually submitted:
php
public static function request_process() {
if ( isset( $_POST['my_crm_submit'] ) ) {
la24_update_options( array(
'api_key' => sanitize_text_field( $_POST['api_key'] ?? '' ),
'enabled' => $_POST['enabled'] ?? '',
), 'my_crm_options' );
}
}Optional: defaults and sanitization rules
You can register per-key metadata for your group — a `default` value and a sanitize `rule` — by hooking the `la24_settings_option` filter, the same way `LA24License_Hook::settings_option()` does for the License module:
php
add_filter( 'la24_settings_option', function ( $settings ) {
$settings['my_crm_options'] = array(
'api_key' => array(
'rule' => 'text_field', // run through Sanitizer::text_field() on every save
'default' => '',
),
'enabled' => array(
'rule' => 'number',
),
);
return $settings;
} );Available `rule` values map directly to methods on `\LoyalAxis\classes\utilities\Sanitizer` — `text_field`, `number`, `url`, `email`, `checkbox`, `html`, and a few `array_of_*` variants for repeatable fields.
This step is optional — `la24_get_option()` / `la24_update_options()` work correctly without it as long as you sanitize `$_POST` yourself before calling `la24_update_options()`, exactly as the examples above do. Registering a `rule` mainly saves you from repeating `sanitize_text_field()` at every call site, and a `default` saves you from repeating `?: 'fallback'` at every read site.
Full Settings Panel Example
For a complete multi-field settings panel with grouped options and success feedback, see the `DealsSettings` class in **developer-guide-la24_extension_examples.md**, modeled directly on the real-world `LA24License_Settings.php` shipped with LA24Core.
