2016年,我已经用Composer三年了。

2013年,我第一次接触Composer。那时候,PHP的依赖管理还是一片混乱,每个项目手动下载库文件,手动require,版本升级全靠复制粘贴。Composer的出现,简直是PHP开发者的福音。

从那以后,Composer就成了我PHP开发中不可或缺的工具。每个新项目,第一件事就是composer init,然后composer require各种库。三年下来,我用Composer管理过几十个项目,上百个依赖。

但是,用了三年,我也踩了不少坑。今天,就来聊聊这些坑,以及我总结出来的经验。

一、Composer是什么

先简单介绍一下Composer,给还没用过的朋友。

Composer是PHP的依赖管理工具,类似于Node.js的npm,Python的pip,Ruby的bundler。它可以帮你:

  • 管理项目的依赖库,自动下载和安装
  • 处理依赖之间的版本关系,自动解决冲突
  • 自动生成autoloader,不用手动require
  • 管理项目的自动加载

用Composer之前,PHP项目引入第三方库,通常是这样的:

  1. 去官网下载库的zip包
  2. 解压到项目的某个目录
  3. 在代码里require库的入口文件
  4. 如果这个库还依赖其他库,还要手动下载那些库
  5. 升级的时候,重新下载,重新覆盖

这个过程非常痛苦,特别是依赖多的时候,版本冲突、文件覆盖、路径问题,各种麻烦。

用了Composer之后,这些问题都解决了。你只需要在composer.json里声明你需要哪些库,然后运行composer install,Composer就会自动帮你下载所有依赖,处理版本关系,生成autoloader。你只需要在代码里require 'vendor/autoload.php',就能用所有的库了。

Composer的出现,是PHP生态的一个里程碑。它让PHP的依赖管理变得和其他现代语言一样简单,也促进了PHP社区的繁荣。现在,几乎所有的PHP框架和库,都支持Composer。

二、版本约束的正确姿势

用Composer,最容易踩的坑,就是版本约束。

很多人写版本约束,喜欢写"vendor/package": "dev-master",或者"vendor/package": "*"。这样写,看起来很方便,永远用最新版。但是,这是非常危险的。

因为,库的新版本,可能会有不兼容的改动(Breaking Change)。如果你用dev-master或者*,每次composer update,都会拉取最新的代码,可能突然就不兼容了,项目就挂了。

我刚用Composer的时候,就踩过这个坑。有一个项目,依赖写的是"monolog/monolog": "dev-master"。有一天,我运行composer update,Monolog升级了,API变了,我的代码里用的方法被移除了,项目直接500错误。查了半天才发现是Monolog升级的问题。

从那以后,我再也不用dev-master*了。

那么,版本约束应该怎么写呢?

Composer支持多种版本约束格式:

1. 精确版本 "vendor/package": "1.2.3" — 只用1.2.3这个版本,不会升级。最安全,但是太死板,不能享受bug修复和小版本更新。

2. 通配符 "vendor/package": "1.2.*" — 用1.2.x的最新版本,不会升级到1.3。比较安全,能享受1.2系列的bug修复。

3. 波浪号(~) "vendor/package": "~1.2" — 等价于>=1.2,<2.0,会升级到1.x的最新版本,但不会升级到2.0。"~1.2.3"等价于>=1.2.3,<1.3

4. 脱字符(^) "vendor/package": "^1.2.3" — 等价于>=1.2.3,<2.0,和~1.2.3类似,但是更遵循语义化版本。对于1.0以下的版本,^0.3等价于>=0.3.0,<0.4

推荐用法

  • 对于遵循语义化版本的库,用脱字符^,比如"^1.2.3"。这样能升级到1.x的最新版本,享受新功能和bug修复,又不会升级到不兼容的2.0。
  • 对于不遵循语义化版本的库,用通配符或者波浪号~,限制在小版本内,比如"1.2."或者"~1.2.3"
  • 绝对不要用dev-master*,除非你明确知道自己在做什么。

另外,还有一个重要的点:composer.json里的版本约束,是"可以接受的版本范围",而实际安装的版本,是由composer.lock文件锁定的。这就引出了下一个坑——lock文件。

三、composer.lock的重要性

很多人,特别是新手,会忽略composer.lock文件,甚至把它加到.gitignore里。这是一个非常大的错误。

