Einstellungsseiten mit der Settings API und register_setting

Inhalt
Fast jedes Plugin braucht eine Seite, auf der Administratoren ein paar Werte ändern können. Erstaunlich viele Plugins bauen sie noch von Hand: mit einem eigenen POST-Handler, einer Capability-Prüfung hier, einer vergessenen Nonce dort und einem Dutzend einzelner Zeilen in wp_options. WordPress bietet dafür seit Version 2.7 die Settings API. Sie stellt das Formular im gewohnten Admin-Layout dar, schickt es an wp-admin/options.php und übernimmt dort die Nonce-Prüfung, Rechteprüfung, Bereinigung und Speicherung. Seit WordPress 4.7 beschreibt register_setting() außerdem die Daten selbst: Typ, Standardwert, Sanitize-Callback und über show_in_rest die Sichtbarkeit im REST-Endpunkt /wp/v2/settings.
Dieser Beitrag baut ein kleines, aber vollständiges Plugin namens „LW Settings Demo“: eine Hinweisleiste, deren drei Einstellungen gebündelt in einer einzigen Option liegen, eine Seite unter „Einstellungen“, einen Sanitize-Callback, der Formular und REST-API gleichermaßen bedient, und ein kurzes Skript, das dieselbe Option mit apiFetch liest und schreibt. Unterwegs wird ein einzelnes Formularfeld vom Eingabeelement bis zu seiner Zeile in wp_options verfolgt, samt der Stelle, an der der REST-Weg abzweigt. Der Core-Quelltext wurde am 29.09.2026 gelesen; laut Versionsprüfung von WordPress.org ist 7.1.2 die aktuelle Version, das Beispiel setzt mindestens WordPress 6.6 voraus.
Vier Aufgaben, vier Funktionsgruppen
Die Settings API zerlegt eine Einstellungsseite in klar getrennte Aufgaben. Jede Funktionsgruppe gehört an einen eigenen Hook. Wer das durcheinanderbringt, bekommt meist eine Seite, die sauber erscheint, aber nichts speichert.
| Funktion | Hook | Aufgabe |
|---|---|---|
add_options_page() |
admin_menu |
Menüeintrag unter „Einstellungen“, Capability für die Anzeige des Menüpunkts |
register_setting() |
init (Erläuterung weiter unten) |
Trägt die Option in die Freigabeliste einer Gruppe ein, hängt Sanitize-Callback, Standardwert und REST-Schema an |
add_settings_section(), add_settings_field() |
admin_init |
Layout: Überschriften, Beschriftungen, Callbacks für die Eingabeelemente |
settings_fields(), do_settings_sections() |
Seiten-Callback | Versteckte Felder mit Nonce, danach alle Abschnitte und Felder der Seite |
add_options_page() ist nur eine dünne Hülle um add_submenu_page() mit options-general.php als Elternseite. Die dort übergebene Capability entscheidet darüber, ob der Menüpunkt erscheint; Benutzer ohne sie erhalten auch beim direkten Aufruf der Seitenadresse den Status 403. Der Docblock verlangt ausdrücklich, dass der Seiten-Callback die Berechtigung noch einmal selbst prüft; deshalb beginnt die Render-Funktion im Beispiel mit current_user_can( 'manage_options' ). Beim Speichern folgt eine dritte, getrennte Prüfung in options.php, wie der Abschnitt zum Speicherweg zeigt.
Formular und Speicherung hängen über die Optionsgruppe zusammen. register_setting( 'lwsd_notice_bar', 'lwsd_settings', … ) nimmt die Option lwsd_settings in die Gruppe lwsd_notice_bar auf, und settings_fields( 'lwsd_notice_bar' ) schickt genau diesen Gruppennamen als verstecktes Feld option_page mit. Die Seiten-ID, die add_settings_section() und do_settings_sections() verwenden, ist ein anderer Bezeichner; im Beispiel entspricht sie dem Menü-Slug lwsd-notice-bar.
Eine Option statt Dutzender
Das Plugin Handbook empfiehlt, zusammengehörige Werte als Array in einer einzigen Option abzulegen, weil Lesen und Schreiben dann in einer Datenbanktransaktion statt in vielen geschieht. Der Quelltext von options.php liefert einen zweiten, weniger offensichtlichen Grund. Beim Speichern durchläuft die Datei alle Optionen, die in der gesendeten Gruppe registriert sind, und ruft für jede update_option() auf. Fehlt ein Feld in den POST-Daten, wird null übergeben. Zwei Formulare in derselben Gruppe überschreiben sich deshalb gegenseitig. Mit einer Array-Option je Formular entfällt diese Falle, und der Sanitize-Callback sieht alle Werte gleichzeitig. Damit lassen sich Regeln über mehrere Felder formulieren, etwa „keine aktive Hinweisleiste ohne Text“.
Der Preis ist die Serialisierung: Das Array steht als serialisierte Zeichenkette in einer Zeile, einzelne Schlüssel lassen sich per SQL nicht filtern. Für Konfigurationswerte spielt das selten eine Rolle. Für Daten, die abgefragt werden müssen, zeigt der Beitrag zu den Leistungsgrenzen von wp_postmeta und eigenen Tabellen die Alternativen.
Autoload bewusst setzen
WordPress 6.6 hat die Autoload-Spalte umgebaut. Der Parameter $autoload von add_option() steht seitdem standardmäßig auf null. In der Spalte landen on und off für ausdrückliche Entscheidungen, auto, wenn niemand entschieden hat, sowie auto-on oder auto-off, wenn ein Filter dynamisch entschieden hat. Die alten Werte yes und no gelten seit 6.7 als veraltet. Optionen über 150.000 Bytes werden standardmäßig nicht automatisch geladen (Filter wp_max_autoloaded_option_size). Laut wp_autoload_values_to_autoload() wird auto derzeit noch geladen; die Dev Note zu 6.6 hält aber fest, dass sich dieser Standard in künftigen Versionen ändern kann.
Für die Settings API ist das relevant, weil options.php update_option() ohne Autoload-Argument aufruft. Existiert die Option noch nicht, ruft update_option() dafür add_option() auf, und die Zeile entsteht mit auto. Wer Autoload ausdrücklich festlegen will, legt die Option selbst an. Das Beispiel macht das bei der Aktivierung mit add_option( …, true ), weil die Hinweisleiste bei jedem Frontend-Aufruf gelesen wird.
Spätere Aufrufe von update_option() ohne Autoload-Argument behalten ein ausdrückliches on oder off bei; neu bewertet werden nur die auto-Varianten. Eine Option, die nur auf einer einzigen Admin-Seite gebraucht wird, bekäme stattdessen false.
Nachträglich lässt sich Autoload mit wp_set_option_autoload() (seit 6.4) ändern, ohne den gespeicherten Optionswert anzufassen. update_option() übernimmt einen neuen Autoload-Wert dagegen nur, wenn sich auch der Optionswert ändert. Was die einzelnen Werte der Spalte in der Praxis bedeuten, behandelt ein eigener, geplanter Beitrag zu Autoload.
Das vollständige Plugin
Die Hauptdatei enthält alles: Registrierung, Seite, Felder, Sanitize-Callback, Frontend-Ausgabe und Aktivierung. Das Präfix lwsd_ gilt durchgehend für Funktionen, Konstanten, die Option und die HTML-IDs.
<?php
/**
* Plugin Name: LW Settings Demo
* Description: Notice bar with a settings page built on the Settings API, also available via /wp/v2/settings.
* Version: 1.0.0
* Requires at least: 6.6
* Requires PHP: 7.4
* Author: Lukas Wojcik
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: lw-settings-demo
*
* @package LW_Settings_Demo
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
const LWSD_OPTION = 'lwsd_settings'; // Row in wp_options.
const LWSD_GROUP = 'lwsd_notice_bar'; // Option group, sent as option_page.
const LWSD_PAGE = 'lwsd-notice-bar'; // Menu slug and settings page ID.
/**
* Default values of the bundled option.
*
* @return array
*/
function lwsd_default_settings() {
return array(
'enabled' => false,
'message' => '',
'position' => 'bottom',
);
}
/**
* Returns the stored settings merged with the defaults.
*
* @return array
*/
function lwsd_get_settings() {
$settings = get_option( LWSD_OPTION );
if ( ! is_array( $settings ) ) {
$settings = array();
}
return wp_parse_args( $settings, lwsd_default_settings() );
}
/**
* Registers the option on init, so admin screens and REST requests both know it.
*/
function lwsd_register_setting() {
register_setting(
LWSD_GROUP,
LWSD_OPTION,
array(
'type' => 'object',
'label' => __( 'Notice bar', 'lw-settings-demo' ),
'description' => __( 'Visibility, text and position of the notice bar.', 'lw-settings-demo' ),
'sanitize_callback' => 'lwsd_sanitize_settings',
'default' => lwsd_default_settings(),
'show_in_rest' => array(
'schema' => array(
'type' => 'object',
'properties' => array(
'enabled' => array(
'type' => 'boolean',
),
'message' => array(
'type' => 'string',
'maxLength' => 200,
),
'position' => array(
'type' => 'string',
'enum' => array( 'top', 'bottom' ),
),
),
),
),
)
);
}
add_action( 'init', 'lwsd_register_setting' );
/**
* Sanitizes the whole settings array.
*
* Runs for options.php and for REST writes, because both end in update_option().
* Must be idempotent: on the first save of a missing option it runs twice.
*
* @param mixed $input Raw value.
* @return array
*/
function lwsd_sanitize_settings( $input ) {
$defaults = lwsd_default_settings();
if ( ! is_array( $input ) ) {
return $defaults;
}
$message = isset( $input['message'] ) ? sanitize_text_field( $input['message'] ) : '';
$position = isset( $input['position'] ) ? sanitize_key( $input['position'] ) : '';
$output = array(
'enabled' => ! empty( $input['enabled'] ),
'message' => mb_substr( $message, 0, 200 ),
'position' => in_array( $position, array( 'top', 'bottom' ), true ) ? $position : $defaults['position'],
);
if ( $output['enabled'] && '' === $output['message'] ) {
$output['enabled'] = false;
// add_settings_error() lives in wp-admin/includes/template.php,
// which a REST request does not load.
if ( function_exists( 'add_settings_error' ) ) {
add_settings_error(
LWSD_OPTION,
'lwsd_empty_message',
__( 'The notice bar stays off until a message is entered.', 'lw-settings-demo' ),
'warning'
);
}
}
return $output;
}
/**
* Adds the page below Settings and loads its script only there.
*/
function lwsd_add_settings_page() {
$hook_suffix = add_options_page(
__( 'Notice bar', 'lw-settings-demo' ),
__( 'Notice bar', 'lw-settings-demo' ),
'manage_options',
LWSD_PAGE,
'lwsd_render_settings_page'
);
if ( $hook_suffix ) {
add_action( 'load-' . $hook_suffix, 'lwsd_load_settings_page' );
}
}
add_action( 'admin_menu', 'lwsd_add_settings_page' );
/**
* Runs only when the settings page is requested.
*/
function lwsd_load_settings_page() {
add_action( 'admin_enqueue_scripts', 'lwsd_enqueue_admin_script' );
}
/**
* Enqueues the REST demo script.
*/
function lwsd_enqueue_admin_script() {
wp_enqueue_script(
'lwsd-settings-rest',
plugins_url( 'js/settings-rest.js', __FILE__ ),
array( 'wp-api-fetch', 'wp-i18n' ),
'1.0.0',
array( 'in_footer' => true )
);
wp_set_script_translations( 'lwsd-settings-rest', 'lw-settings-demo' );
}
/**
* Declares section and fields. Only needed on admin screens.
*/
function lwsd_register_fields() {
add_settings_section(
'lwsd_section_main',
__( 'Display', 'lw-settings-demo' ),
'lwsd_render_section',
LWSD_PAGE
);
add_settings_field(
'lwsd_enabled',
__( 'Show notice bar', 'lw-settings-demo' ),
'lwsd_render_enabled_field',
LWSD_PAGE,
'lwsd_section_main',
array( 'label_for' => 'lwsd_enabled' )
);
add_settings_field(
'lwsd_message',
__( 'Message', 'lw-settings-demo' ),
'lwsd_render_message_field',
LWSD_PAGE,
'lwsd_section_main',
array( 'label_for' => 'lwsd_message' )
);
add_settings_field(
'lwsd_position',
__( 'Position', 'lw-settings-demo' ),
'lwsd_render_position_field',
LWSD_PAGE,
'lwsd_section_main',
array( 'label_for' => 'lwsd_position' )
);
}
add_action( 'admin_init', 'lwsd_register_fields' );
/**
* Section intro.
*/
function lwsd_render_section() {
echo '<p>' . esc_html__( 'All three values are stored together in one option.', 'lw-settings-demo' ) . '</p>';
}
/**
* Checkbox field.
*/
function lwsd_render_enabled_field() {
$settings = lwsd_get_settings();
printf(
'<input type="checkbox" id="lwsd_enabled" name="%1$s[enabled]" value="1" %2$s />',
esc_attr( LWSD_OPTION ),
checked( $settings['enabled'], true, false )
);
}
/**
* Text field.
*/
function lwsd_render_message_field() {
$settings = lwsd_get_settings();
printf(
'<input type="text" class="regular-text" id="lwsd_message" name="%1$s[message]" value="%2$s" maxlength="200" />',
esc_attr( LWSD_OPTION ),
esc_attr( $settings['message'] )
);
}
/**
* Select field.
*/
function lwsd_render_position_field() {
$settings = lwsd_get_settings();
$choices = array(
'top' => __( 'Top of the page', 'lw-settings-demo' ),
'bottom' => __( 'Bottom of the page', 'lw-settings-demo' ),
);
printf( '<select id="lwsd_position" name="%s[position]">', esc_attr( LWSD_OPTION ) );
foreach ( $choices as $value => $label ) {
printf(
'<option value="%1$s" %2$s>%3$s</option>',
esc_attr( $value ),
selected( $settings['position'], $value, false ),
esc_html( $label )
);
}
echo '</select>';
}
/**
* Settings page. The form posts to options.php, not to this page.
*/
function lwsd_render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( LWSD_GROUP );
do_settings_sections( LWSD_PAGE );
submit_button();
?>
</form>
<p>
<button type="button" class="button" id="lwsd-rest-toggle"><?php esc_html_e( 'Toggle via REST API', 'lw-settings-demo' ); ?></button>
<span id="lwsd-rest-status" aria-live="polite"></span>
</p>
</div>
<?php
}
/**
* Prints the notice bar on the front end.
*
* @param string $position Where the calling hook prints: top or bottom.
*/
function lwsd_print_notice( $position ) {
$settings = lwsd_get_settings();
if ( ! $settings['enabled'] || $position !== $settings['position'] || '' === $settings['message'] ) {
return;
}
printf(
'<div class="lwsd-notice lwsd-notice-%1$s" role="status">%2$s</div>',
esc_attr( $position ),
esc_html( $settings['message'] )
);
}
/**
* Top position, needs a theme that calls wp_body_open().
*/
function lwsd_print_top_notice() {
lwsd_print_notice( 'top' );
}
add_action( 'wp_body_open', 'lwsd_print_top_notice' );
/**
* Bottom position.
*/
function lwsd_print_bottom_notice() {
lwsd_print_notice( 'bottom' );
}
add_action( 'wp_footer', 'lwsd_print_bottom_notice' );
/**
* Creates the option with an explicit autoload value.
*/
function lwsd_activate() {
// Read on every front-end request, so autoload it on purpose.
add_option( LWSD_OPTION, lwsd_default_settings(), '', true );
}
register_activation_hook( __FILE__, 'lwsd_activate' );
Warum register_setting an init hängt
Das Beispiel im Plugin Handbook registriert Einstellungen an admin_init. Dieser Hook feuert in wp-admin/admin.php, und ein REST-Aufruf an /wp-json/ lädt diese Datei nie. Eine nur dort registrierte Einstellung funktioniert im Formular, fehlt aber in /wp/v2/settings. Der Core registriert seine eigenen Einstellungen an rest_api_init, und das Beispiel im Developer Blog für React-basierte Einstellungsseiten nutzt init. Das Plugin folgt dem: init deckt options.php, die Einstellungsseite, REST-Aufrufe und das Frontend ab, wo zusätzlich der registrierte Standardwert greift. Abschnitte und Felder sind dagegen reines Admin-Layout und bleiben an admin_init.
Ein Sanitize-Callback für zwei Wege
register_setting() hängt den Callback an den Filter sanitize_option_lwsd_settings. Diesen Filter wendet sanitize_option() an: update_option() ruft sanitize_option() bei jedem Schreibvorgang auf, unabhängig davon, ob der Wert aus options.php oder aus der REST-API stammt. Daraus ergeben sich zwei Anforderungen.
Erstens ist add_settings_error() in wp-admin/includes/template.php definiert, einer Datei, die ein gewöhnlicher REST-Aufruf nicht lädt. Die Prüfung mit function_exists() stellt deshalb sicher, dass die Funktion vor dem Aufruf verfügbar ist. Zweitens muss der Callback idempotent sein: Wendet man ihn erneut auf sein eigenes Ergebnis an, muss dasselbe Ergebnis herauskommen. Fehlt die Option, bereinigt update_option() einmal und add_option() ein zweites Mal. Das Anlegen der Zeile bei der Aktivierung vermeidet diesen Doppeldurchlauf im Normalbetrieb. Der Callback darf sich darauf aber nicht verlassen.
Kein Aufruf von settings_errors() auf der Seite
Bei Seiten unter „Einstellungen“ bindet admin-header.php die Datei options-head.php ein, und die ruft settings_errors() von sich aus auf. Ein zusätzlicher Aufruf im Seiten-Callback würde jede Meldung doppelt ausgeben. Das Handbook-Beispiel verwendet eine Menüseite auf oberster Ebene und ruft settings_errors() deshalb selbst auf. Ein weiteres Detail aus options.php: Die Meldung „Die Einstellungen wurden gespeichert.“ kommt nur hinzu, wenn keine andere Meldung registriert ist. Die Warnung aus dem Sanitize-Callback ersetzt sie, obwohl die übrigen Werte gespeichert wurden.
Was beim Speichern passiert
Das Formular sendet an options.php, nicht an die Einstellungsseite. Die folgenden Schritte stammen direkt aus dem Core-Quelltext und gelten für jede Seite, die auf der Settings API aufbaut.

