replace是声明替代关系而非忽略,需写在composer.json顶层并确保功能完全兼容,否则运行时会Class not found;provide仅参与依赖推导,不阻止安装,不能解决冲突。

composer.json 里写 "replace" 是最直接的解法
虚拟包本身不提供代码,只靠 replace 字段告诉 Composer:“这个包名我来顶替”。比如你写了 "symfony/polyfill-mbstring": "*" 在 replace 里,那 Composer 就不会再尝试装真正的 ext-mbstring 或其他 polyfill 变体——它把名字“占住”了,冲突自然消解。
但注意:这不是“忽略”,而是主动声明替代关系。必须确保被替代的包真能覆盖所有使用场景,否则运行时会报 Class not found 或函数不存在。
-
replace字段写在根composer.json的顶层,和require同级 - 值必须是完整包名 + 版本约束,比如
"ext-mbstring": "*",不能只写"ext-mbstring": [] - 如果被替代的是另一个真实包(如用私有 fork 替代官方包),要确认它的 autoloading、类名、接口完全兼容
别把 "provide" 当成 "replace" 用
provide 是让一个包“声称自己提供了某能力”,常用于 polyfill 包或抽象层封装。但它不阻止 Composer 安装同名的真实包——也就是说,provide 不解决冲突,只参与依赖推导;而 replace 是硬性接管,会从依赖图中移除被替代项。
常见误用:看到某个包在自己的 composer.json 里写了 "provide": {"psr/log": "^3.0"},就以为能压住项目里其他地方对 psr/log:^2.0 的要求。其实不行——只要还有别的包 require "psr/log": "^2.0",冲突照旧。
- 查清哪个包真正触发了冲突,用
composer why-not psr/log:^3.0看阻断链 - 如果发现是某个 polyfill 包只
provide但没replace,得去它源码里确认是否该加replace -
provide更适合声明“我实现了 PSR-XX 接口”,不是用来绕开版本锁的
require-dev 里的虚拟包最容易漏查
很多冲突源头藏在 require-dev 下:比如 phpunit/phpunit 拉进 sebastian/exporter,后者又硬性要求 symfony/console:^5.0,而你的主应用要 ^6.0。这时即使你没在 require 里写任何虚拟包,require-dev 仍全程参与依赖解析。
检查方法很简单:composer show --tree | grep -A3 -B3 "symfony/console",看括号里是不是标着 (locked to 5.4.42) 或 (replaced)。如果是前者,说明锁文件里已固化旧版,得先定位谁引入的它。
- 运行
composer why-not symfony/console:^6.0,输出第一行就是封杀源头 - 如果源头在
require-dev,考虑升级对应 dev 工具(如换orchestra/testbench:^8.0支持 Laravel 11) - 临时方案可加
"conflict": {"symfony/console": ",但这是堵漏洞,不是修逻辑
platform.config.php 不能替代虚拟包语义
有人试图用 config.platform.php = "8.1" 来“假装”环境支持某个扩展,从而绕过 ext-mbstring 这类依赖。这根本无效——platform 只影响 PHP 版本和扩展的 平台需求校验,不改变包名解析逻辑。它不会让 Composer 把 ext-mbstring 当成已安装,也不会触发 replace 行为。
真正需要虚拟化扩展时,唯一可靠路径是:装一个明确 replace 了该扩展的 polyfill 包,并确保它出现在 require 中(而非 require-dev,除非你确定测试环境才需要)。
- 例如用
symfony/polyfill-mbstring,它在自身composer.json里写了"replace": {"ext-mbstring": "*"} - 必须显式
composer require symfony/polyfill-mbstring,不能指望其他包顺带带来 - 删掉
config.platform.ext-mbstring这类无效配置,它只会干扰真实依赖判断
实际操作中最容易被忽略的一点:虚拟包的 replace 关系只在当前项目 composer.json 或所依赖包的 composer.json 中生效。如果你依赖了一个私有包 A,而 A 的 composer.json 里写了 replace,但你在项目里又直接 require 了被 A 替代的那个包,Composer 仍会报冲突——因为你的根声明优先级高于间接声明。