composer.lock文件,记录了项目当前实际安装的所有依赖的精确版本号。有了这个文件,任何人在任何环境运行composer install,都会安装和你完全一样的版本,不会出现"我这里能跑,你那里不能跑"的问题。

如果没有composer.lock,运行composer install的时候,Composer会根据composer.json里的版本约束,解析出最新的符合条件的版本,然后安装。这就意味着,不同时间、不同环境安装的版本可能不一样,可能会出现版本不一致导致的问题。

我踩过这个坑。以前有个项目,团队里有人把composer.lock加到了.gitignore里。结果,我本地开发的时候,依赖的版本是A,测试环境部署的时候,依赖的版本是B,因为B比A新,有个小改动,导致测试环境出了bug,而我本地复现不了。查了很久,才发现是版本不一致的问题。

从那以后,我所有的项目,都会把composer.lock提交到版本库,绝对不会忽略它。

最佳实践

  • composer.jsoncomposer.lock都要提交到版本库
  • 团队成员拉取代码后,运行composer install(不是update),安装lock文件里锁定的版本
  • 只有当你明确要升级某个依赖的时候,才运行composer update vendor/package,然后提交更新后的composer.lock
  • 不要直接运行composer update(不带参数),那样会升级所有依赖,可能会引入不兼容的改动

记住:install是按照lock文件安装,保证版本一致;update是按照json文件解析最新版本,会升级依赖。日常开发用install,只有明确要升级的时候才用update

四、autoload的原理

Composer的另一个强大功能,是自动生成autoloader。你只需要require 'vendor/autoload.php',就能自动加载所有的类,不用手动require。

但是,很多人不知道autoload的原理,也踩过一些坑。

Composer的autoload,支持四种方式:

1. PSR-4 最推荐的方式。按照命名空间和目录的映射关系,自动加载类。比如:

"autoload": {
    "psr-4": {
        "App\\": "src/"
    }
}

这表示,App命名空间下的类,都在src/目录下。比如App\Controllers\HomeController,对应的文件就是src/Controllers/HomeController.php

2. PSR-0 旧的标准,已经不推荐了。和PSR-4类似,但是命名空间的分隔符会被转换成目录分隔符,而且支持下划线转换。

3. classmap 扫描指定的目录和文件,生成类名到文件路径的映射表。这种方式加载速度快,但是每次新增类都要重新生成映射。

4. files 手动指定需要加载的文件,每次请求都会加载。适合加载一些全局函数文件。

常见的坑

坑1:改了autoload配置不生效 很多人改了composer.json里的autoload配置,但是没有运行composer dump-autoload,导致配置不生效,类加载不到。

记住:每次改了autoload配置,都要运行composer dump-autoload重新生成autoloader。

坑2:生产环境不用优化 默认情况下,Composer的autoloader是用PSR-4的规则,每次加载类的时候,都要去文件系统里找文件,性能不是最优的。在生产环境,可以用优化模式,生成classmap,提高加载速度。

运行composer dump-autoload --optimize,或者在安装的时候加--optimize-autoloader参数,就能生成优化的autoloader。

我以前的项目,生产环境没有优化autoloader,后来加了优化之后,接口响应速度提升了10%左右。虽然不是很多,但是对于高并发的项目,还是有意义的。

坑3:开发环境用了优化模式 和上面相反,开发环境不要用优化模式。因为优化模式生成了classmap,如果你新增了类,没有重新生成classmap,类就加载不到。开发环境用默认模式就行,改了autoload配置再运行composer dump-autoload

五、其他常见的坑

除了上面说的版本约束、lock文件、autoload,用Composer还有一些其他的坑。

坑1:内存不足 Composer在解析依赖的时候,会占用很多内存。特别是依赖多的项目,运行composer update的时候,可能会出现内存不足的错误。

解决方法:

  • 增加PHP的内存限制:php -d memory_limit=-1 composer.phar update
  • composer require添加单个依赖,而不是修改json后运行composer update
  • 定期清理Composer的缓存:composer clear-cache

我遇到过很多次内存不足的问题,特别是在依赖比较多的大项目里。后来我都是用php -d memory_limit=-1来运行Composer,就再也没遇到过了。

坑2:国内镜像慢 Composer默认的Packagist仓库在国外,国内访问很慢,有时候甚至连不上。运行composer install,可能要等几十分钟,甚至失败。

解决方法:用国内镜像。

