换了个依赖包,排查三轮才找到真正的卡点
问题
有个 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 不同步。