WordPress 插件最简单的形式,就是 wp-content/plugins 目录下的一个文件夹,里面放着一个 PHP 文件,文件开头有一段注释头用于声明插件名称。保存该文件后,WordPress 就会在「插件」页面中列出这个插件。
如果你一直在往主题的 functions.php 里塞代码片段,你应该已经踩过这个坑:一换主题,代码就跟着没了。把这些代码移到插件里,功能就能在切换主题后依然保留——这正是站点行为应归属插件、而呈现效果应归属主题的主要原因。
本文将完整构建一个小型插件:在页脚输出一条站点通知,并可通过一个能安全保存数据的设置页面进行编辑。每一段代码块都是插件的一个有效状态,因此你可以在任意小节结束后停下来,直接启用当前成果。
要点速览
- 插件就是
wp-content/plugins下的一个文件夹,其中包含一个 PHP 文件,文件的注释头用于声明插件名称;Plugin Name是唯一必填的头部字段。 - 在你用
add_action()或add_filter()把函数挂载到钩子上之前,插件代码不会执行任何操作。 - filter 回调必须返回它接收到的值;如果什么都不返回,被过滤的值就会变成空。
- 一个安全的设置页面包含四层防护:权限检查(capability check)、nonce、输入时的净化(sanitisation)以及输出时的转义(escaping)。
register_activation_hook()用于设置默认值,停用钩子用于清理临时数据,而永久性的数据删除应交由卸载(uninstall)流程处理。
如何创建 WordPress 插件文件与头部信息
创建文件夹 wp-content/plugins/site-notice/,并在其中创建文件 site-notice.php。WordPress 通过读取 plugins 文件夹中的 PHP 文件,并挑出那些以插件头部注释开头的文件来构建「插件」页面。每个带有头部注释的文件都会被视为一个独立插件,所以头部注释只能写在一个文件里。
<?php
/**
* Plugin Name: Site Notice
* Description: Shows a short notice in the site footer, editable from Settings.
* Version: 1.0.0
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: OpenReplay Team
* License: GPL-2.0-or-later
* Text Domain: site-notice
*/
头部信息要求页面列出了所有可识别的字段,包括 Update URI 和 Requires Plugins。你最常用到的几个如下:
| 字段 | WordPress 如何使用它 | 是否必填 |
|---|---|---|
| Plugin Name | 在插件列表中显示 | 是 |
| Description | 显示在插件名称下方 | 否 |
| Version | 显示版本号;用于更新版本比较 | 否 |
| Requires at least / Requires PHP | 在过旧的运行环境中阻止启用 | 否 |
| Author、License、Text Domain | 署名、许可协议、翻译标识符 | 否 |
保存文件后,插件就会出现在「插件」页面上。它可以正常启用,但目前什么也不做。
启用与停用钩子有什么作用?
register_activation_hook() 会在有人开启插件的那一刻执行一次你的回调函数,因此非常适合把初始选项值写入数据库。register_deactivation_hook() 则用于丢弃那些只在插件运行期间才需要的东西,缓存就是典型例子。而永久性地删除数据——包括选项和自定义数据表——应该交给卸载流程处理,因为用户关闭插件时往往是打算之后再重新开启的。
在头部注释下方添加:
function orp_activate() {
add_option( 'orp_notice_text', 'Welcome to the site.' );
}
register_activation_hook( __FILE__, 'orp_activate' );
function orp_deactivate() {
delete_transient( 'orp_notice_cache' );
}
register_deactivation_hook( __FILE__, 'orp_deactivate' );
第一个参数 __FILE__ 指向插件主文件。我们的插件目前还没有任何缓存,但这个停用回调展示了它应有的形态:清理临时产物,保留已保存的选项。每个函数和选项名上的 orp_ 前缀可以避免与核心及其他插件发生命名冲突。
为什么我的插件代码不执行?
插件文件中的代码不会自行运行。只有当你把函数挂载到钩子上之后,WordPress 才会执行它;在此之前,文件是完全静止的。add_action() 接收一个钩子名称和一个可调用对象,还有一个可选的优先级参数,默认值为 10。
function orp_print_notice() {
$notice = get_option( 'orp_notice_text' );
if ( $notice ) {
echo '<p class="orp-notice">' . esc_html( $notice ) . '</p>';
}
}
add_action( 'wp_footer', 'orp_print_notice' );
当主题触发 wp_footer 时,WordPress 会调用挂载在该钩子上的所有函数,包括我们的这个。加载任意前台页面,通知就会出现在 body 结束标签之前。
添加一个 Filter:修改值并返回它
action 让你的函数在 WordPress 生命周期的某个节点上运行;而 filter 会把一个值交给你的函数,并期待你把修改后的值返回回来。filter 回调必须返回一个值。 如果它什么都不返回,PHP 会返回 null,WordPress 会把这个 null 继续传递下去,于是你所过滤的内容就会从站点上消失。在 body_class 回调里漏写 return,body 元素就会失去所有 class;在 the_content 上漏写,每篇文章都会渲染成空白。
function orp_body_class( $classes ) {
if ( get_option( 'orp_notice_text' ) ) {
$classes[] = 'orp-has-notice';
}
return $classes;
}
add_filter( 'body_class', 'orp_body_class' );
这段代码接收 body class 数组,在设置了通知时追加一个 class,然后返回该数组,这样主题就可以在通知生效期间为页面应用不同的样式。
如何构建 WordPress 设置页面?
一个设置页面由三部分注册构成:在 admin_menu 钩子上注册菜单页面,以及在 admin_init 钩子上注册设置项及其所属的 section 和 field,正如 Settings API 章节所述。add_options_page() 会把页面放在「设置」菜单下;register_setting() 用于命名选项,更关键的是指定它的 sanitize_callback。
function orp_settings_menu() {
add_options_page( 'Site Notice', 'Site Notice', 'manage_options',
'orp-site-notice', 'orp_settings_page_html' );
}
add_action( 'admin_menu', 'orp_settings_menu' );
function orp_settings_init() {
register_setting( 'orp_settings', 'orp_notice_text', array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
) );
add_settings_section( 'orp_main', 'Notice', '__return_false', 'orp-site-notice' );
add_settings_field( 'orp_notice_text', 'Notice text',
'orp_notice_field_html', 'orp-site-notice', 'orp_main' );
}
add_action( 'admin_init', 'orp_settings_init' );
function orp_notice_field_html() {
$value = get_option( 'orp_notice_text', '' );
echo '<input type="text" name="orp_notice_text" value="'
. esc_attr( $value ) . '" class="regular-text">';
}
function orp_settings_page_html() {
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( 'orp_settings' );
do_settings_sections( 'orp-site-notice' );
submit_button( 'Save Notice' );
?>
</form>
</div>
<?php
}
表单会提交到 options.php,由核心负责保存。注意,register_setting() 也接受一个 default 参数;我们选择在启用钩子中设置默认值,所以请只选用其中一种机制并保持一致。
如何安全地处理提交的值?
权限检查回答的是「这个用户是否有权保存设置」;而 nonce 回答的是「这个用户是否真的有意提交这个表单」。一个要交付给客户站点的设置页面两者都需要,而且 nonce 绝不能替代权限检查。
在这里,Settings API 已经帮你完成了大部分工作:settings_fields() 会输出 nonce,表单回传时核心会自动校验;同时,除非当前用户拥有 manage_options 权限,否则核心会阻止保存——这正是 wp-admin/options.php 默认对设置页面应用的权限。如果你日后改为编写自定义的表单处理逻辑,请用 wp_nonce_field() 输出 nonce,并用 check_admin_referer() 进行校验。参照手册自身的示例,在页面回调中加入显式的守卫判断:
function orp_settings_page_html() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// ... form as above ...
}
要检查权限(capability),而不要检查诸如 administrator 这样的角色名。剩下的两层防护已经就位:写入数据库前进行净化(作为 sanitize_callback 的 sanitize_text_field),以及输出时进行转义(字段中的 esc_attr()、页脚中的 esc_html())。这两项操作不可互相替代,缺少任何一项,都会让一个已保存的选项变成注入攻击的入口。
启用插件并确认其正常工作
在「插件」页面启用 Site Notice,然后加载前台页面,查看页脚中是否出现通知。到「设置 → Site Notice」中修改文本并刷新页面。如果毫无反应,请按顺序逐项排查:
- 插件是真的已启用了,还是只是存在于目录中?
- 传给
add_action()或add_filter()的每个字符串,是否与已定义的函数名完全一致? <?php之前是否有空白字符或输出?如果有,WordPress 会在启用时报告 “unexpected output”。- 在
wp-config.php中开启WP_DEBUG,并用WP_DEBUG_LOG把错误写入wp-content/debug.log,直接读取真实的 PHP 错误信息,而不是靠猜。
现在,你已经拥有了一个包含默认选项、一个 action、一个会返回值的 filter,以及一个通过 nonce、权限检查和净化函数来保存数据的设置页面的插件。对于所有仍然待在 functions.php 里的代码片段,这就是值得照搬的基准做法:把它挪过来,加上前缀,挂到钩子上,并完整保留「输入净化、输出转义」这一对操作。
常见问题
wp_verify_nonce 和 check_admin_referer 有什么区别?
check_admin_referer() 会同时校验后台页面中表单和 URL 的 nonce 与来源(referrer),校验失败时会以 403 中止请求。wp_verify_nonce() 只检查 nonce 并返回一个由你自行处理的结果,适用于 Ajax 处理器及其他自定义场景。两者都不能替代权限检查:nonce 确认的是用户确有执行该操作的意图,而 current_user_can() 确认的是用户被允许执行该操作。
插件被停用后,它保存的选项会怎样?
不会有任何变化:停用不会清除数据库中的选项,因此用户重新启用插件时设置依然完好。停用回调应当只清理生命周期短暂的内容,例如 transients 或缓存文件。永久性清理属于卸载流程,可以通过 register_uninstall_hook() 实现,或在插件目录中放置一个 uninstall.php 文件。如果使用 uninstall.php,它必须在删除任何数据之前检查 WP_UNINSTALL_PLUGIN 常量是否已定义。
一个 WordPress 插件可以包含多个 PHP 文件吗?
可以,但只有主文件应当包含插件头部注释。WordPress 通过读取 plugins 目录中 PHP 文件的头部注释来发现插件,每个带有头部注释的文件都会作为独立插件显示出来。请在主文件中用 require_once 加载其他文件,并让 register_activation_hook() 之类的调用始终指向插件主文件,因为它们的第一个参数必须引用该文件。
默认选项应该在 register_setting 中设置,还是在启用钩子中设置?
只选用一种机制,不要两者并用。register_setting() 接受一个 default 参数,当数据库中不存在对应值时会返回它;而启用钩子中的 add_option() 则会在启用时真正写入一行数据。启用钩子的做法可以保证该值在每次请求中都存在,包括前台请求;而 register_setting() 的默认值只在注册代码已经执行的场景下生效。两者混用会为同一个选项制造出两个「真相来源」。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k