升级指南应置于README.md顶部、CHANGELOG.md各版本头及GitHub Release描述中;BC break须明确标注旧→新路径及配置示例,并用conflict/provider约束防危险升级。

升级指南该放在哪儿,用户才真能看见
用户不会翻遍仓库找 UPGRADE.md 或点进 Wiki——他们只会在升级失败后顺手搜 “how to upgrade” 或看 composer update 报错时的提示。所以升级说明必须出现在三个位置:README.md 顶部加「Upgrading」小节、CHANGELOG.md 每个大版本头加迁移要点、发布新版本时在 GitHub Release 的描述里直接贴关键步骤。
常见错误是把升级指南写成“对比列表”,比如“v2.0 新增了 Foo::bar()”。这没用。用户真正卡住的是:旧代码调用 LegacyService::run() 突然报 Call to undefined method,而你没说它被移到 NewService::execute() 且构造函数多了一个 $config 参数。
- 所有破坏性变更(BC break)必须在升级指南里对应到具体函数/类/配置项,格式统一为:
旧路径::旧方法()→新路径::新方法() - 如果涉及配置键名变更(如
cache_dir改为cache.path),必须给出完整配置片段示例,而不是只写“重命名了” - 不写“建议先备份”,写“执行
composer update vendor/package:^1.0后,运行phpunit --filter=Legacy确认旧路径测试仍通过”
用 composer.json 的 conflict 和 provide 拦住危险升级
光靠文档拦不住所有人。用户执行 composer update 时,Composer 默认会尝试升到满足约束的最高版本,哪怕新版本和项目里另一个包冲突。这时候得靠 composer.json 的元数据主动干预。
例如 v2.0 移除了对 PHP 7.4 的支持,但用户项目还锁着 "php": "^7.4",不拦住就会装上无法运行的 v2.x。这时在包的 composer.json 里加:
"conflict": {
"php": "<8.0"
}
再比如,你的包拆成了 vendor/package-core 和 vendor/package-legacy,老用户还在用旧包,就得用 provide 声明兼容性:
"provide": {
"vendor/package": "1.*"
}
这样当用户同时 require vendor/package-core 和旧版 vendor/package 时,Composer 会直接报错,而不是让两个版本共存导致行为混乱。
-
conflict优先级高于require,适合拦截环境不兼容(PHP 版本、扩展缺失)或已知冲突的其他包 -
provide不解决实际代码兼容,只用于 Composer 解析依赖图;别用它掩盖 BC break,该改代码还得改 - 别在
conflict里写模糊范围如"*",这会让 Composer 完全拒绝安装,用户根本看不到具体原因
升级脚本不是可选功能,是必须内置的命令
当迁移涉及大量文件修改(比如重命名类、替换模板语法、更新数据库 schema),靠人肉 grep + sed 容易漏。这时候需要一个可执行的升级命令,让用户运行 php vendor/bin/package-upgrade v1-to-v2 自动处理。
这个脚本不能只是“帮你生成 patch”,而要能安全回退:先检查当前版本是否符合升级前提(如是否存在 config/old.php),再备份关键文件(config/app.php → config/app.php.bak-v1),最后才写入新内容。用户中断执行也不该留下半残状态。
- 脚本入口必须放在
bin/目录下,并在composer.json的bin字段注册,否则composer install后不可用 - 参数设计要克制:只接受明确的迁移路径(
v1-to-v2),不搞--force或--dry-run这种让人误判的开关 - 输出必须带上下文:不要只写 “Updated config”,而是 “Renamed
'cache_dir'→'cache.path'inconfig/app.php”
CHANGELOG 里每一行都要能触发一次真实操作
用户查 CHANGELOG 不是为了读故事,是想快速定位“我改了哪几行就能跑起来”。所以条目不能是 “Refactor internal logic”,而要是 “Remove Helper::getCacheKey(); use CacheKeyGenerator::forEntity() instead”。
更关键的是,每个条目背后得有对应的动作锚点:要么链接到升级指南的具体章节,要么在括号里直接写清替换方式。比如:
Deprecated Config::load() (use Config::fromFile() instead)
比这更好的是:
Config::load() removed — replace <code>Config::load('app') with Config::fromFile(__DIR__.'/config/app.php')
- 按语义分组,不用时间倒序:把所有 “Removed” 放一起,“Renamed” 放一起,“Added” 放一起,方便 Ctrl+F 查
- 不写 “Fix bug in X” 这种模糊描述;如果是修复了某个接口的返回类型,就写 “
ApiClient::fetch()now returnsarrayinstead ofstdClass” - 大版本升级的 CHANGELOG 开头必须加警告块(用 标出):“⚠️ 以下变更需手动修改代码,不执行将导致 Fatal error”
最常被忽略的点:没人会反复检查你写的升级指南是否和当前 composer.lock 里的实际版本匹配。上线前拿一个真实旧项目跑一遍 composer update + 指南步骤,比校对十遍文档都管用。










