如何将一个大型单体应用拆分为多个小的Composer包? (微服务化重构)

11次阅读

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

如何将一个大型单体应用拆分为多个小的Composer包? (微服务化重构)

单体应用无法直接“拆成微服务”——Composer 包不是微服务,它只是 PHP 代码的复用单元。强行把业务逻辑按模块切出一 composer require 包,反而会制造更难维护的耦合和发布地狱。

先分清:Composer 包 ≠ 微服务

Composer 包是静态依赖,用于共享工具类、SDK、通用组件(比如 monolog/monologsymfony/http-Foundation)。微服务是运行时独立进程,有自己数据库、API 端点、部署生命周期。你当前的“大型单体”如果还跑在同一个 PHP-FPM 进程里,即使代码挪进 10 个包,仍是单体。

  • 错误做法:git clonemyapp-usermyapp-ordermyapp-payment 三个包,全部 require 进主项目 —— 数据库事务跨包失效,调试要跳 5 个仓库,发布必须全量上线
  • 正确路径:先识别边界上下文(Bounded Context),再决定哪些部分值得独立进程化;其余稳定、复用性强的逻辑,才考虑抽为 Composer 包
  • 典型适合抽包的场景:pdf-generation-sdk封装 TCPDF 封装逻辑)、legacy-ldap-adapter(对接老 LDAP 协议的统一客户端)、idempotency-middlewarelaravel/Symfony 中间件

抽包前必须做好的三件事

跳过这些,包一发版就踩坑。

  • 统一自动加载规则:所有待抽包代码必须已使用 PSR-4 自动加载,且命名空间不与主项目冲突(例如主项目用 App,包必须用 MyOrgUserService
  • 剥离运行时依赖:包里不能直接 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 这些运行时设施 —— 这些由主项目或上层框架注入。

text=ZqhQzanResources