Skip to content

静态分析

静态分析工具在不执行代码的情况下检查代码质量、类型安全和潜在 Bug。它是 PHP 项目质量保障体系中的关键环节,能在开发阶段(而非运行阶段)发现大量问题。PHP 生态中的三大主流静态分析工具为 PHPStan、Psalm 和 Phan,本节将详细介绍 PHPStan 和 Psalm 的使用方法与最佳实践。

前置知识

阅读本节前,建议先了解:命名规范代码风格工具

基础概念

静态分析 vs 动态测试

维度静态分析动态测试
执行代码不需要需要
发现时机编写代码时运行测试时
覆盖范围所有代码路径取决于测试覆盖率
检测类型类型错误、死代码、未使用变量逻辑错误、集成问题
速度较慢

PHPStan 与 Psalm 对比

特性PHPStanPsalm
维护者Ondrej MirtesVimeo/社区
分析精度高(9个级别)高(可比较 3-5 级)
类型推断
框架集成丰富丰富
插件生态大量官方和社区插件内置模板、特性
性能中等
配置方式NEON + PHPXML + PHP
taint analysis插件支持内置支持
Baseline支持支持

PHPStan

安装与基本使用

bash
# 项目安装(推荐)
composer require --dev phpstan/phpstan

# 安装 Laravel 扩展(如使用 Laravel)
composer require --dev phpstan/phpstan-symfony

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

基本命令

bash
# 使用默认级别(0)分析
vendor/bin/phpstan analyse src/

# 使用指定级别分析
vendor/bin/phpstan analyse src/ --level=8

# 分析指定文件
vendor/bin/phpstan analyse src/Service/UserService.php

# 同时分析多个目录
vendor/bin/phpstan analyse src/ tests/

# 输出结果到文件
vendor/bin/phpstan analyse src/ --error-format=json > phpstan-report.json

# 查看所有可用的错误格式
vendor/bin/phpstan help analyse

分析级别详解

PHPStan 提供 0-9 共 10 个级别,每个级别在前一级基础上增加检查严格度:

text
Level 0:基本语法检查(无类型推断)
Level 1:报告隐式混合类型
Level 2:报告联合类型中的隐式 mixed
Level 3:报告 null 返回值类型不匹配
Level 4:报告 void 返回值类型不匹配
Level 5:报告未定义的变量和方法
Level 6:报告类型不兼容的属性赋值
Level 7:报告 foreach 中隐式 mixed
Level 8:报告所有类型的缩小不匹配(推荐)
Level 9:最严格,报告所有可能的类型问题
bash
# Level 0:基础检查
vendor/bin/phpstan analyse src/ --level=0
# 只检查语法、调用了不存在的类/方法/函数

# Level 5:中等严格
vendor/bin/phpstan analyse src/ --level=5
# 报告未定义变量、未定义方法、不匹配的返回类型

# Level 8:高严格度(推荐用于新项目)
vendor/bin/phpstan analyse src/ --level=8
# 全面类型检查,是生产项目的推荐级别

# Level 9:最严格
vendor/bin/phpstan analyse src/ --level=9
# 最高级别,可能会产生较多误报,适合追求极致类型安全的场景

PHPStan 配置文件

在项目根目录创建 phpstan.neon(或 phpstan.neon.dist):

