composer-unused 是目前最靠谱的检测未使用 composer 包的工具,通过静态扫描项目代码中实际调用痕迹(如 use、new)来识别冗余包,而非依赖 autoload 或 require 声明。

composer-unused 能直接列出没被引用的包
它不是 Composer 内置命令,但目前最靠谱的检测工具。原理是扫描 vendor/ 下所有已安装包的源码,再反向检查你的项目代码(src/、tests/ 等)里有没有 use、new、class_exists 这类实际调用痕迹。不靠 autoload 配置或 require 声明来判断——那些只是“可能用”,不是“真在用”。
安装和运行很简单:
composer require --dev phpstan/phpstan composer require --dev composer-unused/composer-unused vendor/bin/composer-unused
常见错误现象:composer-unused 默认只扫 src/,如果你的业务逻辑分散在 app/ 或 lib/,会漏报;另外它默认跳过测试文件,但有些包只在 tests/ 里用,得加 --scan-tests 才能识别。
- 必须先跑
composer install,否则vendor/不全,结果不准 - 如果项目用了动态类名(比如
new $className),composer-unused无法静态分析,这类包会被误标为“未使用” - 它不检测
require-dev包是否只在 CI 脚本里调用(比如phpunit在.github/workflows/test.yml里用),这种得人工核对
为什么不能只看 composer.json 的 require 列表?
因为 require 里写的只是“声明依赖”,不代表当前代码还在用。比如你曾经用过 guzzlehttp/guzzle 发请求,后来改用 curl 手写,但忘了删 composer.json 里的那一行——它就变成冗余项。
更麻烦的是传递依赖:A 包 require B,B require C,你删了 A,但 C 可能还留在 vendor/ 里,且没被任何地方引用。Composer 自己不会主动清理这种“孤儿包”,composer update 也不管它。
-
composer show --tree只能看依赖关系图,看不出哪些叶子节点实际没被调用 -
composer depends <package></package>能查谁依赖它,但查不到“没人依赖它,而且我也没用它” - 手动 grep 类名太容易漏:比如
Str::是illuminate/support的,但你搜use IlluminateSupportStr可能找不到,因为用了use IlluminateSupportStr as Helper
删包前一定要确认是否被自动加载机制“悄悄用到”
有些包虽然没显式 use,但通过 Composer 的 autoload 触发了副作用:比如 symfony/polyfill 系列会在加载时注册函数别名;monolog/monolog 的 MonologHandlerStreamHandler 可能被配置文件里字符串反射加载。
这类情况 composer-unused 通常检测不到,得结合日志和运行时验证:
- 删包后运行
composer dump-autoload -o,如果报Class not found,说明有 autoload files 或 psr-4 映射触发了加载 - 执行一次完整请求(比如跑一个 API 接口或 Artisan 命令),观察是否抛出
ReflectionException或Call to undefined function - 特别留意
config/app.php(laravel)、services.yaml(Symfony)这类配置文件,它们常以字符串形式引用类,静态扫描看不见
composer remove 之后记得检查 lock 文件和 CI 缓存
composer remove <package></package> 会更新 composer.json 和 composer.lock,但 CI 流程里如果用了缓存的 vendor/,旧包可能还在磁盘上,导致本地测不出问题,CI 却失败。
最容易被忽略的是 composer.lock 里残留的包信息:有时 remove 没清干净,或者你手动删了 composer.json 行但没跑 composer update,lock 文件仍保留着 hash 和 require 块。
- 删完立刻跑
composer install --no-dev(模拟生产环境),确认没报错 - Git 提交前检查
composer.lock,搜索刚删的包名,确保整段都消失了 - github Actions / gitlab CI 中,清掉
vendor/缓存或加cache: false跑一次全量安装
真正难的不是发现冗余包,而是确认它没被任何隐式路径调用——配置、反射、事件监听、甚至某些扩展的 extension_loaded() 检查,都可能让它继续生效。每次删之前,最好在日志里埋个钩子,看看它最近一次被加载是什么时候。