Composer包不是微服务,它仅是PHP代码复用单元;微服务是独立运行进程,需独立数据库、API和部署生命周期。

单体应用无法直接“拆成微服务”——Composer 包不是微服务,它只是 PHP 代码的复用单元。强行把业务逻辑按模块切出一堆 composer require 包,反而会制造更难维护的耦合和发布地狱。
先分清:Composer 包 ≠ 微服务
Composer 包是静态依赖,用于共享工具类、SDK、通用组件(比如 monolog/monolog 或 symfony/http-foundation)。微服务是运行时独立进程,有自己数据库、API 端点、部署生命周期。你当前的“大型单体”如果还跑在同一个 PHP-FPM 进程里,即使代码挪进 10 个包,仍是单体。
- 错误做法:
git clone出myapp-user、myapp-order、myapp-payment三个包,全部require进主项目 —— 数据库事务跨包失效,调试要跳 5 个仓库,发布必须全量上线 - 正确路径:先识别边界上下文(Bounded Context),再决定哪些部分值得独立进程化;其余稳定、复用性强的逻辑,才考虑抽为 Composer 包
- 典型适合抽包的场景:
pdf-generation-sdk(封装 TCPDF 封装逻辑)、legacy-ldap-adapter(对接老 LDAP 协议的统一客户端)、idempotency-middleware(Laravel/Symfony 中间件)
抽包前必须做好的三件事
跳过这些,包一发版就踩坑。
-
统一自动加载规则:所有待抽包代码必须已使用 PSR-4 自动加载,且命名空间不与主项目冲突(例如主项目用
App\,包必须用MyOrg\UserService\) -
剥离运行时依赖:包里不能直接 new
$this->container->get('cache')或调用config('database.default')—— 改为构造函数注入或显式传参 -
冻结接口契约:对外暴露的类/方法必须稳定。别在 v1.0.0 包里定义
UserRepository::findByName(),v1.1.0 又改成::findByCriteria()—— 下游项目升级直接炸
抽包实操:从单体里切出一个可发布的包
以从 Laravel 单体中抽出 myorg/email-templates 包为例(管理邮件模板渲染逻辑):
mkdir -p myorg/email-templates/src cp app/Services/EmailTemplateRenderer.php myorg/email-templates/src/ # 不要复制 config/ 或 resources/ —— 包里不放配置文件和视图文件
写 composer.json(关键字段):
{
"name": "myorg/email-templates",
"autoload": {
"psr-4": { "MyOrg\\EmailTemplates\\": "src/" }
},
"require": {
"php": "^8.1",
"illuminate/view": "^10.0 || ^11.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
}
}- 不要
require主项目的app/目录或任何私有包 —— 否则无法独立测试 - 版本号严格遵循 semver:
1.0.0表示 API 冻结,major升级必须破坏兼容性 - 发布到私有 Packagist(如 Satis 或 Private Packagist),而非 packagist.org
最容易被忽略的陷阱:数据库与事务边界
这是最常导致线上事故的点。如果你把 User 模型和 UserRepository 抽进 myorg/user-core 包,但主项目仍直接操作 users 表:
- 主项目执行
DB::transaction()时,包内代码调用的 Eloquent 操作可能不在同一事务中(取决于连接实例是否共享) - 包里加了
boot()方法注册全局事件监听?它会在主项目启动时执行 —— 但主项目可能根本没启用对应事件系统 - 包里用了
Cache::remember()?缓存 key 前缀未隔离,不同包写入同一 key 导致数据污染
真正安全的做法:包只提供能力(如 EmailTemplateRenderer::render($template, $data)),不碰 DB、Cache、Config 这些运行时设施 —— 这些由主项目或上层框架注入。










