从零开始搭一个能下断点的插件骨架:我的本地调试环境配置与最小可运行目录结构
第一次写插件的时候,我直接把代码往 wp-content/plugins 里一丢,改一点刷新一下页面,报错就 var_dump 到处插。三天之后,我得到了一个满是 die() 尸体的代码库和一颗想重装 WordPress 的心。后来才意识到,插件的目录结构和入口文件设计,本质上是在回答一个问题:怎么让调试器能精准地停在你想停的地方,而不是在 WordPress 核心里迷路。
我的目录结构:不是"规范",是"能调试"
我见过很多"最佳实践"把目录拆得特别细,但新手阶段我更倾向于按"调试边界"来切分。这是我的最小可运行骨架:
my-plugin/
├── my-plugin.php # 入口文件:只做"插件身份声明"和"引导启动"
├── bootstrap.php # 实际初始化:类加载、常量、容器绑定
├── src/
│ ├── Admin/ # 后台相关:页面、AJAX、设置
│ │ └── SettingsPage.php
│ ├── Frontend/ # 前台相关:短码、模板干预
│ ├── Core/ # 跨层工具:数据库抽象、HTTP客户端封装
│ └── Contracts/ # 接口定义:方便后面写单元测试时 mock
├── assets/
│ ├── js/ # 源码,未经构建
│ └── css/
├── build/ # 构建产物(如果有 webpack/vite)
├── tests/ # 不是"以后再说",是现在就建
│ └── bootstrap.php # 测试环境自己的引导文件
└── .vscode/ # 调试配置,随仓库走
└── launch.json
关键决策:入口文件 my-plugin.php 里不写业务逻辑。WordPress 的插件头注释必须在这个文件顶部,但类实例化、钩子挂载全挪到 bootstrap.php。这样 PHPStorm 或 VS Code 下断点时,你不会在插件头和 namespace 声明之间反复横跳。
入口文件的"防坑"写法
很多教程的入口文件长这样:
<?php
/**
* Plugin Name: My Plugin
*/
require_once __DIR__ . '/vendor/autoload.php';
$plugin = new MyPlugin();
$plugin->run();
问题在哪?直接执行。如果 MyPlugin 的构造函数里抛异常,WordPress 的插件启用流程会被打断,但错误信息可能吞在 register_activation_hook 的上下文里,你看到的是"插件已启用"或者一个白屏。
我现在的入口文件:
<?php
/**
* Plugin Name: My Plugin
* Version: 0.1.0
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit;
}
define('MY_PLUGIN_DIR', __DIR__);
define('MY_PLUGIN_FILE', __FILE__);
// 延迟到 plugins_loaded,避免在翻译、多站点切换完成前初始化
add_action('plugins_loaded', function () {
require_once MY_PLUGIN_DIR . '/bootstrap.php';
try {
\MyPlugin\Bootstrap::init();
} catch (\Throwable $e) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('[MyPlugin Bootstrap Error] ' . $e);
}
// 后台才显示,避免前台炸给用户看
if (is_admin()) {
add_action('admin_notices', function () use ($e) {
printf(
'<div class="notice notice-error"><p>%s</p></div>',
esc_html('插件启动失败:' . $e->getMessage())
);
});
}
}
});
这个 try-catch 不是"优雅",是调试刚需。本地开发时 WP_DEBUG_LOG 一开,错误直接进 wp-content/debug.log,配合后面要说的 Xdebug 配置,堆栈信息完整保留。
本地调试环境:我用的是"容器+宿主机双轨"方案
不是每个人都想折腾 Docker,但插件开发有个特殊需求:你的代码在宿主机编辑,但 PHP 执行环境在容器里,Xdebug 的通信路径需要额外处理。
我的 docker-compose.yml 精简版:
services:
wordpress:
image: wordpress:6.5-php8.2-apache
volumes:
- ./my-plugin:/var/www/html/wp-content/plugins/my-plugin
- ./wordpress:/var/www/html
environment:
WORDPRESS_DEBUG: 1
extra_hosts:
- "host.docker.internal:host-gateway"
核心技巧:只挂载插件目录,不挂载整个 WordPress。这样你本地换 WordPress 版本只需要改镜像 tag,插件代码始终隔离。
Xdebug 3 的配置(写在 .vscode/launch.json 和容器的 php.ini 里要对应):
; 容器内的 php.ini 追加
[xdebug]
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.log=/tmp/xdebug.log
VS Code 的 launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug (Plugin)",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/var/www/html/wp-content/plugins/my-plugin": "${workspaceFolder}"
},
"ignore": [
"**/wp-includes/**",
"**/wp-admin/includes/**"
]
}
]
}
注意 pathMappings 的对应关系——这是新手最容易卡住的地方。容器里的路径必须和宿主机项目根目录严格映射,否则断点会变灰,提示"unverified breakpoint"。
一个立即可用的"调试仪式"
我现在每次开新插件项目,会按这个顺序验证环境:
- 在
bootstrap.php的init()方法第一行下断点 - 启动 VS Code 的调试监听(F5)
- 刷新插件所在的后台页面
- 确认断点命中,单步能走进
src/下的类 - 故意抛一个异常,确认
admin_notices能显示,且debug.log有记录
第五步特别重要。很多"环境配好了"的幻觉,来自于只测试了"成功路径"。插件开发里,你能不能快速看到失败时的完整上下文,决定了调试效率是五分钟还是五小时。
关于 tests/ 目录的提前介入
我现在的习惯是:在写第一个 admin 页面之前,先把测试引导文件建好。不是要写多少测试,而是WP-CLI 的测试脚手架会帮你建立一个隔离的 WordPress 实例,这个实例本身就可以用来验证"我的插件在干净环境里能不能激活"。
# 在项目根目录
wp scaffold plugin-tests my-plugin --dir=.
生成的 tests/bootstrap.php 会帮你做 activate_plugin() 和 _manually_load_plugin(),这比你在生产站点里反复启用禁用安全得多。
这套骨架跑通之后,我才会开始想"这个插件到底要做什么"。地基不稳的时候堆功能,后面拆起来全是技术债——尤其是当你发现某个钩子在 plugins_loaded 之后才能用,而你的入口文件已经过早初始化了的时候。
你们本地调试环境是怎么搭的?有没有被 Xdebug 的 client_host 坑过(我查过 172.17.0.1、host.docker.internal、甚至手写宿主机 IP 三种方案)?