本文约 5,090 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:插件不被识别,问题往往出在头部
第一次写 WordPress 插件的人,最常见的第一步失败是:把 PHP 文件上传到 wp-content/plugins/,后台插件列表里却什么都没有。检查权限、检查文件名、检查编码,全都正常。
原因几乎总是同一个:主文件缺少规范的头部注释。WordPress 识别插件的方式,是扫描插件目录下的 PHP 文件,读取文件开头的 PHPDoc 注释块,从中提取 Plugin Name: 这一行。没有这一行,这个文件在 WordPress 眼里就不是插件,不管里面的代码写得有多正确。
而且头部注释不只是「让插件被识别」这么简单。它同时是版本管理、翻译加载、兼容性检查、更新机制的数据来源。比如 Text Domain: 写错了,翻译文件永远加载不了;Requires PHP: 没写,用户在 PHP 7.2 上装了你的插件会得到一堆致命错误而不是一句友好提示。
本文逐个讲清每个字段的作用、哪些是必填、哪些容易写错,并给出推荐的目录结构。
原理分析:WordPress 怎么读取插件头部
WordPress 用 get_plugin_data()(定义在 wp-admin/includes/plugin.php)解析插件主文件。它的行为有几个关键细节:
细节一:只读文件开头的一部分。 默认读取前 8KB 内容(早期版本是第一个 8KB),从中提取注释块。所以头部注释必须放在文件最开始,前面不能有别的 PHP 代码。
细节二:只认 PHPDoc 风格。 必须是 /** ... */ 这种块注释,且每行以 字段名: 值 的格式书写。用 // 行注释无效。
细节三:字段顺序无所谓,字段名大小写不敏感。 但惯例是固定的顺序,遵守惯例便于别人阅读。
细节四:只有主文件的头部会被读取。 一个插件可以有几十个 PHP 文件,但只有一个文件的头部会被解析成插件元信息。WordPress 决定「哪个是主文件」的规则是:优先取插件根目录下与主目录同名的 PHP 文件,否则取根目录下的第一个 PHP 文件。
完整字段清单:
| 字段 | 必填 | 作用 |
|---|---|---|
Plugin Name |
是 | 后台显示的名称,也决定插件是否被识别 |
Plugin URI |
否 | 插件主页,后台会显示为链接 |
Version |
强烈建议 | 当前版本号,影响更新检测与缓存 busting |
Description |
否 | 后台列表里的描述 |
Author / Author URI |
否 | 作者信息 |
License / License URI |
建议 | 授权声明,WordPress 官方目录要求 GPL 兼容 |
Text Domain |
翻译必需 | 国际化文本域,必须与插件 slug 一致 |
Domain Path |
否 | 翻译文件相对路径,默认 /languages |
Requires at least |
建议 | 最低 WordPress 版本,低于此版本不允许安装 |
Requires PHP |
建议 | 最低 PHP 版本 |
Network |
否 | 设为 true 表示只能在多站点网络层面激活 |
Update URI |
否 | WordPress 6.1 起支持,用于避免同名插件误匹配官方目录更新 |
其中 Update URI 值得单独说明。它的出现是为了解决一个真实问题:如果你开发了一个内部插件,恰好和官方目录里的某个插件同名,WordPress 会拿你的插件去官方目录查更新,可能弹出错误的「有更新可用」。在头部写上 Update URI: https://example.com/my-plugin 或者干脆 Update URI: false,就能切断这个误匹配。
保姆级实操
第一步:建立规范的目录结构
wp-content/plugins/mmf-demo/
├── mmf-demo.php # 主文件,头部注释在这里
├── readme.txt # 官方目录用,本地插件可选
├── uninstall.php # 卸载时执行(可选)
├── includes/
│ ├── class-admin.php
│ └── class-frontend.php
├── assets/
│ ├── css/admin.css
│ └── js/admin.js
├── languages/
│ └── mmf-demo-zh_CN.po
└── templates/
└── settings-page.php
规则:主文件名与目录名一致;主文件在根目录;includes 放类文件;assets 放静态资源;languages 放翻译。
第二步:写一份完整的主文件头部
<?php
/**
* Plugin Name: MMF 演示插件
* Plugin URI: https://mimangfei.com/plugins/mmf-demo
* Description: 演示插件头部注释的标准写法与目录结构。
* Version: 1.2.0
* Requires at least: 6.4
* Requires PHP: 7.4
* Author: mimangfei
* Author URI: https://mimangfei.com
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: mmf-demo
* Domain Path: /languages
* Update URI: https://mimangfei.com/plugins/mmf-demo
*
* @package MMF_Demo
*/
defined( 'ABSPATH' ) || exit;
// 插件常量:后续所有路径都基于它们,避免硬编码
define( 'MMF_DEMO_VERSION', '1.2.0' );
define( 'MMF_DEMO_FILE', __FILE__ );
define( 'MMF_DEMO_DIR', plugin_dir_path( __FILE__ ) );
define( 'MMF_DEMO_URL', plugin_dir_url( __FILE__ ) );
// 加载翻译
add_action( 'plugins_loaded', function () {
load_plugin_textdomain( 'mmf-demo', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
} );
// 引入功能模块
require_once MMF_DEMO_DIR . 'includes/class-admin.php';
require_once MMF_DEMO_DIR . 'includes/class-frontend.php';
第三步:确认头部被正确解析
# 命令行查看 WordPress 实际读到的元信息
wp plugin get mmf-demo --fields=name,version,description,author,status
<?php
// 在代码里读取任意插件的头部(不需要该插件处于激活状态)
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$data = get_plugin_data( WP_PLUGIN_DIR . '/mmf-demo/mmf-demo.php', false, false );
print_r( $data );
get_plugin_data() 的第二个参数 $markup 控制是否对输出做 HTML 格式化,第三个参数 $translate 控制是否翻译。读取来做判断时两个都应传 false。
第四步:验证 Text Domain 是否真的生效
# 检查翻译文件命名是否与 Text Domain 匹配
ls -1 wp-content/plugins/mmf-demo/languages/
# 正确命名:mmf-demo-zh_CN.mo / mmf-demo-zh_CN.po
# 用 WP-CLI 检查代码里有没有漏写文本域
wp i18n make-pot wp-content/plugins/mmf-demo wp-content/plugins/mmf-demo/languages/mmf-demo.pot --slug=mmf-demo
命名格式必须是 {text-domain}-{locale}.mo,写错一个字符翻译就不会加载,而且没有任何报错。
第五步:声明兼容性并用工具验证
# 检查插件在当前 PHP 版本下有没有语法错误
php -l wp-content/plugins/mmf-demo/mmf-demo.php
# 用 WordPress 编码规范检查代码风格
phpcs --standard=WordPress wp-content/plugins/mmf-demo/
第六步:版本号更新的规范
改代码后必须同步改 Version 字段和对应的常量,否则浏览器会继续用旧版本的静态资源(如果你的资源 URL 里带了版本号):
wp_enqueue_style(
'mmf-demo-admin',
MMF_DEMO_URL . 'assets/css/admin.css',
array(),
MMF_DEMO_VERSION // 版本变化时资源 URL 变化,强制刷新缓存
);
常见踩坑
-
头部前面有空行或 BOM:文件开头必须是
<?php,前面有 UTF-8 BOM 或空行都会导致头部解析失败。用编辑器确认保存为「UTF-8 无 BOM」。 -
用
//写头部:必须是/** */块注释。 -
Text Domain 与插件 slug 不一致:翻译文件会静默加载失败,且没有任何错误提示。
-
Version 改了但常量没同步:静态资源缓存不刷新,表现为「改了 CSS 但前台没变」。
-
主文件放在子目录:WordPress 只在插件根目录找主文件,放
includes/里不会被识别。 -
案例: 某开发者写了一个内部插件,目录名
report,主文件名index.php,头部齐全但没有Update URI。上线后后台持续提示「有更新可用」,点更新后插件被替换成了官方目录里同名的另一个插件,功能全部丢失。加上Update URI: false后提示消失。教训是:内部插件一定要写Update URI,否则可能被官方目录的同名插件劫持更新通道。 -
头部写了
Network: true却在单站点使用:该插件会只能在网络层激活,单站点后台看不到激活按钮。
总结
必记四条:Plugin Name 是唯一决定插件能否被识别的字段;Version 必须与实际代码同步且用于静态资源版本;Text Domain 必须等于插件 slug,翻译文件名格式为 {domain}-{locale}.mo;内部插件加 Update URI 防止更新劫持。
推荐做法:主文件名与目录名一致、用 plugin_dir_path() / plugin_dir_url() 定义路径常量、每个文件开头加 defined( 'ABSPATH' ) || exit;。
写完头部之后,下一步就是让它真正跑起来——从零写一个 WordPress 插件 会带你走完从建文件到后台启用的全过程。而如果插件不再需要,卸载时的清理规范见 uninstall.php 清理数据,那是很多开发者会漏掉的一环。