neon
# phpstan.neon.dist
parameters:
    # 分析级别
    level: 8

    # 分析路径
    paths:
        - src

    # 排除的文件和目录
    excludePaths:
        - src/Infrastructure/Migrations/*
        - src/Kernel.php

    # 报告未使用的参数
    reportUnmatchedIgnoredErrors: false

    # 检查未知的全局函数(PHPStan 扩展)
    bootstrapFiles:
        - vendor/autoload.php

    # 自动发现 Composer autoload
    scanFiles:
        - config/constants.php

    # 扫描目录
    scanDirectories:
        - src

    # 动态返回类型扩展
    universalObjectCratesClasses:
        - stdClass
        - SimpleXMLElement

    # 未使用的标签
    ignoreErrors:
        - '#Call to an undefined method [a-zA-Z]+::#'

    # PHP 版本目标
    phpVersion: 82000  # PHP 8.2

    # Taint analysis(污点分析)
    taintAnalysis:
        enabled: true

    # 内存限制
    memoryLimit: 512M

PHPStan 配置文件也支持 PHP 格式 phpstan.php(推荐用于复杂配置):

php
<?php
declare(strict_types=1);

use PHPStan\Rules\Arrays\ newArrayForEachWithArrayValueTypehintRule;
use PHPStan\Rules\Arrays\ArrayDimFetchRule;

return [
    'parameters' => [
        'level' => 8,
        'paths' => [
            __DIR__ . '/src',
        ],
        'excludePaths' => [
            __DIR__ . '/src/Migrations',
        ],
        'phpVersion' => 82000,
        'checkMissingIterableValueType' => true,
        'checkGenericClassInNonGenericObjectType' => true,
        'reportUnmatchedIgnoredErrors' => true,
        'treatPhpDocTypesAsCertain' => false,
        'dynamicConstantNames' => [
            'APP_ENV',
            'APP_DEBUG',
        ],
        'ignoreErrors' => [
            [
                'message' => '#Call to an undefined method#',
                'path' => __DIR__ . '/src/ThirdParty/*',
            ],
        ],
    ],
];

PHPStan 扩展

bash
# Laravel 框架支持
composer require --dev phpstan/phpstan-symfony

# PHPUnit 支持
composer require --dev phpstan/phpstan-phpunit

# Doctrine ORM 支持
composer require --dev phpstan/phpstan-doctrine

# mockery mock 支持
composer require --dev phpstan/phpstan-mockery

# 安全分析
composer require --dev ekino/phpstan-banned-code

#严格类型检查
composer require --dev thiagocordeiro/phpstan-laravel-prometheus-exporter

PHPStan Baseline

对于已有项目,可以使用 baseline 机制忽略已有的类型问题,专注于不引入新问题:

bash
# 生成 baseline
vendor/bin/phpstan analyse src/ --level=8 --generate-baseline phpstan-baseline.neon

# 使用 baseline 分析(忽略 baseline 中已记录的错误)
vendor/bin/phpstan analyse src/ --level=8

phpstan-baseline.neon 示例:

neon
parameters:
    ignoreErrors:
        -
            message: "#^Variable \\$unused might not be defined\\.$#"
            count: 2
            path: src/Service/legacy.php
        -
            message: "#^Parameter \\#1 \\$data of function process\\(\\) expects array, int given\\.$#"
            count: 1
            path: src/Processor.php

定期清理 baseline

Baseline 不应是永久方案。建议定期清理 baseline 中的条目,逐步提升代码的类型安全级别。

PHPStan 源码示例分析

php
<?php
declare(strict_types=1);

class UserRepository
{
    public function __construct(
        private readonly PDO $db,
    ) {}

    /**
     * Level 0-4:不会报错(缺少返回类型)
     * Level 5+:报告返回类型缺失
     */
    public function findById(int $id)  // ❌ Level 5+: Missing return type
    {
        $stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
        // ❌ Level 1+: 可能返回 false(mixed 类型)
    }

    /**
     * 正确的类型声明
     */
    public function findOneById(int $id): ?array  // ✅
    {
        $stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        // Level 8+ 会检查这里 $result 可能是 false
        return $result !== false ? $result : null;
    }

    /**
     * Level 8:联合类型 narrowing 检查
     */
    public function getUserName(int $id): string
    {
        $data = $this->findOneById($id);

        // ❌ Level 3+: $data 可能为 null
        // return $data['name'];

        // ✅ 正确处理
        if ($data === null) {
            return 'Unknown User';
        }

        return $data['name'];
    }

    /**
     * Level 6+:属性类型检查
     */
    private array $cache = [];

    public function cacheUser(int $id, array $data): void
    {
        // ✅ 类型匹配
        $this->cache[$id] = $data;
    }

    /**
     * Level 5+:未定义方法检查
     */
    public function findActive(): array
    {
        $stmt = $this->db->prepare('SELECT * FROM users WHERE is_active = 1');
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Level 9:strict 检查
     */
    public function processFilters(array $filters): array
    {
        $result = [];

        foreach ($filters as $key => $value) {
            // ❌ Level 9: $key 和 $value 都是 mixed
            $result[$key] = strtoupper((string) $value);
        }

        return $result;
    }
}

