PHP 2026 迁移思考:论如何利用静态分析工具实现 legacy 系统的 8.4 特性平滑升级

生存指南:如何用 PHPStan 和 Psalm 给老古董“换肾”—— PHP 8.4 迁移实战

大家好,欢迎来到今天的讲座。我知道你们现在的状态,我也理解你们现在的恐惧。

你们手头可能都有一个或者几个“老古董”——那些从 PHP 5.3、5.4 甚至 PHP 4 时代流传下来的代码库。它们就像你家那个生了锈的挂钟,虽然还能走,但你知道,只要稍微碰一下,它就会掉出两三个齿轮。而在 2026 年,我们的目标是 PHP 8.4。8.4!这不仅仅是 8.0 的升级,这是一次充满活力、甚至有点疯狂的进化。

很多人问:“为什么要折腾?” “现在跑得好好的,干嘛改?” “改了会不会出事?”

好,今天我就要告诉你们:不改,等它彻底停摆;改,是为了在它停摆之前,让它变成一辆法拉利。

我们将通过“静默之声”——也就是静态分析工具——作为我们的手术刀,逐步剥离 Legacy 代码的腐肉,植入 PHP 8.4 的新鲜血液。


第一部分:与“幽灵代码”共存

首先,我们要承认现实。你的 Legacy 系统里,可能充斥着以下这种令人窒息的代码:

<?php
// 这是谁写的?上帝吗?
function getUserData($id) {
    $res = mysql_query("SELECT * FROM users WHERE id = " . intval($id));
    $row = mysql_fetch_assoc($res);
    if (!$row) return null;

    $obj = new stdClass();
    $obj->name = $row['name'];
    $obj->age  = $row['age'];

    return $obj;
}

这不仅是丑陋,这是危险。没有类型提示,没有命名空间,没有严格模式。如果你现在要在 PHP 8.4 中运行这段代码,PHP 会看着你,假装什么都没发生,直到你试图给 $obj->name 加个空格,然后它告诉你“Undefined property”。

这时候,测试用例会告诉你:“Pass!”。因为测试代码可能就是这样写的。这就是测试的谎言。 测试覆盖了逻辑,但没覆盖类型系统。测试能骗过 PHP,但骗不过你的 IDE 和静态分析工具。

所以,我们引入第一件武器:静态分析工具。想象一下,有一个不知疲倦、眼神犀利、手里拿着红笔的老师,时刻盯着你的屏幕。他不会运行代码,他只看代码的“骨架”。

我们有两个主流选手:Psalm 和 PHPStan。

  • Psalm 是个激进派,他喜欢咬文嚼字,甚至比你自己还清楚你的代码意图。
  • PHPStan 是个温和派,他更擅长在代码跑飞之前把你拽住。

为了演示,我将以 PHPStan(版本 2.x/3.x 即将支持 8.4)为主,但 Psalme 的用法大同小异。

第二部分:建立“静默”的防线

在动手大改之前,我们需要先建立一个规矩。你不能直接把 PHP 版本从 7.4 拉到 8.4,然后指望工具把你的错误都修好。那是不可能的。你得先建立防线。

1. 引入 Composer 和 Strict Types

如果你的 Legacy 系统还没用 Composer,那就别说了,先去修这个,改完再回来听讲座。有了 Composer,我们才能控制依赖。

第一步,在所有 PHP 文件的顶部加上 declare(strict_types=1);

<?php
declare(strict_types=1);

// 现在你的代码从此有了底线
function add(int $a, int $b): int {
    return $a + $b;
}

2. 配置 PHPStan

创建一个 phpstan.neon 文件。这不仅仅是配置,这是给你的工具立规矩。

parameters:
    level: 5  # 从 0 开始,慢慢往上调,不要一口吃成胖子
    paths:
        - src
        - app
    ignoreErrors: [] # 嘿,这里本来应该填你要忽略的错误,但别先填!
    checkGenericClassInNonGenericObjectType: true