settings_fields()gibt vier versteckte Felder aus:option_pagemit dem Gruppennamen,actionmit dem Wertupdate, die Nonce_wpnoncefür die Aktionlwsd_notice_bar-optionsund_wp_http_referermit der Adresse, zu deroptions.phpnach dem Speichern zurückleitet.options.phpbestimmt die nötige Capability:manage_options, änderbar über den Filteroption_page_capability_{$option_page}. Fehlt sie, endet die Anfrage mit Status 403.check_admin_referer( $option_page . '-options' )prüft die Nonce.- Die Gruppe muss auf der Freigabeliste stehen, die
register_setting()befüllt und der Filterallowed_optionserweitern kann. Andernfalls brichtwp_die()ab. - Für jede registrierte Option wird der Wert aus
$_POSTgelesen, bei skalaren Werten getrimmt, mitwp_unslash()behandelt und anupdate_option()übergeben. update_option()führtsanitize_option()und damit den Callback aus, danach die Filterpre_update_option. Hat sich der Wert nicht geändert, wird nichts geschrieben; sonst landet der serialisierte Wert in der Zeile.- Die gesammelten Meldungen wandern für 30 Sekunden in das Transient
settings_errors, danach folgt eine Weiterleitung mitsettings-updated=true. Nach der Weiterleitung liestsettings_errors()das Transient und gibt die Meldungen aus.
Schritt zwei hat eine praktische Folge. Ein Plugin, das seinen Menüpunkt Redakteuren mit der Capability edit_pages zeigt, scheitert beim Speichern, solange der Filter option_page_capability_lwsd_notice_bar weiterhin manage_options verlangt. Menü, Seiten-Callback und Speichern müssen dieselbe Capability voraussetzen. Warum eine Nonce allein nie die Rechteprüfung ersetzt, vertieft ein eigener Beitrag dieser Reihe zu Sanitizing, Escaping, Nonces und Capabilities.
register_setting und der REST-Endpunkt /wp/v2/settings
Registrierte Einstellungen erscheinen standardmäßig nicht in der REST-API. Für skalare Typen genügt show_in_rest mit dem Wert true. Bei array und object braucht die Option ein Schema unter show_in_rest.schema: Für Arrays ohne items meldet der Core einen _doing_it_wrong-Hinweis (seit 5.4), für Objekte legen properties fest, welche Schlüssel existieren. Der Controller baut das endgültige Schema aus type, label (als title), description und default und legt das Schema aus show_in_rest darüber. Zugelassen sind nur die Typen number, integer, string, boolean, array und object; alle anderen Einstellungen werden stillschweigend übergangen.
Der Controller in class-wp-rest-settings-controller.php beantwortet einige Fragen, die die Referenzseiten offenlassen:
- Berechtigung: Lesen und Schreiben verlangen beide
manage_options. Der Endpunkt eignet sich also nicht, um Einstellungen an anonyme Besucher auszuliefern; dafür ist eine eigene Route mit eigenempermission_callbacknötig. - Name: Der Schlüssel in der Antwort entspricht dem Optionsnamen, sofern
show_in_restkeinen anderennamesetzt. - Strenge Objekte: Der Controller ergänzt rekursiv
additionalProperties: falsebei jedem Objekt ohne eigene Regel. Unbekannte Schlüssel in einer Anfrage werden abgelehnt. Enthält der gespeicherte Wert Schlüssel, die das Schema nicht kennt, etwa aus einer älteren Plugin-Version, liefert GET für die gesamte Einstellungnull. - Ersetzen statt Zusammenführen: Ein POST reicht das gesendete Objekt unverändert an
update_option()weiter. Fehlende Eigenschaften bleiben nicht erhalten; im Beispiel füllt der Sanitize-Callback sie mit Standardwerten, undenabledwird stillschweigend zufalse. - null löscht:
nullals Wert ruftdelete_option()auf, es sei denn, der gespeicherte Wert ist ungültig; dann verweigert der Controller mitrest_invalid_stored_valueund Status 500. - Antwort: Nach dem Update ruft der Controller erneut
get_item()auf. Die Antwort zeigt also den Wert nach der Bereinigung, nicht den gesendeten.
Lesen und Schreiben mit apiFetch
Das Skript liegt in js/settings-rest.js und wird nur auf der Einstellungsseite geladen: Das Plugin hängt admin_enqueue_scripts erst im Hook load-{$hook_suffix} ein, dessen Suffix add_options_page() zurückgibt. Für die Authentifizierung reicht die Abhängigkeit wp-api-fetch. Der Core registriert zu diesem Handle zwei Inline-Skripte, eines für die Basis-URL der REST-API und eines für die Nonce-Middleware. Die Anfrage trägt damit ohne zusätzlichen Code die zum Login-Cookie passende wp_rest-Nonce.
/* global wp */
( function () {
'use strict';
const { __ } = wp.i18n;
const button = document.getElementById( 'lwsd-rest-toggle' );
const status = document.getElementById( 'lwsd-rest-status' );
if ( ! button || ! status ) {
return;
}
button.addEventListener( 'click', async () => {
button.disabled = true;
try {
// GET returns every setting registered with show_in_rest, keyed by name.
const settings = await wp.apiFetch( { path: '/wp/v2/settings' } );
const current = settings.lwsd_settings;
if ( ! current ) {
// null: the stored value does not match the registered schema.
status.textContent = __( 'Stored value does not match the schema.', 'lw-settings-demo' );
return;
}
// The endpoint replaces the whole object, so send every property.
const saved = await wp.apiFetch( {
path: '/wp/v2/settings',
method: 'POST',
data: {
lwsd_settings: { ...current, enabled: ! current.enabled },
},
} );
// The response already contains the sanitized value.
status.textContent = saved.lwsd_settings.enabled
? __( 'Notice bar is on.', 'lw-settings-demo' )
: __( 'Notice bar is off.', 'lw-settings-demo' );
} catch ( error ) {
status.textContent = error.message || __( 'Request failed.', 'lw-settings-demo' );
} finally {
button.disabled = false;
}
} );
} )();
Die Schaltfläche liest das gesamte Einstellungsobjekt, kehrt enabled um und schickt das Objekt vollständig zurück. Die Antwort zeigt den Punkt aus dem vorigen Abschnitt: Ist das Textfeld leer, schaltet der Sanitize-Callback die Hinweisleiste wieder ab, und die Statuszeile meldet eine ausgeschaltete Leiste. Das Formular darüber aktualisiert sich nicht selbst und zeigt den neuen Stand erst nach dem Neuladen.
Dasselbe Muster trägt auch eine Einstellungsseite in React. Der WordPress Developer Blog zeigt eine Seite auf Basis von @wordpress/components, die die Option mit apiFetch( { path: '/wp/v2/settings' } ) lädt und per POST speichert, registriert an init mit Schema. Die PHP-Seite bleibt gleich: register_setting() mit Schema und Sanitize-Callback ist weiterhin der einzige Ort, an dem Typ, Standardwert und Bereinigung festgelegt sind. Nur das Rendern des Formulars und options.php fallen weg.
Grenzen und offene Punkte
- Der Code wurde am Core-Quelltext (Branch 7.1 und trunk) geprüft und auf einer Testinstallation mit WordPress 7.1.2 und PHP 8.4 ausgeführt: Die Aktivierung legt die Option mit Autoload
onan, ein erstes Speichern über das Formular nach dem Löschen der Option mitauto. Speichern über das Formular und über/wp/v2/settingshinterließ keine Einträge im Debug-Log, und Plugin Check 2.1.0 bemängelte nur die fehlendereadme.txt. - Wie
autoin künftigen Versionen behandelt wird, ist offen; die Dev Note zu 6.6 lässt ausdrücklich Raum für eine Änderung. - Beim Entfernen des Plugins bleibt seine Option bisher in der Datenbank. Das gehört in
uninstall.php, Thema eines eigenen Beitrags dieser Reihe zum Plugin-Lebenszyklus. - Die obere Position setzt voraus, dass das Theme
wp_body_open()aufruft; sonst funktioniert nur die untere. - Netzwerkeinstellungen in Multisite und
register_setting()im Zusammenspiel mit Seiten der Netzwerkverwaltung wurden nicht untersucht. - Im trunk (7.2 Alpha) wurde
settings_errors()aufwp_admin_notice()umgestellt. Diese Version ist nicht veröffentlicht; der Beitrag beschreibt das Verhalten von 7.1.
Fragen und Antworten
Reicht register_setting() an admin_init, wenn die Einstellung nur im Formular gebraucht wird?
Für das Formular ja: options.php läuft durch wp-admin/admin.php, dort feuert admin_init. Sobald die Einstellung aber in /wp/v2/settings erscheinen soll, fehlt sie, weil ein REST-Aufruf admin_init nicht auslöst. Außerdem greift der registrierte Standardwert von get_option() im Frontend nur, wenn die Registrierung dort ebenfalls läuft. init deckt alle drei Fälle ab.
Warum liefert GET /wp/v2/settings für eine Array- oder Objekt-Option plötzlich null?
Der Controller prüft den gespeicherten Wert gegen das Schema und gibt bei einem Fehler null aus. Häufigste Ursache sind Schlüssel, die das Schema nicht kennt, etwa aus einer früheren Plugin-Version: Objekte ohne eigene Regel erhalten automatisch additionalProperties: false. Abhilfe schafft eine Migration des gespeicherten Werts oder ein erweitertes Schema. Ein POST mit null wird in diesem Zustand mit rest_invalid_stored_value abgelehnt.
Lässt sich über /wp/v2/settings nur ein einzelner Schlüssel eines Objekts ändern?
Nein. Der Controller übergibt das gesendete Objekt unverändert an update_option() und ersetzt damit den gesamten Wert. Ein Client muss das Objekt zuerst lesen, den Schlüssel ändern und das vollständige Objekt zurückschicken. Wer echte Teilaktualisierungen braucht, baut eine eigene Route mit permission_callback.
Genügt die Nonce aus settings_fields() als Schutz der Einstellungsseite?
Nein. Eine WordPress-Nonce hilft beim Schutz vor CSRF. Sie ist zeitlich begrenzt und an eine Aktion und Benutzersitzung gebunden. Sie belegt aber nicht die Herkunft aus dem eigenen Formular und ersetzt weder Authentifizierung noch Berechtigungsprüfung. Die Berechtigung prüft options.php getrennt davon über manage_options oder den Wert des Filters option_page_capability_{$option_page}. Wer die Capability im Menü senkt, muss diesen Filter ebenfalls anpassen, sonst scheitert das Speichern.
Quellen
- Plugin Handbook: Settings API, abgerufen am 29.09.2026
- Plugin Handbook: Using Settings API, abgerufen am 29.09.2026
- Plugin Handbook: Custom Settings Page, abgerufen am 29.09.2026
- Plugin Handbook: Options API, abgerufen am 29.09.2026
- Code Reference: register_setting(), abgerufen am 29.09.2026
- Code Reference: settings_fields(), abgerufen am 29.09.2026
- Code Reference: add_option(), abgerufen am 29.09.2026
- Code Reference: wp_set_option_autoload(), abgerufen am 29.09.2026
- REST API Handbook: Settings, abgerufen am 29.09.2026
- Block Editor Handbook: @wordpress/api-fetch, abgerufen am 29.09.2026
- WordPress Developer Blog: How to use WordPress React components for plugin pages, abgerufen am 29.09.2026
- Make WordPress Core: Registering your settings in WordPress 4.7, abgerufen am 29.09.2026
- Make WordPress Core: New option functions in 6.4, abgerufen am 29.09.2026
- Make WordPress Core: Options API: Disabling autoload for large options, abgerufen am 29.09.2026
- wordpress-develop: wp-admin/options.php (branch 7.1), abgerufen am 29.09.2026
- wordpress-develop: wp-admin/options-head.php (branch 7.1), abgerufen am 29.09.2026
- wordpress-develop: wp-includes/option.php (branch 7.1), abgerufen am 29.09.2026
- wordpress-develop: class-wp-rest-settings-controller.php (branch 7.1), abgerufen am 29.09.2026
- wordpress-develop: wp-admin/includes/plugin.php (branch 7.1), abgerufen am 29.09.2026
- wordpress-develop: wp-admin/includes/template.php (branch 7.1), abgerufen am 29.09.2026
- wordpress-develop: wp-includes/script-loader.php (branch 7.1), abgerufen am 29.09.2026
- WordPress.org version check API (current release), abgerufen am 29.09.2026