刚拆完第三个 Zsens Admin 插件才懂:目录结构不是"照着抄",是调试成本逼出来的
之前接了个二手插件的维护活,原开发者把控制器、模型、视图全塞在一个 includes/ 文件夹里,文件名还是 func1.php、func2.php 这种风格。本地改一行逻辑,调试器里断点打到怀疑人生——根本分不清哪个文件在什么时候被加载。那次之后我才正经坐下来,重新理了一套能让新手少踩坑的目录骨架。
先说我的目录结构,不是从哪个框架抄的,是被 var_dump 和 debug_backtrace 反复教育后的结果:
my-plugin/ ├── my-plugin.php # 唯一入口,只做一件事:bootstrap ├── bootstrap.php # 实际初始化:常量、自动加载、钩子注册 ├── config/ │ └── plugin.php # 版本号、表前缀、默认配置,不掺逻辑 ├── src/ │ ├── Admin/ # 后台专属:菜单、页面、Ajax 端点 │ │ ├── MenuRegistrar.php │ │ └── Controllers/ │ ├── Core/ # 前后台通用:请求封装、响应格式、工具类 │ │ ├── Router.php # 自定义路由解析,不是 WP 的 rewrite │ │ └── Validator.php │ ├── Models/ # 纯数据层,不写 SQL,只调仓库 │ └── Repositories/ # 实际查表的地方,方便 mock 测试 ├── assets/ │ ├── css/ │ ├── js/ │ └── images/ ├── languages/ ├── tests/ # 本地调试的核心阵地 │ ├── phpunit/ │ └── fixtures/ # 假数据、模拟表结构 └── vendor/ # composer 依赖,提交时 ignore
关键认知转变:入口文件必须"薄到透明"。我现在的 my-plugin.php 就 20 行出头:
<?php
/*
Plugin Name: My Plugin
Version: 1.0.0
*/
if (!defined('ABSPATH')) exit;
define('MY_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MY_PLUGIN_URL', plugin_dir_url(__FILE__));
require_once MY_PLUGIN_DIR . 'vendor/autoload.php';
require_once MY_PLUGIN_DIR . 'bootstrap.php';
所有"这个插件是什么、要干嘛"的判断,全扔 bootstrap.php。这样本地调试有个隐藏好处:我可以单独写个 bootstrap-test.php,绕过 WP 的插件加载机制,直接用 PHPUnit 测 Core 层的类,不用启整个后台。
说到本地调试环境,我试过三种方案,最后锁死在一种上:
方案 A:Docker + wp-env(官方方案)
适合团队协作,但启动慢,每次改代码要 npm run wp-env start,我笔记本风扇狂转。而且插件代码是通过 volume 挂载的,有时候文件权限抽风,改完不生效。
方案 B:本地 LNMP + FTP 同步(我戒掉的蠢事)
就是标题里说的,改一行传一次,传完刷新,刷新完发现改错地方。最崩溃的是断点调试——Xdebug 配在本地 PHP,文件路径和服务器对不上,IDE 里断点变灰。
方案 C:本地 PHP 内置服务器 + SQLite(我现在用的)
不是跑完整 WP,而是用 wp-sqlite-db 让 WP 跑在 SQLite 上。整个环境就是一个文件夹,复制到 U 盘里能带走。启动命令就一行:
php -S localhost:8080 -t /path/to/wordpress
配合一个小脚本 dev-watch.php,用 inotifywait(Linux/Mac)或 fswatch 监听 src/ 变动,自动清 OPCache、刷 transients。不是热重载,但刷新即生效,比 FTP 强一百倍。
入口文件调试有个细节,我花了两小时才定位到:WP 的插件激活钩子 register_activation_hook 只认主文件路径。如果你把初始化逻辑拆到 bootstrap.php,钩子必须写在入口文件里,或者显式传 __FILE__。我当初把钩子也挪到 bootstrap,结果激活时数据库表死活不建,因为 WP 找的是 bootstrap.php 的路径,不是插件主文件。
现在我的 bootstrap 里会显式检查:
if (!defined('MY_PLUGIN_FILE')) {
define('MY_PLUGIN_FILE', dirname(__DIR__) . '/my-plugin.php');
}
register_activation_hook(MY_PLUGIN_FILE, [Installer::class, 'activate']);
目录结构定下来后,本地调试还有个隐形收益:错误日志能直接定位到文件。以前全堆在 includes/ 里,报错是 includes/func3.php on line 247,我得打开五个文件数行号。现在报错是 src/Repositories/UserRepository.php on line 89,一眼知道是数据层的问题。
最后分享一个我贴在显示器上的检查清单,每次开新插件前过一遍:
- 入口文件是否只负责
define+require? bootstrap.php里有没有混入业务逻辑?- 本地调试环境能否在 30 秒内从零启动?
- PHPUnit 能否不依赖 WP 全局变量跑通 Core 层?
- 激活/卸载钩子是否用了正确的
__FILE__引用?
第五条我踩过两次,每次都想抽自己。你们本地调试是什么方案?有比 SQLite 更轻量的玩法吗?

