起因
GitHub 用户名从 hizpark 改为 changhorizon,看上去只是一个账号改名。但运行中的 9 个 PHP 包——每个都有自己的 composer.json、命名空间、CI 配置、Packagist 发布——全都绑着旧名字。
改一个字符串不难。但 9 个包联动,有的互相依赖(pps → directory-tree + zip-mover,file-uploader → scoped-storage-strategy + validation-interface),这意味着从文本替换到依赖约束到打包发布的完整链路。
替换策略:两轮 sed,严格区分大小写
改命名空间最怕混乱——hizpark(小写,包名)和 Hizpark(PascalCase,PHP 命名空间)同时存在于同一个 composer.json 中。一个包名变了但命名空间没变,或反过来,都会导致 autoload 找不到类。
解决方案严格区分两轮替换:
# 第一轮:小写 → 包名、URL、LICENSE
sed -i 's/hizpark/changhorizon/g' composer.json README.md .github/workflows/ci.yml LICENSE
# 第二轮:PascalCase → PHP 命名空间
sed -i 's/Hizpark/ChangHorizon/g' composer.json $(find src tests -name '*.php')
两轮 sed 的匹配面正交——hizpark 永远不会匹配到 Hizpark,Hizpark 永远不会匹配到 hizpark。替换是精确的。
依赖链的处理顺序
9 个包中有依赖关系。必须先处理被依赖的包,再处理依赖方:
leaf 包(无外部依赖)
├── zip-mover ← pps 依赖
├── directory-tree ← pps 依赖
├── scoped-storage-strategy ← file-uploader 依赖
└── validation-interface ← file-uploader 依赖
consumer 包(有外部依赖)
├── pps → 更新 require + use 语句
└── file-uploader → 更新 require + use 语句
consumer 包除了 sed 替换外,还需要更新 composer.json 中的版本约束——被依赖的包升了 minor 版本,consumer 包的 require 约束也要同步。
composer.lock:删掉还是保留?
PPS(PHP Project Scaffold)模板默认包含 composer.lock。但在这次 rebrand 中,我选择全部删掉。
理由很简单:library 包的 lock 文件对消费者完全无效。 当别人 composer require changhorizon/zip-mover 时,Composer 完全忽略 zip-mover 自己的 lock 文件,只看 composer.json 的版本约束。lock 文件对 library 的唯一作用是 CI 的确定性安装。权衡后选择删除,CI 改用 composer update --ignore-platform-req=php 替代。
这也暴露了 PPS 模板的一个设计点——它自带了 lock 文件,但对于生成的 library 项目来说,这并不一定合理。
CI 适配的三个改动
删除 lock 文件后,CI 需要调整:
composer install→composer update --ignore-platform-req=php(无 lock 文件时 install 等价于 update,加上忽略平台要求以兼容不同 PHP 版本)hashFiles('composer.lock')→hashFiles('composer.json')(缓存 key 随 composer.json 变化)- 对需要 >=8.3 的包,从 CI 矩阵中移除 PHP 8.2
PHP-CS-Fixer 的版本陷阱
CI 跑在三个 PHP 版本(8.2、8.3、8.4)上。PHP-CS-Fixer 的 @PHP84Migration 规则集在不同 PHP 版本下表现不同——PHP 8.4 下它会要求消除 return (new Config()) 的括号,但我们的包要兼容 8.2/8.3。最终添加 'new_expression_parentheses' => false 禁用该规则。
版本号策略:是全新发布还是延续?
这是最有争议的决策点。hizpark/zip-mover 已经发布到 v1.0.1,现在要发布 changhorizon/zip-mover。新包名下的版本号应该从 v0.1.0 开始,还是延续 v1.1.0?
我的结论:延续现有版本,升 minor。如果一个包在旧 vendor 下已经发布到 v1.0.1,新 vendor 的首版设为 v1.1.0 而非 v0.1.0,这样用户在 Packagist 上看到的是”版本号在增长”的连续感,而不是”从 1.0 掉到 0.1”的倒退感。
| 旧版本 | 新版本 |
|---|---|
| directory-tree v1.0.2 | v1.1.0 |
| zip-mover v1.0.1 | v1.1.0 |
| scoped-storage-strategy v2.0.0 | v2.1.0 |
| paginator v1.0.0 | v1.1.0 |
| crawler v0.1.0 | v0.2.0 |
| pps v0.1.2 | v0.2.0 |
| file-uploader v0.0.1 | v0.1.0 |
| sql-condition(新) | v0.1.0 |
| bs5-renderers(新) | v0.1.0 |
几个值得记录的点
sed 残留清理
.pps.placeholders.php 文件中有 PPS 模板遗留的例值(// e.g., 'hizpark'),README 代码示例中也有旧的 use Hizpark\... 命名空间引用。这些不会被第一轮 sed 's/hizpark/changhorizon' 覆盖(因为它匹配的是小写 hizpark 而非代码中的 Hizpark)。需要单独扫描清理。
包已发布到 Packagist 时
旧包 hizpark/xxx 在 Packagist 上无法直接重命名或转移所有者。我的处理方式是不动旧包,直接提交 changhorizon/xxx 新包。Packagist 的 auto-update 机制会从 GitHub 新仓库自动同步版本和 README。
Library 包不要提交 lock 文件
最终确认了这个决策。Composer 官方文档也明确:library 提交 composer.lock 不是必须的。PPS 模板包含 lock 文件是脚手架的设计选择,不是最佳实践。
总结
整个 rebrand 涉及 9 个包、50+ 个文件、数百处替换,加上 CI、CS-Fixer、PHPStan 的适配工作,是一个全量工程。核心经验:
- 两轮严格大小写匹配的 sed——覆盖包名和命名空间,不重不漏
- 依赖顺序处理——leaf 优先,consumer 随后
- 版本号延续升 minor——保持用户感知的连续性
- lock 文件不提交——library 包的锁没有意义
- REAME 中的代码示例也要检查——
use Hizpark\...不会自动被小写替换覆盖