插件目录"第一块砖"怎么摆:我拆解了五种常见入口文件写法,发现 `plugins_loaded` 里藏了条"暗河"

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

刚开始写插件那会儿,我把所有代码往一个文件里塞,require 满天飞,调试时改一行刷新整个页面,报错信息指向第 800 多行,眼睛都看花。后来拆目录、拆文件,却发现入口文件怎么写直接影响后续能不能愉快地本地调试。这里把我踩过的几种结构摊开聊。

结构一:单文件"裸奔"型

最省事,也是官方示例默认给的:

/*
Plugin Name: 我的插件
*/

// 直接在这里写逻辑
add_action('init', function() {
    // ...
});

问题很明显:没有命名空间、没有自动加载,稍微写多点就变成"意大利面条"。本地调试时想加个断点,Xdebug 停在一堆全局函数里,变量作用域乱成一锅粥。这种结构只适合 50 行以内的功能验证。

结构二:入口文件只做"接线员"

我现在常用的最小可运行骨架:

my-plugin/
├── my-plugin.php          // 仅保留插件头信息 + 常量定义 + 启动器调用
├── bootstrap.php          // 实际做自动加载注册、容器初始化
├── src/
│   ├── Core/
│   │   └── Plugin.php     // 主类,负责绑定钩子
│   └── Admin/
│       └── Menu.php
└── vendor/                // Composer 依赖

入口文件瘦成这样:

<?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 . 'bootstrap.php';

// 延迟到 plugins_loaded,避免过早执行
add_action('plugins_loaded', [\MyPlugin\Core\Plugin::class, 'instance']);

关键点在这里:new Plugin() 或者静态工厂方法挂到 plugins_loaded,而不是在入口文件直接实例化。WordPress 加载插件是按目录字母顺序扫的,如果你的插件依赖另一个插件的函数,直接实例化可能对方还没加载完。

结构三:bootstrap 里的"陷阱门"

bootstrap.php 我写过两版,一版用 Composer autoload,一版手写 SPL 自动加载。本地调试时 Composer 那版有个坑:如果 vendor/autoload.php 存在但依赖没装全,require 不会报错,跑到一半才炸。

现在加了道检查:

// bootstrap.php
$autoload = MY_PLUGIN_DIR . 'vendor/autoload.php';
if (file_exists($autoload)) {
    require_once $autoload;
} else {
    // 本地开发时走手写加载,避免没跑 composer install 就白屏
    spl_autoload_register(function ($class) {
        $prefix = 'MyPlugin\\';
        if (strpos($class, $prefix) !== 0) return;
        
        $relative = substr($class, strlen($prefix));
        $file = MY_PLUGIN_DIR . 'src/' . str_replace('\\', '/', $relative) . '.php';
        
        if (file_exists($file)) require $file;
    });
}

这样同事 clone 下来没装 Composer 也能先跑起来看效果,CI 环境里再强制走 composer install --no-dev

结构四:本地热调试的"偷鸡"技巧

不用每次改代码都手动刷新后台,我在 Plugin.php 里埋了个开发模式开关:

class Plugin {
    private function __construct() {
        $this->define_hooks();
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            $this->enable_dev_tools();
        }
    }
    
    private function enable_dev_tools() {
        // 开发时强制不缓存脚本版本号
        add_filter('script_loader_src', [$this, 'bust_cache'], 10, 2);
        
        // 暴露一个 REST 端点给本地 Vite/Webpack dev server 做热重载 ping
        add_action('rest_api_init', function() {
            register_rest_route('my-plugin/v1', '/ping', [
                'methods' => 'GET',
                'callback' => function() {
                    return ['timestamp' => time(), 'memory' => memory_get_usage()];
                },
                'permission_callback' => '__return_true' // 本地 nginx 限了 IP
            ]);
        });
    }
    
    public function bust_cache($src, $handle) {
        if (strpos($handle, 'my-plugin-') === 0) {
            return add_query_arg('ver', time(), $src);
        }
        return $src;
    }
}

配合 Valet 的 valet link,本地目录直接映射到 .test 域名,Xdebug 的 idekey 设成 PHPStorm 自动识别。最爽的是 bust_cache 这个 filter,改完 JS/CSS 不用清浏览器缓存,版本号自动带时间戳。

结构五:多站点/多环境时的入口"分流"

有次给客户部署,发现他们用了 Bedrock 结构,wp-content 位置变了,plugin_dir_path 返回的路径里多了层 app/。我在入口文件加了环境探测:

// 兼容 Bedrock / 标准 WP / 自定义内容目录
if (!defined('MY_PLUGIN_DIR')) {
    $candidate = dirname(__FILE__);
    // Bedrock 里插件实际在 web/app/plugins,但 WP 常量已经帮我们算好了
    define('MY_PLUGIN_DIR', plugin_dir_path(__FILE__));
}

// 本地 .env 里配,线上走 wp-config.php 的常量
$env_file = MY_PLUGIN_DIR . '.env';
if (file_exists($env_file) && class_exists('Dotenv\Dotenv')) {
    $dotenv = \Dotenv\Dotenv::createImmutable(MY_PLUGIN_DIR);
    $dotenv->safeLoad();
}

这样同一套代码,git clone 下来配个 .env 就能跑,不用改入口文件里的硬编码路径。

一个关于 plugins_loaded 的冷知识

我之前以为所有插件的 plugins_loaded 回调是"平级"执行的,直到有次两个插件都挂了这个钩子,A 插件在回调里 do_action('my_custom_event'),B 插件也监了这个事件,结果执行顺序完全不是我想的那样。

查了源码才发现:plugins_loaded 本身是个钩子,但插件文件是 include 进来的,include 过程中如果某个插件直接调了 do_action('plugins_loaded')(对,有人真这么干),后面的插件文件还没被加载。所以永远别在插件文件顶层触发 plugins_loaded,入口文件只注册、不执行,把执行权交还给 WordPress 的加载流程。

你们入口文件是怎么组织的?有没有遇到过更诡异的加载顺序问题?

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