require 和 require-dev 是运行时类加载边界的声明,非环境开关;require-dev 中的包在 --no-dev 下不会被 autoload,其目录需通过 autoload-dev 显式注册才可本地测试加载。

require 和 require-dev 不是“开发环境”和“生产环境”的开关,而是「类是否会在运行时被业务代码加载」的声明边界。越界就会在 composer install --no-dev 后报 Class not found。
require-dev 里的包,线上根本不存在
Composer 不读 APP_ENV、不看服务器 hostname,它只认命令参数。没加 --no-dev,phpunit 就会进生产镜像;加了,vendor/autoload.php 里连它的 classmap 条目都不会生成。
-
require-dev中的包不会出现在 production autoloader map 中 - 它们的源码目录(如
tests/)也不会被注册进autoload-dev规则,除非你本地执行过composer install(非--no-dev) - CI 流水线里如果缓存了
vendor/但没把--no-dev纳入缓存键,就可能复用错的缓存,导致phpunit没装进去却还跑测试
误把 dev 工具写进 require 的典型错误现象
最常踩的坑是命令少打空格:composer require-dev phpunit/phpunit 是无效命令,Composer 会当成 composer require 处理,直接塞进 require 字段。
- 线上部署后调用
dump()报错?可能是symfony/var-dumper被误装进require-dev,而 debug 模式又没关 - 控制器里写了
new MockeryMockInterface()却没在autoload-dev声明 mock 目录?CI 直接Class not found -
composer show | grep phpunit输出两行,其中一行标着 “required by your project”,说明它进了require,得手动删composer.json里的那行再composer update phpunit/phpunit
运行时判断类是否存在,别用 class_exists()
class_exists('PhpCsFixerFixerLineEndingLineEndingFixer') 这种调用,在 --no-dev 环境下会触发 autoload,找不到类就崩。这不是 Composer 的问题,是你把 dev 工具逻辑混进了运行路径。
- 改用
interface_exists()或function_exists()—— 它们不触发 autoload - 更稳妥的是加环境守卫:
if (app()->environment('local')) { ... }或if (PHP_SAPI === 'cli') { ... } - 别在
config/app.php这类全局配置里做条件加载,容易漏判
autoload-dev 不是给 require-dev 包用的
这个名字极具误导性。autoload-dev 的作用,是告诉 Composer:“当本项目启用了 dev 模式(即装了 require-dev 包)时,请额外把 tests/、stubs/ 这些目录也加进自动加载规则”。它跟 require-dev 包自身的加载无关。
-
phpunit/phpunit自身的自动加载由它自己的composer.json控制,不靠你的autoload-dev - 如果你的测试类在
tests/下,又想在本地跑测试时能被自动加载,就必须在根项目的autoload-dev里声明"psr-4": {"Tests\": "tests/"} - 这个配置对生产环境完全无影响 —— 因为
--no-dev下,autoload-dev根本不生效
真正难的不是分清该放哪,而是守住运行时边界:只要有一处 new、use 或反射调用到了 require-dev 里的类,--no-dev 就会失效。这种耦合往往藏在配置文件、服务提供者或调试钩子里,上线前很难覆盖到。


