当你运行 vendor/bin/phpstan analyse 时,你会发现屏幕上刷屏了。是的,那是你的 Legacy 代码在尖叫。别怕,这是它肺里的废气排出来。

第三部分:PHP 8.4 的特洛伊木马

好了,现在我们有了防线,可以开始引入 PHP 8.4 的新特性了。PHP 8.4 带来了三个改变游戏规则的东西:object 返回类型、属性钩子、以及更强大的静态属性类型。

1. 返回类型 object:终于不用玩猜谜游戏了

在 PHP 8.4 之前,如果你写了一个工厂函数,返回一个对象,但你不确定具体返回什么子类,你通常会写 @return object。或者更糟糕,你写 @return MyClass,但实际返回了 MyChildClass

静态分析工具虽然聪明,但面对这种“手动注释”往往会傻眼。

PHP 8.4 引入了原生 object 返回类型。这意味着:这个函数必须返回一个对象,不管是哪个对象。

Legacy 场景:

// 旧时代的工厂
/**
 * @return stdClass
 */
function createConfig() {
    // 偶尔你会忘了加 return new stdClass();
    return new stdClass();
}

// 静态分析工具只会干瞪眼,或者警告你注释可能不准。

8.4 时代:

// 终于,真相大白
function createConfig(): object {
    // 现在工具可以确保:这里必须有一个 return 语句,而且返回值必须是一个对象实例。
    // 如果你不写 return,或者你 return 了 null,PHPStan 会立刻报警!
    return new stdClass();
}

实操迁移:
不要只改函数签名。去检查那些工厂函数。如果它们返回的是集合(比如数组),那是 array。如果它们返回的是标量(整数、字符串),那是 intstring。只有当返回值真的是一个对象时,才用 object。这能让你的代码在 IDE 里获得完美的智能提示。

2. 属性钩子:消灭丑陋的 Getter/Setter

这是一个重头戏。如果你的 Legacy 系统里充满了这种代码:

class User {
    private string $name;

    public function getName(): string {
        return $this->name;
    }

    public function setName(string $name): void {
        $this->name = $name;
    }
}

你会觉得看着恶心吗?是的,这就是我们以前的做法。为了封装,我们写了两个函数,而逻辑只有一行。

PHP 8.4 引入了 #[Getter]#[Setter] 属性钩子。它们把逻辑从方法里搬到了属性定义里。

8.4 写法:

class User {
    public function __construct(
        public string $name, // 直接公开属性,方便!
    ) {}
}

// 如果你需要一个带验证的 Setter:
class User {
    private string $name;

    public function setName(string $name): void {
        if (strlen($name) < 3) {
            throw new InvalidArgumentException("Name too short");
        }
        $this->name = $name;
    }

    #[Getter] // 8.4 特性:自动生成 getter
    public function getName(): string {
        return $this->name;
    }
}

注意看,getName 方法只有一行,它是多余的。#[Getter] 告诉编译器:“嘿,别为我生成什么乱七八糟的 Getter 了,帮我自动生成,参数和返回值照搬下面这个方法就行。”

静态分析的作用:
当你使用 #[Getter] 时,PHPStan 会知道这个属性已经被代理了。如果你在代码里尝试 $user->name = "Bob";(直接访问),PHPStan 会警告你:“你正在直接修改私有属性,应该用 setName 方法。” 这简直比你自己还严格!

第四部分:进阶手术——静态属性与复杂类型

Legacy 代码最可怕的地方在于静态属性。它们通常被当作全局变量使用,充满了 nullmixed 和类型混乱。

PHP 8.4 允许我们在静态属性上使用联合类型和 nullable 标记。

Legacy 坏习惯:

class Database {
    public static $connection = null;

    public static function connect() {
        if (self::$connection === null) {
            self::$connection = new PDO('...');
        }
        return self::$connection;
    }
}

问题:

  1. $connection 可能是任何东西(除了 PDO 之外的 object 也能赋值进去)。
  2. 它可能是 null
  3. 你在 connect 里读取它时,必须每次检查 === null

8.4 + 静态分析修复:

