Strony ustawień wtyczki z Settings API i register_setting

Spis treści
Prawie każda wtyczka potrzebuje strony, na której administrator może zmienić kilka wartości. Zaskakująco wiele wtyczek wciąż buduje ją ręcznie: własna obsługa żądania POST, sprawdzenie uprawnień w jednym miejscu, zapomniany Nonce w innym i kilkanaście osobnych wierszy w wp_options. WordPress od wersji 2.7 udostępnia do tego Settings API. Interfejs ten wyświetla formularz w znanym układzie panelu administracyjnego i wysyła dane do wp-admin/options.php, gdzie rdzeń sprawdza Nonce i uprawnienia, oczyszcza dane oraz je zapisuje. Od WordPressa 4.7 funkcja register_setting() opisuje ponadto same dane: typ, wartość domyślną, callback oczyszczający oraz, dzięki show_in_rest, widoczność w punkcie końcowym REST /wp/v2/settings.
W tym artykule powstaje mała, ale kompletna wtyczka „LW Settings Demo”: pasek komunikatu, którego trzy ustawienia są zebrane w jednej opcji, strona w menu „Ustawienia”, callback oczyszczający obsługujący zarówno formularz, jak i REST API, oraz krótki skrypt, który tę samą opcję odczytuje i zapisuje przez apiFetch. Po drodze da się prześledzić drogę pojedynczego pola formularza od elementu wejściowego aż do wiersza w wp_options i zobaczyć, w którym miejscu ścieżka REST biegnie inaczej. Kod źródłowy rdzenia przeczytano 29.09.2026; według sprawdzenia wersji na WordPress.org aktualne wydanie to 7.1.2, a przykład wymaga co najmniej WordPressa 6.6.
Cztery zadania, cztery grupy funkcji
Settings API dzieli stronę ustawień na wyraźnie rozdzielone zadania. Każda grupa funkcji należy do własnego hooka. Pomieszanie ich to najczęstszy powód, dla którego strona wyświetla się poprawnie, ale niczego nie zapisuje.
| Funkcja | Hook | Zadanie |
|---|---|---|
add_options_page() |
admin_menu |
Pozycja w menu „Ustawienia”, Capability decydująca o wyświetleniu pozycji |
register_setting() |
init (wyjaśnienie niżej) |
Dopisuje opcję do listy dozwolonych opcji grupy, dołącza callback oczyszczający, wartość domyślną i schemat REST |
add_settings_section(), add_settings_field() |
admin_init |
Układ: nagłówki, etykiety, callbacki wypisujące elementy formularza |
settings_fields(), do_settings_sections() |
Callback strony | Ukryte pola z Nonce, a potem wszystkie sekcje i pola strony |
add_options_page() to cienka nakładka: wywołuje add_submenu_page() z options-general.php jako stroną nadrzędną. Przekazana tu Capability rozstrzyga o tym, czy pozycja pojawi się w menu; użytkownik bez niej dostaje też status 403 przy bezpośrednim otwarciu adresu strony. Docblock funkcji wprost zaznacza, że callback strony musi sam jeszcze raz sprawdzić uprawnienia, dlatego funkcja renderująca w przykładzie zaczyna się od current_user_can( 'manage_options' ). Przy zapisie następuje trzecia, osobna kontrola w options.php, co pokazuje sekcja o ścieżce zapisu.
Formularz i zapis łączy grupa opcji. register_setting( 'lwsd_notice_bar', 'lwsd_settings', … ) przypisuje opcję lwsd_settings do grupy lwsd_notice_bar, a settings_fields( 'lwsd_notice_bar' ) wysyła właśnie tę nazwę grupy w ukrytym polu option_page. Identyfikator strony używany przez add_settings_section() i do_settings_sections() to osobna nazwa; w przykładzie pokrywa się ze slugiem menu lwsd-notice-bar.
Jedna opcja zamiast kilkunastu
Plugin Handbook zaleca przechowywanie powiązanych wartości jako tablicy w jednej opcji, bo odczyt i zapis odbywają się wtedy w jednej transakcji bazy danych, a nie w wielu. Kod options.php dostarcza drugiego, mniej oczywistego argumentu. Przy zapisie plik przechodzi przez wszystkie opcje zarejestrowane w przesłanej grupie i dla każdej wywołuje update_option(). Jeśli w danych POST brakuje pola, przekazywana jest wartość null. Dwa formularze w tej samej grupie nadpisują więc nawzajem swoje wartości. Jedna opcja tablicowa na formularz usuwa tę pułapkę, a callback oczyszczający widzi wszystkie wartości jednocześnie. Dzięki temu da się sformułować reguły obejmujące kilka pól, na przykład „brak aktywnego paska bez treści komunikatu”.
Ceną jest serializacja: tablica trafia do jednego wiersza jako zserializowany ciąg znaków, a pojedynczych kluczy nie da się filtrować w SQL. Przy wartościach konfiguracyjnych rzadko ma to znaczenie. Dla danych, które trzeba przeszukiwać, alternatywy omawia artykuł o granicach wydajności wp_postmeta i własnych tabelach.
Świadome ustawienie autoload
WordPress 6.6 przebudował kolumnę autoload. Parametr $autoload funkcji add_option() ma od tej wersji domyślnie wartość null. W kolumnie zapisywane są on i off dla jawnych decyzji, auto, gdy nikt nie zdecydował, oraz auto-on lub auto-off, gdy decyzję dynamicznie podjął filtr. Stare wartości yes i no są od 6.7 oznaczone jako przestarzałe. Opcje większe niż 150 000 bajtów domyślnie nie są ładowane automatycznie (filtr wp_max_autoloaded_option_size). Według wp_autoload_values_to_autoload() wartość auto jest obecnie nadal ładowana, jednak notatka deweloperska do 6.6 zastrzega, że ta domyślna reguła może się w przyszłych wersjach zmienić.
Dla Settings API ma to znaczenie, ponieważ options.php wywołuje update_option() bez argumentu autoload. Jeśli opcja jeszcze nie istnieje, update_option() wywołuje add_option(), a wiersz powstaje z wartością auto. Aby jawnie ustawić autoload, należy utworzyć opcję samodzielnie. Przykład robi to przy aktywacji za pomocą add_option( …, true ), ponieważ pasek komunikatu jest odczytywany przy każdym żądaniu frontendu.
Późniejsze wywołania update_option() bez argumentu autoload zachowują jawne on lub off; ponownie oceniane są tylko warianty auto. Opcja potrzebna tylko na jednej stronie panelu dostałaby natomiast false.
Później można zmienić autoload za pomocą wp_set_option_autoload() (od 6.4), bez zmiany zapisanej wartości opcji. Natomiast update_option() przyjmuje nową wartość autoload tylko wtedy, gdy zmienia się również treść opcji. Znaczenie poszczególnych wartości kolumny w praktyce omówi osobny, zaplanowany artykuł o autoload.
Kompletna wtyczka
Plik główny zawiera wszystko: rejestrację, stronę, pola, callback oczyszczający, wyjście na frontendzie i aktywację. Prefiks lwsd_ obowiązuje konsekwentnie dla funkcji, stałych, opcji i identyfikatorów HTML.
<?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' );
Dlaczego register_setting działa na hooku init
Przykład w Plugin Handbook rejestruje ustawienia na hooku admin_init. Ten hook jest wywoływany w wp-admin/admin.php, a żądanie REST do /wp-json/ nigdy nie ładuje tego pliku. Ustawienie zarejestrowane tylko tam działa w formularzu, ale nie pojawia się w /wp/v2/settings. Rdzeń rejestruje własne ustawienia na rest_api_init, a przykład z WordPress Developer Blog dla stron ustawień opartych na React korzysta z init. Wtyczka idzie tą drugą drogą: init obejmuje options.php, stronę ustawień, żądania REST oraz frontend, gdzie dodatkowo działa zarejestrowana wartość domyślna. Sekcje i pola to natomiast czysty układ panelu i zostają na admin_init.
Jeden callback oczyszczający dla dwóch ścieżek
register_setting() podpina callback pod filtr sanitize_option_lwsd_settings. Filtr ten stosuje sanitize_option(), którą update_option() wywołuje przy każdym zapisie, niezależnie od tego, czy wartość pochodzi z options.php, czy z REST API. Ma to dwie konsekwencje.
Po pierwsze, add_settings_error() jest zdefiniowana w wp-admin/includes/template.php, pliku, którego zwykłe żądanie REST nie ładuje. Sprawdzenie przez function_exists() pozwala więc ustalić, czy funkcja jest dostępna przed jej wywołaniem. Po drugie, callback musi być idempotentny: ponowne zastosowanie go do własnego wyniku musi dać ten sam wynik. Gdy opcji brakuje, update_option() oczyszcza wartość raz, a add_option() drugi raz. Utworzenie wiersza przy aktywacji zapobiega temu podwójnemu przebiegowi w normalnej pracy, ale callback nie powinien na tym polegać.
Bez wywołania settings_errors() na stronie
Dla stron w menu „Ustawienia” plik admin-header.php dołącza options-head.php, a ten sam wywołuje settings_errors(). Dodatkowe wywołanie w callbacku strony wypisałoby każdy komunikat dwukrotnie. Przykład z Handbooka używa strony menu najwyższego poziomu i dlatego wywołuje settings_errors() samodzielnie. Jeszcze jeden szczegół z options.php: komunikat o zapisaniu ustawień dodawany jest tylko wtedy, gdy nie zarejestrowano żadnego innego. Ostrzeżenie z callbacku oczyszczającego zastępuje go, choć pozostałe wartości zostały zapisane.
Co dzieje się przy zapisie
Formularz wysyła dane do options.php, a nie do strony ustawień. Poniższe kroki pochodzą bezpośrednio z kodu rdzenia i dotyczą każdej strony zbudowanej na Settings API.

