`ReflectionException` 与 `class_exists` 的"死亡时差":我如何在自动加载器里被 Composer 和 WordPress 各捅一刀

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

昨天重构 Zsens Admin 的依赖注入容器时,撞上一个让我愣了五分钟的报错:

```php ReflectionException: Class "Zsens\Admin\Services\ReportEngine" does not exist ```

但文件明明在 `src/Services/ReportEngine.php`,命名空间也对,PSR-4 映射也没改。更诡异的是——这报错不是每次必现,刷新三次出现一次。

我第一反应是 Composer 的 `autoload_static.php` 没更新,composer dump-autoload 三连,没用。然后怀疑是 OPCache,重启,还是抽风。

真正让我警觉的是这个堆栈顺序:

```php #0 /wp-content/plugins/zsens-admin/vendor/composer/ClassLoader.php:571 #1 /wp-content/plugins/zsens-admin/src/Core/Container.php:89 ← 我的反射 #2 /wp-content/plugins/zsens-admin/zsens-admin.php:47 ```

注意 #0 的位置——Composer 的 ClassLoader 已经尝试加载了,说明自动加载器是注册的。但 #1 里我的容器在 new ReflectionClass($class) 时炸了。

我往里加了个 debug:

```php error_log('Before reflect: ' . (class_exists($class, false) ? 'Y' : 'N')); $ref = new ReflectionClass($class); ```

输出是 N,然后爆炸。但 class_exists($id, false) 的第二个参数 false 意思是"不触发自动加载"——这说明 Composer 还没真正执行到加载逻辑?

问题出在 WordPress 的插件加载时序。我的入口文件 `zsens-admin.php` 顶部是:

```php require_once __DIR__ . '/vendor/autoload.php'; ```

但另一个 MU 插件(客户环境里的旧版缓存插件)在 muplugins_loaded 钩子中做了件阴间事:

```php // 某 MU 插件的"优化"代码 spl_autoload_unregister(['Composer\\Autoload\\ClassLoader', 'loadClass']); // ... 它自己的逻辑 ... spl_autoload_register(['Composer\\Autoload\\ClassLoader', 'loadClass'], true, false); ```

看到最后那个 false 了吗?它把 Composer 的自动加载器从队列头部踢到了尾部。而我的容器初始化恰好在 plugins_loaded 早期,此时另一个插件的自动加载器(一个过时的类映射器)挡在前面,遇到不认识的类直接返回 false,根本没轮到 Composer 出手。

更坑的是 class_exists 的行为差异:

```php // 这个在"死亡时差"窗口里会返回 false class_exists('Zsens\Admin\Services\ReportEngine', false); // 但这个会触发完整的自动加载链,反而能成功 class_exists('Zsens\Admin\Services\ReportEngine', true); ```

所以我的容器在反射前做防御性检查,用了 false 想省一次自动加载开销,结果正好踩进时序裂缝。报错信息说"Class does not exist",实际上类文件存在,只是自动加载器还没轮到它。

我的修复不是改 falsetrue 那么简单——那等于放弃性能。而是给容器加了层"预加载缓冲":

```php public function get(string $id): object { // 先尝试无加载检查,避免重复触发 if (!class_exists($id, false)) { // 失败时强制走完整链,同时记录"谁"加载了它 if (!class_exists($id, true)) { throw new NotFoundException("{$id} 无法加载"); } // 成功则缓存到预加载表,下次直接命中 $this->preload[$id] = true; } return $this->resolve($id); } ```

另外给 MU 插件的"优化"加了检测:

```php add_action('muplugins_loaded', function () { $loaders = spl_autoload_functions(); $composerPos = array_search( [ComposerAutoloaderInit::getLoader(), 'loadClass'], $loaders ); if ($composerPos === false || $composerPos > 2) { do_action('zsens_admin_autoloader_hijacked', $loaders); // 记录到系统日志,方便后续排查 } }, PHP_INT_MAX); ```

这个案例给我的教训是:ReflectionException 说"类不存在",不要只盯着文件系统和命名空间。在 WordPress 这种"多插件共享自动加载队列"的环境里,class_exists 的第二个参数、spl_autoload_register 的优先级参数、MU 插件的时序野心,三者叠加能造出完美的"存在性幻觉"。

你遇到过类似的"时序幽灵"吗?特别是那种刷新几次才出现一次的自动加载异常——大概率是注册顺序在打架。

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