内联忽略错误

php
<?php
declare(strict_types=1);

class LegacyService
{
    // 忽略整行错误
    /** @phpstan-ignore-next-line */
    private $legacyProperty;

    // 忽略下一行
    public function legacyMethod(): void
    {
        /** @phpstan-ignore-next-line */
        $data = $this->unknownMethod();
    }

    // 忽略特定类型的错误
    /** @phpstan-ignore method.notFound */
    public function anotherMethod(): void
    {
        $this->unknownMethod();
    }

    // 使用 @var 覆盖推断类型
    public function process(): void
    {
        /** @var array<string, mixed> $config */
        $config = $this->loadConfig();
    }

    // 使用 @phpstan-assert 辅助类型推断
    /**
     * @phpstan-assert !null $value
     */
    public function assertNotNull(mixed $value): void
    {
        if ($value === null) {
            throw new \InvalidArgumentException('Value must not be null');
        }
    }
}

Psalm

安装与基本使用

bash
# 项目安装
composer require --dev vimeo/psalm

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

基本命令

bash
# 初始化配置(交互式)
vendor/bin/psalm --init

# 分析代码
vendor/bin/psalm

# 分析指定文件或目录
vendor/bin/psalm src/Service/

# 显示详细信息
vendor/bin/psalm --show-info=true

# 生成 baseline
vendor/bin/psalm --set-baseline=psalm-baseline.xml

# 使用 baseline 分析
vendor/bin/psalm --use-baseline=psalm-baseline.xml

# 报告类型覆盖率
vendor/bin/psalm --report-type=checkstyle

Psalm 配置文件

xml
<?xml version="1.0"?>
<psalm
    errorLevel="8"
    resolveFromConfigFile="true"
    findUnusedCode="true"
    findUnusedVariables="true"
    phpVersion="8.2"
    cacheDirectory=".psalm-cache"
>
    <!-- 项目文件 -->
    <projectFiles>
        <directory name="src"/>
        <directory name="tests"/>
        <ignoreFiles>
            <directory name="vendor"/>
            <file name="src/Kernel.php"/>
        </ignoreFiles>
    </projectFiles>

    <!-- 插件 -->
    <plugins>
        <pluginClass class="Psalm\PhpUnitPlugin"/>
        <!-- Doctrine 插件 -->
        <pluginClass class="Psalm\DoctrinePlugin"/>
    </plugins>

    <!-- 全局忽略 -->
    <issueHandlers>
        <!-- 忽略特定文件的特定错误 -->
        <PossiblyUndefinedArrayOffset>
            <errorLevel type="suppress">
                <file name="src/Service/LegacyService.php"/>
            </errorLevel>
        </PossiblyUndefinedArrayOffset>

        <!-- 降低某些错误级别 -->
        <MixedAssignment>
            <errorLevel type="info">
                <referencedFunction name="json_decode"/>
            </errorLevel>
        </MixedAssignment>

        <!-- 文档标注 suppress -->
        <MixedArgumentTypeCoercion errorLevel="suppress"/>
        <MixedArrayAccess errorLevel="suppress"/>
    </issueHandlers>
</psalm>

Psalm 类型注解

php
<?php
declare(strict_types=1);

use Psalm\Type\Union;

class TypeSafeService
{
    // @psalm-param 明确参数类型
    /**
     * @psalm-param array{page: int, per_page: int, search?: string} $params
     */
    public function search(array $params): array
    {
        $page = $params['page'];
        $perPage = $params['per_page'];
        $search = $params['search'] ?? '';

        return $this->performSearch($page, $perPage, $search);
    }

