Rector 不止会升级 PHP:框架迁移、规范强制和大规模代码改造

2026-04-19 奥古斯宏 #Rector #代码重构 #框架迁移 #AST #CI
Rector 不只是 PHP 升级工具 -- 自定义规则做框架迁移、CI 里强制代码规范、AST 级别的大规模重命名,它是真正的代码改造引擎

如果你看过我之前写的 Rector 入门介绍,你应该已经知道它能自动升级 PHP 版本、清理死代码、做 early return 重构。但那只是它能力的冰山一角。本质上 Rector 是一个 AST(抽象语法树)级别的代码改造引擎——只要你能用代码描述"把模式 A 改成模式 B",Rector 就能在整个项目里精确执行。这意味着它的用途远不止"升级 PHP":框架迁移、编码规范强制、大规模重命名、技术债务批量清理——这些过去需要团队花几周手工干的活,Rector 可能一条命令就搞定了。

先搞清楚一件事:Rector 到底在干什么

很多人以为 Rector 是文本替换工具的高级版。不是的。它的工作流程是这样的:

源代码 → Tokenizer → AST(语法树)→ 规则匹配 → AST 变换 → 重新生成代码

关键区别在于 AST 这一层。文本替换看到的是字符串,AST 看到的是语义。举个例子:

// 你想把 Order 改成 PurchaseOrder

// 文本替换的风险:
$order = new Order();        // ← 这个该改
echo "Order #1234 shipped"; // ← 这个不该改,但文本替换可能误伤

// AST 替换的结果:
$order = new \App\Model\PurchaseOrder();  // ← 类引用,改了
echo "Order #1234 shipped";              // ← 字符串内容,不动

这就是为什么 Rector 能安全地做全项目级改造——它知道每个符号是什么意思。

Rector GitHub 源码仓库

场景一:框架版本迁移

这是 Rector 除了 PHP 版本升级之外最实用的能力。主流框架都有对应的规则集:

Symfony 迁移

Symfony 的跨版本迁移是出了名的痛苦——每次大版本升级官方都有一篇几十页的迁移指南。Rector 把这些指南变成了可执行的规则:

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

use Rector\Config\RectorConfig;
use Rector\Symfony\Set\SymfonySetList;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    ->withSets([
        // 从 Symfony 5.4 一路升到 7.1
        SymfonySetList::SYMFONY_60,
        SymfonySetList::SYMFONY_61,
        SymfonySetList::SYMFONY_62,
        SymfonySetList::SYMFONY_63,
        SymfonySetList::SYMFONY_64,
        SymfonySetList::SYMFONY_70,
        SymfonySetList::SYMFONY_71,
        // 构造器注入改为属性注入(Symfony 推荐的新写法)
        SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
    ]);

它会帮你处理的事情包括但不限于:

  • AbstractController::get()ContainerInterface 调用改成依赖注入
  • 旧的事件订阅器格式改成新的 Attribute 格式
  • @Route 注解改成 PHP 8 的 #[Route] 属性
  • 配置文件格式的变更(YAML 结构调整)
  • 废弃的服务 ID 替换成新的名称

Rector 规则浏览器 — 搜索 Symfony 相关规则

Laravel 迁移

Laravel 社区维护了 driftingly/rector-laravel 扩展包:

composer require driftingly/rector-laravel --dev
use Rector\Config\RectorConfig;
use RectoLaravel\Set\LaravelSetList;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/app'])
    ->withSets([
        LaravelSetList::LARAVEL_100,   // Laravel 10 的代码风格
        LaravelSetList::LARAVEL_110,   // 升到 Laravel 11 的变更
        LaravelSetList::LARVEL_CODE_QUALITY,  // Laravel 最佳实践
    ]);

处理的内容包括:Facade 改为构造器注入、旧的 Model::factory() 写法更新、中间件格式变更等。

PHPUnit 迁移

测试代码的迁移往往被忽略,但其实同样重要:

use Rector\PHPUnit\Set\PHPUnitSetList;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/tests'])
    ->withSets([
        PHPUnitSetList::PHPUNIT_90,
        PHPUnitSetList::PHPUNIT_91,
        PHPUnitSetList::PHPUNIT_100,
        PHPUnitSetList::PHPUNIT_110,
    ]);

