require 和 require-dev 的划分标准是类是否在运行时被业务代码调用:若会则必须进 require,否则如仅用于测试、CI 或静态分析则属 require-dev;误将 dev 工具混入运行时路径会导致 --no-dev 部署时报错。

require 和 require-dev 不是“开发用”和“生产用”的简单划分
关键判断标准只有一个:这个包的类是否会在运行时被业务代码 new、use 或调用? 如果会,就必须进 require;如果只在 phpunit 命令里跑、只在 CI 脚本里执行、或只在本地 phpstan 扫描时加载,那就属于 require-dev。
常见误判:以为“我在本地写测试才用 PHPUnit”,所以它“只是开发用”——这没错;但更深层风险是:一旦你在某个控制器里写了 new \PHPUnit\Framework\TestCase()(哪怕只是临时调试),上线后执行 composer install --no-dev 就直接报 Class not found。这不是 Composer 的 bug,是你把 dev 工具混进了运行时路径。
composer require 和 composer require --dev 的行为差异
两个命令表面只差一个 --dev,但写入位置和后续影响完全不同:
-
composer require phpunit/phpunit→ 写入composer.json的require字段 → 每次composer install都装,线上也装 -
composer require --dev phpunit/phpunit→ 写入require-dev字段 → 本地默认装,但composer install --no-dev完全跳过
注意:composer require-dev 是无效命令(Composer 不识别该子命令),必须写成 composer require --dev。漏掉空格或写错连字符,就会误装进 require。
autoload-dev 不是给 require-dev 包自己用的
autoload-dev 是个常被误解的配置项。它不是让 phpunit/phpunit 自己能自动加载,而是告诉 Composer:“当本项目启用了 dev 模式(即装了 require-dev 包)时,请额外把测试目录也加进自动加载规则”。
典型用法:
{
"autoload": {
"psr-4": { "App\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Tests\\": "tests/" }
}
}
这意味着:只有你本地执行了 composer install(没加 --no-dev),Tests\\ 命名空间才会被注册进 vendor/autoload.php;线上用 --no-dev,这个映射压根不会生效 —— 即便 tests/ 目录还在,也不会被自动加载。
部署时 --no-dev 不是“可选优化”,而是必须动作
生产环境执行 composer install --no-dev 不只是为了省几 MB 磁盘空间。更重要的是:
- 避免非预期类加载:比如
friendsofphp/php-cs-fixer会注册大量 Symfony 组件,它们可能覆盖或干扰你的运行时依赖版本 - 减少攻击面:
psy/psysh、symfony/var-dumper这类调试工具若留在线上,可能被利用为交互式执行入口 - 防止 CI/CD 流水线污染:某些工具(如
phpstan/phpstan)依赖特定 PHP 扩展或扩展版本,线上环境未必满足,导致部署失败
真正容易被忽略的一点:CI 环境通常要跑测试,所以得装 require-dev;但很多团队把 CI 构建产物直接当部署包推到线上,结果把 phpunit 一起带过去了——这相当于把 IDE 插件打包进发布版本。