    // @psalm-return 明确返回类型
    /**
     * @psalm-return array<array{id: int, name: string, email: string}>
     */
    private function performSearch(int $page, int $perPage, string $search): array
    {
        $stmt = $this->db->prepare('SELECT id, name, email FROM users LIMIT :limit OFFSET :offset');
        $stmt->execute([
            'limit' => $perPage,
            'offset' => ($page - 1) * $perPage,
        ]);

        /** @var list<array{id: int, name: string, email: string}> */
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    // @psalm-template 泛型方法
    /**
     * @psalm-template T
     * @psalm-param class-string<T> $className
     * @psalm-return T
     */
    public function createInstance(string $className): object
    {
        return new $className();
    }

    // @psalm-pure 纯函数
    /**
     * @psalm-pure
     */
    public function calculateTotal(array $items): float
    {
        return array_reduce(
            $items,
            fn (float $carry, array $item): float => $carry + $item['price'] * $item['quantity'],
            0.0,
        );
    }

    // @psalm-assert 类型断言
    /**
     * @psalm-assert array<string, mixed> $data
     */
    public function assertIsAssociativeArray(mixed $data): void
    {
        if (!is_array($data) || array_is_list($data)) {
            throw new \InvalidArgumentException('Expected associative array');
        }
    }

    // @psalm-suppress 局部抑制
    public function processExternalData(string $json): array
    {
        $data = json_decode($json, true);

        if (!is_array($data)) {
            return [];
        }

        /** @psalm-suppress MixedAssignment */
        return $data;
    }
}

Psalm Taint Analysis(污点分析)

php
<?php
declare(strict_types=1);

// 污点分析用于追踪不安全数据从源头到汇点的流向
// 防止 SQL 注入、XSS 等安全漏洞

class UserController
{
    public function __construct(
        private readonly PDO $db,
    ) {}

