浪白的博客の小站
首页项目归档照片墙音乐灵境说说杂谈友链关于
封面

Composer 用了这么多年,几个我认为值得坚持的习惯

写作时间:2026-04-27 19:15:00
# PHP
# Composer
# 工程实践

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 三秒钟,引入的长期维护成本可能要三年。

我现在的原则:能自己写的(几十行以内)就自己写,宁愿多写点代码也不要多一个依赖。 只有涉及到协议实现、加密、复杂解析这些,才找成熟的库。

avatar

浪白

邪恶的浪白

RECOMMENDED

context 传参这件事,我踩过的三个坑

2026-05-06 20:05:00

Go 的 error 处理,用久了反而觉得还行

2026-04-19 22:15:00

用 Go 写了个日志清理小工具

2026-06-18 21:10:00

Table of Contents