一、你写的PHP代码,可能藏着没发现的“隐形bug”

你有没有过这种经历:改了一个小功能,本地测着没问题,提交到测试环境后,突然出现了空指针报错、参数类型不匹配的问题,甚至还得回滚代码?很多时候,这些问题不是你写代码时故意犯的错,而是你没注意到的细节漏洞——比如传了个null却没判断、方法需要数字你传了字符串、某个变量压根没赋值就用了。

以前我们总靠测试、靠代码评审来堵这些漏洞,但测试有盲区,评审也可能漏看细节。有没有什么办法,能在代码提交到服务器之前,就把这些漏洞揪出来?这就要说到静态分析工具了,而PHP生态里最常用的两个,就是PHPStan和Psalm。

二、什么是静态分析?为啥要选PHPStan和Psalm?

静态分析说白了,就是工具不用真的运行你的代码,只通过“读”你的代码,就能找出可能的问题。比如你写了一个方法需要接收数字参数,却传了字符串,工具扫一眼代码就知道不对,不用等代码跑起来报错才发现。

那为啥是PHPStan和Psalm?其实这俩工具功能差不多,都是专门针对PHP的静态分析工具,核心都是帮你找代码里的潜在问题。区别主要在细节:PHPStan的规则更严格,适合对代码质量要求高的项目;Psalm的规则更灵活,支持自定义的类型推断,适合类型比较复杂的项目。但不管选哪个,核心目的都是一样的——提前发现问题。

三、怎么把这俩工具用到日常开发里?(带完整示例)

3.1 第一步:选一个项目做试点(技术栈:PHP 8.1 + Composer + Git)

我们先拿一个简单的PHP项目来做试点,技术栈明确:PHP 8.1(支持类型声明)、Composer(依赖管理)、Git(版本控制)。

首先,我们先准备一个基础的PHP项目结构,先创建一个简单的项目,里面有一个处理用户积分的类,这个类里有几个常见的问题:

<?php
// 项目路径:src/UserScore.php
class UserScore {
    /**
     * 给用户增加积分
     * @param int $userId 用户ID
     * @param int $score 增加的积分(必须是正整数)
     * @return int 增加后的总积分
     */
    public function addScore(int $userId, int $score): int {
        // 这里有个问题:如果$score是负数,也会被加进去
        if ($score < 0) {
            return $this->getTotalScore($userId);
        }
        // 这里还有个问题:getTotalScore可能返回null(比如用户不存在),如果加null会报错
        $newScore = $this->getTotalScore($userId) + $score;
        // 假设这里是保存到数据库的逻辑
        $this->saveScore($userId, $newScore);
        return $newScore;
    }

    /**
     * 获取用户的总积分
     * @param int $userId 用户ID
     * @return int|null 如果用户不存在,返回null
     */
    public function getTotalScore(int $userId): ?int {
        // 模拟从数据库查积分,用户不存在返回null
        return $userId > 0 ? 100 : null;
    }

    /**
     * 保存用户积分到数据库
     * @param int $userId 用户ID
     * @param int $score 积分
     */
    private function saveScore(int $userId, int $score): void {
        // 模拟保存逻辑
    }
}

这个类看起来没问题,但其实藏着两个问题:一是addScore里如果getTotalScore返回null,加$score会触发PHP的类型错误;二是如果我们传一个字符串类型的$score进去,比如addScore(1, '50'),本地运行时PHP 8.1会报错,但如果我们没测到这个场景,提交后就会出问题。

3.2 第二步:安装并配置PHPStan/Psalm

我们先装PHPStan,用Composer安装:

# 在项目根目录运行,安装PHPStan到开发依赖
composer require --dev phpstan/phpstan

安装完后,需要给PHPStan做配置,创建一个phpstan.neon配置文件:

# 项目路径:phpstan.neon
includes:
    - vendor/phpstan/phpstan/conf/config.neon
parameters:
    # 指定要分析的代码目录
    paths:
        - src
    # 规则级别:级别越高越严格,0是最松,9是最严
    level: 6
    # 允许的PHP版本,和项目一致
    phpVersion: 80100

配置里的level很重要,新手别直接开9级,先从6级开始,这个级别会检查最常见的问题:比如空指针、参数类型不匹配、未定义的变量等。

装Psalm的话,命令类似:

composer require --dev vimeo/psalm

Psalm的配置文件是psalm.xml,运行下面的命令会自动生成基础配置:

./vendor/bin/psalm --init

生成的配置里,也可以调整规则级别,比如把errorLevel设为6,和PHPStan保持一致。

3.3 第三步:本地运行工具,看看效果

先运行PHPStan,看看它能发现什么问题:

# 项目根目录运行
./vendor/bin/phpstan analyse

运行后你会看到输出的错误:

Line 14 in src/UserScore.php: Unsafe operation between int and null (int|null).

这个错误正好指出了我们的问题:$this->getTotalScore($userId)可能返回null,和$score(int)相加是不安全的。

