用途
本技能用于让 Symfony 应用在版本迭代中保持可升级状态,重点解决“长期忽略弃用告警后,大版本升级从一下午变成三个月”的问题。它按需阅读:当出现弃用告警、需要更新 recipe 或面临新大版本时使用。贯穿始终的一条规则是:先升到当前大版本的最后一个次版本(last-minor-first)。
发布生命周期与“次版本无 BC 破坏”的含义
Symfony 每 6 个月发布一个次版本,增加功能与弃用告警,但不破坏现有代码:能在 x.0 上运行的代码也能在 x.4 上运行。每 2 年发布一个大版本,只移除已弃用的内容。大版本的最后一个次版本(x.4)是 LTS,承载你需要的全部弃用通知;在 x.4 上做到零弃用,基本等同于准备好升级到 (x+1).0。因此不要从 x.2 直接跳到 (x+1).0,而应先到 x.4、跑绿、清掉弃用,再升大版本,每一步都可单独回滚。
“次版本无 BC 破坏”不等于“什么都没变”:次版本可能修改 recipe、修改它不再拥有的配置文件默认值,或弃用 services.yaml 依赖的选项。它保证的是你的代码继续工作,而不是项目与全新生成的项目完全一致,这个差距由 composer recipes 来衡量。
查看当前版本状态
用 php bin/console about 查看版本、是否 LTS、维护终止与生命周期终止日期。这些日期编译在 Symfony\Component\HttpKernel\Kernel 的 END_OF_MAINTENANCE 与 END_OF_LIFE 常量中,是已安装代码的事实。普通次版本的两个日期相同;只有 LTS 会把两者拉开数年。
用以下命令查看锁定约束:
php -r '$j = json_decode(file_get_contents("composer.json"), true); echo $j["extra"]["symfony"]["require"], PHP_EOL;'
extra.symfony.require 是 Flex 设置,Flex 安装的 PackageFilter 会把该约束之外的 symfony/* 版本从解析器中隐藏,导致被忽视的项目执行 composer update 时报告“nothing to update”。环境变量 SYMFONY_REQUIRE 可覆盖它,用于在不改 composer.json 的情况下试下一个次版本。
弃用告警的归属分类
PHPUnit 13 按调用方把弃用分为三类:self(在 <source> 下的文件或测试中触发,属于你,应删除弃用代码)、direct(你的代码调用了随后弃用自身的第三方 API,属于你,修调用点)、indirect(第三方调用第三方,不属于你,升级依赖或等待)。recipe 的 phpunit.dist.xml 默认带 ignoreIndirectDeprecations="true",套件会在前两类失败、对第三类保持沉默。注意两个陷阱:摘要行仍显示 “OK, but there were issues!” 而退出码为 1,CI 是对的;ignoreSuppressionOfDeprecations="true" 不是可选项,因为 Symfony 的 trigger_deprecation() 是 @trigger_error(...) 静默的,没有该属性 PHPUnit 会忽略所有 Symfony 弃用。临时把 ignoreIndirectDeprecations 设为 false 是升级规划手段,可看出哪些依赖尚未准备好。
Recipe 漂移
项目创建时 Flex 为每个包运行 recipe 并写入 security.yaml、phpunit.dist.xml、Dockerfile、.env 等,上游 recipe 此后可能已修改而你的文件没有。用 composer recipes 查看全部并标记过时项,composer recipes -o 只看过时项(项目最新时不输出任何内容)。更新用 composer recipes:update symfony/framework-bundle,它计算原 recipe、新 recipe 与当前文件并应用三方补丁,暂存到 git,用 git diff --cached 审查;索引脏时拒绝运行。大版本升级时尤其重要,因为新大版本的 recipe 往往就是新配置所在。
Rector
Rector 已包含在 jakzal/phpqa 镜像中(jakzal/phpqa:1.125.1-php8.4-alpine 中为 2.6.2),捆绑 Symfony、Doctrine、PHPUnit 规则集,无需加入 composer.json。运行示例:
docker run --rm -u $(id -u):$(id -g) -v "$PWD":/project -w /project \
jakzal/phpqa:1.125.1-php8.4-alpine rector process --dry-run --no-progress-bar
--dry-run 在会改动时退出码为 2,可当作检查使用。带版本号的规则集已移除,rector-symfony 的 SymfonySetList 现在只含 COMPOSER_BASED、SYMFONY_CODE_QUALITY、SYMFONY_CONSTRUCTOR_INJECTION 和 CONFIGS;替代方案把规则绑定到实际安装的包版本:
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__.'/src', __DIR__.'/tests'])
->withComposerBased(symfony: true, doctrine: true, phpunit: true);
Rector 擅长机械重写(注解转属性、类重命名、签名变更),不擅长需要判断的改动。建议每次只处理一个关注点,审查 diff 并单独提交。注意便利标志:加上 ->withImportNames() 会把单文件 diff 变成四个文件,把无关测试中的全限定名也重写。
大版本升级顺序
- 先检查依赖:
composer why-not symfony/framework-bundle 9.0列出所有阻碍包。 - 在最后一个次版本上跑绿:测试通过、composer audit 干净、recipe 已更新。
- 清除弃用(self 与 direct),再打开 indirect 检查依赖还欠什么。
- 提升约束:extra.symfony.require 以及 require / require-dev 中 Flex 写为 8.1.* 的 symfony/* 行。
- 解析:
composer update "symfony/*" --with-all-dependencies,阅读 Composer 拒绝的内容。 - 依次运行测试套件、recipe、应用。
被放弃或不兼容的依赖
composer audit --abandoned=report 列出作者已声明停止维护的包。被放弃不是紧急事件,而是从下个大版本开始的倒计时。选项从差到好:锁定大版本并停止升级(只是转移问题)、fork(由你维护)、用框架自身机制替换(Symfony 吸收了大量 bundle,很多最终落到这里)。应说明选择哪一种及原因。
适用场景与限制
适用于 Symfony 应用的版本升级规划与执行、弃用清理、recipe 更新和依赖阻塞排查。不覆盖:CI 配置、PHPStan、phpunit.dist.xml 内容(见 symfony-yoandev-quality);升级合并后的部署顺序(见 symfony-yoandev-deployment);编写或修复迁移(见 symfony-yoandev-doctrine);判断测试是否值得保留(见 symfony-yoandev-testing)。