composer.json 应该怎么写?PHP 项目的依赖和版本管理基础
对于 PHP 项目来说,composer.json 并不是一个简单的“安装软件包列表”,而是整个项目依赖关系的声明文件。项目需要什么 PHP 版本,需要哪些第三方库,生产环境和开发环境分别依赖什么,以及项目应该如何自动加载自己的代码,都可以在这个文件中定义。
理解 composer.json,实际上是在理解现代 PHP 项目是怎样管理依赖的。
一个最简单的 PHP 项目,可以从这样的 composer.json 开始:
{
"name": "example/my-project",
"description": "A PHP application",
"type": "project",
"require": {
"php": "^8.2",
"monolog/monolog": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
这里最重要的是 require 和 require-dev。
require 表示项目运行所需要的依赖。例如一个网站运行时需要日志库、数据库组件或者 HTTP 客户端,这些应该放在 require 中。
require-dev 则用于开发和测试。例如 PHPUnit、PHPStan、PHP_CodeSniffer 等工具通常只在开发环境使用。生产服务器执行 composer install --no-dev 时,可以不安装这些开发依赖。
这两个字段的区别并不是“重要依赖”和“不重要依赖”,而是“生产运行时需要”与“开发阶段需要”。
Composer 的核心机制是依赖解析。
例如项目声明:
{
"require": {
"monolog/monolog": "^3.0"
}
}
这并不是告诉 Composer “永远安装 Monolog 3.0.0”,而是在告诉 Composer,项目接受符合这个版本约束的 Monolog 版本。
Composer 随后会分析这个包自己的依赖,以及其他直接依赖和间接依赖之间的关系,寻找一个能够同时满足所有版本约束的依赖集合。
这里也需要纠正一个常见误解。
如果项目 A 需要某个库的 ^1.0,项目中的另一个依赖需要同一个库的 ^2.0,Composer 并不会简单地“优先选择最新版本”然后强行安装。
如果两个约束无法同时满足,Composer 通常会报告依赖冲突。解决方案是升级或降级其中一个依赖、调整版本约束,或者寻找兼容的版本组合。
这也是 Composer 和简单复制 PHP 类库最大的区别。
版本号写法尤其重要。
1.2.3 表示一个非常具体的版本约束,而 ^1.2.3 和 ~1.2.3 则允许一定范围内的版本。
对于 Composer 常见的语义版本规则,可以简单理解为:
^1.2.3
允许 1.x 的兼容更新,但不进入 2.0.0
~1.2.3
允许 1.2.x 的更新,但通常不会进入 1.3.0
1.2.3
只允许这个版本
不过,实际版本约束还受到 Composer 对不同主版本以及零版本号规则的处理影响,所以不能把 ^ 永远理解成“只要小于下一个整数版本”。对于正常遵循语义化版本的库,^1.2.3 最常见的含义是允许 >=1.2.3 <2.0.0。
很多项目最初会把版本写得非常死,例如:
"monolog/monolog": "3.0.0"
这样做确实可以限制版本变化,但对于普通应用项目而言,通常没有必要把所有依赖都固定到一个具体版本。Composer 本身就通过 composer.lock 来记录经过解析后的具体版本。
这两个文件的职责不同。
composer.json 描述的是“项目允许使用什么依赖”。
composer.lock 描述的是“这一次解析之后,实际使用了哪些具体版本”。
因此,对于应用程序项目,通常应该把 composer.lock 一起提交到 Git。
例如开发人员运行:
composer update
Composer 会重新解析依赖,并按照 composer.json 中的约束寻找新的可用版本,然后更新 composer.lock。
而在服务器部署时,通常使用:
composer install --no-dev
如果存在 composer.lock,Composer 会根据锁定文件安装已经确定的版本,而不是每次部署都重新选择一套依赖。
这也是为什么不要把“composer.json 改了但 composer.lock 没改”当成正常的版本管理方式。如果修改了依赖声明,通常应该让 Composer 重新解析相关依赖,并把更新后的 composer.lock 一起提交。
如果只想更新某一个依赖,也可以针对具体包执行更新,而不是每次让整个依赖树全部升级。
Composer 的另一个重要功能是自动加载。
现代 PHP 项目通常不需要手动写几十个 require 或 require_once。例如:
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
表示 App 命名空间下的类可以按照 PSR-4 规则从 src/ 目录自动加载。
项目入口文件通常只需要:
require __DIR__ . '/vendor/autoload.php';
然后就可以使用 Composer 管理的类。
如果修改了 autoload 配置,需要重新生成自动加载文件:
composer dump-autoload
生产环境还可以使用:
composer dump-autoload -o
优化自动加载。
这里也需要纠正原文中的一个概念:optimize-autoloader 并不是“加快依赖解析速度”。它主要影响的是 Composer 生成和运行时的自动加载机制,而不是 Composer 如何解决版本冲突。
如果希望把自动加载优化作为项目配置的一部分,可以使用:
{
"config": {
"optimize-autoloader": true
}
}
大型生产项目通常也会在部署时使用优化自动加载。
PHP 版本约束同样应该写进 composer.json。
例如:
"require": {
"php": "^8.2"
}
这意味着项目的运行环境必须满足相应的 PHP 版本要求。
如果开发机器运行 PHP 8.3,而生产服务器只有 PHP 8.1,Composer 就应该尽早暴露这个问题,而不是等程序上线以后才发现语法或者依赖无法运行。
还有一个容易混淆的配置是 config.platform。
正确的结构应该是:
{
"config": {
"platform": {
"php": "8.2.0"
}
}
}
而不是把 platform 直接放在 composer.json 顶层。
config.platform 的作用,是告诉 Composer 在依赖解析时按照指定的平台版本进行判断。例如开发人员电脑安装的是 PHP 8.3,但生产环境实际上是 PHP 8.2,那么可以通过 platform 设置让 Composer按照 PHP 8.2 的兼容条件解析依赖。
不过它并不会把电脑上的 PHP 8.3 “变成” PHP 8.2。
这一点非常重要。
如果服务器实际上只有 PHP 8.1,那么 composer.json 写:
"platform": {
"php": "8.2.0"
}
并不会 magically 把 PHP 8.1 升级成 PHP 8.2。它只是改变 Composer 的依赖解析视角。因此部署前仍然必须检查服务器实际 PHP 版本。
Composer 还会把 PHP 扩展当作平台依赖。
例如:
"require": {
"php": "^8.2",
"ext-curl": "*",
"ext-mbstring": "*",
"ext-gd": "*"
}
这里的 ext-curl、ext-mbstring 和 ext-gd 不是普通 Composer 软件包,而是 PHP 环境提供的扩展。
Composer 可以检查它们是否存在,但 Composer 并不会像安装普通 PHP 包一样替你安装这些扩展。
遇到“为什么 Composer 安装失败”的问题时,可以使用:
composer show --platform
查看当前 PHP 环境和已经提供的平台依赖。
处理复杂依赖冲突时,composer why 和 composer why-not 非常有用。
例如:
composer why monolog/monolog
可以帮助查看为什么项目需要这个包。
而:
composer why-not monolog/monolog 3.0.0
可以用于分析为什么某个指定版本无法安装。
这比遇到冲突以后直接删除 composer.lock 或者随便更换一个依赖包更加可靠。
对于私有依赖,也可以在 composer.json 中声明自定义仓库。例如公司内部维护的 Git 仓库:
{
"repositories": [
{
"type": "vcs",
"url": "https://gitlab.example.com/team/private-package.git"
}
],
"require": {
"company/private-package": "^1.0"
}
}
不过 repositories 的作用主要是告诉 Composer 去哪里寻找包,并不等于把 GitLab、GitHub 等服务的账号密码直接写进 composer.json。
认证信息应该通过 Composer 的认证机制、环境变量、CI/CD 的安全凭据等方式处理。不要把访问令牌、密码或者私钥直接提交到 Git 仓库。
Composer 还支持 scripts,可以把项目中的常用操作统一起来。
例如:
{
"scripts": {
"test": "phpunit",
"analyse": "phpstan analyse",
"cs": "phpcs"
}
}
之后可以直接运行:
composer test
composer analyse
composer cs
这样团队成员不需要记住一大串命令。
不过,自动执行脚本也需要谨慎设计。尤其是 post-install-cmd、post-update-cmd 这类生命周期脚本,如果里面放置大量修改文件或者执行外部程序的操作,部署环境应该提前审查,避免第三方依赖安装时产生不必要的副作用。
安全方面,现在 Composer 项目应该把依赖审计作为正常开发流程的一部分。
可以运行:
composer audit
检查当前依赖是否存在已知安全漏洞。
如果发现漏洞,不能简单理解成“Composer 有问题”。Composer 只是把依赖关系和已知安全公告呈现出来,真正需要处理的是存在问题的第三方包,以及是否存在可以升级到的安全版本。
一个实际项目的 composer.json 通常不需要写得非常复杂。
例如一个普通 PHP 网站可能已经足够:
{
"name": "mnewstv/website",
"description": "MNewsTV website",
"type": "project",
"require": {
"php": "^8.2",
"ext-curl": "*",
"ext-mbstring": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
}
}
然后执行:
composer install
Composer 会建立 vendor/ 目录,并生成相应的自动加载文件。
通常不需要把 vendor/ 目录提交到 Git,因为它可以根据 composer.json 和 composer.lock 在部署时重新安装。
一个比较规范的 PHP 项目流程通常是这样的:
开发阶段修改 composer.json,然后执行 Composer 命令解析依赖;Composer 更新 composer.lock;开发人员测试程序;composer.json 和 composer.lock 一起提交到版本控制系统;生产环境执行 composer install --no-dev;最后根据部署环境生成优化后的 autoload。
这样,项目依赖就从“某个人电脑上安装了哪些 PHP 包”,变成了可以被代码库明确描述、重复安装和部署的工程配置。
理解 composer.json 最重要的不是记住几十个字段,而是理解几个文件之间的关系:composer.json 负责声明项目需要什么,composer.lock 负责锁定这次解析得到的具体版本,vendor 负责保存实际安装的依赖,vendor/autoload.php 负责把这些类加载进 PHP 程序。
当这几个概念分清楚以后,Composer 就不再只是一个“下载 PHP 插件的工具”,而成为 PHP 项目控制依赖、版本、自动加载、开发工具和部署环境的重要基础设施。对于一个需要长期维护的 PHP 项目来说,真正麻烦的从来不是安装第一个包,而是几年之后依然能够知道这个项目为什么依赖它、依赖哪个版本,以及升级以后会发生什么。
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
分享 Facebook | X | WhatsApp | LinkedIn
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP