本文约 7,750 字,预计阅读 20 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:自己拼表单,迟早要还的
插件做到一定规模就需要配置项。很多人的第一反应是直接写一个 HTML 表单:在 admin_menu 里加个菜单,页面里 echo 一个 <form>,POST 到 options.php 或者自己处理 $_POST,然后 update_option() 存起来。
这套做法能跑,但问题会在后面集中爆发:没有 nonce 校验会被 CSRF;字段值不做净化会被注入;每次新增字段都要改三处代码;错误提示要自己写;而且和 WordPress 后台的视觉风格不一致,看起来像个外来物。
Settings API 就是 WordPress 为了消除这些重复劳动而提供的标准方案。它把设置页拆成「设置组(setting)→ 区块(section)→ 字段(field)」三级结构,你只需要声明结构,字段的渲染、表单提交、nonce 校验、数据保存、错误提示都由核心接管。
理解这套结构之后,加一个字段只需要加一次 add_settings_field(),非常省事。本文给出完整可运行的写法。
原理分析:Settings API 的三级结构
Settings API 的核心是四个函数和三个层级:
| 层级 | 注册函数 | 作用 |
|---|---|---|
| 设置组 | register_setting( $group, $option_name, $args ) |
声明一个选项及其净化回调 |
| 区块 | add_settings_section( $id, $title, $cb, $page ) |
页面上逻辑分组,可输出说明文字 |
| 字段 | add_settings_field( $id, $title, $cb, $page, $section, $args ) |
单个输入控件 |
| 渲染 | settings_fields() / do_settings_sections() / submit_button() |
在页面里输出表单、字段和按钮 |
三者的关系:$page 参数(也叫 menu slug)是纽带,把所有区块和字段绑定到同一个页面上;$group 是 settings_fields() 输出时用的名字,它决定了表单提交后核心去哪个组里找字段做净化。
数据流向是这样的:
- 后台访问设置页 → 核心渲染你注册的字段;
- 用户点保存 → 表单 POST 到
options.php; options.php校验 nonce 和能力,然后根据settings_fields( $group )输出的组名,找到该组下所有注册的选项;- 对每个选项调用你在
register_setting()里声明的sanitize_callback; - 净化后的值写入
wp_options; - 重定向回设置页,带上
settings-updated=true。
一个关键设计是:每个选项在 wp_options 里就是一条完整的记录。你可以把整个设置数组存在一个 option 里(推荐,减少数据库条目),也可以每个字段一个 option。前者需要自己在净化回调里组装数组。
另外 settings_fields() 会自动输出 nonce 字段和 option_page 隐藏字段,这就是它比手写表单安全的原因之一。不过 nonce 只保护这个表单,页面本身的访问还需要你自己判断 capability,这部分见 nonce 与 capability 安全。
保姆级实操
第一步:注册菜单页
add_action( 'admin_menu', 'mmf_add_settings_menu' );
function mmf_add_settings_menu() {
add_menu_page(
'MMF 插件设置', // 页面标题(<title>)
'MMF 设置', // 菜单标题
'manage_options', // 访问所需能力
'mmf-settings', // menu slug,也是 $page 参数
'mmf_render_settings_page', // 渲染回调
'dashicons-admin-generic', // 图标
80 // 菜单位置
);
// 也可以挂到「设置」下面,更适合轻量插件
add_options_page(
'MMF 插件设置',
'MMF 设置',
'manage_options',
'mmf-settings',
'mmf_render_settings_page'
);
}
manage_options 是管理员专属能力,普通插件设置用这个即可。如果只希望编辑也能改,可以换成 edit_posts(但通常不建议)。
第二步:注册设置、区块与字段
add_action( 'admin_init', 'mmf_register_settings' );
function mmf_register_settings() {
// 1) 注册设置组与净化回调
register_setting(
'mmf_settings_group', // 组名
'mmf_options', // 选项名(wp_options 里的 option_name)
array(
'type' => 'array',
'sanitize_callback' => 'mmf_sanitize_options',
'default' => array(
'api_key' => '',
'per_page' => 10,
'enable' => 0,
),
)
);
// 2) 注册区块
add_settings_section(
'mmf_section_basic',
'基础配置',
function () {
echo '<p>配置插件的基础行为。</p>';
},
'mmf-settings'
);
// 3) 注册字段
add_settings_field(
'api_key',
'API 密钥',
'mmf_field_api_key',
'mmf-settings',
'mmf_section_basic'
);
add_settings_field(
'per_page',
'每页条数',
'mmf_field_per_page',
'mmf-settings',
'mmf_section_basic'
);
add_settings_field(
'enable',
'启用功能',
'mmf_field_enable',
'mmf-settings',
'mmf_section_basic'
);
}
第三步:写字段渲染回调
function mmf_field_api_key() {
$options = get_option( 'mmf_options', array() );
$value = isset( $options['api_key'] ) ? $options['api_key'] : '';
printf(
'<input type="text" name="mmf_options[api_key]" value="%s" class="regular-text" />',
esc_attr( $value )
);
echo '<p class="description">从服务商后台获取。</p>';
}
function mmf_field_per_page() {
$options = get_option( 'mmf_options', array() );
$value = isset( $options['per_page'] ) ? absint( $options['per_page'] ) : 10;
printf(
'<input type="number" name="mmf_options[per_page]" value="%d" min="1" max="100" class="small-text" />',
$value
);
}
function mmf_field_enable() {
$options = get_option( 'mmf_options', array() );
$checked = ! empty( $options['enable'] ) ? 1 : 0;
printf(
'<input type="checkbox" name="mmf_options[enable]" value="1" %s />',
checked( 1, $checked, false ) // 第三个参数 false 表示返回而不输出
);
}
checked() 是核心提供的辅助函数,用于输出 checked='checked',同样的还有 selected() 和 disabled()。
第四步:写净化回调
这里是安全的关键节点,所有用户输入必须在这里净化:
function mmf_sanitize_options( $input ) {
$output = array();
$output['api_key'] = isset( $input['api_key'] )
? sanitize_text_field( wp_unslash( $input['api_key'] ) )
: '';
$per_page = isset( $input['per_page'] ) ? absint( $input['per_page'] ) : 10;
if ( $per_page < 1 || $per_page > 100 ) {
$per_page = 10;
add_settings_error(
'mmf_options',
'per_page_invalid',
'每页条数必须在 1 到 100 之间,已重置为 10。',
'error'
);
}
$output['per_page'] = $per_page;
$output['enable'] = empty( $input['enable'] ) ? 0 : 1;
return $output;
}
add_settings_error() 记录的错误会在页面重定向后由 settings_errors() 自动显示。
第五步:渲染页面
function mmf_render_settings_page() {
// 页面级能力校验,不能只依赖菜单的 capability
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( '你没有权限访问此页面。' );
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php settings_errors( 'mmf_options' ); ?>
<form method="post" action="options.php">
<?php
settings_fields( 'mmf_settings_group' ); // 输出 nonce 与 option_page
do_settings_sections( 'mmf-settings' ); // 输出所有区块与字段
submit_button( '保存设置' ); // 输出提交按钮
?>
</form>
</div>
<?php
}
表单的 action 必须是 options.php,这样核心才会接管保存流程。
第六步:在插件里读取配置
function mmf_get_option( $key, $default = '' ) {
$options = get_option( 'mmf_options', array() );
return array_key_exists( $key, $options ) ? $options[ $key ] : $default;
}
// 使用
$per_page = absint( mmf_get_option( 'per_page', 10 ) );
第七步:验证
# 确认选项已写入
wp option get mmf_options --format=json
# 手动改一个值测试
wp option update mmf_options '{"api_key":"abc","per_page":20,"enable":1}' --format=json
常见踩坑
-
忘了
admin_init上注册:register_setting()必须在admin_init或更早执行,挂错钩子会导致保存时报「选项页面未注册」。 -
settings_fields()的组名与register_setting()不一致:保存会失败且不报错。 -
净化回调没有 return:选项被保存成空,且不会报错。
-
字段名与 option 数组键不匹配:
name="mmf_options[api_key]"里的键必须和净化回调里读的键一致。 -
只在菜单上判断 capability:直接访问
?page=mmf-settings时仍需页面内的current_user_can()校验。 -
案例: 某插件的设置页写了表单也写了
update_option,但没用 Settings API,自己处理$_POST。上线后被安全扫描器报出 CSRF 与存储型 XSS:表单没有 nonce,任何站外页面都能构造请求改配置;字段值直接入库,前端输出时未转义,管理员填的一段带<script>的文本被原样渲染到前台。改用 Settings API 后,nonce 由settings_fields()自动输出,净化集中在sanitize_callback里,两个问题一起解决。教训是:不要重复实现核心已经提供的机制。 -
把所有字段塞进一个 option 却忘了合并旧值:净化回调返回什么就存什么,没返回的键会丢。要么合并
get_option()的旧值,要么确保每次都返回完整结构。
总结
Settings API 的固定套路是五个函数:add_menu_page / add_options_page 建入口 → register_setting 声明选项与净化 → add_settings_section 分组 → add_settings_field 加字段 → settings_fields() + do_settings_sections() + submit_button() 渲染。
三条硬规范:所有用户输入在 sanitize_callback 里净化(对照表见 数据验证与转义函数);页面渲染回调里再判断一次 current_user_can();表单 action 固定为 options.php。
菜单和页面只是入口,安全才是核心——保存动作的防护写法见 nonce 与 capability 安全。而如果插件最终被删除,这些设置项该如何清理,写在 uninstall.php 清理数据 里。
