从零开始搭一个 WordPress 插件:我的目录结构、入口文件与本地调试环境踩坑实录

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

刚开始写插件那会儿,我把所有代码塞进一个 zsens-admin.php,前端后端、AJAX、短代码全挤在一起,调试时改一行崩三处。后来拆过三个商业插件的源码,才理解目录结构不是"规范强迫症",是降低调试认知负担的刚需。这篇记录我现在的脚手架,以及本地环境怎么配才能"秒级复现问题"。

一、我的目录结构:按"加载阶段"分层,不是按"功能类型"

很多教程教你按 admin/public/includes/ 分,我早期也这么干。但实际开发中,你更常问的是"这段代码在哪个生命周期跑",而不是"这是前端还是后端"。所以我改成了按加载时序组织:

zsens-admin/
├── zsens-admin.php          # 入口:只做"是否该加载"的判断
├── bootstrap/
│   ├── loader.php             # 类自动加载器(PSR-4 简化版)
│   └── dependencies.php       # 检查 PHP/WP 版本、依赖插件
├── early/
│   ├── constants.php          # 定义常量(必须在插件加载前可用)
│   └── mu-fallback.php        # 若需 MU 插件模式时的兼容入口
├── hooks/
│   ├── register-activation.php
│   ├── register-deactivation.php
│   └── register-uninstall.php
├── core/
│   ├── Plugin.php             # 主类: orchestration,不干事
│   ├── Admin.php              # 后台相关钩子聚合
│   ├── Frontend.php           # 前台相关钩子聚合
│   └── Ajax.php               # AJAX 端点注册(注意:注册≠处理)
├── modules/                   # 实际功能模块,按需懒加载
│   ├── user-levels/
│   │   ├── class-user-levels.php
│   │   └── assets/            # 模块级资源,不堆到全局
│   └── community-bridge/
│       └── ...
├── vendor/                    # Composer 依赖(提交 lock,不提交 vendor 到 SVN)
└── tests/
    ├── phpunit/
    │   └── bootstrap.php      # 加载 WP 测试框架
    └── e2e/
        └── playwright.config.js

关键变化:core/ 里的类只负责"什么时候挂载什么",具体实现下沉到 modules/。这样调试时,我先看 Plugin.phprun() 方法就能定位问题阶段,不用在 2000 行代码里翻。

二、入口文件:越薄越好,但要防"直接访问"和"重复定义"两种死法

我的 zsens-admin.php 现在长这样,注释比代码多:

<?php
/**
 * Plugin Name: Zsens Admin
 * Version: 2.1.0
 * Requires PHP: 7.4
 */

// 死法一:直接访问 PHP 文件
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// 死法二:被其他插件以不同路径重复加载
if ( defined( 'ZSENS_ADMIN_VERSION' ) ) {
    return; // 静默退出,不抛异常(避免 WSOD)
}

define( 'ZSENS_ADMIN_VERSION', '2.1.0' );
define( 'ZSENS_ADMIN_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'ZSENS_ADMIN_PLUGIN_URL', plugin_dir_url( __FILE__ ) );

// 版本门槛:在自动加载前就要拦掉
if ( version_compare( PHP_VERSION, '7.4', '<' ) ) {
    add_action( 'admin_notices', function () {
        echo '<div class="error"><p>Zsens Admin 需要 PHP 7.4+</p></div>';
    } );
    return;
}

// 自动加载器:不依赖 Composer 也能工作
require_once ZSENS_ADMIN_PLUGIN_DIR . 'bootstrap/loader.php';

// 依赖检查(比如是否装了 WooCommerce)
require_once ZSENS_ADMIN_PLUGIN_DIR . 'bootstrap/dependencies.php';
if ( ! Zsens\Dependencies::check() ) {
    return; // dependencies.php 里自己处理通知
}

// 终于,启动
$plugin = new Zsens\Core\Plugin();
$plugin->run();

注意 defined( 'ZSENS_ADMIN_VERSION' ) 这个 guard。我踩过的坑:某客户用 require_oncefunctions.php 里又加载了一次我的插件,导致类重复声明直接 500。现在先检查常量,重复加载时优雅退出。

三、本地调试环境:我用 Docker Compose 但用现成镜像

试过 LocalWP、DevKinsta、各种一键脚本,最后回归手写 docker-compose.yml。不是折腾,是需要精确复现客户环境:PHP 7.4 vs 8.1、MySQL 5.7 vs 8.0、是否开 OPCache,这些组合问题只在特定版本出现。

我的 docker-compose.yml 核心结构:

version: '3.8'

services:
  wp:
    build:
      context: ./docker/php
      args:
        PHP_VERSION: ${PHP_VERSION:-8.1}  # .env 里切版本
    volumes:
      - ./:/var/www/html/wp-content/plugins/zsens-admin:delegated
      - wp-core:/var/www/html             # WP 核心分离,不污染项目
      - ./docker/php/php.ini:/usr/local/etc/php/conf.d/99-custom.ini:ro
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DEBUG: 1                  # 强制 WP_DEBUG = true
      # 我的自定义:自动安装测试数据
      ZSENS_INSTALL_FIXTURES: 1

  db:
    image: mysql:${MYSQL_VERSION:-8.0}
    # MySQL 8.0 默认 caching_sha2_password,旧 PHP 连不上
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - db-data:/var/lib/mysql
      - ./docker/mysql/init:/docker-entrypoint-initdb.d:ro

  # 关键:单独的 phpMyAdmin 不如 Adminer,但我要的是...
  mailhog:
    image: mailhog/mailhog
    # 拦截所有 wp_mail(),调试邮件模板不用真发

volumes:
  wp-core:
  db-data:

几个刻意的设计:

  • WP 核心用命名卷 wp-core:项目目录只挂载插件本身,wp-adminwp-includes 不在 git 里,但容器销毁后核心还在,重启秒开。
  • delegated 挂载标志:macOS 上 Docker 文件同步的救星,代码变更延迟从 3 秒降到可感知即时。
  • MailHog 不是摆设:插件有邮件通知功能时,不用配置 SMTP,所有邮件进 localhost:8025 的 Web UI。

四、调试技巧:让错误信息"砸到脸上"

WP_DEBUG 只是起点。我的 docker/php/php.ini 里还有这些:

display_errors = On
error_reporting = E_ALL
log_errors = On
error_log = /var/log/php/errors.log

; Xdebug 3 配置,只开开发环境
zend_extension=xdebug.so
xdebug.mode=debug,develop
xdebug.start_with_request=trigger      # 不是自动启动,用浏览器插件触发
xdebug.client_host=host.docker.internal
xdebug.log=/var/log/xdebug.log

重点在 xdebug.start_with_request=trigger。以前开自动模式,每个请求都进调试,Docker 性能暴跌。现在装个 Xdebug Helper 浏览器插件,点一下才连 IDE,平时零开销。

另外,我在插件里埋了个开发模式探针,只在 WP_DEBUG && current_user_can('manage_options') 时输出:

add_action( 'shutdown', function () {
    if ( ! defined( 'ZSENS_ADMIN_DEBUG_PANEL' ) || ! ZSENS_ADMIN_DEBUG_PANEL ) {
        return;
    }
    $hooks = $GLOBALS['wp_filter'];
    // 输出当前请求挂载的钩子、执行时间、内存峰值
评论0
回复 · 0
还没有回复
微信客服 微信客服