Composer静默采用重复包声明中的最后一个版本,导致实际安装版本与预期不符;需通过全局搜索、composer show、--dry-run等命令及时发现,并执行rm composer.lock后update清理残留。

Composer 本身不阻止你在 composer.json 里重复写同一个包,但它不会“合并”或“报错”,而是静默取最后一个声明——这会导致你自以为锁定了某个版本,实际生效的是另一行,后续所有操作都基于这个被掩盖的值。
重复包声明为什么不会立刻报错
Composer 解析 composer.json 时,对同一键(如 "monolog/monolog")的多次赋值,按 JSON 解析规则只保留最后一次。也就是说:
-
"monolog/monolog": "^2.8"写了两遍,第二遍是"^3.0",最终生效的是^3.0 - 终端不会提示、也不会中断,
composer install照常运行,但装的版本和你“以为”的不一致 -
composer show monolog/monolog显示的 version 是 lock 文件里记录的,而 lock 文件又受这个静默覆盖影响,形成误导闭环
怎么快速发现 composer.json 里的重复包
别等报错才查。人工搜索最直接,配合几个命令交叉验证:
- 用编辑器全局搜索
"vendor/name"(注意引号和斜杠),看是否在require或require-dev中出现多次 - 运行
composer show vendor/name,如果输出多个版本线索(比如显示 “installed: 3.0.0” 但 “available: 2.10.0, 3.0.0”),说明锁文件里可能存了歧义记录 - 执行
composer update --dry-run -v | grep "vendor/name",观察是否反复出现解析路径,暗示依赖图被干扰
删重复项后为什么 vendor 还有旧类或 autoload 错误
因为 composer remove 不等于“清理现场”,它只改 composer.json 和 composer.lock;而 vendor/ 和 autoload_psr4.php 是滞后更新的:
- 删完重复项必须跑
composer update vendor/name(推荐)或composer update,否则vendor/里残留旧文件,autoload_psr4.php仍映射已删除的路径 -
composer dump-autoload不会删旧映射,只补新——若旧映射还在composer.lock里,它就会继续生成 - 最保险做法:删重复项 →
rm composer.lock→composer update vendor/name→ 检查vendor/composer/autoload_psr4.php是否只剩一份该包映射
require-dev 里的重复包更危险
它不像 require 那样总被加载,但会在 CI 或本地带 --dev 的场景下触发冲突,且更难排查:
-
composer install --no-dev时看似正常,但一旦 CI 加了--dev或某生产包间接依赖了require-dev里的工具(比如phpunit/phpunit被phpstan/phpstanrequire),就突然爆 autoload 冲突 - 某些测试工具(如
infection/infection)会在自己的composer.json里写"autoload": {"psr-4": {"App\": "../src/"}},把你项目 src 加进它的加载路径——而你的代码又用了它的类,形成隐式循环 - 检查方式:
grep -r '"autoload"' vendor/*/composer.json | grep -i '../src',重点扫require-dev引入的包
真正麻烦的不是重复本身,而是它让 composer.json 失去可信度——你没法再靠它一眼看出项目到底依赖什么。每次修改前先 composer show --tree 对照,比事后 debug 快得多。


















