PHP组件化生态建设:从私有Composer仓库到跨团队组件复用的完整治理框架》
·
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
文化推广话术、遗留代码如何渐进式抽组件、组件健康度自动巡检脚本)再深挖,随时说。
更多推荐


所有评论(0)