LW IT Solutions logo LW IT Solutions logo LW IT Solutions
« Przegląd bloga /Wtyczki i triki WordPress/Tutorials / Strony ustawień wtyczki z Settings API i...
Ten artykuł w innych językach:

Strony ustawień wtyczki z Settings API i register_setting

Strony ustawień wtyczki z Settings API i register_setting
Spis treści
  1. Cztery zadania, cztery grupy funkcji
  2. Jedna opcja zamiast kilkunastu
  3. Kompletna wtyczka
  4. Co dzieje się przy zapisie
  5. register_setting a punkt końcowy /wp/v2/settings
  6. Odczyt i zapis przez apiFetch
  7. Ograniczenia i otwarte kwestie
  8. Pytania i odpowiedzi
  9. Źródła

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.

Od pola formularza do wp_options: ścieżka zapisu w Settings API
options.php sprawdza Capability, Nonce i listę dozwolonych; REST dołącza przy update_option(), gdzie działa callback
  1. settings_fields() wypisuje cztery ukryte pola: option_page z nazwą grupy, action z wartością update, Nonce _wpnonce dla akcji lwsd_notice_bar-options oraz _wp_http_referer z adresem, na który options.php przekierowuje po zapisie.
  2. options.php ustala wymaganą Capability: manage_options, z możliwością zmiany przez filtr option_page_capability_{$option_page}. Bez niej żądanie kończy się statusem 403.
  3. check_admin_referer( $option_page . '-options' ) weryfikuje Nonce.
  4. Grupa musi znajdować się na liście dozwolonych, którą wypełnia register_setting(), a rozszerza filtr allowed_options. W przeciwnym razie wp_die() przerywa żądanie.
  5. Dla każdej zarejestrowanej opcji wartość jest odczytywana z $_POST, przycinana, jeśli jest skalarna, przepuszczana przez wp_unslash() i przekazywana do update_option().
  6. update_option() uruchamia sanitize_option(), a więc callback, potem filtry pre_update_option. Jeśli wartość się nie zmieniła, nic nie jest zapisywane; w przeciwnym razie zserializowana wartość trafia do wiersza.
  7. Zebrane komunikaty trafiają na 30 sekund do transientu settings_errors, po czym następuje przekierowanie z settings-updated=true. Po przekierowaniu settings_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łasnym permission_callback.
  • Nazwa: klucz w odpowiedzi odpowiada nazwie opcji, chyba że show_in_rest ustawia inne name.
  • Ścisłe obiekty: kontroler rekurencyjnie dodaje additionalProperties: false do 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 zwraca null dla 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, a enabled po cichu zmienia się na false.
  • null usuwa: wartość null wywołuje delete_option(), chyba że zapisana wartość jest nieprawidłowa; wtedy kontroler odmawia z kodem rest_invalid_stored_value i 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 – z auto. Zapis przez formularz i przez /wp/v2/settings nie zostawił wpisów w logu debugowania, a Plugin Check 2.1.0 zgłosił jedynie brak pliku readme.txt.
  • Sposób traktowania auto w 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 na wp_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

Lukas Wójcik

Lukas Wójcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Kontakt

Masz pytanie lub pomysł na projekt internetowy? Napisz krótko, o co chodzi. Formularz korzysta z reCAPTCHA do ochrony przed spamem.

Napisanie komentarza

Doświadczenia z innymi wtyczkami lub środowiskami hostingowymi oraz pytania o konfigurację są tu mile widziane.

Adres e-mail nie jest publikowany. Pola obowiązkowe oznaczono gwiazdką.