🔍搜索⌘K

换了个依赖包,排查三轮才找到真正的卡点

问题

有个 PHP 接口服务用的 JWT 库上游已经停更了,想换成社区在维护的 fork。换包这件事本身不大,但换完构建就一路报”类找不到”。

现象

一共三轮报错,每轮都不一样:

第一轮,跑 php artisan config:cache 报:

Class "Vendor\JWTAuth\Providers\JWT\Provider" not found

第二轮,把配置文件改完,跑 php artisan package:discover 报:

Class "Vendor\JWTAuth\Providers\LaravelServiceProvider" not found

第三轮,代码里的引用全改干净了,构建还是失败,报的还是上面那个 LaravelServiceProvider 找不到。

怎么解决

三轮是三层不一样的问题,一个比一个隐蔽。

第一轮:配置文件里写死了旧包的类名。

config/jwt.php 这个文件是当年从旧包里 publish 出来的,里面有几处直接引用了旧包的具体类,其中一处还拿旧包里的常量当算法名用:

'alg' => \Vendor\JWTAuth\Providers\JWT\Provider::ALGO_HS256,

新 fork 把命名空间整个换掉了(Vendor\JWTAuth\ → Fork\JWTAuth\),而且没有留兼容别名——不是加个 class_alias 就能糊弄过去的那种改法,是新包里根本没提供旧名字。同时新包还把这个常量删了。

也就是说换包不是”改一行 composer.json”就完事,配置文件里凡是写死命名空间的地方都得跟着改。

第二轮:业务代码里也写死了。

这次是 4 个文件里有 use Vendor\JWTAuth\... 这种写法(认证中间件、模型、解析器、服务提供者各一个)。

这里我犯了个错值得记:我一开始判断”这个服务只是纯转发,不直接用 JWT”,所以觉得不可能有代码引用。实际是我没读代码就下了结论。翻了一遍才发现 4 个文件都在用。以后遇到”某处不可能有引用”这种判断,先去搜一遍再说。

第三轮:代码全对了,构建还是错的。

这是最绕的一轮。composer.json 改了、lock 也更新了、业务代码里 Vendor 一个都不剩了,构建还是报同样的错。

真正的原因是:构建阶段根本没重装依赖。

看一下构建用的 Dockerfile,stg 那一步只跑了 composer dump-autoload——这个命令只根据已有的 vendor 目录重新生成自动加载映射文件,它不会去下载或更新任何包。

而这个项目的 stg/prod 镜像不是从源码现场构建的,是从一个预置在镜像仓库里的 builder 基础镜像继承来的。那个基础镜像里,vendor 目录是当初打包这个镜像时烤进去的——装的是旧包,vendor/composer/installed.json 里登记的 provider 类也是旧的。

所以整条链路是:预置镜像里的旧 vendor → dump-autoload 照着旧 installed.json 生成映射 → artisan 启动时去读映射 → 找不到新包里的类 → 报 not found。

我改了源码,但构建用的那个镜像仓库里的东西根本没动过,自然不会生效。

最后的解法是绕开这条路:把 stg/prod 改成自包含的多阶段构建,builder 阶段直接 composer install --no-dev --optimize-autoloader,按当前 lock 把 vendor 重新装一遍。这样每次构建都从当前 lock 出发,不依赖任何预置镜像里已经有的东西。

改完之后,“改了依赖但构建没生效”这条路就彻底断了——不需要谁记得去重建并推送基础镜像。

结论

这次最大的收获不是”怎么换包”,而是”为什么改了没生效”这类问题该怎么查。

第一,“我改了但没生效”先怀疑构建环境里已经有的东西,而不是怀疑自己的代码。 尤其当报错信息和你刚改的东西对不上的时候。判断方法很直接:把你以为生效的那份产物,和构建时实际用的那份,比一下是不是同一个。这次就是——我以为构建会读源码目录的 vendor,其实读的是镜像里烤好的那份。

第二,预置的基础镜像是个看不见的依赖。 它不在你的代码仓库里,不在版本控制里,但构建结果完全依赖它。composer dump-autoload 和 composer install 的区别在这里要命:前者是”照着现有的重新贴标签”,后者才是”按锁文件重新装”。想让构建可重复,就得让它自己装依赖。

第三,换包之前先确认新包提供什么、不提供什么。 命名空间换了、常量删了、没有兼容层——这些都是”看起来还是那个东西,实际已经不一样”的地方。升级前把新包的公开接口和旧的列一遍,比事后一个个报错强。

另外补一个当时也被绕进去的点:composer.lock 记录的是依赖的版本,不记录 PHP 运行时的版本。composer.json 里那个 require.php 只是一条平台约束,用来检查”当前 PHP 版本满足不满足要求”,它不会去安装、更不会切换 PHP——真正把 PHP 版本钉死的是 Dockerfile 里基础镜像那一行。顺带还有个连带影响:composer.json 里改了 require.php 之后,lock 文件里记的 hash 就对不上了,得跑一次 composer update --lock 刷新一下(只是刷新 lock 的 hash,不升级依赖版本),否则构建时会报 lock 不同步。