再运行Psalm,输出的错误类似:

src/UserScore.php:14:36: Unsafe operation between int and null (int|null).

这说明两个工具都能精准找到这个潜在的bug。

接下来我们修复这个问题,修改addScore方法:

<?php
// 修复后的src/UserScore.php
class UserScore {
    public function addScore(int $userId, int $score): int {
        if ($score < 0) {
            return $this->getTotalScore($userId) ?? 0; // 加空合并运算符,避免null
        }
        $total = $this->getTotalScore($userId);
        // 先判断total是否为null,处理空的情况
        if ($total === null) {
            // 比如用户不存在,先给初始积分
            $total = 0;
        }
        $newScore = $total + $score;
        $this->saveScore($userId, $newScore);
        return $newScore;
    }

    public function getTotalScore(int $userId): ?int {
        return $userId > 0 ? 100 : null;
    }

    private function saveScore(int $userId, int $score): void {
    }
}

再运行PHPStan和Psalm,错误就消失了。

四、怎么让工具成为“代码守门员”?(结合持续集成)

刚才的步骤是本地运行,但如果有人忘了跑工具就提交代码怎么办?这就要把工具集成到持续集成(CI)流程里——说白了,就是每次有人提交代码到Git,服务器会自动跑PHPStan/Psalm,只有工具没报错,代码才能通过检查,进入后续的测试、部署环节。

我们以GitLab CI为例,写一个简单的CI配置,这个配置会在每次提交代码时自动运行PHPStan:

# 项目路径:.gitlab-ci.yml
stages:
  - test # 定义阶段,这里先做测试阶段

phpstan: # 任务名称
  stage: test # 属于test阶段
  image: php:8.1-cli # 用PHP 8.1的官方镜像
  before_script: # 运行任务前的准备工作
    - apt-get update && apt-get install -y unzip # 安装unzip,用来解压Composer
    - curl -sS https://getcomposer.org/installer | php # 安装Composer
    - mv composer.phar /usr/local/bin/composer # 把Composer设为全局命令
    - composer install --no-dev --optimize-autoloader # 安装项目依赖,这里--no-dev是因为生产环境不需要开发依赖,但CI需要开发依赖的话可以去掉
    - composer require --dev phpstan/phpstan # 重新安装PHPStan(因为--no-dev会跳过开发依赖)
  script: # 任务的核心命令
    - ./vendor/bin/phpstan analyse # 运行PHPStan
  only: # 只有提交到指定分支时才运行这个任务
    - main
    - develop

如果用GitHub Actions,配置类似,核心都是自动跑工具。

这样一来,任何提交到main或develop分支的代码,都必须通过PHPStan的检查才能通过CI,从流程上杜绝了“忘了跑工具”的情况,让静态分析真正成为代码质量的底线。

五、工具的优缺点和注意事项

5.1 优点

首先是提前发现问题,减少线上bug。以前很多问题要到测试甚至线上才发现,现在本地或CI阶段就能揪出来,节省了排查问题的时间。 其次是统一代码规范。比如团队里有人习惯写弱类型的代码,有人喜欢强类型,工具会强制大家符合规范,避免代码风格混乱。 最后是减少评审压力。代码评审时,评审人不用再纠结“这里有没有空指针”“参数类型对不对”,这些问题工具已经解决了,评审人可以把精力放在逻辑合理性上。

5.2 缺点

第一个是学习成本。新手可能看不懂工具输出的错误信息,比如“Unsafe operation between int and null”,得花时间理解。 第二个是规则级别不好把控。如果开太严,会出现很多“误报”,比如有些代码逻辑上没问题,但工具觉得有问题,反而影响开发效率;开太松又起不到作用。 第三个是对旧项目不友好。如果是一个写了很久的旧项目,一开始开工具可能会报几百个错误,改起来很麻烦。

5.3 注意事项

第一,旧项目不要直接开最高级别。先从低级别开始,比如先开3级,只检查最严重的问题,把旧代码逐步改完,再慢慢提高级别。 第二,不要盲目禁用规则。如果工具报了一个你觉得没问题的错误,别急着在配置里禁用规则,先想想是不是自己的代码有问题。比如工具报“参数类型不匹配”,可能是你漏了类型声明,而不是工具错了。 第三,结合本地和CI。本地跑工具能让开发者在提交前就发现问题,避免提交后CI报错被打回;CI跑工具是最后一道防线,确保没人跳过本地检查。

六、总结

静态分析不是花架子,而是能实实在在提升代码质量的工具。把PHPStan和Psalm集成到开发和CI流程里,相当于给代码加了一道“过滤网”,提前把潜在的bug拦下来,让代码质量不再靠开发者的自觉,而是靠流程和工具来保障。

刚开始用的时候可能会觉得麻烦,比如改代码时多了一步跑工具,或者CI报错要改代码,但习惯之后,你会发现线上bug少了,评审效率高了,代码整体也更健壮了。毕竟,能提前发现的问题,总比等到线上出问题再解决要好得多。