执行 composer update vendor/package 默认不升级间接依赖,必须加 --with-all-dependencies 才会重算整个依赖树并更新所有可解的间接依赖。

composer update vendor/package 默认不升级间接依赖
当你执行 composer update monolog/monolog 时,Composer 的行为其实相当“克制”。它只会更新 monolog/monolog 这个包本身,以及那些被它直接声明在 require 里的依赖(比如 psr/log)。至于那些只被其他包引用、你的项目里没有显式声明的间接依赖(例如,symfony/polyfill-php80 被 monolog 的某个子包带进来),Composer 默认是不会去碰的。
这种“精准打击”的设计初衷是好的,但实际操作中却容易埋下隐患。举个例子,新版本的 monolog 可能要求 psr/log ^2.0,而你的 composer.lock 文件里还锁着 1.1.0。结果呢?包是更新了,但运行时却可能抛出 Class not found 或者方法不存在的错误。问题出在哪?往往不是目标包没升级成功,而是它的某个间接依赖被“卡”在了旧版本。
- 一个间接依赖是否会被升级,完全取决于它是否出现在你项目
composer.json的require或require-dev区块里。 - 如果没写,那它就是个“过客”,
update vendor/package这个命令默认不会去动它,哪怕上游包已经悄悄弃用了它的某个版本。 - 想让这些“隐形”的依赖也跟着动起来?答案很明确:必须加上
--with-all-dependencies这个选项,否则它们大概率会原地不动。
--with-all-dependencies 是唯一可靠方式
这里有个常见的误解:--with-all-dependencies 并不是要“升级你项目里的所有包”。它的核心作用是,将目标包及其整个依赖树中所有可解析的包,都纳入版本重新计算的范围——这其中就包括了那些你没在 composer.json 里声明的间接依赖。
举个例子,你运行 composer update guzzlehttp/guzzle --with-all-dependencies。这时,Composer 不仅会检查 Guzzle 本身,还会同时检查 psr/http-client、ralouphie/getallheaders、symfony/polyfill-intl-idn 等所有被 Guzzle 带进来的间接依赖包,并依据当前的版本约束,尝试将它们升级到最新的兼容版本。
- 别担心,它依然受到你
composer.json中版本约束的限制,不会越界升级主版本(比如从^2.0强行跳到3.x)。 - 如果某个间接依赖被多个顶层包共同引用(比如
symfony/event-dispatcher同时被lara vel/framework和monolog使用),Composer 会努力寻找一个所有调用方都能接受的版本。实在找不到,它才会报错。 - 升级前心里没底?加上
--dry-run选项预览一下:composer update guzzlehttp/guzzle --with-all-dependencies --dry-run,这样就能清清楚楚看到哪些间接依赖会被动到。
间接依赖升级失败时,先查谁在拦路
升级时最头疼的,莫过于看到 Your requirements could not be resolved 这类报错。表面上看是 Guzzle 升级失败了,但问题的根源,很可能藏在某个间接依赖里——它被另一个包死死锁定了版本,导致整个依赖树无法协调。
遇到这种情况,别靠猜。Composer 提供了两个非常实用的命令来定位问题:
composer why-not psr/http-client:2.0—— 直接查询是哪个包在阻止psr/http-client升级到 v2.0 版本。composer why guzzlehttp/guzzle—— 查看你的项目里有哪些包在引用 Guzzle,确认它是不是被lara vel/framework这类大型框架包“兜底”管理着。- 如果排查后发现,是某个仅用于开发的包(比如
phpunit/phpunit)在拖后腿,可以尝试临时加上--no-dev选项再执行更新命令。
这里有个关键点需要注意:composer outdated 命令显示的“可升级”列表,对间接依赖是无效的。因为它只扫描你 composer.json 里明确写明的包,而不会去扫描 composer.lock 文件里躺着的那些“幽灵依赖”。
真正锁死间接依赖,只能靠显式声明
如果你想让某个关键的间接依赖版本彻底稳定下来,不乱动,Composer 并没有提供一个“全局冻结”的开关。最稳妥、也是唯一可靠的方法,就是在 composer.json 的 require 区块里,把它显式地声明出来并锁定版本。
- 比如,
guzzlehttp/guzzle带来了psr/http-client ^1.0,但你的业务代码也直接调用了这个包。那么,最保险的做法就是手动加一行:"psr/http-client": "1.0.3"。 - 这样一来,无论 Guzzle 后续如何升级,
psr/http-client都会被强制钉在1.0.3这个版本上。对于 Composer 来说,它从此就变成了一个需要被严肃对待的顶层依赖。 - 需要警惕的是,慎用
replace这类高级配置来屏蔽包——这很容易引发依赖求解器的混乱和崩溃,尤其是在持续集成(CI)这类自动化环境中。
说到底,间接依赖的“隐形”特性是一把双刃剑。省心的时候,它自动跟随,悄无声息;一旦出事,它又最难排查。还有一个容易被忽略的细节:composer.lock 文件里有时会混入已删除包的残留记录,这会导致针对单个包的更新行为出现异常。因此,定期运行 composer validate --strict 命令检查一下项目状态,是个不错的习惯。
