WordPress 插件开发实战:Hook 机制与自定义功能
掌握 Action/Filter 钩子体系,从零开发功能完整的 WordPress 插件
WordPress 插件体系是它扩展能力的核心所在:几乎所有”非主题职责”的功能都可以通过插件实现。而支撑插件体系的,是 WordPress 精心设计的 Hook(钩子)机制——它允许你在不修改核心代码的前提下,向系统的特定执行点注入自定义逻辑。
本文将从插件的基本结构讲起,逐步覆盖 Hook 机制、Shortcode、Widget、设置页面、AJAX 以及安全实践,带你完整走一遍插件开发流程。
一、插件结构与规范
1.1 最小插件结构
一个插件本质上就是位于 wp-content/plugins/ 目录下的一个 PHP 文件。但规范的做法是创建独立文件夹,包含以下结构:
my-plugin/
├── my-plugin.php # 主文件(入口)
├── includes/
│ ├── class-my-plugin.php # 主类
│ ├── shortcodes.php # 短代码
│ └── ajax.php # AJAX 处理
├── admin/
│ ├── settings-page.php # 设置页面
│ └── settings-callbacks.php
├── assets/
│ ├── css/
│ └── js/
└── uninstall.php # 卸载时清理
主文件顶部必须包含插件头注释:
<?php
/**
* Plugin Name: My Plugin
* Plugin URI: https://example.com/my-plugin
* Description: 一个示例插件
* Version: 1.0.0
* Author: Your Name
* License: GPL v2 or later
* Text Domain: my-plugin
* Domain Path: /languages
*/
// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// 定义常量
define( 'MY_PLUGIN_VERSION', '1.0.0' );
define( 'MY_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
define( 'MY_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
// 加载主类
require_once MY_PLUGIN_PATH . 'includes/class-my-plugin.php';
// 初始化
function my_plugin_init() {
load_plugin_textdomain( 'my-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
My_Plugin::instance();
}
add_action( 'plugins_loaded', 'my_plugin_init' );
1.2 单例模式的主类
<?php
// includes/class-my-plugin.php
class My_Plugin {
private static $instance = null;
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->includes();
$this->init_hooks();
}
private function includes() {
require_once MY_PLUGIN_PATH . 'includes/shortcodes.php';
require_once MY_PLUGIN_PATH . 'includes/ajax.php';
}
private function init_hooks() {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
public function enqueue_assets() {
wp_enqueue_style(
'my-plugin-style',
MY_PLUGIN_URL . 'assets/css/style.css',
array(),
MY_PLUGIN_VERSION
);
wp_enqueue_script(
'my-plugin-script',
MY_PLUGIN_URL . 'assets/js/script.js',
array( 'jquery' ),
MY_PLUGIN_VERSION,
true
);
}
}
1.3 卸载清理
uninstall.php 在插件被删除时执行,用于清理数据:
<?php
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// 删除插件创建的选项
delete_option( 'my_plugin_settings' );
// 删除自定义文章类型的文章
$posts = get_posts( array(
'post_type' => 'my_cpt',
'numberposts' => -1,
'post_status' => 'any',
) );
foreach ( $posts as $post ) {
wp_delete_post( $post->ID, true );
}
注意区分三种生命周期钩子:
| 钩子 | 触发时机 | 用途 |
|---|---|---|
register_activation_hook | 启用插件时 | 建表、设置默认选项、刷新固定链接 |
register_deactivation_hook | 停用插件时 | 清理临时数据、刷新固定链接 |
uninstall.php / register_uninstall_hook | 删除插件时 | 彻底清理所有数据 |
二、Hook 机制:Action 与 Filter
2.1 两类钩子的区别
| 钩子类型 | 作用 | 返回值 | 典型场景 |
|---|---|---|---|
| Action | 在特定执行点运行代码 | 无返回 | 发邮件、记录日志、输出内容 |
| Filter | 过滤并返回修改后的数据 | 必须返回数据 | 修改内容、替换标题、调整参数 |
// Action:挂载到 wp_footer 执行点
add_action( 'wp_footer', function() {
echo '<!-- 由 My Plugin 注入 -->';
} );
// Filter:修改 the_content 输出
add_filter( 'the_content', function( $content ) {
return $content . '<p>© ' . date( 'Y' ) . '</p>';
} );
2.2 优先级与参数个数
add_action 和 add_filter 接受四个参数:
add_filter( $tag, $callback, $priority = 10, $accepted_args = 1 );
$priority:数字越小越先执行,默认 10。同优先级按注册顺序执行。$accepted_args:回调函数接收的参数个数,默认 1。
// 优先执行(数字小)
add_filter( 'the_title', 'my_filter_early', 5 );
// 最后执行(数字大)
add_filter( 'the_title', 'my_filter_late', 20 );
// 接收多个参数
add_filter( 'post_link', function( $permalink, $post, $leavename ) {
// $permalink, $post, $leavename 三个参数
return $permalink;
}, 10, 3 );
2.3 移除钩子
// 移除自己注册的钩子
remove_action( 'wp_footer', 'my_callback', 20 );
remove_filter( 'the_content', 'my_filter', 10 );
// 移除类方法
remove_filter( 'the_content', array( 'Some_Class', 'method' ), 10 );
// 移除对象方法
remove_filter( 'the_content', array( $instance, 'method' ), 10 );
// 移除所有挂载在某钩子上的回调
remove_all_actions( 'wp_footer' );
remove_all_filters( 'the_content' );
2.4 自定义钩子
插件可以定义自己的钩子供其他插件扩展:
// 在插件执行点触发 Action
do_action( 'my_plugin_before_process', $post_id );
// 处理数据并触发 Filter
$data = apply_filters( 'my_plugin_process_data', $default_data, $post_id );
// 在执行点触发带额外参数的 Action
do_action( 'my_plugin_after_process', $post_id, $status );
其他插件可以挂载到这些钩子:
add_filter( 'my_plugin_process_data', function( $data, $post_id ) {
$data['extra'] = '附加字段';
return $data;
}, 10, 2 );
这套机制让插件可以被无限扩展,是 WordPress 生态繁荣的基石。
三、Shortcode 短代码
Shortcode 让用户在编辑器中用类似 [my_shortcode] 的语法插入动态内容。
3.1 基础 Shortcode
// 注册
add_shortcode( 'greeting', function( $atts ) {
$atts = shortcode_atts( array(
'name' => '访客',
'time' => '上午',
), $atts, 'greeting' );
return sprintf(
'<p>%s好,%s!</p>',
esc_html( $atts['time'] ),
esc_html( $atts['name'] )
);
} );
使用方式:[greeting name="张三" time="下午"]
3.2 带内容的 Shortcode
add_shortcode( 'box', function( $atts, $content = null ) {
$atts = shortcode_atts( array(
'type' => 'info',
), $atts, 'box' );
return sprintf(
'<div class="box box-%s">%s</div>',
esc_attr( $atts['type'] ),
do_shortcode( $content ) // 支持嵌套短代码
);
} );
使用方式:
[box type="warning"]
这里是警告内容,可以嵌套 [greeting name="李四"]。
[/box]
3.3 在 Gutenberg 中支持 Shortcode
Gutenberg 有专门的”短代码区块”。同时可以注册一个动态 Block,在服务端用 render_callback 输出内容:
register_block_type( 'my-plugin/greeting', array(
'attributes' => array(
'name' => array( 'type' => 'string', 'default' => '访客' ),
'time' => array( 'type' => 'string', 'default' => '上午' ),
),
'render_callback' => function( $atts ) {
return do_shortcode( sprintf(
'[greeting name="%s" time="%s"]',
esc_attr( $atts['name'] ),
esc_attr( $atts['time'] )
) );
},
) );
四、Widget 小工具
经典编辑器(小工具区块)时代 Widget 广泛使用,在区块编辑器时代仍可通过”经典小工具”区块兼容。
4.1 注册 Widget
class My_Plugin_Recent_Posts_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'my_plugin_recent_posts',
'近期文章(My Plugin)',
array( 'description' => '显示带缩略图的近期文章' )
);
}
// 前端输出
public function widget( $args, $instance ) {
echo $args['before_widget'];
if ( ! empty( $instance['title'] ) ) {
echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'];
}
$query = new WP_Query( array(
'posts_per_page' => $instance['count'] ?? 5,
'ignore_sticky_posts' => true,
) );
echo '<ul class="my-plugin-recent-posts">';
while ( $query->have_posts() ) : $query->the_post();
echo '<li>';
if ( has_post_thumbnail() ) {
echo '<a href="' . get_permalink() . '">' . get_the_post_thumbnail( null, 'thumbnail' ) . '</a>';
}
echo '<a href="' . get_permalink() . '">' . get_the_title() . '</a>';
echo '</li>';
endwhile;
wp_reset_postdata();
echo '</ul>';
echo $args['after_widget'];
}
// 后台表单
public function form( $instance ) {
$title = $instance['title'] ?? '近期文章';
$count = $instance['count'] ?? 5;
?>
<p>
<label for="<?php echo $this->get_field_id( 'title' ); ?>">标题:</label>
<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>"
name="<?php echo $this->get_field_name( 'title' ); ?>" type="text"
value="<?php echo esc_attr( $title ); ?>">
</p>
<p>
<label for="<?php echo $this->get_field_id( 'count' ); ?>">数量:</label>
<input class="tiny-text" id="<?php echo $this->get_field_id( 'count' ); ?>"
name="<?php echo $this->get_field_name( 'count' ); ?>" type="number"
value="<?php echo esc_attr( $count ); ?>" min="1" max="20">
</p>
<?php
}
// 保存设置
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = sanitize_text_field( $new_instance['title'] );
$instance['count'] = absint( $new_instance['count'] );
return $instance;
}
}
// 注册 Widget
add_action( 'widgets_init', function() {
register_widget( 'My_Plugin_Recent_Posts_Widget' );
} );
五、设置页面与 Options API
5.1 Options API 基础
| 函数 | 作用 |
|---|---|
get_option( $key, $default ) | 读取选项 |
update_option( $key, $value ) | 更新选项(不存在则创建) |
delete_option( $key ) | 删除选项 |
选项存储在 wp_options 表中,适合少量配置数据。大量数据建议用自定义表或 CPT。
5.2 注册设置页面
使用 Settings API 创建后台配置页面:
class My_Plugin_Settings {
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_menu' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
}
public function add_menu() {
add_options_page(
'My Plugin 设置',
'My Plugin',
'manage_options',
'my-plugin-settings',
array( $this, 'render_page' )
);
}
public function register_settings() {
// 注册一个设置组
register_setting( 'my_plugin_options_group', 'my_plugin_settings', array(
'sanitize_callback' => array( $this, 'sanitize' ),
) );
// 添加设置区块
add_settings_section(
'my_plugin_main_section',
'基本设置',
'__return_empty_string',
'my-plugin-settings'
);
// 添加字段
add_settings_field(
'api_key',
'API Key',
array( $this, 'field_api_key' ),
'my-plugin-settings',
'my_plugin_main_section'
);
add_settings_field(
'cache_ttl',
'缓存时间(秒)',
array( $this, 'field_cache_ttl' ),
'my-plugin-settings',
'my_plugin_main_section'
);
}
public function sanitize( $input ) {
$output = array();
$output['api_key'] = sanitize_text_field( $input['api_key'] );
$output['cache_ttl'] = absint( $input['cache_ttl'] );
return $output;
}
public function field_api_key() {
$options = get_option( 'my_plugin_settings' );
$value = $options['api_key'] ?? '';
printf(
'<input type="text" class="regular-text" name="my_plugin_settings[api_key]" value="%s">',
esc_attr( $value )
);
}
public function field_cache_ttl() {
$options = get_option( 'my_plugin_settings' );
$value = $options['cache_ttl'] ?? 3600;
printf(
'<input type="number" name="my_plugin_settings[cache_ttl]" value="%d" min="0">',
esc_attr( $value )
);
}
public function render_page() {
?>
<div class="wrap">
<h1>My Plugin 设置</h1>
<form action="options.php" method="post">
<?php
settings_fields( 'my_plugin_options_group' );
do_settings_sections( 'my-plugin-settings' );
submit_button( '保存设置' );
?>
</form>
</div>
<?php
}
}
new My_Plugin_Settings();
5.3 读取设置
$options = get_option( 'my_plugin_settings' );
$api_key = $options['api_key'] ?? '';
$ttl = $options['cache_ttl'] ?? 3600;
六、AJAX 集成
WordPress 提供了统一的 AJAX 入口 admin-ajax.php,无论前端还是后台请求都走这个端点。
6.1 前端 AJAX 流程
PHP 端注册处理函数:
// includes/ajax.php
// 已登录用户
add_action( 'wp_ajax_my_plugin_like', 'my_plugin_handle_like' );
// 未登录用户
add_action( 'wp_ajax_nopriv_my_plugin_like', 'my_plugin_handle_like' );
function my_plugin_handle_like() {
// 校验 nonce
check_ajax_referer( 'my_plugin_nonce', 'nonce' );
$post_id = absint( $_POST['post_id'] ?? 0 );
if ( ! $post_id ) {
wp_send_json_error( array( 'message' => '参数错误' ) );
}
// 更新点赞数
$current = (int) get_post_meta( $post_id, '_like_count', true );
$new = $current + 1;
update_post_meta( $post_id, '_like_count', $new );
wp_send_json_success( array( 'count' => $new ) );
}
前端脚本:
// assets/js/script.js
jQuery( function( $ ) {
$( '.like-button' ).on( 'click', function() {
var $btn = $( this );
$.post( myTheme.ajaxUrl, {
action: 'my_plugin_like',
nonce: myTheme.nonce,
post_id: $btn.data( 'post-id' )
}, function( res ) {
if ( res.success ) {
$btn.find( '.count' ).text( res.data.count );
} else {
alert( res.data.message );
}
} );
} );
} );
6.2 传递 ajaxurl
前端默认没有 ajaxurl 变量,需要通过 wp_localize_script 传入:
wp_localize_script( 'my-plugin-script', 'myPlugin', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'my_plugin_nonce' ),
) );
6.3 wp_send_json 工具函数
| 函数 | 作用 |
|---|---|
wp_send_json( $data ) | 返回 JSON 并退出 |
wp_send_json_success( $data ) | 返回 {"success":true,"data":...} |
wp_send_json_error( $data ) | 返回 {"success":false,"data":...} |
wp_die() | 安全终止脚本 |
七、安全与数据验证
安全是插件开发的生命线。WordPress 推荐遵循”信任边界”原则:所有来自用户的数据都不可信,必须验证、净化、转义。
7.1 数据处理三原则
| 阶段 | 函数 | 用途 |
|---|---|---|
| 验证 | is_email() / ctype_digit() / absint() | 检查是否符合预期格式 |
| 净化 | sanitize_text_field() / sanitize_email() / sanitize_file_name() | 清理输入 |
| 转义 | esc_html() / esc_attr() / esc_url() / wp_kses_post() | 安全输出 |
7.2 Nonce 防 CSRF
Cross-Site Request Forgery(CSRF)是常见攻击。WordPress 用 Nonce(Number Used Once)防御:
// 生成 nonce
$nonce = wp_create_nonce( 'my_action' );
// 表单中隐藏字段
wp_nonce_field( 'my_action', 'my_nonce_field' );
// 验证
if ( ! isset( $_POST['my_nonce_field'] ) || ! wp_verify_nonce( $_POST['my_nonce_field'], 'my_action' ) ) {
wp_die( '安全校验失败' );
}
// AJAX 中验证
check_ajax_referer( 'my_action', 'nonce' );
7.3 能力检查
// 检查当前用户是否有指定权限
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( '权限不足' );
}
// 检查文章编辑权限
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_die( '无权编辑此文章' );
}
7.4 SQL 注入防护
WordPress 的 $wpdb 默认会对数据进行转义,但仍应使用预处理语句:
global $wpdb;
// ❌ 危险:直接拼接
$sql = "SELECT * FROM {$wpdb->prefix}my_table WHERE name = '" . $_POST['name'] . "'";
// ✅ 使用 prepare
$sql = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}my_table WHERE name = %s AND status = %d",
$_POST['name'],
1
);
$results = $wpdb->get_results( $sql );
常用 $wpdb 方法:
| 方法 | 用途 |
|---|---|
$wpdb->get_results( $sql ) | 查询多行 |
$wpdb->get_row( $sql ) | 查询单行 |
$wpdb->get_var( $sql ) | 查询单值 |
$wpdb->insert( $table, $data ) | 插入 |
$wpdb->update( $table, $data, $where ) | 更新 |
$wpdb->delete( $table, $where ) | 删除 |
这些方法内部已做格式化处理,但仍建议传入正确的格式说明符(%s / %d / %f)。
八、插件国际化
让你的插件支持多语言:
- 所有字符串用
__()/_e()/_x()/esc_html__()等函数包裹,并传入 Text Domain:
$title = __( 'My Plugin Settings', 'my-plugin' );
_e( 'Save Changes', 'my-plugin' );
-
加载语言文件(见 1.1 节)。
-
用 Poedit 或 WP-CLI 生成
.pot/.po/.mo文件:
wp i18n make-pot . languages/my-plugin.pot
- 处理带占位符的字符串:
printf(
/* translators: %s: user name */
__( 'Welcome, %s!', 'my-plugin' ),
esc_html( $user_name )
);
九、调试与日志
开发期推荐开启 WP_DEBUG:
// wp-config.php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true ); // 写入 wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false );
代码中记录日志:
if ( WP_DEBUG && WP_DEBUG_LOG ) {
error_log( '[My Plugin] ' . $message );
}
Query Monitor 插件是排查性能与钩子执行顺序的神器,开发期必备。
十、完整开发流程清单
| 步骤 | 检查项 |
|---|---|
| 规划 | 明确功能边界,判断是否适合用插件实现 |
| 结构 | 创建独立目录,主文件含插件头,遵循单一职责 |
| 安全 | 所有入口做 Nonce 校验、能力检查、数据净化 |
| Hook | 用 Action/Filter 接入系统,避免直接修改核心 |
| 资源 | 用 wp_enqueue_* 加载,避免重复加载 |
| 数据 | Options 存配置,CPT 存内容,自定义表存高频结构化数据 |
| 国际化 | 字串全部包裹,生成 .pot 文件 |
| 卸载 | 实现 uninstall.php 清理数据 |
| 测试 | 不同 PHP 版本、WP 版本、主题组合下测试 |
| 发布 | 配 README.txt,遵循插件库规范 |
结语
WordPress 插件开发的精髓在于 Hook 机制——它让你能在系统任意执行点注入逻辑,而无需改动核心代码。本文核心要点:
- Action / Filter 是插件的灵魂,理解优先级与参数传递;
- Shortcode 是面向内容编辑者的扩展接口;
- Widget / Block 是面向布局的扩展单元;
- Settings API 提供规范的后台配置页面;
- AJAX 通过统一入口实现前后端交互;
- 安全 是底线:验证、净化、转义、Nonce、能力检查缺一不可。
掌握这些后,你可以开发从简单功能增强到复杂业务系统的各类插件。建议阅读 WordPress 核心代码中的 Hook 应用,学习官方插件的实现模式,持续提升工程化水平。