class Database {
    // 现在我们可以指定具体的类型了!
    public static PDO|null $connection = null;

    public static function connect(): PDO {
        // PHPStan 现在知道 $connection 必须是 PDO 或者 null
        if (self::$connection === null) {
            self::$connection = new PDO('...');
        }
        // 因为我们在 if 里检查了 null,PHPStan 知道这里返回的一定是 PDO
        return self::$connection; 
    }
}

为什么这很重要?
当你用 PHPStan 开启严格模式后,任何对 Database::$connection 的访问,如果不处理 null 的情况,都会报错。这强迫你写出防御性代码。

第五部分:平滑迁移的节奏感

很多人问我:“专家,我不可能一天把几千行代码都改成 8.4 吧?”

当然不可能。你是想修好房子,不是想拆了重建。我们要采用“分层渗透”策略。

第一步:配置关卡(Level 0 -> Level 5)

别急着改代码。先配置 PHPStan。把 level 提到 5。修复所有的错误。这一步通常非常痛苦,因为你会发现无数个潜在的 Bug(比如未使用的变量、可能为 null 的变量)。但这就像体检,发现病灶总比等到癌症晚期好。

第二步:引入 Strict Types 和类型提示

不要试图一次性给所有函数加上返回类型。从新文件开始,从最核心的文件开始。对于老文件,可以先给参数加类型。等静态分析不再抱怨,再给返回值加类型。

第三步:引入 8.4 特性

当你觉得代码已经比较“干净”了,就开始引入 object 返回类型和属性钩子。

  • 先重构一个简单的 Model 类。
  • 然后重构一个 Service 类。
  • 最后处理那些复杂的、几百行的 Controller。

第四步:Polyfill

如果你的 Legacy 系统里引用了第三方库(比如十年前的某个 CMS),而该库不支持 PHP 8.4 的 object 类型,你可能会遇到错误。

这时候,不要去改那个第三方库(通常你也没权限)。创建一个适配器或者使用 Composer 的 patcher 功能。

// 这是一个给旧库用的适配器
final class LegacyUserAdapter {
    public function __construct(
        private LegacyUser $user,
    ) {}

    // 把旧库的对象包装成新风格的 object
    public function toObject(): object {
        return $this->user;
    }
}

第六部分:心态建设与团队协作

讲到这里,技术细节讲完了。但我知道你们最担心的是什么。是团队阻力

“我加了 declare(strict_types=1),所有人都要改。”
“我把返回类型加上了,测试挂了。”
“我把那个属性改成 #[Getter],代码审查(Code Review)被驳回了。”

这就是迁移的艺术。平滑升级不是没有变化,而是可控的变化。

  1. 从“运行通过”到“类型安全”:告诉团队,测试通过只是及格线。静态分析通过才是真正的安全。
  2. 增量式反馈:不要让所有人一次面对几千个报错。让他们每天只修 50 个最严重的错误。
  3. 拥抱性能提升:PHP 8.4 的 RCU(Read-Copy-Update)机制能让你的老旧业务在并发下性能翻倍。这是你们老板最喜欢听的故事。“看,只要我们改一点代码,服务器负载就降了一半。”

结语:老树发新芽

回到 2026 年的视角。十年前,我们还在争论要不要用 foreach 代替 while。现在,我们站在了 PHP 8.4 的门槛上。

利用静态分析工具,我们不再是被动地修补漏洞,而是主动地重构架构。我们就像拿着手术刀的外科医生,在 psalm.xml 的指挥下,精准地剔除 Legacy 代码的冗余,植入 8.4 的新特性。

不要害怕报错。报错是工具在对你说话。 如果 PHPStan 报错了,那不是在刁难你,它是在说:“嘿,这里有个坑,跳过去!”

现在,拿起你的工具,打开你的终端。开始你的第一次 phpstan analyse 吧。你会发现,古老的代码虽然面目可憎,但它的骨架里依然藏着逻辑。利用 PHP 8.4,我们将释放这些逻辑,让它们焕发出第二春。

谢谢大家。现在,去干活吧!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注