比如 PHPUnit\Framework\TestCase 中废弃的方法会被替换成新写法,数据提供者的格式也会统一。

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

场景二:用 CI 强制编码规范

大多数团队的 code review 会挑出一些重复性的问题:这里缺了返回类型声明、那里用了 strpos 而不是 str_contains、这个 if 嵌套太深了……这些问题每次都说,每次都有人犯。与其在 code review 里反复唠叨,不如让 CI 来当坏人。

基础配置:不允许不符合规范的代码合进来

# .github/workflows/rector.yml
name: Code Quality Gate
on: [pull_request]

jobs:
  rector:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
      - run: composer install
      - run: vendor/bin/rector process --dry-run

PR 提交后如果 Rector 发现任何可以改进的地方,CI 就会报错。开发者需要在本地运行 vendor/bin/rector process 修复后再提交。

进阶配置:区分"必须修"和"建议修"

不是所有规则都应该阻断合并。可以把规则分成两个级别:

// rector.php — 必须执行的规范(CI 阻断)
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    ->withSets([
        SetList::TYPE_DECLARATION,     // 必须有类型声明
        SetList::CODE_QUALITY,         // 代码质量基本要求
        SetList::DEAD_CODE,            // 不能有死代码
        SetList::EARLY_RETURN,         // 嵌套 if 要 early return
    ])
    ->withSkip([
        __DIR__ . '/src/Migrations/*', // 数据库迁移文件除外
    ]);
// rector-suggest.php — 建议性优化(CI 不阻断,仅报告)
return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    ->withSets([
        SetList::PRIVATIZATION,       // 建议 public 改 private
        SetList::NAMING,              // 命名规范建议
    ]);
# CI 中必须通过的跑 dry-run(有改动就失败)
- run: vendor/bin/rector process --config rector.php --dry-run

# 建议性的只输出报告,不阻断
- run: vendor/bin/rector process --config rector-suggest.php --dry-run || true
- run: echo "以上是建议性优化,不影响合并"

这样做的效果是:code review 可以专注于逻辑和架构问题,不再浪费时间在格式和风格上

场景三:大规模重命名和命名空间重组

项目发展到一定阶段,总会遇到重构需求:某个类要改名、一组类要从 App\Legacy 迁到 App\Core、接口命名要从 IUserInterface 改成 UserInterface。这种全项目范围的手工修改既枯燥又容易漏。

类名全项目替换

入门文章里演示过基本的 RenameClassRector。实际项目中你可能一次要改几十个类:

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\Name\RenameClassRector;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    ->withRules([RenameClassRector::class])
    ->withConfiguredRule(RenameClassRector::class, [
        // 一次性批量映射所有要改的类
        'App\\Legacy\\Models\\User' => 'App\\Models\\User',
        'App\\Legacy\\Models\\Order' => 'App\\Models\\Order',
        'App\\Legacy\\Models\\Product' => 'App\\Models\\Product',
        'App\\Legacy\\Services\\UserService' => 'App\\Services\\UserService',
        'App\\Legacy\\Services\\PaymentService' => 'App\\Services\\PaymentService',
        'App\\Legacy\\Utils\\Helper' => 'App\\Support\\Helper',
        'App\\Legacy\\Utils\\Validator' => 'App\\Support\\Validator',
        // old interfaces with I prefix
        'App\\Contracts\\IUserRepository' => 'App\\Contracts\\UserRepository',
        'App\\Contracts\\IOrderService' => 'App\\Contracts\\OrderService',
    ]);

use 语句、实例化、类型提示、注解中的类名引用——全部自动替换。而且基于 AST,不会误伤注释或字符串里的同名文字。

命名空间前缀批量替换

如果你的项目要把整个命名空间下的所有类都改名,用 RenameClassRector 批量映射就行——把旧命名空间下的每个类都列出来:

use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\Name\RenameClassRector;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    ->withRules([RenameClassRector::class])
    ->withConfiguredRule(RenameClassRector::class, [
        // 整个 Acme\Old 命名空间下的类逐一映射
        'Acme\\Old\\Models\\User' => 'App\\New\\Models\\User',
        'Acme\\Old\\Models\\Order' => 'App\\New\\Models\\Order',
        'Acme\\Old\\Services\\UserService' => 'App\\New\\Services\\UserService',
        // ... 其他类
    ]);

