搜索全站
插件 20 分钟阅读

插件开发:用 add_menu_page 与 Settings API 创建设置页面

从 add_menu_page 建菜单到 register_setting 保存数据,本文给出 Settings API 的标准写法:分组、区块、字段三级结构,含净化回调、错误提示与完整可运行代码。

U
UFO_PF 2016-08-21 · 阅读 20 分钟
插件 插件
阅读提示
本文约 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() 输出时用的名字,它决定了表单提交后核心去哪个组里找字段做净化。

数据流向是这样的:

  1. 后台访问设置页 → 核心渲染你注册的字段;
  2. 用户点保存 → 表单 POST 到 options.php;
  3. options.php 校验 nonce 和能力,然后根据 settings_fields( $group ) 输出的组名,找到该组下所有注册的选项;
  4. 对每个选项调用你在 register_setting() 里声明的 sanitize_callback;
  5. 净化后的值写入 wp_options;
  6. 重定向回设置页,带上 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

常见踩坑

  1. 忘了 admin_init 上注册:register_setting() 必须在 admin_init 或更早执行,挂错钩子会导致保存时报「选项页面未注册」。

  2. settings_fields() 的组名与 register_setting() 不一致:保存会失败且不报错。

  3. 净化回调没有 return:选项被保存成空,且不会报错。

  4. 字段名与 option 数组键不匹配:name="mmf_options[api_key]" 里的键必须和净化回调里读的键一致。

  5. 只在菜单上判断 capability:直接访问 ?page=mmf-settings 时仍需页面内的 current_user_can() 校验。

  6. 案例: 某插件的设置页写了表单也写了 update_option,但没用 Settings API,自己处理 $_POST。上线后被安全扫描器报出 CSRF 与存储型 XSS:表单没有 nonce,任何站外页面都能构造请求改配置;字段值直接入库,前端输出时未转义,管理员填的一段带 <script> 的文本被原样渲染到前台。改用 Settings API 后,nonce 由 settings_fields() 自动输出,净化集中在 sanitize_callback 里,两个问题一起解决。教训是:不要重复实现核心已经提供的机制。

  7. 把所有字段塞进一个 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 清理数据 里。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。