PHP 组件化生态建设:从私有 Composer 仓库到跨团队组件复用的完整治理框架

  下面把组件化生态拆成 10 个台阶,每一阶都给「干什么」+「完整配置/代码」+「大白话」。目标:让团队 A 写的轮子,团队 B
  三分钟装上就能用。

  ---
  一、先讲清动机(为什么要建私有生态)

  没有组件化生态                  有组件化生态
  ─────────────────────────       ─────────────────────────
  每个项目复制粘贴 SDK            composer require 一行装好
  谁改谁知道,没人维护            版本号清楚,CHANGELOG 可查
  踩过的坑别人继续踩              一次修复,全公司受益
  新人三周才能跑起来              三分钟拉好脚手架

  大白话:组件化的本质是把"组织里的隐性知识"沉淀成"显性的、可版本化的二进制资产"。代码复用只是表象,真正复用的是「设计、
  踩坑经验、最佳实践」。

  ---
  二、整体地图

  ┌──────────────────────────────────────────────┐
  │ 第10阶:度量与运营(健康度看板)             │
  │ 第9阶:贡献机制(Inner Source 文化)         │
  │ 第8阶:脚手架与模板(Skeleton)              │
  │ 第7阶:版本治理(SemVer / 兼容性策略)       │
  │ 第6阶:CI/CD 流水线(自动发版)              │
  │ 第5阶:质量门禁(测试 / 静态分析 / 文档)    │
  │ 第4阶:组件设计规范(接口 / 命名 / 分层)    │
  │ 第3阶:私有仓库(Satis / Packagist 私有版)  │
  │ 第2阶:组件分类(Kernel / Domain / App)     │
  │ 第1阶:边界识别(什么该抽,什么不该抽)      │
  └──────────────────────────────────────────────┘

  ---
  三、第 1 阶:边界识别(最难也最关键)

  三问法

  Q1:跨过 3 个项目了吗?        ←没有:先别抽
  Q2:抽出来后,能独立测试吗?    ←不能:耦合还没解开
  Q3:稳定吗?接口 3 个月没变了吗? ←没有:让它再飞一会儿

  该抽 vs 不该抽

  ┌───────────────────────────────┬────────────────────────┐
  │            ✅ 该抽            │       ❌ 不该抽        │
  ├───────────────────────────────┼────────────────────────┤
  │ 鉴权 SDK / 日志组件           │ 业务规则(订单状态机) │
  ├───────────────────────────────┼────────────────────────┤
  │ 内部 API Client               │ 一次性的脚本           │
  ├───────────────────────────────┼────────────────────────┤
  │ 公司基础设施封装(Kafka/OSS) │ 强 UI 耦合的页面       │
  ├───────────────────────────────┼────────────────────────┤
  │ 通用工具(雪花 ID、签名)     │ 频繁变动的需求         │
  └───────────────────────────────┴────────────────────────┘

  大白话:业务代码别碰,工具/基础设施先抽。订单逻辑抽出来,下一版业务一改,所有依赖方一起爆。先抽稳定的"地基"。

  ---
  四、第 2 阶:组件分类(建立心智模型)

  三层组件模型

  ┌──────────────────────────────────────┐
  │ App 层组件   ←业务相关,少数项目复用 │
  │  例:order-sdk, member-toolkit       │
  ├──────────────────────────────────────┤
  │ Domain 层组件 ←领域抽象,跨业务线复用│
  │  例:money, address, identity        │
  ├──────────────────────────────────────┤
  │ Kernel 层组件 ←技术基建,全公司复用  │
  │  例:logger, http-client, snowflake  │
  └──────────────────────────────────────┘
          依赖方向:上层 →下层(单向)

  命名规范

  vendor 名:公司缩写  acme/
  组件名:  小写连字符  acme/snowflake-id
                       acme/api-client-billing
                       acme/laravel-sentry-bridge   ←框架适配单独包

  大白话:组件分层 = 让人一眼看懂"该不该用、能不能改"。Kernel 包改一次全公司发版,要慎重;App 包改了影响小,可激进。

  ---
  五、第 3 阶:私有 Composer 仓库

  方案对比

  ┌───────────────────────────┬─────────────────┬────────────────────────────────┐
  │           方案            │    适用规模     │             大白话             │
  ├───────────────────────────┼─────────────────┼────────────────────────────────┤
  │ Satis(静态)             │ 小团队,<100 包 │ 跑命令生成静态 JSON,最省事    │
  ├───────────────────────────┼─────────────────┼────────────────────────────────┤
  │ Private Packagist(SaaS) │ 中型公司        │ 官方出品,付费但省心           │
  ├───────────────────────────┼─────────────────┼────────────────────────────────┤
  │ Packagist 自托管(开源)  │ 大型组织        │ 全功能,运维成本高             │
  ├───────────────────────────┼─────────────────┼────────────────────────────────┤
  │ Gitea/GitLab Composer     │ 已有 GitLab     │ 直接用 GitLab Package Registry │
  └───────────────────────────┴─────────────────┴────────────────────────────────┘

  推荐起手式:Satis(够 80% 公司用了)

  composer create-project composer/satis /opt/satis

  /opt/satis/satis.json:

  {
    "name": "Acme Private Packagist",
    "homepage": "https://packagist.acme.local",
    "repositories": [
      { "type": "vcs", "url": "git@gitlab.acme.com:php-libs/snowflake.git" },
      { "type": "vcs", "url": "git@gitlab.acme.com:php-libs/http-client.git" },
      { "type": "vcs", "url": "git@gitlab.acme.com:php-libs/logger.git" }
    ],
    "require-all": true,
    "archive": {
      "directory": "dist",
      "format": "tar",
      "skip-dev": true
    },
    "require-dependencies": false,
    "require-dev-dependencies": false
  }

  构建 + 发布:

  php bin/satis build satis.json /var/www/packagist
  # 用 Nginx 把 /var/www/packagist 暴露成 https://packagist.acme.local

  项目里使用

  // composer.json
  {
    "repositories": [
      { "type": "composer", "url": "https://packagist.acme.local" },
      { "packagist.org": false }       // 强制走私有源
    ],
    "require": {
      "acme/snowflake-id": "^1.0",
      "acme/http-client": "^2.1"
    }
  }

  鉴权(HTTP Basic / Token)

  auth.json(不要提交!):

  {
    "http-basic": {
      "packagist.acme.local": {
        "username": "ci-user",
        "password": "${COMPOSER_AUTH_TOKEN}"
      }
    }
  }

  大白话:私有源是组件化的底盘。没有它,复用就靠拷代码、靠 git submodule,一年内必崩。Satis
  半天能搭好,没必要一上来就上重量级方案。

  ---
  六、第 4 阶:组件设计规范(包内长什么样)

  标准目录结构

  acme/snowflake-id/
  ├── src/
  │   ├── Generator.php
  │   ├── Contract/IdGenerator.php   ←接口先行
  │   └── Exception/
  ├── tests/
  │   ├── Unit/
  │   └── Feature/
  ├── docs/
  │   ├── README.md
  │   ├── UPGRADE.md                  ←升级指南
  │   └── examples/
  ├── .github/workflows/
  ├── composer.json
  ├── phpstan.neon
  ├── phpunit.xml
  ├── CHANGELOG.md
  └── LICENSE

  composer.json 模板

  {
    "name": "acme/snowflake-id",
    "description": "雪花算法 ID 生成器,支持 worker-id 自动分配",
    "type": "library",
    "license": "proprietary",
    "require": {
      "php": "^8.2"
    },
    "require-dev": {
      "phpunit/phpunit": "^11.0",
      "phpstan/phpstan": "^1.11",
      "friendsofphp/php-cs-fixer": "^3.50",
      "infection/infection": "^0.29"
    },
    "autoload": {
      "psr-4": { "Acme\\Snowflake\\": "src/" }
    },
    "autoload-dev": {
      "psr-4": { "Acme\\Snowflake\\Tests\\": "tests/" }
    },
    "extra": {
      "branch-alias": { "dev-main": "1.x-dev" }
    },
    "config": {
      "sort-packages": true
    },
    "minimum-stability": "stable"
  }

  接口先行(关键原则)

  <?php
  // src/Contract/IdGenerator.php
  namespace Acme\Snowflake\Contract;

  interface IdGenerator
  {
      public function next(): int;
      public function batch(int $count): array;
  }

  <?php
  // src/Generator.php
  namespace Acme\Snowflake;

  use Acme\Snowflake\Contract\IdGenerator;

  final class Generator implements IdGenerator
  {
      public function __construct(
          private readonly int $workerId,
          private readonly int $epoch = 1704067200000,
      ) {}

      public function next(): int { /* ... */ }
      public function batch(int $count): array { /* ... */ }
  }

  大白话:永远导出接口,不导出实现。用户 type-hint 接口,未来你换实现不会让任何人崩溃。这是组件能长期演进的前提。

  框架适配单独成包

  acme/snowflake-id              ←纯 PHP,不依赖任何框架
  acme/snowflake-id-laravel      ←Laravel ServiceProvider(薄薄一层)
  acme/snowflake-id-symfony      ←Symfony Bundle

  // acme/snowflake-id-laravel/composer.json
  {
    "name": "acme/snowflake-id-laravel",
    "require": {
      "acme/snowflake-id": "^1.0",
      "illuminate/support": "^11.0"
    },
    "extra": {
      "laravel": {
        "providers": ["Acme\\Snowflake\\Laravel\\ServiceProvider"]
      }
    }
  }

  大白话:核心包不绑框架,否则用 Laravel 的项目和用 Hyperf
  的项目永远没法共用。把框架胶水代码独立成包是行业最佳实践(参考 monolog/monolog vs monolog-bridge)。

  ---
  七、第 5 阶:质量门禁(包必须达标才能发版)

  .github/workflows/ci.yml

  name: Quality Gate
  on: [push, pull_request]

  jobs:
    test:
      strategy:
        matrix:
          php: ['8.2', '8.3', '8.4']
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
        - uses: shivammathur/setup-php@v2
          with: { php-version: '${{ matrix.php }}', coverage: xdebug }
        - run: composer install
        - run: vendor/bin/phpstan analyse --level=max
        - run: vendor/bin/php-cs-fixer fix --dry-run --diff
        - run: vendor/bin/phpunit --coverage-clover=coverage.xml
        - run: vendor/bin/infection --min-msi=80 --min-covered-msi=85
        - name: BC Check
          run: vendor/bin/roave-backward-compatibility-check

  向后兼容性自动检查(神器)

  composer require --dev roave/backward-compatibility-check
  vendor/bin/roave-backward-compatibility-check

  输出:

  [BC] CHANGED: The number of required arguments for Acme\Snowflake\Generator#next() has changed from 0 to 1

  大白话:Kernel 层组件破坏兼容 = 全公司炸。BC Check 工具会自动对比 main 分支和上个 tag 的 API 差异,破坏兼容直接红灯。

  ---
  八、第 6 阶:CI/CD 自动发版

  自动 Tag + Release

  # .github/workflows/release.yml
  name: Release
  on:
    push:
      branches: [main]

  jobs:
    release:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
          with: { fetch-depth: 0 }

        - uses: googleapis/release-please-action@v4
          with:
            release-type: php
            package-name: acme/snowflake-id

        - name: Notify Satis
          if: ${{ steps.release.outputs.release_created }}
          run: |
            curl -X POST https://packagist.acme.local/webhook \
                 -H "Authorization: Bearer ${{ secrets.SATIS_TOKEN }}" \
                 -d '{"repo":"acme/snowflake-id"}'

  Satis Webhook 自动构建

  // /var/www/packagist/webhook.php
  <?php
  $secret = getenv('SATIS_TOKEN');
  if ($_SERVER['HTTP_AUTHORIZATION'] !== "Bearer {$secret}") {
      http_response_code(403); exit;
  }
  exec('cd /opt/satis && php bin/satis build satis.json /var/www/packagist 2>&1', $out, $code);
  echo $code === 0 ? 'OK' : implode("\n", $out);

  Conventional Commits 驱动版本号

  feat: 新增批量生成 API           →升 minor (1.2.0 →1.3.0)
  fix: 修复 worker-id 冲突         →升 patch (1.2.0 →1.2.1)
  feat!: 移除 deprecated 方法       →升 major (1.2.0 →2.0.0)

  大白话:人不应该决定版本号,提交信息决定。每次合并 main,机器人自动算出版本号、改 CHANGELOG、打 Tag、推
  Satis——零人工。

  ---
  九、第 7 阶:版本治理(SemVer 落地)

  版本约束策略(用户侧)

  {
    "require": {
      "acme/kernel-logger":  "^2.3",     // 推荐:跟主版本
      "acme/legacy-helper":  "~1.4.0",   // 老组件:锁次要版本
      "acme/experimental":   "1.0.0-beta.3"  // 实验包:精确版本
    }
  }

  包内的兼容性策略文档

  COMPATIBILITY.md:

  # 兼容性承诺

  ## 公开 API(受 SemVer 保护)
  - `src/Contract/*` 下所有接口
  - 标 @api 注解的类和方法

  ## 内部 API(不受保护)
  - `src/Internal/*`
  - 未标注 @api 的类——视为实现细节,可能随时改

  ## 弃用流程
  1. v1.5.0:标记 @deprecated,控制台 warning
  2. v1.x:保留至少 6 个月或 2 个 minor 版本
  3. v2.0.0:移除

  /**
   * @api  ←公开承诺
   */
  final class Generator implements IdGenerator { /* ... */ }

  /**
   * @internal  ←实现细节,外部别用
   */
  final class WorkerIdAllocator { /* ... */ }

  大白话:SemVer 不是潜规则,是合同。明确说"哪些是承诺、哪些可以乱改",用户才敢用,你才敢演进。

  ---
  十、第 8 阶:脚手架与模板

  项目脚手架(用户起项目时用)

  composer create-project acme/skeleton-laravel my-new-app

  acme/skeleton-laravel 是一个预装好公司组件 + 配置 + CI的 Laravel 模板:

  {
    "name": "acme/skeleton-laravel",
    "type": "project",
    "require": {
      "laravel/framework": "^11.0",
      "acme/kernel-logger": "^2.0",
      "acme/auth-sdk": "^3.0",
      "acme/api-gateway-client": "^1.0",
      "acme/observability": "^1.5"
    }
  }

  组件脚手架(写新组件时用)

  composer create-project acme/skeleton-package my-new-lib

  my-new-lib 一拉下来就有:测试、PHPStan、CS-Fixer、CI、CHANGELOG、README 模板,新人不用再问"这玩意儿目录怎么建"。

  大白话:模板就是把"最佳实践冻结成可执行的初始状态"。模板每升一次级,整个组织的下限就升一次。

  ---
  十一、第 9 阶:贡献机制(Inner Source)

  三个角色

  ┌──────────┐   提 Issue   ┌──────────┐
  │  使用者   │ ───────────→│  维护者   │
  └──────────┘              └──────────┘
       ↑                         ↓
       │ 提 PR                    │ Code Review
       └─────  贡献者 ────────────┘

  治理文档(每个仓库必须有)

  MAINTAINERS.md:

  - @alice  全权(可发版)
  - @bob    全权
  - @carol  代码审查(不可发版)

  # SLA
  - Issue:3 工作日内首次响应
  - PR:5 工作日内 review

  CONTRIBUTING.md:

  ## 怎么提 PR
  1. Fork →拉分支 `feat/xxx`
  2. 写测试(覆盖率 ≥80%)
  3. PR 标题用 Conventional Commits
  4. 至少 1 名 Maintainer + 1 名其他 Reviewer 通过

  ## 不会被合并的 PR
  - 没有测试
  - 没有 CHANGELOG 条目
  - 破坏 BC 但没标 BREAKING CHANGE

  Inner Source 三原则

  1. Discoverable:在内部 Packagist 能搜到
  2. Reusable:装上就能跑,文档齐全
  3. Contributable:外人能提 PR,不必加入团队

  大白话:组件不能让一个团队私有化。一个组件如果只有 A 团队能改、B 团队只能等,那它就是债。Inner Source
  文化让所有人都可以"修复 + 回贡献"。

  ---
  十二、第 10 阶:度量与运营

  健康度指标(每月看)

  // scripts/component-health.php
  $health = [
      'name'              => 'acme/snowflake-id',
      'downloads_30d'     => 1247,            // 装机量
      'dependents_count'  => 23,              // 多少项目用
      'open_issues'       => 4,
      'avg_issue_age'     => 6.2,             // 天
      'last_release_days' => 12,              // 多久没发版了
      'coverage'          => 92.4,
      'phpstan_level'     => 9,
      'bc_breaks_last_year' => 0,
  ];

  健康度评级

  A 级(绿):每月发版 + 覆盖率 >85% + 0 BC 破坏 + Issue<7天响应
  B 级(黄):上述任一不达标
  C 级(红):3 个月没动 + Issue 堆积 + 用户投诉
  D 级(黑):deprecated,进入退役流程

  看板(Grafana)

  ┌────────────────────────────────────────────┐
  │ 组件生态总览                                │
  ├────────────────────────────────────────────┤
  │ 总组件数:       86                         │
  │ 月活跃发版:     34                         │
  │ A 级占比:       64%                        │
  │ 平均装机量/包:  18.7 项目                  │
  │ 跨团队贡献 PR:  127(上月 89)             │
  └────────────────────────────────────────────┘

  退役流程

  deprecated →标记 @deprecated →通知用户 →6 个月后下线

  大白话:没度量的生态会熵增到死。每月要有人看数据,把僵尸包退役、把活跃包奖励,生态才不会变成垃圾场。

  ---
  十三、最佳落地节奏

  ┌────────────┬────────┬─────────────────────────────────────────┐
  │    阶段    │  时间  │                 干什么                  │
  ├────────────┼────────┼─────────────────────────────────────────┤
  │ 第 1 月    │ 立基础 │ Satis 搭起来,定 3 个最该抽的组件       │
  ├────────────┼────────┼─────────────────────────────────────────┤
  │ 第 2 月    │ 树标杆 │ 把 1 个组件做到 A 级,作为模板          │
  ├────────────┼────────┼─────────────────────────────────────────┤
  │ 第 3 月    │ 复制   │ 用脚手架批量孵化 5-10 个组件            │
  ├────────────┼────────┼─────────────────────────────────────────┤
  │ 第 4-6 月  │ 推广   │ 老项目逐步替换为组件依赖                │
  ├────────────┼────────┼─────────────────────────────────────────┤
  │ 第 6-12 月 │ 治理   │ 度量看板上线,每月健康度评级            │
  ├────────────┼────────┼─────────────────────────────────────────┤
  │ 第二年     │ 文化   │ Inner Source 全面铺开,跨团队 PR 常态化 │
  └────────────┴────────┴─────────────────────────────────────────┘

  ---
  十四、最容易踩的 8 个坑

  1. 一上来就抽业务:业务变得快,抽出来天天破坏 BC,没人敢用。先抽稳定的工具。
  2. 不分 Kernel/Domain/App:所有包混一起,没法定不同的发版纪律。
  3. 核心包绑框架:require: laravel/framework 写进 Kernel 包,跨框架彻底没法用。
  4. 没 BC 检查:手抖删了一个 public 方法,下游全炸。Roave BC Check 必须进 CI。
  5. 没 SLA 的"开放":说欢迎贡献,PR 三个月没人理,第二次没人提了。
  6. 私有源没鉴权:内网泄露 = 全公司源代码外流。
  7. 版本号随便打:不遵守 SemVer,用户定 ^1.0 升级即崩,从此只敢锁死版本。
  8. 包太碎或太大:一个工具函数就开个包是噪音;3000 行的"工具集"没人能看懂。大小标准:单一职责,README 一页能讲完。

  ---
  十五、最后三句真话

  1. 组件化不是技术问题,是协作问题——工具再好,没有人维护、没有人贡献,库就是僵尸。
  2. 先有标杆,再有规模——把一个包做到极致(文档、测试、发版、响应),其他人会自然模仿。
  3. 生态的成熟度 = 平均健康度,不是顶级包的数量——1个 A 级胜过 100 个 C 级。

  照这套抄完,半年内能搭起一个让团队主动来用、来贡献的 PHP
  内部生态——这才是组件化的终态:不是"被允许复用",而是"被想要复用"。

  需要我针对某一阶(比如Satis 高可用部署、Inner Source
  文化推广话术、遗留代码如何渐进式抽组件、组件健康度自动巡检脚本)再深挖,随时说。

更多推荐