replace不是替换命令,它仅在依赖解析阶段声明兼容性而不删除文件、不修改autoload;必须手动remove旧包、配conflict防类冲突,并检查下游依赖是否仍引用旧包。

不能靠 replace 完成“原子组件替换”——它不删包、不换文件、不改 autoload,只在依赖解析阶段撒个谎。
为什么 replace 不是替换命令
replace 字段的作用仅限于 Composer 解析依赖树时的逻辑判断:它告诉 Composer “我(当前项目或包)已提供 old-vendor/package 的全部能力”,从而让 Composer 在安装阶段跳过拉取该包。但它完全不触碰 vendor 目录、不修改 autoload、不删除任何已有文件。
- 如果你的
composer.json里还留着"old-vendor/package": "^2.5"在require中,replace彻底失效——Composer 会坚持装它 - 即使你写了
"replace": {"old-vendor/package": "*"},只要旧包已存在 vendor 里,它就继续待着,类照样能被加载 - 运行时两个同名 class 同时存在?
Fatal error: Cannot declare class X立刻触发,replace不拦、不报、不救
真正完成替换的三步闭环操作
所谓“原子替换”,是指从声明、清理到防冲突的完整链路,缺一不可:
-
第一步:手动移除旧包 —— 必须执行
composer remove old-vendor/package,它会同步删vendor/、更新composer.json和composer.lock、重建 autoload 映射;别手删目录或硬改 JSON -
第二步:新包中配
conflict—— 在替代包(比如你的 fork 或轻量实现)的composer.json里加:"conflict": {"old-vendor/package": "*"};否则下游依赖仍可能悄悄把旧包拉回来 -
第三步:验证谁还在拖后腿 —— 运行
composer depends old-vendor/package,如果输出非空,说明有其他已安装包(如some-tool)仍硬依赖旧包;必须升级它、fork 打 patch,或改用repositories+ 强制require指向你的版本
CI 和新机器上失败的常见原因
本地跑通 ≠ 替换成功。最容易忽略的是 composer.lock 的固化效应和下游依赖的隐性绑定:
-
composer.lock里若还存着旧包路径和哈希,composer install会照单全收,无视你的replace声明 - 某个 dev-only 包(如
phpunit/phpunit)间接 require 了旧包,而它没出现在composer show --tree默认视图里,得加--dev参数才看得见 - 框架核心包(如
laravel/framework)千万别用replace去“覆盖”,它的服务容器绑定、自动发现、事件监听都依赖原包结构,运行时大概率挂掉
真正难的不是写那行 "replace",而是确认所有依赖路径都被切断、所有 autoload 映射都指向你提供的实现、所有 CI 环境都从干净状态重装——漏掉任意一环,“替换”就只是个幻觉。











