Atualizado em 19/06/2026
Quase todo plugin sério precisa de uma tela de configuração no admin. A tentação é montar um <form> à mão, processar o $_POST num hook qualquer e chamar update_option(). Funciona — e é por aí que entram XSS, CSRF e campos que "às vezes salvam". O WordPress já resolveu esse problema com a Settings API: um conjunto de funções que orquestra o ciclo inteiro (renderização, nonce, capability, sanitização e salvamento) de forma padronizada e auditável. Este guia mostra a arquitetura completa, com o porquê de cada peça.
Options API × Settings API: camadas distintas
Confundir as duas é o equívoco conceitual mais comum. São coisas diferentes, em níveis diferentes:
- Options API — a camada de persistência.
get_option(),add_option()eupdate_option()leem e gravam valores na tabelawp_options. É de uso geral: você a chama em qualquer lugar (no front-end, em um cron, no admin) para ler ou salvar uma configuração. - Settings API — a camada de interface e fluxo. Não armazena nada por si só; ela registra quais opções existem, desenha as seções e campos no admin e intercepta o envio do formulário para o arquivo
options.php, validando o nonce e a capability antes de chamar a Options API por baixo.
Dito de outro modo: a Settings API orquestra o formulário; a Options API guarda o dado. Você pode usar a Options API sem a Settings API (para um simples update_option() num hook próprio), mas dificilmente o contrário compensa — a Settings API só faz sentido junto da Options API.
Por que não fazer "na mão"? Porque o options.php centraliza três proteções que você teria de reimplementar (e provavelmente esquecer alguma): verificação de nonce contra CSRF, checagem de capability do usuário, e a chamada do seu sanitize_callback antes de gravar. Padronizar isso é o que torna o código previsível e seguro.
O mapa das funções
Antes do código, fixe o papel de cada função. Há dois grupos: o que você registra (no admin_init) e o que você imprime (no HTML da página).
| Função | Onde | Papel |
|---|---|---|
register_setting() | admin_init | Declara a opção, o grupo a que pertence e o sanitize_callback que limpa o valor antes de salvar. |
add_settings_section() | admin_init | Cria um agrupamento visual de campos (título + descrição) dentro de uma página. |
add_settings_field() | admin_init | Registra um campo dentro de uma seção e indica o callback que desenha o <input>. |
settings_fields() | HTML do form | Imprime os campos ocultos do grupo: nonce, action e option_page. Liga o form ao options.php. |
do_settings_sections() | HTML do form | Renderiza todas as seções e campos registrados para aquela página, na ordem. |
1. A página no menu (hook admin_menu)
Primeiro registramos a página de admin. Para configurações, o lugar canônico é o submenu Configurações, via add_options_page(); para algo de primeiro nível, add_menu_page(). Em ambos, o quarto argumento é a capability exigida — use manage_options para restringir a administradores. O registro acontece no hook admin_menu:
<?php
add_action( 'admin_menu', 'dwp_registrar_pagina_opcoes' );
function dwp_registrar_pagina_opcoes() {
add_options_page(
'Configurações do Meu Plugin', // <title> da página
'Meu Plugin', // rótulo no menu Configurações
'manage_options', // capability exigida
'meu-plugin', // slug (vira ?page=meu-plugin)
'dwp_render_pagina_opcoes' // callback que imprime o HTML
);
}
O slug (meu-plugin) identifica a página na URL e — atenção — é o mesmo identificador que usaremos em do_settings_sections() e como contexto em add_settings_section()/add_settings_field(). A capability não é decoração: ela é a autorização. Mesmo que alguém descubra a URL, o WordPress nega o acesso a quem não tem manage_options.
2. O HTML da página (o callback de render)
O callback informado acima imprime a tela. A estrutura é sempre a mesma e cada linha tem função: o <form> aponta para options.php via POST; settings_fields() injeta o nonce e o grupo; do_settings_sections() desenha os campos; submit_button() imprime o botão "Salvar alterações".
<?php
function dwp_render_pagina_opcoes() {
// Defesa em profundidade: nega quem não tem a capability.
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php settings_errors(); // exibe mensagens add_settings_error() ?>
<form action="options.php" method="post">
<?php
// Imprime nonce + option_page do grupo (CSRF + roteamento).
settings_fields( 'meu_plugin_grupo' );
// Renderiza seções e campos registrados para esta página.
do_settings_sections( 'meu-plugin' );
// Botão "Salvar alterações".
submit_button();
?>
</form>
</div>
<?php
}
Repare que o formulário não tem action de salvamento próprio: ele posta para options.php, que é o handler do núcleo. É ele quem lê o option_page oculto (impresso por settings_fields()), valida o nonce, confere a capability, chama o sanitize_callback e grava. Por isso o grupo (meu_plugin_grupo) tem que ser idêntico ao do register_setting() — é a chave que liga o envio ao registro.
3. O registro (hook admin_init)
Agora a peça central. Tudo que a Settings API precisa conhecer — a opção, suas seções e campos — é registrado no hook admin_init. Adotamos a boa prática de guardar tudo num único array de opção (meu_plugin_opcoes):
<?php
add_action( 'admin_init', 'dwp_registrar_settings' );
function dwp_registrar_settings() {
// (a) Declara a opção, o grupo e quem sanitiza antes de salvar.
register_setting(
'meu_plugin_grupo', // grupo (= settings_fields)
'meu_plugin_opcoes', // nome da opção (array único)
array(
'type' => 'array',
'sanitize_callback' => 'dwp_sanitizar_opcoes',
'default' => array(),
)
);
// (b) Uma seção visual dentro da página 'meu-plugin'.
add_settings_section(
'meu_plugin_secao_geral', // id da seção
'Configurações gerais', // título exibido
'dwp_descricao_secao', // callback de descrição (pode ser null)
'meu-plugin' // página (= do_settings_sections / slug)
);
// (c) Os campos, ligados à seção acima.
add_settings_field(
'campo_email', // id do campo
'E-mail de notificação', // <label>
'dwp_render_campo_email', // callback que imprime o input
'meu-plugin', // página
'meu_plugin_secao_geral', // seção a que pertence
array( 'label_for' => 'campo_email' ) // associa o <label> ao input
);
add_settings_field(
'campo_limite',
'Limite de itens',
'dwp_render_campo_limite',
'meu-plugin',
'meu_plugin_secao_geral',
array( 'label_for' => 'campo_limite' )
);
}
function dwp_descricao_secao() {
echo '<p>Ajustes gerais do plugin.</p>';
}
Três identificadores precisam ser consistentes ou nada funciona: o grupo (meu_plugin_grupo, igual em register_setting e settings_fields), a página (meu-plugin, igual em add_settings_section, add_settings_field e do_settings_sections) e o nome da opção (meu_plugin_opcoes). O label_for não é obrigatório, mas melhora a acessibilidade ao ligar o <label> da coluna esquerda ao id do campo.
4. O callback de um campo (ler e escapar)
Cada campo registrado precisa de um callback que imprime o <input>. Aqui valem duas regras: leia o valor atual com get_option() (lembrando que guardamos um array) e escape a saída com esc_attr(). Como o name usa a notação de array, o WordPress entrega o valor já agrupado em $_POST['meu_plugin_opcoes']:
<?php
function dwp_render_campo_email() {
$opcoes = get_option( 'meu_plugin_opcoes', array() );
$valor = isset( $opcoes['email'] ) ? $opcoes['email'] : '';
?>
<input type="email"
id="campo_email"
name="meu_plugin_opcoes[email]"
value="<?php echo esc_attr( $valor ); ?>"
class="regular-text">
<p class="description">Receberá os avisos do plugin.</p>
<?php
}
function dwp_render_campo_limite() {
$opcoes = get_option( 'meu_plugin_opcoes', array() );
$valor = isset( $opcoes['limite'] ) ? (int) $opcoes['limite'] : 10;
?>
<input type="number" min="1" max="100" step="1"
id="campo_limite"
name="meu_plugin_opcoes[limite]"
value="<?php echo esc_attr( $valor ); ?>">
<?php
}
O name="meu_plugin_opcoes[email]" é o que faz tudo cair sob a mesma opção: o navegador envia meu_plugin_opcoes como um array com as chaves email e limite. Por isso o sanitize_callback receberá o array inteiro. E note: mesmo num campo numérico, escapamos com esc_attr() na saída — escapar é função do contexto de impressão, não da confiança no dado.
5. O sanitize_callback (validar é aqui)
Este é o coração da segurança da tela. O sanitize_callback recebe o valor cru do $_POST e retorna o que será gravado. Como usamos um array, ele recebe o array inteiro e devolvemos uma versão limpa — campo a campo, descartando qualquer chave que não esperamos:
<?php
function dwp_sanitizar_opcoes( $entrada ) {
$saida = array();
// E-mail: valida e normaliza; se inválido, avisa e mantém o anterior.
$email = isset( $entrada['email'] ) ? sanitize_email( $entrada['email'] ) : '';
if ( '' !== $email && ! is_email( $email ) ) {
add_settings_error(
'meu_plugin_opcoes',
'email_invalido',
'O e-mail informado é inválido. O valor anterior foi mantido.',
'error'
);
$antigas = get_option( 'meu_plugin_opcoes', array() );
$saida['email'] = isset( $antigas['email'] ) ? $antigas['email'] : '';
} else {
$saida['email'] = $email;
}
// Limite: inteiro forçado para a faixa 1..100.
$limite = isset( $entrada['limite'] ) ? (int) $entrada['limite'] : 10;
$saida['limite'] = max( 1, min( 100, $limite ) );
return $saida; // SÓ o que está aqui é salvo.
}
Tudo que não retornar do sanitize_callback é descartado — essa é a defesa contra campos injetados (alguém forjando um $_POST com chaves extras). Por isso construímos um array $saida novo e copiamos apenas as chaves conhecidas, em vez de "limpar" o array de entrada e devolvê-lo. Validação no JavaScript do formulário é só conveniência; a fronteira de confiança é o servidor.
Feedback ao usuário com add_settings_error()
No menu Configurações, o WordPress já mostra automaticamente o aviso "Configurações salvas." quando o salvamento ocorre. Para mensagens próprias — sobretudo erros de validação — use add_settings_error() dentro do sanitize_callback (como no exemplo acima) e garanta a chamada de settings_errors() no topo da página. Em páginas fora do menu Configurações, o settings_errors() é obrigatório até para o aviso de sucesso aparecer.
Armadilhas comuns
| Sintoma | Causa provável | Correção |
|---|---|---|
| Nada é salvo ao enviar | Grupo do settings_fields() ≠ register_setting() | Use a mesma string de grupo nos dois |
| Campos não aparecem | Página do do_settings_sections() ≠ slug usado nos add_settings_* | Unifique o identificador da página |
| "Options page not found" | register_setting fora do admin_init | Registre tudo no hook admin_init |
| Valor some ao salvar | Chave não retornada no sanitize_callback | Copie a chave para o array de saída |
| Formulário recarrega sem efeito | action do form não é options.php | Use action="options.php" method="post" |
| HTML quebrado/XSS no campo | Saída sem esc_attr() | Escape sempre ao imprimir o value |
Boas práticas
- Uma opção em array, não dezenas soltas. Menos linhas em
wp_options, umregister_setting, umsanitize_callbacke limpeza trivial na desinstalação (delete_option( 'meu_plugin_opcoes' )). - Sanitize na entrada, escape na saída. São deveres independentes: o
sanitize_callbacklimpa o que entra;esc_attr()/esc_textarea()protegem o que sai. Veja segurança no código. - Prefixe tudo (grupo, opção, funções) com o slug do seu plugin para evitar colisões no escopo global.
- Registre nos hooks certos. Menu em
admin_menu, configurações emadmin_init— essa separação está documentada e é o que faz ooptions.phpreconhecer suas opções. Entenda os hooks envolvidos. - Use os helpers do form do admin —
checked(),selected()edisabled()— para marcar estados semifespalhados pelo HTML. - Defina
show_in_restemregister_setting()quando a opção também precisar ser lida/escrita via REST API (por exemplo, por um bloco do editor).
Com esse esqueleto você tem uma tela de opções segura por construção: o núcleo cuida do nonce, da capability e do salvamento, e você concentra a inteligência onde ela importa — no sanitize_callback. A partir daí, adicionar campos é repetir o trio add_settings_field → callback de render → chave no sanitizador.
Perguntas frequentes
Qual a diferença entre a Options API e a Settings API?
A Options API é a camada de armazenamento: get_option(), add_option() e update_option() leem e gravam valores na tabela wp_options. Você pode usá-la sozinha em qualquer lugar. A Settings API é uma camada acima: ela orquestra a tela de opções no admin — desenha as seções e campos, processa o POST para options.php, chama o seu sanitize_callback e grava com a Options API por baixo. Em resumo: a Settings API monta o formulário e a Options API guarda o dado.
Por que meus campos não salvam?
A causa nº 1 é o grupo divergente: o primeiro argumento de register_setting( $group, ... ) tem que ser exatamente o mesmo passado a settings_fields( $group ) no formulário. Se não baterem, o options.php rejeita o envio (falha na verificação de nonce/capability) e nada é salvo. A nº 2 é registrar o register_setting fora do hook admin_init — o options.php só conhece as configurações registradas ali. A nº 3 é apontar o <form> para um arquivo que não seja options.php.
Onde eu valido os dados enviados?
No sanitize_callback declarado em register_setting(). Ele recebe o valor cru vindo do $_POST e deve retornar o valor já limpo, que é o que será gravado. É o ponto central de validação e sanitização — não confie em validação só no JavaScript do formulário. Quando guardar tudo num único array de opção, o callback recebe o array inteiro e você sanitiza campo a campo, descartando chaves inesperadas.
Devo usar uma opção para cada campo ou um array só?
Prefira um único array (ex.: get_option( 'meu_plugin_opcoes' ) retornando ['email' => ..., 'limite' => ...]). Vantagens: uma única linha em wp_options (menos consultas e menos lixo), um único register_setting e um único sanitize_callback onde você valida o conjunto de uma vez. Dezenas de opções soltas poluem o banco e dificultam a limpeza na desinstalação.
Como mostro a mensagem "Configurações salvas"?
No contexto de add_options_page() (menu Configurações), o WordPress já exibe automaticamente o aviso de sucesso. Em outros menus, ou para mensagens de erro de validação, use add_settings_error() dentro do sanitize_callback e chame settings_errors() no topo da página. Assim você dá feedback preciso ("o e-mail informado é inválido") em vez de um sucesso genérico.
A Settings API escapa os valores na saída automaticamente?
Não. A Settings API cuida do salvamento; a exibição é com você. No callback de cada campo, ao imprimir o value de um <input>, sempre use esc_attr() (ou esc_textarea(), checked(), selected()). Escapar na saída é uma regra de segurança que vale mesmo para dados que você mesmo sanitizou na entrada.