replace 仅在依赖解析时声明替代关系,不卸载旧包、不重定向类加载、不覆盖文件;必须手动 remove 旧包,配置 autoload 和 conflict,并确保 API 兼容。

replace 字段不会自动卸载旧包、不重定向类加载、也不覆盖任何文件——它只在 composer install 或 composer update 解析依赖图时起作用,告诉 Composer “这个包已由我满足,别装它了”。
replace 不会帮你删掉已安装的包
你改完 composer.json 加上 "replace": {"old-vendor/package": "*"},然后直接 composer install,old-vendor/package 依然稳稳躺在 vendor/ 里。Composer 不会执行任何删除操作。
- 必须先手动运行
composer remove old-vendor/package,这是不可跳过的一步 - 如果该包被其他已安装依赖(比如
some-tool)硬性require,那下次 CI 或新机器上composer install还是会把它拉回来 - 检查它是否真被其他包依赖:用
composer show --tree | grep old-vendor/package
replace 后类找不到?autoload 没继承,得自己配
Composer 不会把被 replace 包的 autoload 配置自动抄到你包里。你声明替换了 acme/legacy-utils,但它用 "psr-4": {"Acme\": "src/"},而你的包用的是 "MyOrg\" 命名空间,运行时就报 Class not found。
- 先查原包 autoload:运行
composer show -p acme/legacy-utils,看它是psr-4、classmap还是files - 在你自己的
composer.json中完整复现相同映射,哪怕只是把命名空间 alias 一下 - 改完后务必执行
composer dump-autoload -o,再用composer show -p确认规则生效
为什么加了 replace 还报冲突?conflict 得配齐
replace 声明“我顶替它”,但不阻止别人继续 require 它;如果某个依赖仍明确写 "old-vendor/package": "^1.2",而你的包没声明兼容版本或没加 conflict,Composer 就会卡在依赖解析阶段报错。
- 必须在
replace同级加"conflict": {"old-vendor/package": "*"},强约束共存关系 - 版本约束要对齐:若原包只支持
^1.0,你写"replace": {"old-vendor/package": "*"}可能导致解析失败;建议写成"^1.0 || ^1.1"更稳妥 - 多个包同时 replace 同一个包?Composer 直接报错:
Package old-vendor/package is replaced by ... and ...,必须人工 resolve
replace 不是 provide,别拿它去“实现接口”
想用自定义日志包替代 psr/log?写 "replace": {"psr/log": "*"} 是错的。这会让 Composer 认为你“就是 psr/log 包本身”,而它是个虚拟规范包,没有实际代码——结果是依赖图崩掉,autoload 也无从谈起。
- 实现接口用
provide:如"provide": {"psr/log-implementation": "^1.0 || ^2.0"} - 替换具体包才用
replace:目标必须是真实存在、有源码、有 autoload 的包,比如monolog/monolog、symfony/polyfill-mbstring - 二者可共存:一个 fork 包既
provide接口能力,又replace原包,但语义不能混
最常被忽略的一点:replace 生效的前提是你已经移除了旧包,并且你的代码真的提供了全部 public API——它不校验功能,只校验包名和版本声明。运行时报错,从来不是 replace 没生效,而是你没配全 autoload 或漏写了 conflict。











