Skip to content
// 0x
Go back
0x10 // 工具设计

全量 Vendor Rebrand:9 个 PHP 包从 hizpark 到 changhorizon 的实践记录

起因

GitHub 用户名从 hizpark 改为 changhorizon,看上去只是一个账号改名。但运行中的 9 个 PHP 包——每个都有自己的 composer.json、命名空间、CI 配置、Packagist 发布——全都绑着旧名字。

改一个字符串不难。但 9 个包联动,有的互相依赖(ppsdirectory-tree + zip-moverfile-uploaderscoped-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 永远不会匹配到 HizparkHizpark 永远不会匹配到 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 需要调整:

  1. composer installcomposer update --ignore-platform-req=php(无 lock 文件时 install 等价于 update,加上忽略平台要求以兼容不同 PHP 版本)
  2. hashFiles('composer.lock')hashFiles('composer.json')(缓存 key 随 composer.json 变化)
  3. 对需要 >=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.2v1.1.0
zip-mover v1.0.1v1.1.0
scoped-storage-strategy v2.0.0v2.1.0
paginator v1.0.0v1.1.0
crawler v0.1.0v0.2.0
pps v0.1.2v0.2.0
file-uploader v0.0.1v0.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 的适配工作,是一个全量工程。核心经验:


Share this post on:

Previous Post
「未实现」还是「不实现」:PPS 脚手架的设计选择
Next Post
mPDF 6.0 动态图片渲染踩坑:当 base64、PNG、绝对路径全都失效