settings_fields()wypisuje cztery ukryte pola:option_pagez nazwą grupy,actionz wartościąupdate, Nonce_wpnoncedla akcjilwsd_notice_bar-optionsoraz_wp_http_refererz adresem, na któryoptions.phpprzekierowuje po zapisie.options.phpustala wymaganą Capability:manage_options, z możliwością zmiany przez filtroption_page_capability_{$option_page}. Bez niej żądanie kończy się statusem 403.check_admin_referer( $option_page . '-options' )weryfikuje Nonce.- Grupa musi znajdować się na liście dozwolonych, którą wypełnia
register_setting(), a rozszerza filtrallowed_options. W przeciwnym raziewp_die()przerywa żądanie. - Dla każdej zarejestrowanej opcji wartość jest odczytywana z
$_POST, przycinana, jeśli jest skalarna, przepuszczana przezwp_unslash()i przekazywana doupdate_option(). update_option()uruchamiasanitize_option(), a więc callback, potem filtrypre_update_option. Jeśli wartość się nie zmieniła, nic nie jest zapisywane; w przeciwnym razie zserializowana wartość trafia do wiersza.- Zebrane komunikaty trafiają na 30 sekund do transientu
settings_errors, po czym następuje przekierowanie zsettings-updated=true. Po przekierowaniusettings_errors()odczytuje transient i wyświetla komunikaty.
Krok drugi ma praktyczną konsekwencję. Wtyczka, która pokazuje swoją pozycję menu redaktorom z uprawnieniem edit_pages, nie zapisze ustawień, dopóki filtr option_page_capability_lwsd_notice_bar nadal wymaga manage_options. Wyświetlanie menu, callback strony i zapis muszą wymagać tego samego uprawnienia. Dlaczego sam Nonce nigdy nie zastępuje sprawdzenia uprawnień, wyjaśnia szczegółowo osobny artykuł tej serii o sanitizacji, escapowaniu, Nonce i Capabilities.
register_setting a punkt końcowy /wp/v2/settings
Zarejestrowane ustawienia domyślnie nie pojawiają się w REST API. Dla typów skalarnych wystarczy show_in_rest z wartością true. Przy array i object opcja potrzebuje schematu w show_in_rest.schema: dla tablic bez items rdzeń zgłasza komunikat _doing_it_wrong (od 5.4), a dla obiektów properties określają, jakie klucze istnieją. Kontroler buduje ostateczny schemat z type, label (jako title), description i default, a następnie nakłada na niego schemat z show_in_rest. Akceptowane są wyłącznie typy number, integer, string, boolean, array i object; pozostałe ustawienia są po cichu pomijane.
Kontroler w class-wp-rest-settings-controller.php odpowiada na kilka pytań, które strony referencyjne pozostawiają otwarte:
- Uprawnienia: zarówno odczyt, jak i zapis wymagają
manage_options. Punkt końcowy nie nadaje się więc do przekazywania ustawień anonimowym odwiedzającym; do tego potrzebna jest osobna trasa z własnympermission_callback. - Nazwa: klucz w odpowiedzi odpowiada nazwie opcji, chyba że
show_in_restustawia innename. - Ścisłe obiekty: kontroler rekurencyjnie dodaje
additionalProperties: falsedo każdego obiektu bez własnej reguły. Nieznane klucze w żądaniu są odrzucane. Jeśli zapisana wartość zawiera klucze nieznane schematowi, na przykład pozostałe po starszej wersji wtyczki, GET zwracanulldla całego ustawienia. - Zastąpienie zamiast scalenia: POST przekazuje wysłany obiekt bez zmian do
update_option(). Brakujące właściwości nie są zachowywane; w przykładzie callback oczyszczający uzupełnia je wartościami domyślnymi, aenabledpo cichu zmienia się nafalse. - null usuwa: wartość
nullwywołujedelete_option(), chyba że zapisana wartość jest nieprawidłowa; wtedy kontroler odmawia z kodemrest_invalid_stored_valuei statusem 500. - Odpowiedź: po aktualizacji kontroler ponownie wywołuje
get_item(). Odpowiedź pokazuje zatem wartość po oczyszczeniu, a nie wysłaną.
Odczyt i zapis przez apiFetch
Skrypt znajduje się w js/settings-rest.js i jest ładowany wyłącznie na stronie ustawień: wtyczka podpina admin_enqueue_scripts dopiero w hooku load-{$hook_suffix}, którego sufiks zwraca add_options_page(). Do uwierzytelnienia wystarcza zależność wp-api-fetch. Rdzeń rejestruje dla tego uchwytu dwa skrypty inline, jeden z bazowym adresem REST API i jeden z middleware dla Nonce. Żądanie niesie więc bez dodatkowego kodu Nonce wp_rest pasujący do ciasteczka logowania.
/* 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;
}
} );
} )();
Przycisk odczytuje cały obiekt ustawień, odwraca enabled i odsyła obiekt w komplecie. Odpowiedź ilustruje punkt z poprzedniej sekcji: przy pustym polu komunikatu callback oczyszczający z powrotem wyłącza pasek, a wiersz statusu informuje o wyłączonym pasku. Formularz powyżej nie odświeża się sam i pokazuje nowy stan dopiero po przeładowaniu strony.
Ten sam wzorzec przenosi się na stronę ustawień zbudowaną w React. WordPress Developer Blog pokazuje stronę opartą na @wordpress/components, która wczytuje opcję przez apiFetch( { path: '/wp/v2/settings' } ) i zapisuje ją żądaniem POST, z rejestracją na init i schematem. Strona PHP pozostaje taka sama: register_setting() ze schematem i callbackiem oczyszczającym jest nadal jedynym miejscem, w którym zdefiniowano typ, wartość domyślną i oczyszczanie. Odpada jedynie renderowanie formularza i options.php.
Ograniczenia i otwarte kwestie
- Kod sprawdzono względem kodu źródłowego rdzenia (gałąź 7.1 i trunk) i uruchomiono na testowej instalacji WordPressa 7.1.2 z PHP 8.4: aktywacja tworzy opcję z autoload
on, a pierwszy zapis przez formularz po usunięciu opcji – zauto. Zapis przez formularz i przez/wp/v2/settingsnie zostawił wpisów w logu debugowania, a Plugin Check 2.1.0 zgłosił jedynie brak plikureadme.txt. - Sposób traktowania
autow przyszłych wersjach pozostaje otwarty; notatka deweloperska do 6.6 wprost zostawia miejsce na zmianę. - Wtyczka nie usuwa swojej opcji przy odinstalowaniu. To zadanie dla
uninstall.php, tematu osobnego artykułu tej serii o cyklu życia wtyczki. - Górna pozycja wymaga, by Theme wywoływał
wp_body_open(); w przeciwnym razie działa tylko dolna. - Ustawień sieciowych w Multisite ani
register_setting()w połączeniu ze stronami panelu sieci nie badano. - W gałęzi trunk (7.2 alfa)
settings_errors()przestawiono nawp_admin_notice(). Ta wersja nie została wydana, więc artykuł opisuje zachowanie wersji 7.1.
Pytania i odpowiedzi
Czy register_setting() na hooku admin_init wystarcza, gdy ustawienie jest potrzebne tylko w formularzu?
Dla formularza tak: options.php przechodzi przez wp-admin/admin.php, gdzie wywoływany jest admin_init. Gdy jednak ustawienie ma pojawić się w /wp/v2/settings, będzie go brakowało, ponieważ żądanie REST nie uruchamia admin_init. Również zarejestrowana wartość domyślna dla get_option() działa na frontendzie tylko wtedy, gdy rejestracja odbywa się także tam. init obejmuje wszystkie trzy przypadki.
Dlaczego GET /wp/v2/settings nagle zwraca null dla opcji typu array lub object?
Kontroler sprawdza zapisaną wartość względem schematu i przy błędzie zwraca null. Najczęstszą przyczyną są klucze nieznane schematowi, na przykład ze starszej wersji wtyczki: obiekty bez własnej reguły automatycznie otrzymują additionalProperties: false. Rozwiązaniem jest migracja zapisanej wartości albo rozszerzony schemat. W tym stanie żądanie POST z null zostaje odrzucone z kodem rest_invalid_stored_value.
Czy przez /wp/v2/settings da się zmienić tylko jeden klucz obiektu?
Nie. Kontroler przekazuje wysłany obiekt bez zmian do update_option() i tym samym zastępuje całą wartość. Klient musi najpierw odczytać obiekt, zmienić klucz i odesłać obiekt w komplecie. Prawdziwe częściowe aktualizacje wymagają własnej trasy z permission_callback.
Czy Nonce z settings_fields() wystarcza do ochrony strony ustawień?
Nie. Nonce WordPressa pomaga chronić przed CSRF. Jest ograniczony czasowo i powiązany z akcją oraz sesją użytkownika, ale nie dowodzi, że żądanie pochodzi z własnego formularza, ani nie zastępuje uwierzytelniania lub sprawdzania uprawnień. Uprawnienia options.php sprawdza osobno, przez manage_options lub wartość zwróconą przez filtr option_page_capability_{$option_page}. Wtyczka, która obniża wymagane uprawnienie swojej pozycji menu, musi dostosować również ten filtr, inaczej zapis się nie powiedzie.
Źródła
- Plugin Handbook: Settings API, dostęp 29.09.2026
- Plugin Handbook: Using Settings API, dostęp 29.09.2026
- Plugin Handbook: Custom Settings Page, dostęp 29.09.2026
- Plugin Handbook: Options API, dostęp 29.09.2026
- Code Reference: register_setting(), dostęp 29.09.2026
- Code Reference: settings_fields(), dostęp 29.09.2026
- Code Reference: add_option(), dostęp 29.09.2026
- Code Reference: wp_set_option_autoload(), dostęp 29.09.2026
- REST API Handbook: Settings, dostęp 29.09.2026
- Block Editor Handbook: @wordpress/api-fetch, dostęp 29.09.2026
- WordPress Developer Blog: How to use WordPress React components for plugin pages, dostęp 29.09.2026
- Make WordPress Core: Registering your settings in WordPress 4.7, dostęp 29.09.2026
- Make WordPress Core: New option functions in 6.4, dostęp 29.09.2026
- Make WordPress Core: Options API: Disabling autoload for large options, dostęp 29.09.2026
- wordpress-develop: wp-admin/options.php (branch 7.1), dostęp 29.09.2026
- wordpress-develop: wp-admin/options-head.php (branch 7.1), dostęp 29.09.2026
- wordpress-develop: wp-includes/option.php (branch 7.1), dostęp 29.09.2026
- wordpress-develop: class-wp-rest-settings-controller.php (branch 7.1), dostęp 29.09.2026
- wordpress-develop: wp-admin/includes/plugin.php (branch 7.1), dostęp 29.09.2026
- wordpress-develop: wp-admin/includes/template.php (branch 7.1), dostęp 29.09.2026
- wordpress-develop: wp-includes/script-loader.php (branch 7.1), dostęp 29.09.2026
- WordPress.org version check API (current release), dostęp 29.09.2026