    /**
     * ✅ 安全:使用参数化查询
     */
    public function safeSearch(Request $request): array
    {
        // request()->get() 是 taint source
        $search = $request->get('search', '');

        // 参数化查询阻止了 taint 传播
        $stmt = $this->db->prepare('SELECT * FROM users WHERE name LIKE :search');
        $stmt->execute(['search' => "%{$search}%"]);

        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * ❌ 不安全:字符串拼接 SQL(Psalm 会报告 taint 错误)
     */
    public function unsafeSearch(Request $request): array
    {
        // @psalm-taint-sink sql
        $search = $request->get('search', '');
        $sql = "SELECT * FROM users WHERE name LIKE '%{$search}%'";

        $stmt = $this->db->query($sql);  // ❌ Taint 汇点

        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * XSS 防护
     */
    public function renderUser(Request $request): string
    {
        $name = $request->get('name', '');

        // ✅ 安全:转义输出
        return htmlspecialchars($name, ENT_QUOTES, 'UTF-8');

        // ❌ 不安全:直接输出
        // return "<div>Hello, {$name}</div>";  // Taint sink
    }
}

配置最佳实践

推荐的项目配置

yaml
# phpstan.neon.dist - 推荐配置
parameters:
    level: 8
    paths:
        - src
    excludePaths:
        - src/Migrations
        - src/Kernel.php
        - src/Console/Kernel.php
    bootstrapFiles:
        - vendor/autoload.php
    phpVersion: 82000
    memoryLimit: 512M

    # 类型覆盖相关
    checkMissingIterableValueType: true
    checkGenericClassInNonGenericObjectType: true
    reportUnmatchedIgnoredErrors: true
    treatPhpDocTypesAsCertain: false

    # 自动发现 Composer autoload
    scanDirectories:
        - src

    # 第三方库的类型存根
    scanFiles:
        - vendor/stubs/globals.php

    # 忽略外部库的命名
    universalObjectCratesClasses:
        - stdClass
        - SimpleXMLElement

框架集成

Laravel 项目 PHPStan 配置

bash
composer require --dev nunomaduro/larastan
neon
# phpstan.neon
includes:
    - vendor/nunomaduro/larastan/extension.neon

parameters:
    level: 8
    paths:
        - src
        - app
        - tests
    excludePaths:
        - app/Providers
    # ... 其他配置

Symfony 项目 PHPStan 配置

bash
composer require --dev phpstan/phpstan-symfony
neon
# phpstan.neon
includes:
    - vendor/phpstan/phpstan-symfony/extension.neon

parameters:
    level: 8
    symfony:
        container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml
        console_application_loader: bin/console
    # ... 其他配置

CI 集成

GitHub Actions 配置

yaml
# .github/workflows/static-analysis.yml
name: PHP Static Analysis

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  phpstan:
    name: PHPStan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          coverage: none
          tools: composer:v2

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-phpstan-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-phpstan-

      - name: Install dependencies
        run: composer install --prefer-dist --no-progress --no-interaction

      - name: Run PHPStan
        run: vendor/bin/phpstan analyse --no-progress --error-format=github

  psalm:
    name: Psalm
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          coverage: none

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-psalm-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-psalm-

      - name: Install dependencies
        run: composer install --prefer-dist --no-progress --no-interaction

      - name: Run Psalm
        run: vendor/bin/psalm --output-format=github --no-cache

      - name: Psalm Taint Analysis
        run: vendor/bin/psalm --taint-analysis

GitLab CI 配置

yaml
# .gitlab-ci.yml
phpstan:
  stage: test
  image: composer:latest
  before_script:
    - composer install --prefer-dist --no-progress --no-interaction
  script:
    - vendor/bin/phpstan analyse --no-progress --error-format=json > phpstan-report.json
    - if [ $? -ne 0 ]; then cat phpstan-report.json; exit 1; fi
  artifacts:
    reports:
      codequality: phpstan-report.json
    when: on_failure
    paths:
      - phpstan-report.json
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

Composer 脚本集成

json
{
    "scripts": {
        "phpstan": "phpstan analyse --no-progress",
        "phpstan-baseline": "phpstan analyse --generate-baseline=phpstan-baseline.neon",
        "psalm": "psalm --no-cache",
        "psalm-baseline": "psalm --set-baseline=psalm-baseline.xml",
        "static-analysis": [
            "@phpstan",
            "@psalm"
        ],
        "stan-baseline": "@phpstan-baseline",
        "taint-analysis": "psalm --taint-analysis"
    }
}

Baseline 使用策略

引入 Baseline 的流程

bash
# 1. 在现有项目上首次运行
vendor/bin/phpstan analyse src/ --level=5 --generate-baseline=phpstan-baseline.neon

# 2. 将 baseline 提交到版本控制
git add phpstan-baseline.neon
git commit -m "Add PHPStan baseline"

# 3. 后续分析自动使用 baseline
vendor/bin/phpstan analyse src/ --level=5

# 4. 逐步清理 baseline
# 修复代码中的类型问题
# 重新生成 baseline
vendor/bin/phpstan analyse src/ --level=5 --generate-baseline=phpstan-baseline.neon

# 5. 当 baseline 足够小或为零时
# 移除 baseline
rm phpstan-baseline.neon
git rm phpstan-baseline.neon

Baseline 更新策略

bash
# CI 中检测 baseline 是否过期
# 如果新引入的错误不在 baseline 中,CI 应该失败
vendor/bin/phpstan analyse src/ --level=8 --no-progress

# 定期检查 baseline 条目是否可以清理
vendor/bin/phpstan analyse src/ --level=8 --allow-empty-baseline

# 比较新旧 baseline 差异
diff phpstan-baseline.neon phpstan-baseline.neon.new

实战示例

完整的类型安全重构示例

php
<?php
declare(strict_types=1);

// ===== 重构前(类型不安全) =====
class OrderProcessorBefore
{
    private $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function process($orderId, $data)
    {
        $order = $this->findOrder($orderId);
        $order['status'] = $data['status'];
        $order['updated_at'] = date('Y-m-d H:i:s');
        $this->saveOrder($order);
        return $order;
    }

    private function findOrder($id)
    {
        return $this->db->query("SELECT * FROM orders WHERE id = " . $id)->fetch();
    }
}

// ===== 重构后(类型安全) =====
enum OrderStatus: string
{
    case Pending = 'pending';
    case Processing = 'processing';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';
}

final class OrderProcessor
{
    public function __construct(
        private readonly PDO $db,
    ) {}

    /**
     * @param array{status: string, notes?: string} $data
     * @return array{id: int, status: string, updated_at: string}
     */
    public function process(int $orderId, array $data): array
    {
        $order = $this->findOrder($orderId);

        if ($order === null) {
            throw new \RuntimeException("Order #{$orderId} not found");
        }

        $this->validateStatusTransition($order['status'], $data['status']);

        $order['status'] = $data['status'];
        $order['notes'] = $data['notes'] ?? $order['notes'];
        $order['updated_at'] = (new \DateTimeImmutable())->format('Y-m-d H:i:s');

        $this->saveOrder($order);

        return $order;
    }

    /**
     * @return array{id: int, status: string, notes: string, created_at: string, updated_at: string}|null
     */
    private function findOrder(int $id): ?array
    {
        $stmt = $this->db->prepare('SELECT * FROM orders WHERE id = :id');
        $stmt->execute(['id' => $id]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        return $result !== false ? $result : null;
    }

    /**
     * @param array{id: int, status: string, notes: string, created_at: string, updated_at: string} $order
     */
    private function saveOrder(array $order): void
    {
        $stmt = $this->db->prepare(
            'UPDATE orders SET status = :status, notes = :notes, updated_at = :updated_at WHERE id = :id',
        );
        $stmt->execute([
            'id' => $order['id'],
            'status' => $order['status'],
            'notes' => $order['notes'],
            'updated_at' => $order['updated_at'],
        ]);
    }

    private function validateStatusTransition(string $currentStatus, string $newStatus): void
    {
        $allowedTransitions = [
            'pending' => ['processing', 'cancelled'],
            'processing' => ['shipped', 'cancelled'],
            'shipped' => ['delivered'],
        ];

        $current = strtolower($currentStatus);
        $new = strtolower($newStatus);

        if (!isset($allowedTransitions[$current])) {
            throw new \InvalidArgumentException("Cannot transition from '{$currentStatus}'");
        }

        if (!in_array($new, $allowedTransitions[$current], true)) {
            throw new \InvalidArgumentException(
                "Cannot transition from '{$currentStatus}' to '{$newStatus}'",
            );
        }
    }
}

注意事项

常见问题

避免过度使用 @phpstan-ignore

内联忽略应作为最后手段,而非首选。过多的内联忽略会导致类型安全体系形同虚设。正确做法是修复代码类型问题或使用 baseline。

php
<?php
declare(strict_types=1);

// ❌ 避免大面积忽略
/** @phpstan-ignore-all */
class BadExample { }

// ❌ 避免忽略可以修复的问题
/** @phpstan-ignore-next-line */
echo $undefinedVariable;

// ✅ 正确做法:声明类型
private string $variable = '';
echo $this->variable;

// ✅ 正确做法:使用 null 检查
if ($variable !== null) {
    echo $variable;
}

性能优化

bash
# PHPStan 缓存
# 默认缓存目录:/tmp/phpstan
# 可通过 --tmp-dir 指定
vendor/bin/phpstan analyse --no-progress --tmp-dir=.phpstan-cache

# 使用 PHP 反射缓存
vendor/bin/phpstan analyse --no-progress --memory-limit=1G

# 并行分析(PHPStan 不支持多进程,但可以通过分目录实现)
vendor/bin/phpstan analyse src/Domain --no-progress &
vendor/bin/phpstan analyse src/Application --no-progress &
wait

最佳实践

  1. 新项目使用最高可行级别:新项目从 Level 8 开始,而非 Level 0
  2. 渐进式升级:已有项目使用 baseline,然后逐步升级级别
  3. CI 中始终运行:将静态分析集成到 CI 中,确保不引入新的类型问题
  4. 与测试互补:静态分析不能替代测试,两者结合使用效果最佳
  5. 定期更新工具:PHPStan 和 Psalm 频繁更新,定期更新可获得更好的分析能力
  6. 不要忽略所有错误:仔细审查每个需要忽略的错误,确保没有遗漏真实问题
  7. 团队统一级别:整个团队使用相同的分析级别和配置

下一节

继续学习:项目目录结构

参考链接