欢迎光临

PHP 静态分析与代码质量实战:PHPStan、Psalm 与架构级 lint 的完整工作流

在 PHP 项目从快速迭代走向长期维护的过程中,代码质量往往是最先失控的维度。单元测试能捕获逻辑错误,却无法发现类型不匹配、未定义方法调用、死代码路径等结构性问题。PHP 作为一门动态语言,类型系统直到 7.x 才逐步完善,大量遗留代码仍然缺乏类型声明。静态分析工具(Static Analysis)正是在这个缺口上补位的关键基础设施——它在不运行代码的前提下,通过数据流分析和类型推导发现潜在缺陷,将 Bug 扼杀在提交之前。

本文将系统性地介绍 PHP 生态中两大静态分析引擎——PHPStan 和 Psalm——的核心原理、配置实践与进阶用法,并结合架构级 lint 工具(如 Deptrac、PHPMD)构建一套从方法级到模块级的分层质量守护体系。无论你是维护百万行遗留系统的架构师,还是在新项目中追求零 Bug 提交的工程师,都能从中找到可落地的实践方案。

PHP Code Quality

一、为什么 PHP 项目需要静态分析

动态语言的灵活性是双刃剑。PHP 允许你在运行时修改对象属性、调用不存在的方法(通过 __call 魔术方法)、混合传递不同类型的参数。这种自由度在原型阶段是优势,在规模化阶段是灾难。以下几类问题在 PHP 项目中极为常见,却无法被测试轻易捕获:

  • 类型不匹配:函数期望 int,传入 string;数组结构不符合预期
  • 空值引用:对可能为 null 的变量调用方法,导致 Call to a member function on null
  • 未定义符号:拼写错误的方法名、已删除的类常量、不存在的 Trait
  • 死代码:永远不会执行的分支、未使用的 import 和私有方法
  • 架构违规:Controller 直接调用 DAO、跨模块的隐式依赖

静态分析工具通过模拟程序执行路径(符号执行)和推导变量类型(类型推导),在不运行代码的情况下识别上述问题。根据 PHPStan 官方统计,在 level 5(中等严格度)下,典型 PHP 项目中每 1000 行代码能发现 3-8 个潜在缺陷——其中相当比例是运行时偶发的空值错误和类型错误。

二、PHPStan 核心原理与配置实战

PHPStan 由 Ondřej Mirtes 创建,是 PHP 生态中用户量最大的静态分析引擎。它的核心思想是渐进式严格:从 level 0(基本检查)到 level 9(最严格),你可以逐步提升分析强度,避免一次性面对数百个误报。

2.1 安装与基础配置

通过 Composer 安装 PHPStan:


1
2
3
4
5
# 项目本地安装(推荐)
composer require --dev phpstan/phpstan

# 验证安装
vendor/bin/phpstan --version

