composer remove --dev 是 Composer 2.5+ 的原子操作,确保多包卸载全成功或全回退,但仅限卸载动作本身;需配合 composer install --no-dev 和 composer dump-autoload -o 清理 vendor 与 autoload-dev 残留,否则仍存 CI 失败风险。

composer remove --dev 不简化原子操作,它本身就是 Composer 2.5+ 引入的原子操作机制——但很多人误以为“批量删 dev 包 = 自动瘦身”,结果 vendor 没小、autoload 还残留、CI 构建失败。
真正需要关注的,是「原子性」在什么环节起效、什么环节失效,以及怎么配合其他命令形成闭环。
为什么 composer remove --dev a/b c/d 能原子执行?
Composer 2.5 开始把 remove 命令重构为事务式操作:所有包要么全部卸载成功,要么全部回退,不会出现只删了一半的状态。这依赖于内部的锁文件校验和临时 vendor 快照机制。
但注意:remove 的原子性仅作用于「卸载动作本身」,不延伸到后续的 autoload 清理、require-dev 手动残留、或间接依赖未释放等问题。
- 必须用空格分隔包名,不能加引号或逗号:
composer remove --dev phpunit/phpunit friendsofphp/php-cs-fixer - 如果某个包被
phpstan/phpstan显式 require,remove会直接报错并中止,不会删掉其他包 - 终端显示 “Successfully removed” 后,仍需手动检查
composer.json的require-dev是否真清空——某些中断场景下会残留空行或注释后未删的条目
composer depends 是查依赖链的唯一可靠方式
当你执行 composer remove --dev phpunit/phpunit 报错 “Package is required by another package”,别急着删 phpstan,先跑:
composer depends phpunit/phpunit
输出类似:
phpstan/phpstan 1.10.42 requires phpunit/phpunit (^9.5 || ^10.0)
这时你有两个选择:
- 升级
phpstan到不再 require PHPUnit 的版本(如 v2+ 已移除该依赖) - 改用
composer config --unset require-dev.phpunit/phpunit手动删配置项,再composer install --no-dev强制重建 —— 但这跳过了原子校验,风险更高
别信 composer show --tree,它只展示当前已装包的依赖树,不反映 require-dev 中未安装但声明了的间接依赖。
composer install --no-dev 和 composer dump-autoload -o 缺一不可
删完包只是第一步。vendor/ 目录里可能还躺着旧包的类文件,vendor/autoload_dev.php 也可能保留着已删包的加载规则,导致本地测试通过、CI 失败。
-
composer install --no-dev会彻底重建vendor/,只装require里的包,清掉所有 dev-only 文件 -
composer dump-autoload -o会重生成优化版自动加载映射,同时清除autoload-dev区块中已不存在的命名空间路径(比如"tests/": ["tests/"]对应的包已被删,但映射还在) - 漏掉
-o参数会导致 autoload 文件体积膨胀、PHP 启动变慢,尤其在 Laravel 或 Symfony 项目中明显
behat/behat,但 laravel/pint 可能悄悄拉了 phpunit/phpunit 作为子依赖——这种隐式链不会被 remove 检测,也不会出现在 composer depends 输出里。要发现它,得靠 composer show --tree | grep phpunit 或 CI 阶段的 composer outdated --direct --no-dev。


















