mcpField
W
wp-plugin-development

wp-plugin-development

github.com/jorgerosal开发11361 次安装21 周前更新
代码仓库

关于 wp-plugin-development

面向 WordPress 插件开发者的架构审查技能,可用于评估代码结构及 WordPress.org 发布准备情况。它检查生命周期钩子、设置、扩展机制、命名和国际化,并将安全专项审查交由独立技能处理。

主要功能

  1. 01审查插件元数据与架构
  2. 02检查生命周期钩子和清理
  3. 03检查设置与钩子用法
  4. 04评估命名和国际化
  5. 05按文件与严重程度整理发现

适用场景

  1. 01准备 WordPress.org 插件审查
  2. 02审计启用与卸载行为
  3. 03检查自定义内容类型和钩子
  4. 04核对插件国际化

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jorgerosal/wordpress-skills --skill wp-plugin-development

安装后在 Claude Code 中输入/wp-plugin-development确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
api-patterns.md
architecture-patterns.md
hooks-guide.md
oop-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)

代码审查工作流

按照以下六个步骤系统地审查插件:

  1. 识别插件类型和上下文

    • 提交到 WordPress.org → 应用最严格的标准(符合 Plugin Check 要求)
    • 私有/企业插件 → 命名方面更灵活,无需 readme.txt
    • 必须使用的插件(mu-plugin)→ 加载方式不同(无需激活钩子)
    • Drop-in 插件 → 使用特殊文件(object-cache.php、db.php),适用不同模式
  2. 检查插件头信息的完整性和元数据

    • 必需:包含 Plugin Name 字段
    • 建议:版本、描述、作者、文本域、最低要求版本、PHP 要求、许可证
    • 确认文本域与插件 slug 完全一致(使用小写字母和连字符,不使用下划线)
  3. 扫描 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 个字符
  4. 检查 WARNING 模式(不符合规范但仍可运行)

    • 面向用户的字符串缺少国际化(i18n)
    • 调用 __() 或 _e() 时未提供文本域参数
    • 硬编码的 WordPress 路径(/wp-content/plugins/)
    • register_setting() 中缺少 sanitize_callback
    • 缺少 show_in_rest,影响 Gutenberg 兼容性
    • 管理后台代码在前端加载(缺少 is_admin() 检查)
  5. 记录 INFO 改进项(最佳实践和优化建议)

    • 可以使用 PHP 命名空间,而不是添加前缀
    • 插件头信息中缺少版本要求(Requires at least、Requires PHP)
    • 可以添加 show_in_rest,以支持 Block Editor
    • 管理后台资源全局入队(应按条件加载)
    • 头信息中缺少 Domain Path
  6. 根据上下文调整严重级别并生成报告

    • 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 TABLEdbDelta() 用于 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 命令进行详细安全审计。

评论

支持 Markdown