在项目根目录创建配置文件 phpstan.neon:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
parameters:
    level: 5
    paths:
        - src
        - tests
    excludePaths:
        - src/Legacy/*
    scanDirectories:
        - vendor/some/package/src
    universalObjectCratesClasses:
        - stdClass
        - SimpleXMLElement
    checkMissingIterableValueType: true
    checkGenericClassInNonGenericObjectType: true

关键配置项说明:

参数 作用 推荐值
level 分析严格度,0-9 新项目从 5 开始,遗留项目从 1 开始
paths 扫描目录 通常为 src 和 tests
excludePaths 排除目录 遗留代码、生成代码
universalObjectCratesClasses 标记为任意属性均可的类 stdClass 等
checkMissingIterableValueType 要求迭代器有类型声明 true

2.2 分析级别详解与升级策略

PHPStan 的 10 个级别对应不同的分析深度。理解每个级别的检查内容是制定升级策略的基础:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Level 0-1: 基础存在性检查
# - 未定义的类、方法、函数、常量
# - 调用不存在的方法(无 __call 魔术方法时)

# Level 2-3: 类型声明检查
# - 验证参数类型与声明是否一致
# - 检查返回值类型是否匹配

# Level 4-5: 空值安全与混合类型
# - 可能的 null 调用
# - mixed 类型的操作(字符串拼接数组等)
# - 未检查的迭代器类型

# Level 6-7: 严格类型推导
# - 变量类型在分支中可能变化
# - 泛型类型参数验证
# - 闭包参数与返回值推导

# Level 8-9: 最严格
# - 所有代码必须有完整类型声明
# - 不允许任何 mixed
# - 死代码检测

升级策略:在 CI 中设置 level: max(即当前能通过的最高级别),然后逐步提升。每提升一级,修复新出现的问题后再升级下一级。一个实用的 CI 配置:


1
2
3
4
5
6
7
8
9
10
# .github/workflows/static-analysis.yml
name: Static Analysis
on: [push, pull_request]
jobs:
  phpstan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: composer install --no-progress
      - run: vendor/bin/phpstan analyse --no-progress --error-format=github

2.3 处理误报:baseline 与忽略

遗留项目中首次运行 PHPStan 往往产出上千条错误。baseline 机制允许你冻结当前错误,只关注新增错误:


1
2
3
4
5
6
7
8
9
10
11
# 生成 baseline 文件
vendor/bin/phpstan analyse --generate-baseline

# 这会在项目根目录生成 phpstan-baseline.neon
# 内容示例:
parameters:
    ignoreErrors:
        -
            message: '#^Call to an undefined method#'
            path: src/Legacy/OldService.php
            count: 12

对于单个需要忽略的错误行,可以使用 PHPDoc 注解:


1
2
3
4
5
6
7
8
class UserService
{
    public function findUser(int $id): ?User
    {
        // @phpstan-ignore return.type
        return $this->legacyLookup($id);
    }
}

注意:baseline 是过渡工具,不是长期方案。最佳实践是每月清理一批 baseline 错误,逐步减少总数。

三、Psalm 核心原理与独特能力

Psalm 由 Vimeo/Mashape 团队开发,与 PHPStan 并列为 PHP 生态的两大静态分析引擎。两者共享绝大多数检查能力,但 Psalm 在以下几个领域有独特优势:

  • Taint 分析:追踪用户输入到敏感操作的数据流,发现注入漏洞
  • 更精细的泛型支持:对复杂泛型有更好的推导
  • 自动修复:–alter 模式可以自动添加类型声明

3.1 安装与配置


1
2
3
4
5
6
composer require --dev vimeo/psalm

# 初始化配置
vendor/bin/psalm --init

# 生成 psalm.xml 配置文件

典型 psalm.xml 配置:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0"?>
<psalm
    errorLevel="5"
    resolveFromConfigFile="true"
    findUnusedCode="true"
    findUnusedVariablesAndParams="true"
    findUnusedPsalmSuppress="true"
>
    <projectFiles>
        <directory name="src" />
        <ignoreFiles>
            <directory name="src/Legacy" />
        </ignoreFiles>
    </projectFiles>
    <issueHandlers>
        <PossiblyNullReference errorType="suppress" />
    </issueHandlers>
</psalm>

3.2 Taint 分析:SQL 注入与 XSS 防护

Psalm 的 Taint 分析是其最有价值的独特功能。启用后在 CI 中可以自动检测注入漏洞:


1
2
3
4
5
# 启用 taint 分析
vendor/bin/psalm --taint-analysis

# 或在 psalm.xml 中配置
# <psalm taintAnalysis="true">

以下代码会被 Psalm 标记为安全漏洞:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UserRepository
{
    public function findByEmail(PDO $db, string $email): ?array
    {
        // Psalm Taint: Detected tainted SQL
        // $_GET['email'] -> $email -> 直接拼入 SQL
        $sql = "SELECT * FROM users WHERE email = '" . $email . "'";
        return $db->query($sql)->fetch();
    }
   
    // 修复:参数化查询
    public function findByEmailSafe(PDO $db, string $email): ?array
    {
        $stmt = $db->prepare("SELECT * FROM users WHERE email = ?");
        $stmt->execute([$email]);
        return $stmt->fetch();
    }
}

Taint 分析会追踪数据从源头(source)到汇(sink)的完整路径:

类型 Source(源头) Sink(汇)
SQL 注入 $_GET, $_POST, $_REQUEST PDO::query(), mysqli::query()
XSS $_GET, $_POST echo, htmlspecialchars()(配置错误时)
命令注入 $_GET, $_POST exec(), system(), shell_exec()
路径遍历 $_GET, $_POST fopen(), file_get_contents(), include

3.3 自动修复:类型声明补全


1
2
3
4
5
6
7
8
# 自动添加缺失的类型声明(安全模式,需确认)
vendor/bin/psalm --alter --issues=all

# 仅修复特定问题类型
vendor/bin/psalm --alter --issues=MissingParamType,MissingReturnType

# 预览但不修改(dry-run)
vendor/bin/psalm --alter --issues=all --dry-run

自动修复对于大规模遗留项目的类型声明补全非常高效。一次 –alter 运行可以为数千个方法签名添加类型声明,比手动操作效率高两个数量级。建议在独立的 Git 分支上运行,review 后再合并。

四、PHPStan vs Psalm:如何选择

两个引擎各有优势,选择取决于项目需求:

维度 PHPStan Psalm
社区规模 更大(Composer 下载量约 3x) 较小但活跃
Taint 分析 不支持 核心优势
泛型支持 优秀 更精细
自动修复 不支持 –alter 模式
框架扩展 更多(Laravel, Symfony, Doctrine) 相对较少
运行速度 略快 略慢(增量分析弥补)
误报率 中等 略低
学习曲线 较平缓 中等

推荐策略

  • Web 应用项目(有用户输入):优先 Psalm,利用 Taint 分析
  • 库/SDK 开发:优先 PHPStan,社区覆盖面更广
  • 大型遗留系统改造:先用 Psalm 的 –alter 补全类型,再用 PHPStan 的 baseline 渐进提升
  • 安全敏感项目:两者并用,Psalm 负责安全扫描,PHPStan 负责类型完整性

五、架构级 lint:Deptrac 与 PHPMD

方法级别的类型安全只是代码质量的第一层。当项目规模超过 50 个模块后,架构层面的问题开始显现:循环依赖、跨层调用、职责混乱。这类问题不在 PHPStan/Psalm 的检测范围内——它们分析的是类型流,不是依赖流。

5.1 Deptrac:依赖规则引擎

Deptrac 定义模块之间的依赖方向,检测违规引用:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# depfile.yaml
layers:
    - name: Controller
      collectors:
        - type: className
          regex: .*Controller.*
    - name: Service
      collectors:
        - type: className
          regex: .*Service.*
    - name: Repository
      collectors:
        - type: className
          regex: .*Repository.*

ruleset:
    Controller:
        - Service       # Controller 可以依赖 Service
    Service:
        - Repository    # Service 可以依赖 Repository
    Repository: []      # Repository 不能依赖任何其他层

1
2
3
# 安装与运行
composer require --dev qossmic/deptrac-shim
vendor/bin/deptrac analyse --config-file=depfile.yaml

运行后会输出类似结果:


1
2
3
4
5
Controller\UserController has a dependency on Repository\UserRepository
  in src/Controller/UserController.php:42
 
  Controller -> Repository (SKIPPED Service layer)
  Expected: Controller -> Service -> Repository

5.2 PHPMD:代码复杂度检测

PHPMD(PHP Mess Detector)检测代码的复杂度与设计问题:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ruleset name="Custom PHPMD Rules">
    <rule ref="rulesets/codesize.xml">
        <exclude name="TooManyPublicMethods" />
    </rule>
    <rule ref="rulesets/codesize.xml/CyclomaticComplexity">
        <properties>
            <property name="reportLevel" value="10" />
        </properties>
    </rule>
    <rule ref="rulesets/cleancode.xml">
        <exclude name="StaticAccess" />
    </rule>
    <rule ref="rulesets/unusedcode.xml" />
    <rule ref="rulesets/design.xml">
        <exclude name="CouplingBetweenObjects" />
    </rule>
</ruleset>

1
2
# 运行
vendor/bin/phpmd src xml ruleset.xml

PHPMD 能检测到的问题包括:圈复杂度超标(大于10)、过长参数列表、未使用的私有方法、深层嵌套等。这些坏味道虽不直接导致 Bug,却是技术债的可靠指标。

六、完整 CI 工作流:分层质量守护

将上述工具组合成一套分层的 CI 工作流,每层关注不同维度:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# .github/workflows/quality-gate.yml
name: Quality Gate
on: [push, pull_request]

jobs:
  # 第一层:快速反馈(小于30s)
  syntax-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: php -l src/  # PHP 语法检查

  # 第二层:静态类型分析(2-5min)
  phpstan:
    runs-on: ubuntu-latest
    needs: syntax-check
    steps:
      - uses: actions/checkout@v4
      - run: composer install --no-progress
      - run: vendor/bin/phpstan analyse --no-progress --error-format=github

  # 第三层:安全扫描(3-8min)
  psalm-taint:
    runs-on: ubuntu-latest
    needs: syntax-check
    steps:
      - uses: actions/checkout@v4
      - run: composer install --no-progress
      - run: vendor/bin/psalm --taint-analysis

  # 第四层:架构检查(1-3min)
  deptrac:
    runs-on: ubuntu-latest
    needs: syntax-check
    steps:
      - uses: actions/checkout@v4
      - run: composer install --no-progress
      - run: vendor/bin/deptrac analyse --config-file=depfile.yaml

  # 第五层:复杂度与设计(1-2min)
  phpmd:
    runs-on: ubuntu-latest
    needs: syntax-check
    steps:
      - uses: actions/checkout@v4
      - run: composer install --no-progress
      - run: vendor/bin/phpmd src text ruleset.xml

  # 质量门禁:所有层必须通过
  quality-gate:
    runs-on: ubuntu-latest
    needs: [phpstan, psalm-taint, deptrac, phpmd]
    if: always()
    steps:
      - name: Check all jobs passed
        run: |
          if [ "${{ needs.phpstan.result }}" != "success" ] ||
             [ "${{ needs.psalm-taint.result }}" != "success" ] ||
             [ "${{ needs.deptrac.result }}" != "success" ] ||
             [ "${{ needs.phpmd.result }}" != "success" ]; then
            echo "Quality gate failed"
            exit 1
          fi
          echo "Quality gate passed"

七、遗留系统改造实战路线图

对于缺少类型声明、没有测试覆盖的老项目,全面引入静态分析是痛苦但必要的。以下是经过验证的改造路线图:

阶段一:建立基线(1-2 周)

  1. 安装 PHPStan level 1,运行并生成 baseline
  2. 在 CI 中运行 PHPStan,baseline 错误不计入失败
  3. 安装 Psalm(不启用 taint),生成 baseline
  4. 记录两个工具的错误基线数量,作为改造进度度量

阶段二:类型补全(2-4 周)

  1. 使用 Psalm –alter 自动添加参数和返回类型
  2. 在独立分支运行,review 后合并
  3. 升级 PHPStan 到 level 3-4
  4. 清理对应的 baseline 条目

阶段三:空值安全(2-3 周)

  1. 升级 PHPStan 到 level 5-6
  2. 系统性地添加可空标记和空值检查
  3. 引入 assert() 和 instanceof 类型缩小
  4. 消灭 Mixed 类型警告

阶段四:架构治理(持续)

  1. 引入 Deptrac,定义核心依赖规则
  2. 启用 Psalm Taint 分析
  3. 设置 PHPMD 圈复杂度阈值
  4. 每月提升 PHPStan 级别,直到达到 level 8

每个阶段的成功标准明确:baseline 错误数持续下降,PHPStan 级别持续上升。在改造过程中,CI 始终是绿色的——baseline 保护你免受新错误的困扰,同时确保代码质量持续改善。

八、常见陷阱与最佳实践

8.1 避免过度抑制

@phpstan-ignore 和 @psalm-suppress 是必要的过渡工具,但滥用会降低分析有效性。建议:

  • 每次抑制必须附带原因注释:// @phpstan-ignore-next-line – Legacy API, refactoring in Q3
  • 在 CI 中检测未使用的抑制(Psalm 的 findUnusedPsalmSuppress)
  • 每季度审查所有抑制注解,移除不再需要的

8.2 增量分析加速

大型项目全量分析耗时可能超过 10 分钟。利用增量分析缩短反馈循环:


1
2
3
4
5
6
7
8
# PHPStan 增量分析(仅分析变更文件)
vendor/bin/phpstan analyse --no-progress --ansi

# Psalm 增量分析(缓存推导结果)
vendor/bin/psalm --no-cache=false  # 默认启用缓存

# 只分析 Git 变更文件(适用于 pre-commit hook)
git diff --name-only --diff-filter=ACM HEAD | grep '\.php$' | xargs vendor/bin/phpstan analyse

8.3 pre-commit 集成


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: phpstan
        name: PHPStan
        entry: vendor/bin/phpstan analyse --no-progress --memory-limit=512M
        language: system
        types: [php]
        pass_filenames: false
      - id: psalm
        name: Psalm
        entry: vendor/bin/psalm --no-cache --no-progress
        language: system
        types: [php]
        pass_filenames: false

8.4 Composer scripts 统一入口


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# composer.json
{
    "scripts": {
        "analyse": [
            "@phpstan",
            "@psalm",
            "@deptrac"
        ],
        "phpstan": "vendor/bin/phpstan analyse --no-progress",
        "psalm": "vendor/bin/psalm --no-cache",
        "psalm-taint": "vendor/bin/psalm --taint-analysis",
        "deptrac": "vendor/bin/deptrac analyse --config-file=depfile.yaml",
        "phpmd": "vendor/bin/phpmd src text ruleset.xml"
    }
}

统一入口的好处是开发者只需执行 composer analyse 即可运行所有检查,降低使用门槛。CI 和本地使用同一套命令,保证一致性。

结语

静态分析不是银弹,但它是 PHP 项目质量保障体系中性价比最高的投资。PHPStan 提供了渐进式类型安全升级路径,Psalm 带来了独特的安全漏洞检测能力,Deptrac 和 PHPMD 补位了架构级与设计级约束。当这四层防护在 CI 中协同工作,你获得的不仅是更少的 Bug——更是对代码变更的信心,和对架构演进的掌控力。

从今天开始:在项目中运行 composer require –dev phpstan/phpstan,创建 phpstan.neon,设置 level 1,在 CI 中跑起来。这是零成本的第一步,也是最长旅程的起点。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » PHP 静态分析与代码质量实战:PHPStan、Psalm 与架构级 lint 的完整工作流
分享到: 更多 (0)