Composer不解析子目录中的composer.json,只加载根目录或远程包解压后的主composer.json;正确做法是将子模块声明为独立包并用path仓库引入,或使用Composer 2.5+的workspaces功能。

不能直接用 composer require 安装“包含子模块”的包——Composer 不解析嵌套的 composer.json,它只认根目录下的一个 composer.json 和其声明的依赖。
为什么 require 会忽略子目录里的 composer.json
Composer 的安装逻辑是单入口的:它只读取项目根目录(或你 require 的远程包解压后)的 composer.json,然后递归解析其中 require 字段列出的包。子目录里哪怕有十个 composer.json,只要没被主配置显式声明为独立包,就完全不会被加载或安装。
常见误操作是把模块代码连同自己的 composer.json 直接扔进 src/ 或 packages/ 下,然后执行 composer require myorg/myapp ——结果只有顶层包被装进来,子模块的依赖(比如 monolog/monolog)根本不会出现在 vendor/ 里,运行时直接报 Class not found。
正确做法:把子模块声明为独立包,用 path 仓库引入
核心不是“让 Composer 扫描子目录”,而是“让每个子模块变成 Composer 能识别的包”,再通过本地路径源(type: "path")接入主项目。
- 每个子模块必须有唯一
name(如"myapp/payment"),且该 name 必须和你在主项目中require时写的完全一致(大小写敏感) - 主项目
composer.json的repositories里要明确列出每个子模块路径:{ "repositories": [ { "type": "path", "url": "modules/payment" }, { "type": "path", "url": "modules/user" } ] } -
require字段里写的是包名,不是路径:"myapp/payment": "*",不是"modules/payment": "*" - 子模块自身
composer.json中的require是有效的——它们会被 Composer 当作该包的依赖,统一装进主项目的vendor/目录
容易踩的坑:软链失效、autoload 不生效、版本冲突
即使配置全对,以下问题仍高频出现:
-
composer install后子模块代码没进vendor/?检查url路径是否拼错,或该目录下根本没有composer.json(空目录不行) - 类能
new但自动加载失败?确认子模块的autoload配置(如psr-4)映射的是它自己的src/目录,而不是相对主项目的路径 - 两个子模块都 require 了
guzzlehttp/guzzle,但版本不同(比如 ^7.0 vs ^8.0)?Composer 会尝试合并,失败则报could not resolve packages——此时必须手动在主项目中用require锁定一个兼容版本,或让子模块降级依赖 - 执行
composer update后子模块被删了?因为"*"版本约束不锁定具体 commit,改用"dev-main as 1.0.0"或打 git tag 后写"1.0.0"更稳
替代方案:用 workspaces(仅限 Composer 2.5+)
如果你用的是 Composer ≥2.5,可以启用 workspaces 功能,它本质是 path 仓库的语法糖,但更简洁:
- 在主项目
composer.json里加:"workspaces": ["modules/*"]
- 子模块目录名必须匹配通配符(如
modules/payment),且各自有合法composer.json - 之后
composer require myapp/payment就能自动识别,不用手写repositories - 注意:
workspaces不支持嵌套通配符(如"modules/**/composer.json"),也不支持 Windows 的反斜杠路径
真正麻烦的从来不是怎么写配置,而是当子模块之间存在循环依赖、autoload 命名空间重叠、或 PHP 版本要求不一致时,Composer 会静默跳过某些包——这时候得靠 composer show --tree 和 composer why-not 一层层挖。


















