关于 wp-plugin-development
面向 WordPress 插件开发者的架构审查技能,可用于评估代码结构及 WordPress.org 发布准备情况。它检查生命周期钩子、设置、扩展机制、命名和国际化,并将安全专项审查交由独立技能处理。
主要功能
- 01审查插件元数据与架构
- 02检查生命周期钩子和清理
- 03检查设置与钩子用法
- 04评估命名和国际化
- 05按文件与严重程度整理发现
适用场景
- 01准备 WordPress.org 插件审查
- 02审计启用与卸载行为
- 03检查自定义内容类型和钩子
- 04核对插件国际化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jorgerosal/ wordpress-skills --skill wp-plugin-development安装后在 Claude Code 中输入/wp-plugin-development确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api-patterns.mdarchitecture-patterns.mdhooks-guide.mdoop-patterns.md详细介绍
WordPress 插件开发审查技能
概述
针对适用于 WordPress 6.x+ 的 WordPress 插件,系统地审查插件架构。核心原则: WordPress 插件通过标准化的 API 生态扩展功能,包括生命周期钩子(激活/停用/卸载)、Settings API、钩子系统(操作/筛选器)、自定义文章类型/分类法,以及国际化。审查将验证插件架构、命名规范、WordPress.org 合规标准,并参照 wp-security-review 检查安全相关模式(nonce、功能权限、清理)。按文件分组报告问题,标注行号和严重级别(CRITICAL/WARNING/INFO),并提供 BAD/GOOD 代码对,展示正确实现方式。
使用场景
适用于:
- 插件代码架构审查
- 提交至 WordPress.org Plugin Directory 前的检查
- 审核插件生命周期钩子(激活/停用/卸载)
- 审查自定义文章类型或分类法的注册
- 验证 Settings API 的实现
- 检查钩子系统的使用方式(操作/筛选器、优先级、移除)
- 国际化(i18n/l10n)审计
- 验证是否符合 WordPress Plugin Check(PCP)规范
- 检测前缀和命名空间冲突
不适用于:
- 主题开发(如有可用的 wp-theme-development,请使用)
- Gutenberg 区块开发(如有可用的 wp-block-development,请使用)
- WooCommerce 扩展开发(如有可用的 wp-woocommerce-dev,请使用)
- 仅针对安全性的审计(如需全面的安全分析,请使用 wp-security-review)
- 仅针对性能的审计(请使用 wp-performance-review)
代码审查工作流
按照以下六个步骤系统地审查插件:
-
识别插件类型和上下文
- 提交到 WordPress.org → 应用最严格的标准(符合 Plugin Check 要求)
- 私有/企业插件 → 命名方面更灵活,无需
readme.txt - 必须使用的插件(mu-plugin)→ 加载方式不同(无需激活钩子)
- Drop-in 插件 → 使用特殊文件(object-cache.php、db.php),适用不同模式
-
检查插件头信息的完整性和元数据
- 必需:包含
Plugin Name字段 - 建议:版本、描述、作者、文本域、最低要求版本、PHP 要求、许可证
- 确认文本域与插件 slug 完全一致(使用小写字母和连字符,不使用下划线)
- 必需:包含
-
扫描 CRITICAL 模式(会破坏功能或导致被 WordPress.org 拒绝)
- PHP 文件中缺少
defined( 'ABSPATH' ) || exit;检查 - 全局作用域中的函数/类未添加前缀(存在命名空间冲突风险)
- 缺少用于设置/清理的激活/停用钩子
- 头信息中的文本域与 i18n 函数调用中的文本域不匹配
- 没有卸载清理逻辑(缺少 uninstall.php 或 register_uninstall_hook)
- 在
init钩子上调用flush_rewrite_rules()(每次加载页面都会执行开销较大的操作) - 使用
$wpdb->query()直接创建数据库表,而不是使用dbDelta() - 筛选器回调缺少返回语句
- REST API 路由没有
permission_callback(WordPress 5.5+ 要求) - 文章类型键超过 20 个字符
- 分类法名称超过 32 个字符
- PHP 文件中缺少
-
检查 WARNING 模式(不符合规范但仍可运行)
- 面向用户的字符串缺少国际化(i18n)
- 调用
__()或_e()时未提供文本域参数 - 硬编码的 WordPress 路径(
/wp-content/plugins/) register_setting()中缺少sanitize_callback- 缺少
show_in_rest,影响 Gutenberg 兼容性 - 管理后台代码在前端加载(缺少
is_admin()检查)
-
记录 INFO 改进项(最佳实践和优化建议)
- 可以使用 PHP 命名空间,而不是添加前缀
- 插件头信息中缺少版本要求(
Requires at least、Requires PHP) - 可以添加
show_in_rest,以支持 Block Editor - 管理后台资源全局入队(应按条件加载)
- 头信息中缺少
Domain Path
-
根据上下文调整严重级别并生成报告
- WordPress.org 上下文:执行最严格的检查(Plugin Check 未通过 = CRITICAL)
- 私有插件上下文:更灵活(仍需添加前缀,
readme.txt可选) - 必须使用的插件:调整检查预期(不适用激活钩子)
- 使用下方的输出格式生成报告
- 如果发现安全问题,添加说明:“检测到安全问题。运行
/wp-sec-review以进行全面的安全分析。”
特定文件类型检查
主插件文件(例如 my-plugin.php)
插件头信息(PLG-02):
- 严重:缺少
Plugin Name字段 → WordPress 无法识别该插件 - 警告:缺少
Text Domain→ 破坏国际化功能 - 警告:文本域与插件 slug 不匹配 → 被 WordPress.org 拒绝
- 信息:缺少
Requires at least或Requires PHP→ 无法强制执行版本要求
ABSPATH 检查(PLG-03):
- 严重:文件顶部缺少
defined( 'ABSPATH' ) || exit;→ 可能被直接访问 - 检测模式:PHP 文件的前 10 行中没有 ABSPATH 检查
前缀(PLG-04):
- 严重:全局作用域中的函数未加前缀 → 与其他插件发生命名空间冲突
- 严重:全局作用域中的类未加前缀 → 其他插件使用相同名称时会导致致命错误
- 严重:全局变量未加前缀 → 存在变量冲突风险
- 警告:选项/瞬态未加前缀 → 数据库键冲突
- 模式:
function [a-z_]+\(中的函数名没有插件前缀(至少 4~5 个字符) - 替代方案:使用 PHP 命名空间可免去添加前缀的需要
激活/停用钩子(PLG-04):
- 警告:插件有初始化需求,但缺少
register_activation_hook()→ 无法初始化 - 警告:插件有临时数据,但缺少
register_deactivation_hook()→ 停用时不会清理 - 模式:查找
register_activation_hook( __FILE__, 'callback' )
卸载文件(uninstall.php)
WP_UNINSTALL_PLUGIN 常量检查(PLG-05):
- 严重:
uninstall.php未检查WP_UNINSTALL_PLUGIN→ 可能被直接执行 - 模式:
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { exit; }
清理完整性(PLG-05):
- 警告:未清理选项(
delete_option())→ 插件数据残留在数据库中 - 警告:未清理瞬态(
delete_transient())→ 数据库膨胀 - 警告:未清理自定义表(
DROP TABLE)→ 留下孤立表 - 警告:未清理用户元数据(
delete_metadata( 'user', ... ))→ 用户数据仍然保留 - 适用范围:仅适用于插件创建了这些类型的数据的情况
自定义文章类型文件
register_post_type() 的用法(PLG-06):
- 严重:文章类型键超过 20 个字符 → 截断问题、固定链接问题
- 严重:文章类型键未加前缀 → 与 WordPress 核心或其他插件的查询变量冲突
- 警告:缺少
show_in_rest => true→ 无法在区块编辑器中使用 - 警告:在
init上调用flush_rewrite_rules()→ 性能问题(每次加载页面都会执行开销较大的数据库写入) - 模式:挂接到
init的register_post_type( 'key', array( ... ) ) - 最佳实践:文章类型键最多 20 个字符,仅使用小写字母数字字符、连字符和下划线
重写规则刷新时机(PLG-06):
- 严重:在
init钩子上调用flush_rewrite_rules()→ 每次加载页面都会写入数据库 - 正确模式:仅在注册文章类型后,于激活钩子中调用
分类法文件
register_taxonomy() 的用法(PLG-07):
- 严重:分类法名称超过 32 个字符 → 数据库错误
- 严重:分类法名称未加前缀 → 与核心分类法或其他插件发生冲突
- 警告:缺少
show_in_rest => true→ 无法在区块编辑器中使用 - 模式:挂接到
init的register_taxonomy( 'name', 'post_type', array( ... ) ) - 最佳实践:分类法名称最多 32 个字符,且仅使用小写字母和下划线
Settings API 文件
register_setting() 用法(PLG-08):
- 严重:
add_settings_field()没有对应的register_setting()→ 字段不会保存 - 警告:
register_setting()没有sanitize_callback→ 存储的数据未经清理 - 警告:
register_setting()没有type参数 → 类型推断不可靠 - 模式:在
admin_init上调用register_setting( 'option_group', 'option_name', array( ... ) )
Settings API 工作流(PLG-09):
- 警告:
add_settings_section()的页面 slug 与do_settings_sections()调用不匹配 → 区块不会渲染 - 警告:
add_settings_field()的区块 ID 与add_settings_section()的 ID 不匹配 → 字段不会渲染 - 警告:表单缺少
settings_fields()调用 → 没有 nonce 保护,选项不会保存 - 警告:表单缺少
do_settings_sections()调用 → 字段不会渲染 - 模式:完整工作流需要
register_setting()→add_settings_section()→add_settings_field()→ 包含settings_fields()和do_settings_sections()的表单 HTML
Hooks 用法文件
add_action/add_filter 模式(PLG-10):
- 警告:顺序重要时未指定钩子优先级 → 可能以错误的顺序运行
- 警告:回调需要多个参数时未指定
accepted_args→ 参数会被截断 - 信息:为清晰起见,可以显式指定优先级(即使使用默认值 10)
- 模式:
add_action( 'hook_name', 'callback', $priority, $accepted_args )
Filter 返回值要求(PLG-10):
- 严重:Filter 回调不返回值 → 会中断 Filter 链并导致致命错误
- 模式:
add_filter( 'hook', 'callback' ),其中回调没有 return 语句 - 检查:每个 Filter 回调都必须返回一个值
移除 Hook(PLG-10):
- 警告:
remove_action()或remove_filter()的参数与注册时不匹配 → 无法移除 Hook - 模式:优先级和 accepted_args 必须与原始
add_action()/add_filter()调用完全一致 - 示例:
add_action( 'init', 'callback', 15 )需要remove_action( 'init', 'callback', 15 )
用于扩展性的自定义 Hook(PLG-10):
- 信息:插件可以通过
do_action()或apply_filters()提供自定义 Hook,以实现扩展性 - 模式:
do_action( 'myplugin_after_save', $data )或apply_filters( 'myplugin_modify_output', $output )
国际化(i18n)文件
文本域匹配(PLG-11):
- 严重:插件头部中的文本域与 i18n 调用中的文本域不匹配 → 翻译加载失败
- 严重:文本域使用下划线而不是连字符 → 被 WordPress.org 拒绝
- 严重:文本域与插件 slug 不匹配 → Plugin Check 失败
- 模式:
Text Domain: my-plugin必须与插件文件夹/文件名完全匹配
i18n 函数用法(PLG-11):
- 警告:
__()或_e()未带文本域参数 → 回退到核心翻译(不正确) - 警告:在可翻译字符串中嵌入变量 → 导致翻译提取失败
- 模式:始终使用
__( 'Text', 'text-domain' ),不要使用__( 'Text' ) - 模式:使用
sprintf( __( 'Text %s', 'domain' ), $var ),不要使用__( "Text $var", 'domain' )
复数形式(PLG-11):
- 警告:使用 if/else 而不是
_n()处理复数形式 → 不支持复数规则复杂的语言 - 模式:对于依赖数量的字符串,使用
_n( 'singular', 'plural', $count, 'domain' )
结合上下文的翻译(PLG-11):
- 信息:同一个英文单词有不同译法时,可使用
_x()消除歧义 - 模式:
_x( 'Post', 'noun', 'domain' )与_x( 'Post', 'verb', 'domain' )
转义翻译变体(PLG-11):
- 信息:使用
esc_html__()或esc_attr__(),合并翻译和转义操作 - 模式:使用
echo esc_html__( 'Text', 'domain' ),而不是echo esc_html( __( 'Text', 'domain' ) )
翻译加载(PLG-11):
- 信息:对于 WordPress 4.6+,
load_plugin_textdomain()是可选的(会从 translate.wordpress.org 自动加载) - 模式:若要调用,请挂接到
init:add_action( 'init', 'load_textdomain_callback' )
REST API 端点(PLG-13,表层检查)
register_rest_route() 用法:
- 严重:缺少
permission_callback→ WordPress 5.5+ 会报错,端点被阻止 - 严重:在写入操作中使用
permission_callback => '__return_true'→ 允许未经身份验证的访问 - 模式:在
rest_api_init钩子上使用register_rest_route( 'namespace/v1', '/route', array( ... ) ) - 注意:对于公开的只读端点,
__return_true是正确的
命名空间和版本控制:
- 警告:命名空间中缺少版本号 → 没有 API 演进路径
- 模式:使用
myplugin/v1,而不只是myplugin
参数验证:
- 警告:参数中缺少
sanitize_callback→ 输入未经清理 - 警告:参数中缺少
validate_callback→ 接受无效输入 - 深入了解安全性:请参阅 wp-security-review 技能
AJAX 处理程序(PLG-14,表层检查)
钩子注册:
- 警告:对高频调用使用
admin-ajax.php→ 可考虑使用 REST API 以获得更好的性能 - 模式:对已验证身份的请求,使用
add_action( 'wp_ajax_action_name', 'callback' ) - 模式:对公开请求,使用
add_action( 'wp_ajax_nopriv_action_name', 'callback' )
安全检查:
- 简要提醒:AJAX 处理程序需要验证 nonce(
check_ajax_referer())并检查权限 - 深入了解安全性:请参阅 wp-security-review 技能
管理菜单(PLG-15,表层检查)
add_menu_page/add_submenu_page:
- 警告:缺少权限参数 → 没有访问控制
- 警告:页面回调未验证
current_user_can()→ 仅有权限参数还不够 - 模式:
add_menu_page( $title, $menu_title, 'manage_options', $slug, $callback )
数据库表(PLG-16,表层检查)
创建表:
- 严重:使用
$wpdb->query( "CREATE TABLE..." )而不是dbDelta()→ 没有升级路径,且可能出现字符集问题 - 模式:
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta( $sql ); - 模式:使用
$wpdb->prefix作为表名(不要硬编码wp_) - 模式:使用
$wpdb->get_charset_collate()设置字符集/排序规则
dbDelta SQL 格式要求:
- 警告:SQL 未按 dbDelta 的要求格式化 → 表创建会静默失败
- 要求:PRIMARY KEY 后有两个空格,= 两侧不加空格,每个字段单独占一行
Cron 任务(PLG-17,表层检查)
wp_schedule_event() 的用法:
- 警告:使用
wp_schedule_event()时未检查wp_next_scheduled()→ 产生重复的计划任务 - 警告:停用时未清除计划事件 → 插件停用后 Cron 任务仍会继续运行
- 模式:调用
wp_schedule_event()前,先检查if ( ! wp_next_scheduled( 'hook' ) ) - 模式:在停用钩子中调用
wp_clear_scheduled_hook( 'hook' )
Transient(PLG-18,表层检查)
set_transient() 的用法:
- 警告:Transient 键未添加前缀 → 与其他插件发生键冲突
- 警告:Transient 未设置过期时间 → 行为类似持久化选项
- 模式:
set_transient( 'mypl_key', $value, HOUR_IN_SECONDS )
快速检测的搜索模式(PLG-21)
使用这些 rg 命令快速扫描插件。按严重程度分类。
严重模式
# Main plugin file should contain "Plugin Name" header
# If this returns nothing, the plugin likely lacks a valid main header.
rg -n "Plugin Name:" . -g '*.php'
# PHP files missing ABSPATH check (direct file access possible)
rg -L --iglob '*.php' "defined\s*\(\s*['\"]ABSPATH['\"]\s*\)|defined\s*\(\s*'ABSPATH'\s*\)" .
# Unprefixed function declarations in global scope (namespace collision risk)
rg -n "^function\s+[a-z_][a-z0-9_]*\s*\(" . -g '*.php'
# Text domain mismatch (compare header vs i18n calls)
# First: inspect the main plugin file header for the expected text domain
rg -n "Text Domain:" . -g '*.php'
# Then: compare that slug with translation calls across the plugin
rg -n "__\(|_e\(|_n\(|_x\(" . -g '*.php'
# flush_rewrite_rules outside activation/deactivation (expensive operation on every page load)
rg -n "flush_rewrite_rules" . -g '*.php'
# Filter callbacks missing return statement (manual follow-up required)
rg -n "add_filter\s*\(" . -g '*.php'
# register_rest_route without permission_callback (manual per-route review)
rg -n "register_rest_route\s*\(" . -g '*.php'
警告模式
# Missing register_activation_hook (no setup mechanism)
rg -n "register_activation_hook\s*\(" . -g '*.php'
# Missing uninstall cleanup (no uninstall.php and no register_uninstall_hook)
test -f uninstall.php || rg -n "register_uninstall_hook\s*\(" . -g '*.php'
# i18n functions without obvious text domain parameter (manual confirmation required)
rg -n "__\(|_e\(" . -g '*.php'
# Hardcoded /wp-content/plugins/ paths (breaks on custom WordPress installations)
rg -n "/wp-content/plugins/" . -g '*.php'
# Direct CREATE TABLE with $wpdb->query (should use dbDelta)
rg -n "wpdb->query\s*\(.*CREATE TABLE" . -g '*.php'
# Post type key length check (max 20 characters)
rg -n "register_post_type\s*\(" . -g '*.php'
# Missing show_in_rest in register_post_type (manual per-registration review)
rg -n "register_post_type\s*\(" . -g '*.php'
信息模式
# Missing "Requires at least" or "Requires PHP" in plugin header
rg -n "Requires at least:|Requires PHP:" . -g '*.php'
# Functions not using PHP namespaces (could modernize)
rg -n "^function\s+myprefix_" . -g '*.php'
# Admin code loading on frontend (manual context check)
rg -n "add_action\s*\(\s*['\"]admin_" . -g '*.php'
# Missing Domain Path in plugin header
rg -n "Domain Path:" . -g '*.php'
平台上下文(PLG-12)
WordPress 插件的分发环境各不相同。请根据上下文调整审查严重程度。
WordPress.org 提交环境
适用最严格的标准。 Plugin Check(PCP)工具会强制执行以下要求:
Plugin Check(PCP)合规要求:
- 正确的插件头部信息(必须包含 Plugin Name,建议包含 Text Domain)
- Text Domain 必须与插件 slug 完全匹配(使用小写字母和连字符,不使用下划线)
- 所有 PHP 文件都必须检查 ABSPATH(
defined( 'ABSPATH' ) || exit;) - 所有 i18n 函数都必须包含 text domain 参数
- 不得使用已弃用的 WordPress 函数
- 许可证兼容性(WordPress.org 要求使用 GPLv2+)
- 使用格式正确的 readme.txt(或 readme.md)
- 不得硬编码
/wp-content/路径(请使用WP_CONTENT_DIR或辅助函数) - 所有全局作用域的函数、类和选项都必须使用前缀
验证方法: 提交前运行 Plugin Check 工具:
# Via WP-CLI
wp plugin install plugin-check --activate
wp plugin check my-plugin.php
# Or via WordPress Admin
# Navigate to: Plugins > Add New > Search "Plugin Check"
# Then: Tools > Plugin Check > Select your plugin
WordPress.org 环境下的严重程度升级:
- 缺少 text domain → 严重(原为警告)
- Text Domain 不匹配 → 严重(原为警告)
- 缺少 ABSPATH 检查 → 严重(原为警告)
- 使用已弃用的函数 → 严重(原为信息)
私有/企业插件环境
要求更灵活。 无需满足 WordPress.org 提交要求。
宽松要求:
- 不要求提供 readme.txt(可接受内部文档)
- 可使用专有许可证(无 GPL 要求)
- 命名规范更灵活(不过仍应使用前缀以避免冲突)
- 可使用自定义更新机制(无需使用 WordPress.org 更新系统)
仍需注意:
- 添加前缀以避免与其他插件冲突
- 使用生命周期钩子进行正确的设置和清理
- 遵循安全最佳实践(参见 wp-security-review)
必须使用的插件(mu-plugins)环境
加载行为不同。 必须使用的插件会在常规插件之前自动加载。
主要区别:
- 从
wp-content/mu-plugins/目录按字母顺序加载 - 在常规插件之前加载
- 无法通过管理界面停用
- 没有激活/停用钩子(它们不会激活或停用,只会加载)
- 无法通过 WordPress 管理界面更新(需手动替换文件)
针对 mu-plugins 的审查调整:
- 警告(而非严重):缺少
register_activation_hook()→ mu-plugins 的预期情况 - 警告(而非严重):缺少
register_deactivation_hook()→ mu-plugins 的预期情况 - 仍需检查:正确使用前缀、ABSPATH 检查、i18n 和安全实践
检测 mu-plugin 环境的模式:
// Check if plugin is loaded as must-use
if ( strpos( __FILE__, WPMU_PLUGIN_DIR ) !== false ) {
// Running as must-use plugin - skip activation hooks
}
插件插入文件上下文
具有特定名称的特殊文件。 插入文件会替代 WordPress 核心功能。
常见插入文件:
object-cache.php→ 对象缓存后端(Redis、Memcached)db.php→ 自定义数据库类advanced-cache.php→ 页面缓存系统db-error.php→ 自定义数据库错误页面maintenance.php→ 自定义维护模式页面
插入文件的审核调整:
- 跳过标准插件标头检查(插入文件不使用插件标头)
- 跳过激活钩子检查(插入文件会自动加载)
- 跳过文本域检查(通常供内部使用,不面向用户)
- 重点关注:与 WordPress 核心对该类型插入文件的预期是否兼容
检测插入文件的模式:
// Drop-ins are in wp-content/ root, not plugins/
// Check filename against known drop-in names
$drop_ins = array( 'object-cache.php', 'db.php', 'advanced-cache.php', 'db-error.php', 'maintenance.php' );
快速参考:插件架构模式
按关注点整理的常见插件模式。所有示例均遵循 WordPress PHP 编码规范(括号内加空格,使用 array() 而非 [],采用 Yoda 条件)。
插件标头
包含所有推荐字段的完整标头块:
❌ BAD:最简标头
<?php
/**
* Plugin Name: My Plugin
*/
✅ GOOD:包含所有推荐字段的完整标头
<?php
/**
* Plugin Name: My Awesome Plugin
* Plugin URI: https://example.com/my-awesome-plugin
* Description: Does amazing things with WordPress
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: John Doe
* Author URI: https://example.com
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-awesome-plugin
* Domain Path: /languages
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
前缀和命名空间
避免污染全局命名空间:
❌ BAD:没有前缀(存在命名空间冲突风险)
<?php
function save_settings() {
update_option( 'plugin_settings', $_POST['data'] );
}
class Plugin_Settings {
// ...
}
add_action( 'admin_init', 'save_settings' );
✅ GOOD:使用带前缀的函数和类
<?php
function mypl_save_settings() {
update_option( 'mypl_settings', $_POST['data'] );
}
class MyPL_Settings {
// ...
}
add_action( 'admin_init', 'mypl_save_settings' );
✅ BETTER:使用 PHP 命名空间
<?php
namespace MyPlugin\Admin;
function save_settings() {
update_option( 'mypl_settings', $_POST['data'] ); // Still prefix option names
}
class Settings {
// ...
}
add_action( 'admin_init', __NAMESPACE__ . '\save_settings' );
激活/停用/卸载生命周期
妥善管理插件生命周期:
❌ BAD:没有生命周期钩子
<?php
// Plugin registers CPT on init but never flushes rewrites
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* args */ ) );
}
✅ GOOD:包含激活、停用和卸载的完整生命周期
<?php
// Main plugin file: my-plugin.php
// Activation hook
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_activate() {
// Set default options
add_option( 'mypl_version', '1.0.0' );
// Register CPT before flushing
mypl_register_cpt();
// Flush rewrite rules ONLY on activation
flush_rewrite_rules();
}
// Deactivation hook
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
function mypl_deactivate() {
// Clear transients
delete_transient( 'mypl_cache' );
// Flush rewrite rules
flush_rewrite_rules();
}
// Init hook for normal operation
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* args */ ) );
}
// Uninstall file: uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete all plugin data
delete_option( 'mypl_version' );
delete_option( 'mypl_settings' );
// Drop custom table
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}mypl_data" );
自定义文章类型和分类法
正确注册 CPT/分类法并管理重写规则:
❌ BAD:每次页面加载时都刷新重写规则
<?php
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'book', array( // Missing prefix, no show_in_rest
'public' => true,
) );
flush_rewrite_rules(); // CRITICAL: Runs on EVERY page load
}
✅ GOOD:使用前缀、支持 Gutenberg,仅在激活时刷新
<?php
add_action( 'init', 'mypl_register_cpt_and_taxonomy' );
function mypl_register_cpt_and_taxonomy() {
// Register custom post type (max 20 chars, prefixed)
register_post_type(
'mypl_book',
array(
'labels' => array(
'name' => __( 'Books', 'my-plugin' ),
'singular_name' => __( 'Book', 'my-plugin' ),
),
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'books' ),
'supports' => array( 'title', 'editor', 'thumbnail' ),
'show_in_rest' => true, // Gutenberg support
)
);
// Register custom taxonomy (max 32 chars, prefixed)
register_taxonomy(
'mypl_genre',
'mypl_book',
array(
'labels' => array(
'name' => __( 'Genres', 'my-plugin' ),
'singular_name' => __( 'Genre', 'my-plugin' ),
),
'hierarchical' => true,
'public' => true,
'show_in_rest' => true, // Gutenberg support
)
);
}
// Flush rewrites ONLY on activation
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_activate() {
mypl_register_cpt_and_taxonomy(); // Register first
flush_rewrite_rules(); // Then flush (only once)
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
function mypl_deactivate() {
flush_rewrite_rules(); // Clean up rewrite rules
}
Settings API
完整的 Settings API 工作流:
❌ BAD:调用 add_settings_field 但未调用 register_setting(无法保存)
<?php
add_action( 'admin_init', 'mypl_settings_init' );
function mypl_settings_init() {
add_settings_section( 'mypl_section', 'Settings', null, 'mypl-settings' );
add_settings_field(
'mypl_api_key',
'API Key',
'mypl_api_key_callback',
'mypl-settings',
'mypl_section'
);
// Missing register_setting() - field won't save!
}
✅ GOOD:完整的 Settings API 工作流
<?php
add_action( 'admin_init', 'mypl_register_settings' );
function mypl_register_settings() {
// 1. Register setting (declares option name and sanitization)
register_setting(
'mypl_options_group', // Option group
'mypl_settings', // Option name
array(
'type' => 'array',
'sanitize_callback' => 'mypl_sanitize_settings',
'default' => array(
'api_key' => '',
'enabled' => false,
),
)
);
// 2. Add settings section
add_settings_section(
'mypl_main_section',
__( 'Main Settings', 'my-plugin' ),
'mypl_section_callback',
'mypl-settings'
);
// 3. Add settings fields
add_settings_field(
'mypl_api_key',
__( 'API Key', 'my-plugin' ),
'mypl_api_key_callback',
'mypl-settings',
'mypl_main_section'
);
add_settings_field(
'mypl_enabled',
__( 'Enable Feature', 'my-plugin' ),
'mypl_enabled_callback',
'mypl-settings',
'mypl_main_section'
);
}
function mypl_section_callback() {
echo '<p>' . esc_html__( 'Configure your plugin settings below.', 'my-plugin' ) . '</p>';
}
function mypl_api_key_callback() {
$options = get_option( 'mypl_settings' );
$value = isset( $options['api_key'] ) ? $options['api_key'] : '';
?>
<input type="text"
name="mypl_settings[api_key]"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text">
<?php
}
function mypl_enabled_callback() {
$options = get_option( 'mypl_settings' );
$checked = isset( $options['enabled'] ) && $options['enabled'];
?>
<input type="checkbox"
name="mypl_settings[enabled]"
value="1"
<?php checked( $checked, true ); ?>>
<?php
}
function mypl_sanitize_settings( $input ) {
$sanitized = array();
if ( isset( $input['api_key'] ) ) {
$sanitized['api_key'] = sanitize_text_field( $input['api_key'] );
}
$sanitized['enabled'] = isset( $input['enabled'] ) && $input['enabled'] ? true : false;
return $sanitized;
}
// Settings page HTML
function mypl_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form method="post" action="options.php">
<?php
// Output nonce, action, option_page fields
settings_fields( 'mypl_options_group' );
// Output sections and fields
do_settings_sections( 'mypl-settings' );
submit_button();
?>
</form>
</div>
<?php
}
钩子系统
管理优先级的操作和筛选器:
❌ BAD:筛选器没有 return 语句(导致致命错误)
<?php
add_filter( 'the_content', 'mypl_modify_content' );
function mypl_modify_content( $content ) {
if ( is_single() ) {
$content .= '<p>Footer text</p>';
}
// Missing return! Fatal error.
}
✅ GOOD:完整使用钩子、优先级和返回值
<?php
// Action with priority (runs before core at priority 10)
add_action( 'init', 'mypl_register_cpt', 9 );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* args */ ) );
}
// Filter with return (CRITICAL)
add_filter( 'the_content', 'mypl_modify_content', 10, 1 );
function mypl_modify_content( $content ) {
if ( is_single() ) {
$content .= '<p>Footer text</p>';
}
return $content; // ALWAYS return in filters
}
// Multiple parameters with accepted_args
add_filter( 'wp_mail', 'mypl_modify_email', 10, 1 );
function mypl_modify_email( $args ) {
$args['from'] = 'Custom <[email protected]>';
return $args;
}
// Removing hooks (MUST match exact registration params)
add_action( 'wp_footer', 'mypl_footer_code', 15 );
// To remove: priority must match
remove_action( 'wp_footer', 'mypl_footer_code', 15 );
// Create custom hooks for extensibility
function mypl_process_data( $data ) {
// Allow other plugins to modify data
$data = apply_filters( 'mypl_before_process', $data );
// Process data
$result = process( $data );
// Allow other plugins to hook after processing
do_action( 'mypl_after_process', $result );
return $result;
}
国际化
匹配文本域并使用占位符:
❌ BAD:将变量嵌入可翻译字符串
<?php
$count = 5;
echo __( "You have $count messages", 'my-plugin' ); // Breaks translation extraction
// Missing text domain
echo __( 'Save Settings' );
// Text domain mismatch
// Plugin folder: my-awesome-plugin
echo __( 'Save', 'my_plugin' ); // Underscore instead of dash
✅ GOOD:使用 sprintf 占位符和正确的文本域
<?php
// Basic translation with text domain
echo __( 'Settings saved successfully', 'my-awesome-plugin' );
// Translation with echo
_e( 'Click here to continue', 'my-awesome-plugin' );
// Placeholders with sprintf (NOT embedded variables)
$count = 5;
echo sprintf(
__( 'You have %d messages', 'my-awesome-plugin' ),
$count
);
// Pluralization with _n()
echo sprintf(
_n(
'One post found',
'%d posts found',
$count,
'my-awesome-plugin'
),
number_format_i18n( $count )
);
// Context-aware translation with _x()
echo _x( 'Post', 'noun - blog post', 'my-awesome-plugin' );
echo _x( 'Post', 'verb - submit', 'my-awesome-plugin' );
// Escaped output + translation
echo '<h1>' . esc_html__( 'Welcome', 'my-awesome-plugin' ) . '</h1>';
echo '<input placeholder="' . esc_attr__( 'Enter your name', 'my-awesome-plugin' ) . '">';
// Load text domain on init (optional in WP 4.6+)
add_action( 'init', 'mypl_load_textdomain' );
function mypl_load_textdomain() {
load_plugin_textdomain(
'my-awesome-plugin',
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages'
);
}
REST API 注册(概览)
❌ BAD:缺少 permission_callback(WordPress 5.5+ 会报错)
<?php
add_action( 'rest_api_init', 'mypl_register_routes' );
function mypl_register_routes() {
register_rest_route( 'mypl/v1', '/posts', array(
'methods' => 'GET',
'callback' => 'mypl_get_posts',
// Missing: 'permission_callback' (WordPress 5.5+ blocks this)
) );
}
✅ GOOD:包含 permission_callback 和验证的完整 REST 端点
<?php
add_action( 'rest_api_init', 'mypl_register_routes' );
function mypl_register_routes() {
// Public read-only endpoint
register_rest_route(
'mypl/v1', // Namespace with version
'/posts',
array(
'methods' => 'GET',
'callback' => 'mypl_get_posts',
'permission_callback' => '__return_true', // Explicitly public (OK for read-only)
'args' => array(
'per_page' => array(
'default' => 10,
'sanitize_callback' => 'absint',
'validate_callback' => function( $param ) {
return is_numeric( $param ) && $param > 0 && $param <= 100;
},
),
),
)
);
// Protected write endpoint
register_rest_route(
'mypl/v1',
'/posts/(?P<id>\d+)',
array(
'methods' => 'DELETE',
'callback' => 'mypl_delete_post',
'permission_callback' => function() {
return current_user_can( 'delete_posts' );
},
'args' => array(
'id' => array(
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
),
),
)
);
}
function mypl_get_posts( $request ) {
$per_page = $request->get_param( 'per_page' );
$posts = get_posts( array( 'posts_per_page' => $per_page ) );
return rest_ensure_response( $posts );
}
function mypl_delete_post( $request ) {
$post_id = $request->get_param( 'id' );
$result = wp_delete_post( $post_id, true );
if ( ! $result ) {
return new WP_Error(
'delete_failed',
__( 'Failed to delete post', 'my-plugin' ),
array( 'status' => 500 )
);
}
return rest_ensure_response( array( 'deleted' => true, 'id' => $post_id ) );
}
注意: 有关 REST 端点的安全细节(nonce 验证、能力检查、输入清理),请参阅 wp-security-review skill。
AJAX 处理程序(概览)
✅ GOOD:注册 AJAX 处理程序
<?php
// Authenticated users
add_action( 'wp_ajax_mypl_save_data', 'mypl_save_data_callback' );
// Public (non-authenticated)
add_action( 'wp_ajax_nopriv_mypl_save_data', 'mypl_save_data_callback' );
function mypl_save_data_callback() {
// Security checks (see wp-security-review for depth)
check_ajax_referer( 'mypl_nonce', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( 'Insufficient permissions' );
}
$data = sanitize_text_field( $_POST['data'] );
// Process data
update_option( 'mypl_data', $data );
wp_send_json_success( 'Data saved' );
}
条件加载(PLG-20)
将管理端代码与公共端代码分开:
❌ BAD:每个页面都加载管理端代码
<?php
add_action( 'wp_enqueue_scripts', 'mypl_enqueue_admin_assets' );
function mypl_enqueue_admin_assets() {
// Admin assets loading on public pages (unnecessary)
wp_enqueue_script( 'mypl-admin', plugin_dir_url( __FILE__ ) . 'admin.js' );
}
✅ GOOD:按条件加载管理端代码
<?php
// Only load admin code in admin context
if ( is_admin() ) {
require_once plugin_dir_path( __FILE__ ) . 'admin/class-admin.php';
}
// Admin-specific assets
add_action( 'admin_enqueue_scripts', 'mypl_enqueue_admin_assets' );
function mypl_enqueue_admin_assets() {
wp_enqueue_script(
'mypl-admin',
plugin_dir_url( __FILE__ ) . 'admin/admin.js',
array( 'jquery' ),
'1.0.0',
true
);
}
// Public-facing assets
add_action( 'wp_enqueue_scripts', 'mypl_enqueue_public_assets' );
function mypl_enqueue_public_assets() {
wp_enqueue_script(
'mypl-public',
plugin_dir_url( __FILE__ ) . 'public/public.js',
array( 'jquery' ),
'1.0.0',
true
);
}
函数/类存在性检查(PLG-19)
避免冲突导致致命错误:
✅ GOOD:检查函数和类是否存在
<?php
// Function existence check
if ( ! function_exists( 'mypl_init' ) ) {
function mypl_init() {
// Plugin initialization
}
}
// Class existence check
if ( ! class_exists( 'MyPL_Plugin' ) ) {
class MyPL_Plugin {
// Plugin class
}
}
严重性定义(PLG-23)
| 严重性 | 定义 | 示例 |
|---|---|---|
| CRITICAL | 会导致 WordPress.org 拒绝,或破坏插件功能,或造成致命错误 | 缺少 Plugin Name 标头、未加前缀的全局函数导致命名空间冲突、文本域不匹配、缺少 ABSPATH 检查、未使用 dbDelta 直接创建数据库表、筛选器回调缺少 return 语句、自定义文章类型键超过 20 个字符、分类法名称超过 32 个字符、在 init 钩子上调用 flush_rewrite_rules()、REST 路由没有 permission_callback(WP 5.5+) |
| WARNING | 不标准的模式,会导致可维护性或兼容性问题,但插件仍可正常运行 | 缺少国际化(i18n)、__() 未指定文本域、硬编码 /wp-content/ 路径、register_setting() 缺少 sanitize_callback、缺少 show_in_rest,无法支持 Gutenberg、调用 wp_schedule_event() 前未检查是否已存在计划任务、transient 未设置过期时间 |
| INFO | 改进最佳实践和实现现代化的机会 | 可以使用 PHP 命名空间而不是添加前缀、标头中缺少版本要求(Requires at least、Requires PHP)、管理后台代码在前端加载、标头中缺少 Domain Path、可以使用 _x() 进行上下文感知的翻译 |
输出格式(PLG-23)
按文件归类报告问题,并标注行号和严重性标签。每个问题都使用 BAD/GOOD 代码对。
# WordPress Plugin Review: my-awesome-plugin
## FILE: my-awesome-plugin.php
### Line 15: CRITICAL - Missing ABSPATH check
Direct file access possible. Add ABSPATH check at top of all PHP files.
❌ BAD:
<?php
/**
* Plugin Name: My Plugin
*/
function mypl_init() { ... }
✅ GOOD:
<?php
/**
* Plugin Name: My Plugin
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function mypl_init() { ... }
### Line 45: WARNING - Missing text domain in i18n call
Translation won't work. Always include text domain parameter.
❌ BAD:
echo __( 'Save Settings' );
✅ GOOD:
echo __( 'Save Settings', 'my-awesome-plugin' );
## FILE: includes/class-cpt.php
### Line 23: CRITICAL - flush_rewrite_rules on init hook
Expensive DB write on every page load. Move to activation hook.
❌ BAD:
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'book', array( /* ... */ ) );
flush_rewrite_rules(); // Runs on EVERY request
}
✅ GOOD:
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'mypl_book', array( /* ... */ ) );
}
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_activate() {
mypl_register_cpt();
flush_rewrite_rules(); // Only on activation
}
## SUMMARY
**Total issues: 15**
- CRITICAL: 4 (must fix for WordPress.org submission)
- WARNING: 8 (non-standard patterns)
- INFO: 3 (best practice improvements)
**WordPress.org readiness:** NOT READY - 4 critical issues block submission
**Security note:** This review focused on plugin architecture and WordPress.org compliance. Security issues detected (missing nonce in AJAX handler, unsanitized input in form). Run `/wp-sec-review` for comprehensive security analysis.
常见误判(PLG-24)
以下模式看似有问题,但实际上没有问题:
| 模式 | 为什么这不是问题 | 上下文 |
|---|---|---|
| 没有激活钩子的 mu-plugin | 必须使用的插件会自动加载,无需激活或停用 | wp-content/mu-plugins/ 中的 mu-plugin 通常如此 |
| 没有标准标头的 Drop-in 插件 | Drop-in 使用特殊文件名,而不是插件标头 | object-cache.php、db.php、advanced-cache.php 等均属有效情况 |
| 没有 readme.txt 的私有插件 | 只有提交到 WordPress.org 时才需要 readme.txt | 私有/企业插件使用内部文档 |
GET 请求的 REST permission_callback 使用 __return_true | 公开的只读端点不需要身份验证 | 用于公开 REST API 读取操作的正确模式 |
| Settings API 的 sanitize_callback 更改类型 | 可以将复选框字符串转换为布尔值、将文本转换为数组等 | 这是有效的转换,不是错误 |
| WordPress 核心字符串没有文本域 | 核心字符串已经翻译 | 将字符串传递给 wp_die()、将核心字符串传递给 __() 时,这是有效做法 |
| 使用命名空间的短前缀(3 个字符) | PHP 命名空间可防止命名冲突 | 使用 namespace MyPlugin\Feature; 时可以接受 |
| 插件没有设置流程时不注册激活钩子 | 并非所有插件都需要数据库表、选项或刷新重写规则 | 简单插件(仅使用短代码、筛选器)可以不注册 |
在管理设置保存时调用 flush_rewrite_rules() | 用户明确更改固定链接结构时可以接受 | 在 slug/rewrite 发生更改的设置页面中有效 |
直接调用 $wpdb->query() 执行 DROP TABLE | dbDelta() 用于 CREATE/ALTER,而非 DROP | 在 uninstall.php 中使用是正确模式 |
| 内部调试字符串缺少 i18n | 面向开发者的字符串无需翻译 | 用于 error_log()、内部注释时有效 |
register_taxonomy() 未设置 show_in_rest | 并非所有分类法都需要 Gutenberg 界面 | 仅用于后台的分类法可以不设置 |
WordPress.org 合规检查清单(PLG-12)
提交到 WordPress.org 之前,快速检查是否符合 Plugin Check 标准:
- 主插件文件中包含 Plugin Name 标头
- Text Domain 与插件 slug 完全一致(使用小写字母和连字符,不使用下划线)
- 所有 PHP 文件均检查 ABSPATH(
defined( 'ABSPATH' ) || exit;) - 所有 i18n 函数均包含 text domain 参数
- 未使用已弃用的 WordPress 函数
- 包含许可证标头(建议使用 GPLv2 或更高版本)
- 包含格式正确的 readme.txt(或 readme.md)
- 不含硬编码的
/wp-content/或/wp-content/plugins/路径 - 所有全局作用域的函数、类、选项和临时数据均使用适当的前缀
- 包含 uninstall.php 或 register_uninstall_hook,以便清理
- 不含
eval()、base64_decode()或混淆代码 - 已指定版本要求(
Requires at least、Requires PHP)
验证命令:
wp plugin install plugin-check --activate
wp plugin check my-plugin.php
深入参考资料
如需了解高级插件开发模式,请加载以下配套参考文档:
| 任务 | 要加载的参考资料 |
|---|---|
| 插件文件结构、单例模式、自动加载、依赖注入、主插件类架构 | references/architecture-patterns.md |
| Action/filter 生命周期、优先级系统、钩子移除、自定义钩子、可插拔函数、钩子命名约定 | references/hooks-guide.md |
| 基于类的插件架构、PHP 命名空间、PSR-4 自动加载、trait 用法、抽象类、服务容器 | references/oop-patterns.md |
| Settings API 工作流、Options API 最佳实践、REST API 模式验证、Transients API、使用 dbDelta 创建自定义数据库表 | references/api-patterns.md |
**注意:**参考文档提供深入内容。对于标准插件审查,本 SKILL.md 已足够完整。
**安全交叉参考:**遇到与安全相关的模式(未使用 nonce 的表单处理程序、未进行能力检查的 AJAX、未验证权限的 REST 端点)时,本技能会简要提醒,但全面的安全分析请参阅 wp-security-review。三步安全模式(nonce + capability + sanitize)适用于所有会改变状态的操作。使用 /wp-sec-review 命令进行详细安全审计。