Composer 2.0 起采用自研 Solver 类,融合 DFS、单元传播与 CDCL,将依赖建模为布尔命题并用逻辑子句约束版本互斥与规则。

Composer 的依赖解析不是“试版本”,而是逻辑证明
Composer 从 2.0 开始用的不是传统回溯+重试,也不是调用 MiniSat/Z3 这类外部 SAT 求解器,而是自研的 Solver 类——一个融合 DFS 搜索、单元传播(unit propagation)和冲突驱动子句学习(CDCL)的约束引擎。它不把 monolog/monolog:2.9.0 当成普通字符串,而是建模为一个布尔命题:选中即 true,未选即 false;同一包不同版本互斥,靠逻辑子句硬性约束。
你写下的每条规则都会被翻译成逻辑条件:
-
"require": {"symfony/console": "^6.0"}→ 必须选中6.0.x中至少一个版本,且不能选5.x或7.x "conflict": {"php": " → 所有 <code>php版本-
"replace": {"old/package": "*"}→ 加入¬(new/package ∧ old/package)这样的排斥子句
最终所有约束汇成一个巨大的合取范式(CNF)公式,求解器的任务就是找出一组 true/false 赋值,让整个公式为真——这组赋值,就是 composer.lock 里那一行行精确版本号的数学依据。
为什么 composer update 卡在 “Resolving dependencies…”?
这不是卡住,是正在做全局逻辑推演。搜索空间随包数量指数增长,尤其当你同时 require 多个大生态包(如 Laravel + Symfony + Doctrine + Phpstan),每个包又带几十个可选版本时,组合爆炸极容易发生。
常见诱因包括:
- 手动改了
composer.json但没删composer.lock,Solver 会以 lock 为起点做“最小变更”,反而锁死路径,掩盖真实可行解 - 混用了不兼容的稳定性标记,比如同时 require
"monolog/monolog": "dev-main"和"phpunit/phpunit": "^9.6"(后者要求 stable) - 平台配置(
config.platform.php)与实际运行环境脱节,导致 solver 错误排除大量合法版本
实操建议:
- 加
-v看最后尝试的包链:composer update -v | tail -30 - 局部更新比全局更可控:
composer update --with-dependencies my/package - 临时清空 lock 再试:
rm composer.lock && composer update(仅限开发环境)
composer install 为什么几乎秒出结果?
因为 composer install 不运行求解器,它只校验 composer.lock 是否满足当前 composer.json 的约束。这个文件不是缓存,也不是日志,而是上一次 Solver 成功输出的「已验证可行解」——包含每个包的完整版本号、源类型、SHA256 校验和、安装路径及依赖映射。
所以它的流程极简:
- 读取
composer.lock - 检查其中每个包是否仍满足
composer.json中的require规则(例如"^6.0"是否覆盖了 lock 里的6.4.2) - 校验包完整性(dist hash / source commit)
- 按 lock 记录逐个安装
一旦发现 lock 中某个包不满足新写的 require(比如你把 "symfony/console": "^5.4" 改成 "^6.0",但 lock 还锁着 5.4.23),就会报错:Your lock file does not contain a compatible set of packages.
理解冲突报错的本质:不是“找不到版本”,是“数学上无解”
当你看到 Your requirements could not be resolved to an installable set of packages,这不是 Composer “没努力找”,而是它的求解器已经穷尽所有逻辑可能,并给出形式化反证:不存在一组变量赋值,能让全部约束同时成立。
典型场景:
- A 包要求
"laravel/framework": "^10.0",B 包要求"laravel/framework": "^9.0",且二者都出现在同一依赖路径上 - 你设了
"config.platform.php": "8.0",但某包的require是"php": "^8.1",solver 直接剪掉所有 php 8.1+ 的分支 - 某个包通过
provide声明替代了另一个包,但替代逻辑与 conflict 规则形成闭环矛盾
此时别急着降级或删包——先用 composer why-not vendor/package:version 定位最小冲突集,它返回的那几行,就是 SAT 求解器“数学证明无解”时提取出的核心矛盾链。
真正难的从来不是怎么装包,而是看懂 solver 给你的那句报错到底在说哪个逻辑断言崩了。










