composer本身不支持多环境动态切换依赖,必须通过--no-dev、composer_dev_mode和运行时变量协同控制,严禁拆分composer.json或依赖配置文件,且所有环境必须共用同一份composer.lock。

Composer 本身不支持“多配置文件动态切换依赖”,硬拆 composer.json 是高维护成本陷阱,不是多环境方案——它会破坏 composer.lock 一致性,导致本地能跑、线上报错。
require-dev 不是环境开关,而是作用域声明
很多人误把 require-dev 当成“开发环境专用依赖”,其实它只表示“非运行时必需”。Composer 完全不感知 APP_ENV 或服务器角色,只认安装命令是否带 --no-dev。
- 把
monolog/monolog放进require-dev→ 生产环境执行composer install --no-dev后直接Class not found - 把
phpunit/phpunit放进require→ 测试框架打进生产镜像,白占体积、埋安全风险 -
autoload-dev仍会让测试类被加载(比如Tests\*),但生产 autoloader classmap 里不会包含它们——除非你手动跑composer dump-autoload --optimize且没加--no-dev
COMPOSER_DEV_MODE=0 不等于 --no-dev,优先用参数
COMPOSER_DEV_MODE 环境变量确实能让 Composer 忽略 require-dev 解析,但它只是辅助手段,且行为不如 --no-dev 明确可靠。
-
--no-dev是命令级开关,强制跳过require-dev安装和 autoload 注册,CI/CD 脚本里漏掉它,就会把laravel/pint或phpstan/phpstan打进生产镜像 -
COMPOSER_DEV_MODE=0会被其他环境变量或脚本覆盖,尤其在 Docker 多层构建中容易失效 - Dockerfile 中推荐写法:
RUN COMPOSER_DEV_MODE=0 composer install --no-dev --optimize-autoloader,双重保险
想按环境装不同包?别碰 composer.json 的 require 字段
composer.json 是静态 JSON 文件,${APP_ENV} 插值只在 config.http-basic、repositories[].url 等极少数字段生效,且仅做一次 getenv() 替换。它不是模板引擎,不支持逻辑判断。
- 试图在
require里写"guzzlehttp/guzzle": "${GULP_VERSION}"→ 解析失败,报JSON decode error - 用
platform声明"ext-redis": "8.2.0"→ 只骗过依赖解析,运行时若真没装ext-redis,照样Call to undefined function redis_connect() - 真正可行的隔离方式:用
COMPOSER=composer.prod.json composer install --no-dev,但必须接受vendor/和composer.lock全量重建,且两个配置文件无法共享 lock,协作成本陡增
PHP 版本不一致?问题不在 composer.json,而在 shell 的 php 命令
报错 Your PHP version (7.4.33) does not satisfy that requirement 却看到 php -v 是 8.2?说明 Composer 实际调用的不是你以为的那个 PHP。
- 查真实路径:
which php和ls -l $(which php),再对比composer --version输出里的 “PHP” 字段 - Mac 上 Homebrew 切换版本后常需
brew link --force php@8.2,否则which php还指向/usr/bin/php - CI/CD 或 Docker 中最稳做法:不用
composer install,改用/usr/bin/php8.2 /tmp/composer.phar install,绕过 PATH 查找
多环境依赖差异,本质是部署流程控制问题,不是配置文件能解决的。最容易被忽略的是:所有环境必须共用同一份 composer.lock,且生产部署永远走 composer install --no-dev,而不是靠改配置来“模拟”环境。











