从"Hello World"到能下断点:我搭第一个插件目录时走过的结构弯路

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 125 浏览 0 回复

刚开始写插件那会儿,我把所有东西塞进一个文件,my-plugin.php 里既有头部声明又有业务逻辑还有样式输出,本地调试全靠 var_dump 刷页面。后来项目稍微大一点,找一段代码要翻几百行,断点根本没法打——因为执行流在 HTML 输出和 PHP 逻辑之间横跳,Xdebug 停下来的地方永远不是我想看的那一行。

这篇记录我后来固定下来的目录骨架,以及怎么让本地环境真正"可调试"。

一、目录结构:从"一坨"到"有层有级"

我现在新开插件必定先建这几个目录,哪怕第一个版本只有两百行代码:

my-plugin/
├── my-plugin.php          # 唯一入口,只做"挂号"和"分流"
├── bootstrap.php          # 容器、常量、自动加载注册
├── src/
│   ├── Admin/             # 后台相关:页面、Ajax、菜单注册
│   ├── Public/            # 前台相关:短码、小工具、脚本
│   ├── Core/              # 跨层通用:数据库、钩子调度器、工具函数
│   └── Contracts/         # 接口定义,解耦用
├── assets/
│   ├── css/
│   ├── js/
│   └── images/
├── languages/             # 从第一天就留好位置
├── tests/                 # 单元测试,后面会说怎么接进本地
└── vendor/                # composer 依赖(如果有)

关键约定:入口文件里不写任何业务逻辑。我的 my-plugin.php 通常长这样——

<?php
/**
 * Plugin Name: My Plugin
 * Version:     0.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

define( 'MY_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
define( 'MY_PLUGIN_URL',  plugin_dir_url( __FILE__ ) );

require_once MY_PLUGIN_PATH . 'bootstrap.php';

bootstrap.php 负责把自动加载挂好、把主类实例化,然后让主类自己去决定什么时候注册钩子。这样入口文件永远干净,调试时我一眼就知道"程序从哪进"。

二、自动加载:别用 require 堆路径

早期我手写一堆 require_once,改个目录结构就报错。现在直接用 composer 的 PSR-4,composer.json 里加一段:

{
    "autoload": {
        "psr-4": {
            "MyPlugin\\": "src/"
        }
    }
}

本地开发时 composer dump-autoload 随时刷新映射。生产打包再考虑优化成 classmap,开发阶段 PSR-4 最省事。

三、本地调试环境:不是"能跑 WordPress"就行

我试过几种方案,最后固定用 Docker + Xdebug + VS Code,因为能精确控制 PHP 版本和扩展,且断点能穿透到插件代码里。

我的 docker-compose.yml 核心片段(PHP 服务部分):

services:
  wordpress:
    image: wordpress:6.4-php8.2-apache
    volumes:
      - ./wordpress:/var/www/html
      - ./my-plugin:/var/www/html/wp-content/plugins/my-plugin  # 插件挂载为卷
      - ./php.ini:/usr/local/etc/php/conf.d/xdebug.ini          # 覆盖配置
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: root
      WORDPRESS_DB_PASSWORD: secret
    ports:
      - "8080:80"

xdebug.ini 内容(Xdebug 3 写法):

zend_extension=xdebug.so
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 的 .vscode/launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9003,
            "pathMappings": {
                "/var/www/html/wp-content/plugins/my-plugin": "${workspaceFolder}"
            }
        }
    ]
}

这里有个坑:pathMappings 必须精确映射到插件目录,不能只映射到 /var/www/html,否则断点会飘到 WordPress 核心文件里,或者干脆不生效。我为此浪费过两小时。

四、让错误"炸"在屏幕上:开发期的错误级别

插件入口或者 bootstrap.php 里,开发环境我会强制打开错误显示:

if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    error_reporting( E_ALL );
    ini_set( 'display_errors', '1' );
}

配合 wp-config.php 里的 define( 'WP_DEBUG', true );define( 'WP_DEBUG_LOG', true );,错误同时进文件。但注意:生产环境绝对不要 display_errors,这个开关我后面会包进环境判断里。

五、一个最小可运行的"骨架插件"

把上面串起来,一个能下断点、有目录层级、能自动加载的最小插件长这样。核心类 src/Core/Plugin.php

<?php
namespace MyPlugin\Core;

class Plugin {
    public function __construct() {
        add_action( 'admin_menu', [ $this, 'register_menu' ] );
    }

    public function register_menu(): void {
        add_menu_page(
            'My Plugin',
            'My Plugin',
            'manage_options',
            'my-plugin',
            [ $this, 'render_page' ]
        );
    }

    public function render_page(): void {
        // 故意留一个断点位置,测试 Xdebug 是否穿透
        $version = get_plugin_data( MY_PLUGIN_PATH . 'my-plugin.php' )['Version'];
        printf( '<div class="wrap"><h1>My Plugin %s</h1></div>', esc_html( $version ) );
    }
}

bootstrap.php 里实例化:

<?php
require_once MY_PLUGIN_PATH . 'vendor/autoload.php';

use MyPlugin\Core\Plugin;

new Plugin();

装完插件、在 render_page 里打个断点、F5 启动监听、刷新后台页面——如果 VS Code 能停住,整个链路就通了。

六、我踩过的两个具体坑

1. Docker 卷挂载后插件"消失"

WordPress 官方镜像的 wp-content/plugins 是镜像自带的,我把本地插件目录挂载进去时,如果路径写错,会看到一个空目录或者插件被覆盖。解决:确保挂载目标路径和容器内实际路径一致,且 docker-compose down -v 清除旧卷再重建。

2. Xdebug 3 的端口变化

Xdebug 2 默认 9000,Xdebug 3 改成 9003。我本地环境混过两个版本,launch.json 里端口对不上时,调试器永远"监听中"但不断住。现在我的 xdebug.ini 里显式写死 client_port=9003,不再依赖默认。

七、下一步:把测试接进来

目录里留了 tests/,我目前用 phpunit + brain/monkey 做单元测试,用 wp-env 做集成测试。但这块还在磨合,等跑顺了再开帖记录。

如果你也在搭第一个插件的本地环境,欢迎贴出你的目录结构或者踩过的坑。特别是用 LocalWP、Lando 或者其他方案的老哥,想听听实际体验对比。

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