provide字段声明当前包提供某虚拟包能力,仅参与依赖解析而不安装代码,必须写在被require的实现包中,键为标准虚拟包名,值可为版本号或通配符。

provide 字段声明“我实现了这个抽象能力”
虚拟包本身没有代码、不下载、不出现在 vendor/ 里,provide 是唯一让它参与依赖解析的方式。它告诉 Composer:“当前这个包(或项目)能当 psr/log、corp/base-contract 这类抽象包来用”。
常见错误现象:composer install 后发现虚拟包对应的实际实现没被装进来,或下游包报“找不到 psr/cache-implementation”——大概率是 provide 写在了错误位置(比如写进了子包的 composer.json,但主项目没 require 它)。
-
provide必须写在**被 require 的那个包**的composer.json顶级字段中,根项目(即你自己的composer.json)里写provide是无效的 - 键名必须是标准虚拟包名(如
"psr/log": "3.0.0"),不能带空格或非法字符 - 值可以是具体版本(
"2.0.0")、版本范围("^2.0")或通配符("*"),但通配符会削弱版本约束力,慎用 - 它不阻止别人
require真实实现,只解决“满足接口要求”这一层;想彻底排除某个实现,得靠replace+conflict
replace 字段声明“我不需要你,我就是你”
replace 不是“替换文件”,而是向 Composer 声明:当前项目已覆盖某个包的功能,因此不必再安装它。它常用于用内嵌实现或轻量版替代重型依赖,比如用自定义 mbstring polyfill 替掉 symfony/polyfill-mbstring。
常见错误现象:composer install 后旧包还在 vendor/ 里,甚至运行时报 Fatal error: Cannot declare class——根本原因是只写了 replace,没做三件事:手动 composer remove、加 conflict、检查下游是否还在 require 旧包。
- 必须先执行
composer remove old-vendor/package,否则 Composer 会坚持安装它(replace在 require 存在时完全失效) - 必须在同一个
composer.json中配"conflict": {"old-vendor/package": "*"},否则类加载器看到两个同名类直接崩溃 - 用
composer depends old-vendor/package查谁还在依赖它;只要输出非空,就说明有“钉子户”,得升级或 fork 那个包 -
replace对框架核心包(如laravel/framework)基本无效——Composer 可能放过,但自动加载和服务容器绑定会立刻翻脸
provide 和 replace 混用的真实场景
单独用 provide 只能“满足契约”,单独用 replace 只能“挡掉安装”,真正要落地一个内部基础库封装方案(比如统一日志、异常、HTTP 客户端),往往得组合用。
例如:你有个私有框架项目 corp/framework,它内置了 PSR-3 日志实现,并希望所有业务组件都基于 corp/logging-contract v2.x 开发。这时你会:
- 在
corp/framework的composer.json里写:"provide": {"corp/logging-contract": "2.0.0", "psr/log": "3.0.0"} - 同时写:
"replace": {"monolog/monolog": "*"}(假设你不用 Monolog,全走自研) - 并配:
"conflict": {"monolog/monolog": "*"},防类冲突 - 下游业务组件只需
"require": {"corp/logging-contract": "^2.0"},Composer 就能识别出corp/framework已提供且满足
关键点在于:这种组合不是一次配置就完事,每次引入新组件前,都要用 composer show --tree 确认依赖图里没意外拉进被 replace 的包——因为 lock 文件可能缓存旧路径,掩盖真实问题。











