EP03. "wp-plugin-development 插件架构审查"
🔒 登录后可标记已读- 插件头信息漏写 Text Domain、函数名没加前缀、CPT 注册在
init里每次都flush_rewrite_rules()——这些坑要么让 WordPress.org 直接拒审,要么在别人服务器上跟其他插件打架,wp-plugin-development就是揪这些 - 跟
wp-security-review关注漏洞、wp-performance-review关注速度不同,这个 skill 专盯插件本身的架构:生命周期钩子、CPT/taxonomy 注册、Settings API、hooks 设计、国际化、WordPress.org 上架合规 - 六步流程:先判断插件类型和场景(WordPress.org 提交 / 私有插件 / mu-plugin / drop-in,各自标准不同),再依序抓 CRITICAL → WARNING → INFO
- 前置知识:先看过 Overview 系列「Claude Code 是什么与新手上手」「进阶功能:Skills、Plugins 与 Routines」,以及 EP01「wp-security-review 安全审查」了解这套 skill 包的调用方式
重点内容
这个 Skill 是做什么的
系统审查 WordPress 插件架构是否符合规范:插件头信息完整度、ABSPATH 检查、函数/类前缀、激活/停用/卸载生命周期、CPT 与 taxonomy 注册、Settings API 完整流程、hooks 系统(action/filter 的 priority、返回值、移除)、国际化(i18n/l10n)、WordPress.org 提交合规(Plugin Check)。跟安全相关的具体做法(nonce、sanitize)它只是提醒一句,深挖交给 wp-security-review。
适合用在:
- 插件代码架构审查
- WordPress.org 插件目录提交前检查
- 审查激活/停用/卸载生命周期钩子
- 审查自定义 post type 或 taxonomy 注册
- 校验 Settings API 实现
- 检查 hooks 系统用法(action/filter 的 priority、移除)
- 国际化(i18n/l10n)审计
- WordPress Plugin Check(PCP)合规验证
- 前缀/命名空间冲突检测
不适合用在:
- 主题开发(交给
wp-theme-development) - Gutenberg block 开发(交给
wp-block-development) - WooCommerce 扩展开发
- 纯安全审查(交给
wp-security-review) - 纯性能审查(交给
wp-performance-review)
触发方式
| 方式 | 写法 | 说明 |
|---|---|---|
| 斜线指令(完整版) | /wp-plugin-review [path] | 完整审查,按文件分组输出,附 BAD/GOOD 代码对照 |
| 斜线指令(快速版) | /wp-plugin [path] | 只抓关键架构问题,适合日常小改动后快速过一遍 |
| 自然语言 | 「审查这个插件的架构」「check this plugin for WordPress.org submission」 | Claude 判断意图符合就自动触发 |
场景决定标准松紧
同一个反模式,在不同发行场景下的严重度不一样:
| 场景 | 判断标准 |
|---|---|
| WordPress.org 提交 | 最严格,Plugin Check 工具会强制这些要求:Text Domain 必须跟插件 slug 完全一致(小写、连字符不能用底线)、所有 PHP 文件都要有 ABSPATH 检查、所有 i18n 函数必须带 text domain 参数、不能用已弃用函数、必须 GPLv2+ 授权、要有 readme.txt |
| 私有 / 企业内部插件 | 更灵活,不强制 readme.txt,可以用私有授权协议,命名可以不那么严格(但前缀还是要有,避免跟其他插件冲突) |
| Must-use 插件(mu-plugin) | 自动加载、不能通过后台停用,没有激活/停用钩子(因为它们不走激活/停用流程),这不是缺陷 |
Drop-in 插件(如 object-cache.php、db.php) | 用特定文件名替换 WordPress 核心功能,不用标准插件头信息,不用激活钩子,审查重点是跟核心预期行为的兼容性 |
实操示例
// ❌ CRITICAL:没有前缀,跟其他插件命名冲突风险高
function save_settings() {
update_option( 'plugin_settings', $_POST['data'] );
}
class Plugin_Settings { /* ... */ }
add_action( 'admin_init', 'save_settings' );
// ✅ GOOD:函数/类都加前缀
function mypl_save_settings() {
update_option( 'mypl_settings', $_POST['data'] );
}
class MyPL_Settings { /* ... */ }
add_action( 'admin_init', 'mypl_save_settings' );
// ✅ BETTER:直接用 PHP namespace(选项名还是要前缀)
namespace MyPlugin\Admin;
function save_settings() {
update_option( 'mypl_settings', $_POST['data'] );
}
add_action( 'admin_init', __NAMESPACE__ . '\save_settings' );
// ❌ CRITICAL:CPT 注册在 init 钩子里,flush_rewrite_rules() 每次页面加载都跑一遍
add_action( 'init', 'mypl_register_cpt' );
function mypl_register_cpt() {
register_post_type( 'book', array( 'public' => true ) ); // 没前缀,没 show_in_rest
flush_rewrite_rules(); // 每个请求都会跑,非常昂贵
}
// ✅ GOOD:CPT 注册照常挂 init,flush_rewrite_rules() 只在激活钩子跑一次
add_action( 'init', 'mypl_register_cpt_and_taxonomy' );
function mypl_register_cpt_and_taxonomy() {
register_post_type( 'mypl_book', array(
'public' => true,
'has_archive' => true,
'show_in_rest' => true, // Gutenberg 支持
) );
}
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_activate() {
mypl_register_cpt_and_taxonomy(); // 先注册
flush_rewrite_rules(); // 只在激活时跑一次
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
function mypl_deactivate() {
flush_rewrite_rules();
}
// ❌ CRITICAL:filter 回调没有 return,直接破坏整条 filter 链、导致致命错误
add_filter( 'the_content', 'mypl_modify_content' );
function mypl_modify_content( $content ) {
if ( is_single() ) {
$content .= '<p>Footer text</p>';
}
// 漏了 return!
}
// ✅ GOOD:filter 回调永远要 return
add_filter( 'the_content', 'mypl_modify_content', 10, 1 );
function mypl_modify_content( $content ) {
if ( is_single() ) {
$content .= '<p>Footer text</p>';
}
return $content; // 永远要 return
}
// ❌ CRITICAL:变量直接内插进可翻译字符串,破坏翻译提取
$count = 5;
echo __( "You have $count messages", 'my-plugin' );
echo __( 'Save Settings' ); // 漏了 text domain
echo __( 'Save', 'my_plugin' ); // 底线而不是连字符,跟插件 slug 不一致
// ✅ GOOD:用 sprintf 传占位符,text domain 跟插件 slug 完全对齐
$count = 5;
echo sprintf(
__( 'You have %d messages', 'my-awesome-plugin' ),
$count
);
echo sprintf(
_n( 'One post found', '%d posts found', $count, 'my-awesome-plugin' ),
number_format_i18n( $count )
);
怎么安装
这个 skill 是 wordpress-skills 这个开源仓库(jorgerosal/wordpress-skills)打包的 18 个技能之一,安装一次,18 个技能一起到位,不用逐个装:
Claude Code:
| 方式 | 指令 | 适用场景 |
|---|---|---|
| 装进单个项目(推荐) | git submodule add https://github.com/jorgerosal/wordpress-skills.git .claude/plugins/wordpress-skills | 只想在这个项目用,团队成员 clone 项目就一起有 |
| 装到自己账号 | git clone https://github.com/jorgerosal/wordpress-skills.git ~/.claude/plugins/wordpress-skills | 所有项目都能用 |
| 只装这一个 skill | cp -r claude-skills/wp-plugin-development ~/.claude/skills/ | 只想要插件架构审查这一个功能,不要其他 17 个 |
装完重启 Claude Code,进到一个 WordPress 项目里跑 /wordpress-skills:wp-plugin-review 验证有没有装成功(用 marketplace/submodule 方式装的话,指令前面会带插件命名空间 wordpress-skills:;用「只装这一个 skill」的方式则不带命名空间,直接 /wp-plugin-review)。
Claude Desktop / claude.ai: 走 Settings → Capabilities → Skills,上传技能文件夹(把 claude-skills/wp-plugin-development 这个文件夹打包上传,里面要包含 SKILL.md)——跟 Claude Code 的 git submodule/marketplace 安装方式不同,Desktop 端是手动上传 UI,装好之后同样能用自然语言或 slash 指令触发。
常见错误
- ❌ mu-plugin 没有激活/停用钩子就被当成缺陷打回去——must-use 插件本来就自动加载,不走激活/停用流程,这是正常的
- ❌ drop-in 插件(
object-cache.php等)没有标准插件头信息也被扣分——drop-in 用特定文件名替换核心功能,不需要标准头信息 - ❌ 私有/企业插件没有 readme.txt 也被要求补——readme.txt 只有 WordPress.org 提交才强制要求
- ❌ REST API 的 GET 端点用
__return_true被当成漏洞——公开只读端点故意开放是正确写法,只有写操作用__return_true才该报警 - ❌ Settings API 的
sanitize_callback把 checkbox 字符串转成布尔值、文字转数组,被当成 bug——这是合法的类型转换 - ❌
uninstall.php里直接用$wpdb->query()执行DROP TABLE被要求改成dbDelta()——dbDelta()是给 CREATE/ALTER 用的,DROP 用原生$wpdb->query()才是对的写法 - 💡 判断严重度前先确认插件的分发场景(WordPress.org / 私有 / mu-plugin / drop-in),同一个模式在不同场景下标准完全不同
Sources
官方文档:
- wordpress-skills(GitHub 仓库)— https://github.com/jorgerosal/wordpress-skills
- wp-plugin-development SKILL.md — https://github.com/jorgerosal/wordpress-skills/blob/main/claude-skills/wp-plugin-development/SKILL.md