目前比较稳定的国内镜像有:

  • 阿里云Composer镜像:https://mirrors.aliyun.com/composer/
  • 腾讯云Composer镜像:https://mirrors.cloud.tencent.com/composer/
  • 华为云Composer镜像:https://mirrors.huaweicloud.com/repository/php/

配置方法:

composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/

这样配置之后,全局都会用阿里云镜像,速度快很多。

我从2014年开始用国内镜像,那时候Packagist在国内访问特别慢,经常超时。用了国内镜像之后,composer install从几十分钟变成了几分钟,体验好了很多。

坑3:版本冲突 有时候,两个依赖需要同一个库的不同版本,就会出现版本冲突。Composer会报错,告诉你哪个依赖需要哪个版本,无法解决。

解决方法:

  • 仔细看错误信息,找到冲突的依赖和版本
  • 升级或降级其中一个依赖,让它们需要的版本兼容
  • 如果实在无法解决,可以用conflict或者replace来强制指定版本(不推荐,可能会有问题)

版本冲突是比较头疼的问题,特别是依赖多的项目。我的经验是,尽量用比较新的、维护活跃的库,这些库通常会及时更新,和其他库的兼容性也比较好。

坑4:require和require-dev搞混 composer.json里有两个部分:requirerequire-devrequire是生产环境需要的依赖,require-dev是开发环境需要的依赖(比如测试框架、代码检查工具)。

很多人把所有依赖都写在require里,包括开发工具。这样,生产环境也会安装这些开发工具,不仅浪费空间,还可能有安全隐患。

正确的做法是:

  • 生产环境需要的库,写在require
  • 只有开发环境需要的库(phpunit、phpcs、php-cs-fixer等),写在require-dev
  • 生产环境部署的时候,用composer install --no-dev,不安装开发依赖

我以前的项目,就把phpunit写在了require里,生产环境也安装了phpunit,虽然没什么大问题,但是不规范。后来改到了require-dev里,部署的时候加--no-dev,就规范了。

坑5:不看changelog就升级 很多人运行composer update,升级依赖,但是不看changelog(更新日志)。结果,升级之后,API变了,代码不兼容了,项目挂了。

正确的做法是:升级依赖之前,先看一下这个库的changelog,了解新版本有哪些改动,有没有不兼容的地方。如果有不兼容的改动,要先修改你的代码,再升级。

特别是大版本升级(比如从1.x升到2.x),通常都会有不兼容的改动,一定要仔细看changelog,做好测试再升级。

六、团队协作和部署中的最佳实践

最后,总结一下团队协作和部署中的最佳实践。

团队协作

  1. composer.jsoncomposer.lock都要提交到版本库
  2. 团队成员拉取代码后,运行composer install,不要运行composer update
  3. 新增依赖,用composer require vendor/package,不要手动修改json
  4. 升级依赖,用composer update vendor/package,然后提交更新后的lock文件
  5. 代码审查的时候,也要审查composer.json和composer.lock的改动

部署

  1. 生产环境用composer install --no-dev --optimize-autoloader,不安装开发依赖,优化autoloader
  2. 部署脚本里,不要运行composer update,只运行composer install
  3. 如果部署环境不能联网,可以在本地打包vendor目录,然后上传到服务器
  4. 部署前,在测试环境测试通过,再部署到生产环境

日常维护

  1. 定期运行composer outdated,查看哪些依赖有新版本
  2. 有新版本的时候,评估是否需要升级,看changelog,做好测试
  3. 定期清理不用的依赖,保持依赖列表干净
  4. 关注依赖的安全公告,有安全漏洞及时升级

七、写在最后

Composer用了三年,这些依赖管理的坑你踩过吗?

三年里,我踩了很多坑,也学到了很多东西。从最开始的dev-master乱用,到现在的版本约束、lock文件、autoload优化,我对Composer的理解越来越深,用得也越来越熟练。

Composer是一个伟大的工具,它彻底改变了PHP的依赖管理,促进了PHP生态的繁荣。作为PHP开发者,我们应该掌握好这个工具,了解它的原理,避开那些坑,让它更好地为我们服务。

希望这篇文章,能帮到正在用Composer的你。如果你也踩过什么坑,或者有什么经验,欢迎在评论区分享。

最后,用一句话总结:Composer虽好,但是不要乱用。版本约束要合理,lock文件要提交,autoload要优化,升级要看changelog。做到这些,你就能用好Composer,避开那些坑。