Composer 是 PHP 生态的基石,但很多细节大家用法不一样。写几个我自己固定遵守的习惯,以及踩过的坑。
一、版本约束用 ^,不用 * 或 >=
{
"require": {
"monolog/monolog": "^3.5",
"guzzlehttp/guzzle": "*",
"symfony/console": ">=5.0"
}
}
* 和 >= 的问题是没有上界。哪天依赖发了个 6.0,破坏性变更直接把你的项目搞挂,而且是在你什么都没改的情况下。
^ 的语义是"允许不破坏兼容的更新":^3.5 意味着 >=3.5.0 <4.0.0。这个范围符合同一个主版本内不破坏兼容的约定(语义化版本)。
但语义化版本是约定,不是保证。 有些库会在小版本里偷偷改行为。所以还有第二条。
二、composer.lock 一定要提交到版本控制
这个基本是共识了,但还是见过不提交的。理由通常是"我们用的都是库,没有应用"——就算是库,CI 也需要可复现的环境。
lock 文件锁定了整棵依赖树的精确版本(包括间接依赖)。没有它,composer install 每次拿到的可能是不同的东西。
对应地:
- 部署用
composer install(严格按 lock 装) - 只有主动升级依赖时用
composer update
我见过部署脚本里写 composer update 的,那等于每次部署都在随机升级依赖,出问题都不知道是哪次引入的。
三、composer update 单独一次只升一个包
# 不要这样
composer update
# 这样
composer update monolog/monolog
全量 update 会一次性升级几十个包,如果测试挂了,你得在这个大杂烩里找原因。
单独升一个包,跑测试,提交,再升下一个。慢,但出了问题定位成本低得多。
如果确实需要全量更新(比如要升 PHP 大版本),那就单独开一个分支,专门做这件事。
四、--dev 和 --no-dev 要分清
# 生产部署
composer install --no-dev --optimize-autoloader --no-interaction
# 本地开发
composer install
--no-dev 不装 require-dev 里的东西(PHPUnit、PHPStan、faker 这些)。生产环境装它们既浪费空间又增加攻击面。
--optimize-autoloader(-o)把 PSR-4 的类映射转成静态数组,省掉运行时查找。生产环境必开。
--no-interaction(-n)避免 CI 里因为某个交互提示卡住。
有个坑:--optimize-autoloader 之后,如果代码里有类名大小写不对,会在运行时找不到类,而开发环境(不优化)是能跑通的。因为优化后是精确匹配。所以 CI 里最好用生产的参数装一遍跑测试。
五、定期跑 composer audit
composer audit
会列出依赖里的已知安全问题(CVE)。Composer 2.4 之后内置了。
老版本用 roave/security-advisories:
composer require --dev roave/security-advisories:dev-latest
它会在 update 时直接阻止你装已知有漏洞的版本,比较强硬但有效。
六、composer outdated 而不是盲目升级
composer outdated --direct
只列直接依赖,输出清晰。看哪些落后很多版本,评估升级成本。
--strict 模式在 CI 里用,有落后版本就返回非零退出码。不过一般项目很难保持全绿,所以我只用在核心依赖上。
七、什么时候不该加依赖
这条可能是最重要的。
我现在加依赖之前会问:
这个库有多少代码? 有的库就为了一个函数,拉进来几十个文件。
它自己依赖了什么? composer show --tree 看看。有些库会间接拉进整个 symfony 组件集。
最近一次提交是什么时候? 三年没更新的库要小心,可能不兼容新版本 PHP。
有没有更简单的办法? 比如只是想把数组转成 XML,二十行代码能写完,为什么要引一个库?
我删过的依赖里,印象最深的是为了"格式化文件大小"(1.5 MB 这种)引入的库。那个库有依赖,还要配一个 helper 函数。后来手写了八行:
function humanSize(int $bytes, int $precision = 1): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = 0;
$size = (float) $bytes;
while ($size >= 1024 && $i < count($units) - 1) {
$size /= 1024;
$i++;
}
return round($size, $precision) . ' ' . $units[$i];
}
少一个依赖就少一个升级负担、少一个安全面。
但反过来,像 guzzle、monolog、symfony/console 这种,自己写不现实,该用就用。
八、私有包和认证
内网包用 repositories 配:
{
"repositories": [
{
"type": "vcs",
"url": "git@gitlab.internal:team/package.git"
}
]
}
或者用 path 类型做本地开发:
{
"repositories": [
{ "type": "path", "url": "../my-package", "options": { "symlink": true } }
]
}
symlink: true 之后,改本地包代码立即生效,不用每次 composer update。
不要把仓库密码写进 composer.json。 用 auth.json(加入 .gitignore)或者环境变量:
export COMPOSER_AUTH='{"http-basic":{"repo.internal":{"username":"x","password":"y"}}}'
CI 里用环境变量注入。
九、CI 里加缓存
Composer 下载依赖很慢,CI 里一定要缓存:
# GitHub Actions
- uses: actions/cache@v4
with:
path: ~/.composer/cache
key: composer-${{ hashFiles('composer.lock') }}
或者用 ramsey/composer-install 这个 action,它把这些都封好了。
十、composer validate
composer validate --strict
检查 composer.json 格式、字段拼写、版本约束语法。放 CI 里能拦住不少低级错误(比如 requrie 拼错)。
加了 --strict 之后,连"没写 description""没写 license"都会报。库项目建议开,应用项目可能太严。
最后
Composer 本身设计得挺好的,问题是它的便利性让人容易滥用。
一个 composer require 三秒钟,引入的长期维护成本可能要三年。
我现在的原则:能自己写的(几十行以内)就自己写,宁愿多写点代码也不要多一个依赖。 只有涉及到协议实现、加密、复杂解析这些,才找成熟的库。