从零开始搭一个能下断点的插件骨架:我的本地调试环境配置与最小可运行目录结构

插件开发 2 浏览 0 回复 返回上级

第一次写插件的时候,我直接把代码往 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"。

一个立即可用的"调试仪式"

我现在每次开新插件项目,会按这个顺序验证环境:

  1. bootstrap.phpinit() 方法第一行下断点
  2. 启动 VS Code 的调试监听(F5)
  3. 刷新插件所在的后台页面
  4. 确认断点命中,单步能走进 src/ 下的类
  5. 故意抛一个异常,确认 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.1host.docker.internal、甚至手写宿主机 IP 三种方案)?

评论0
回复 · 0
还没有回复
微信客服 微信客服