如果涉及的类很多(几十上百个),可以先写个脚本从代码库里扫描出所有类名,生成完整的映射表再喂给 Rector。这比手工一个个改可靠得多。

场景四:技术债务批量清理

老项目里总有一些"历史遗留"的代码模式,单个看都不严重,但几百个文件加起来就是巨大的维护负担。Rector 可以一次性把这些模式全部统一。

实际案例:一个文件同时触发多条规则

来看一段集齐了各种老项目味道的代码:

// 清理前
class UserService
{
    private $repository;
    private $cache;

    public function __construct($repo, $cache)  // 无类型参数
    {
        $this->repository = $repo;               // 手动赋值
        $this->cache = $cache;
    }

    public function isActive($user)               // 无返回类型
    {
        if ($user !== null) {
            if ($user->status === 'active') {     // 三层嵌套 if
                if (strtotime($user->lastLoginAt) > time() - 86400 * 30) {
                    return true;
                }
            }
        }
        return false;
    }

    private function formatName($user)             // 未使用的私有方法
    {
        return $user->firstName . ' ' . $user->lastName;
    }

    private function unusedHelper()                 // 死代码
    {
        return 'nobody calls me';
    }
}

function check_string_exists($haystack, $needle)   // 无类型 + 旧 API
{
    if (strpos($haystack, $needle) !== false) {   // strpos 应该用 str_contains
        return true;
    }
    return false;
}

启用四套规则集,一次运行:

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    ->withSets([
        SetList::PHP_80,            // 同时升级语法(str_contains 等)
        SetList::CODE_QUALITY,      // 通用质量改进
        SetList::DEAD_CODE,         // 死代码清除
        SetList::EARLY_RETURN,      // 嵌套 if → early return
        SetList::TYPE_DECLARATION,  // 类型声明补全
    ]);

结果——同一个文件,8 条规则同时生效

5 套规则同时运行的效果

// 清理后
class UserService
{
    public function __construct(                        // [1] 构造器属性提升
        private $repository,
        private $cache
    ) {}

    public function isActive($user): bool              // [2] 返回类型声明
    {                                                  // [3] 嵌套 if → early return
        if ($user === null) {
            return false;
        }
        if ($user->status !== 'active') {
            return false;
        }
        return strtotime((string) $user->lastLoginAt) > time() - 86400 * 30;
    }
    // [4] formatName() 和 unusedHelper() 被删除了 —— 未使用的私有方法
}

function check_string_exists($haystack, $needle): bool  // [5][6] 参数+返回类型
{                                                         // [7] strpos → str_contains
    return str_contains((string) $haystack, (string) $needle);
}

更多常见的"技术债务"模式

除了上面这些,还有一些高频场景:

重复条件判断简化

// 前:同一变量反复比较
if ($role === 'admin' || $role === 'superadmin' || $role === 'owner') { ... }

// 后:自动改为 in_array
if (in_array($role, ['admin', 'superadmin', 'owner'], true)) { ... }

空值检查简化

// 前:多个空值判断堆在一起
if ($data === false || $data === null || $data === '') { ... }

// 后:自动合并为 in_array
if (in_array($data, [false, null, ''], true)) { ... }

这些看起来都是小改动,但在一个 500 文件的项目里,可能有上百处这样的模式。手动改不仅慢,而且容易遗漏。

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

场景五:自定义规则——把团队约定写成代码

内置规则覆盖不了你团队的特殊约定?那就自己写。Rector 的自定义规则 API 其实很直观:

最简单的自定义规则示例

假设你们团队有个约定:所有 Controller 方法必须以 action 为前缀(某种旧项目的遗留规范),现在要去掉这个前缀:

<?php
// rules/RemoveActionPrefixRector.php
namespace App\Rector;

use PhpParser\Node;
use Rector\Rector\AbstractRector;
use Rector\ValueObject\MethodName;
use Rector\Contract\Rector\RectorInterface;

final class RemoveActionPrefixRector extends AbstractRector implements RectorInterface
{
    public function getRuleDefinition(): \Rector\Rule\RuleDefinition
    {
        return new \Rector\Rule\RuleDefinition(
            '去掉 Controller 方法名的 action 前缀',
            [
                new \Rector\ValueObject\CodeSample(
                    <<<'CODE_SAMPLE'
class UserController extends BaseController
{
    public function actionList() {}
    public function actionShow() {}
}
CODE_SAMPLE
                    ,
                    <<<'CODE_SAMPLE'
class UserController extends BaseController
{
    public function list() {}
    public function show() {}
}
CODE_SAMPLE
                ),
            ]
        );
    }

    /**
     * @return string[]
     */
    public function getNodeTypes(): array
    {
        return [Node\Stmt\ClassMethod::class];
    }

    public function refactor(Node $node): ?Node
    {
        /** @var Node\Stmt\ClassMethod $node */
        if (!str_starts_with($node->name->toString(), 'action')) {
            return null;
        }

        $newName = substr($node->name->toString(), 6); // 去掉 'action' 前缀
        $node->name = new Node\Identifier($newName);

        return $node;
    }
}

注册到配置中:

use Rector\Config\RectorConfig;
use App\Rector\RemoveActionPrefixRector;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    ->withRules([RemoveActionPrefixRector::class]);

核心就三个方法:

方法 作用
getRuleDefinition() 定义规则的名称和 before/after 示例
getNodeTypes() 声明你要处理哪种 AST 节点
refactor() 对匹配到的节点做实际的修改,返回修改后的节点或 null(不修改)

更复杂的规则可以利用 $this->nodeNameResolver 解析类型、用 $this->betterNodeFinder 查找父节点、用 $this->fileModifier 同时修改多个文件。Rector 内部的 142 条核心规则都是用同样的 API 写的——你看它们的源码就能学会几乎所有技巧。

Rector 规则源码目录

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

渐进式改造策略

Rector 最强大的用法之一是配合 git 做渐进式改造。不要试图一次把所有规则全打开然后 process——那样 diff 太大,review 不过来。

推荐做法:分批推进

# 第一批:只跑 TYPE_DECLARATION(最安全的改动)
vendor/bin/rector process --config rector-type-only.php
git commit -m "refactor: 补全类型声明"

# 第二批:跑 CODE_QUALITY + EARLY_RETURN
vendor/bin/rector process --config rector-quality.php
git commit -m "refactor: 代码质量改进"

# 第三批:跑 DEAD_CODE(风险最高,单独一批)
vendor/bin/rector process --config rector-deadcode.php
git commit -m "refactor: 清除死代码"

# 第四批:框架迁移
vendor/bin/rector process --config rector-framework.php
git commit -m "refactor: Symfony 5.4 → 7.1 迁移"

每批都是独立的 commit,出了问题可以用 git revert 精确回滚某一批。而且每批的 diff 都足够小,code review 时能看清每一处改动是否合理。

安全网:always 运行测试套件

# 在每次 Rector 执行前后都跑测试
vendor/bin/phpunit

# 或者写成一步
vendor/bin/rector process && vendor/bin/phpunit

如果 Rector 改坏了什么,测试会立刻抓住。这也是为什么我强烈建议先在有良好测试覆盖率的项目上使用 Rector——没有测试的保护,大规模自动化改造就是在裸奔。

总结

Rector 的本质是一个 可编程的代码改造引擎。PHP 版本升级只是它最显眼的应用场景,但它能做的事情远不止于此:框架迁移让你不再害怕大版本升级;CI 集成让 code review 从机械劳动变成真正的技术讨论;AST 级别的重命名让大规模重构不再提心吊胆;自定义规则让团队约定变成了可执行的代码;分批渐进的策略让改造过程可控且可回滚。

如果你正在维护一个有一定规模的老项目,或者你的团队在 code review 里反复说同样的话,Rector 值得认真考虑一下。它不能替代架构设计,但它能把那些"我知道应该改但实在懒得动"的体力活变成一行命令。

参考资料

最近浏览
累计浏览次数:1
评论
点击登录
phpreturn,PHP武器库,专注PHP领域的项目和资讯,收录和介绍PHP相关项目。

本站所有权利归 phpreturn.com 所有

举报/反馈/投稿邮箱:phpreturn@ulthon.com

鲁ICP备19027671号-2