PHP 静态分析工具实战 PHPStan 和 Psalm 完全指南

说起来有点丢人,我以前特别讨厌静态分析,觉得就是瞎折腾。直到有一次,PHPStan 救了我一命,差点让我丢了饭碗的那种救命。

当时我给支付功能写了一段代码,自己觉得写得挺好,手工测试也过了,单元测试也绿了,看起来没毛病。结果同事非要我跑一下 PHPStan,我心想这不是多此一举吗?没想到一跑就炸了,发现了一个类型错误,这玩意儿会让支付金额算错!

就这么一个 bug,彻底改变了我的想法。以前觉得 IDE 里那些红色波浪线烦死了,现在觉得它们就是代码的保镖。现在让我不用静态分析写 PHP,就像让我不系安全带开车一样心慌。

原文链接-PHP 静态分析工具实战 PHPStan 和 Psalm 完全指南

静态分析到底有啥用:不只是抓错字

那次支付的事儿让我想明白了,静态分析不是用来抓拼写错误的,而是用来抓那些你自己看不出来的逻辑问题。写代码的时候,你脑子里想的都是正常情况,PHPStan 想的是各种能出错的地方。

静态分析就像个特别较真的代码审查员,什么都要质疑一遍。类型对不上、空指针、死代码,这些问题它都能揪出来。就好比有个强迫症同事,专门盯着你累了或者飘了的时候写的烂代码。

PHPStan:我的编程好帮手

自从那次支付的事儿之后,PHPStan 就成了我写代码的标配。一开始是被逼着用的,后来发现这玩意儿真香。最牛的地方是它懂 Laravel,Eloquent 关系、中间件这些 Laravel 的黑魔法它都认识,别的工具经常搞不定。

第一次跑 PHPStan 的时候我差点崩溃——我以为挺干净的代码库居然报了 847 个错误。不过修这些错误的过程中,我学到的 PHP 类型安全知识比之前几年加起来都多。

安装和基本设置

# 安装 PHPStan
composer require --dev phpstan/phpstan # 创建 phpstan.neon 配置文件
touch phpstan.neon
# phpstan.neon
parameters:
level: 5
paths:
- app
- tests
excludePaths:
- app/Console/Kernel.php
- app/Http/Kernel.php
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
ignoreErrors:
- '#Unsafe usage of new static#'

分析级别:从 0 到 8 的血泪史

PHPStan 有 10 个级别,这玩意儿教会了我什么叫循序渐进。一开始我想装逼,直接跳到级别 9,想证明自己是个"严肃的开发者"。结果级别 3 就把我整懵了,2000 多个错误,差点让我怀疑人生。后来我老实了,按部就班来:

# 级别 0 - 基本检查
vendor/bin/phpstan analyze --level=0 # 级别 5 - 严格性和实用性的良好平衡
vendor/bin/phpstan analyze --level=5 # 级别 9 - 非常严格,几乎捕获所有问题
vendor/bin/phpstan analyze --level=9

Laravel 集成

# 安装 Laravel 扩展
composer require --dev nunomaduro/larastan
# 为 Laravel 更新的 phpstan.neon
# 更多 Laravel 特定配置,请参见:
# https://mycuriosity.blog/level-up-your-laravel-validation-advanced-tips-tricks
parameters:
level: 5
paths:
- app
includes:
- ./vendor/nunomaduro/larastan/extension.neon

高级 PHPStan 配置

# phpstan.neon
parameters:
level: 6
paths:
- app
- tests # 忽略特定模式
ignoreErrors:
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder#'
- '#Method App\\Models\\User::find\(\) should return App\\Models\\User\|null but returns Illuminate\\Database\\Eloquent\\Model\|null#' # 自定义规则
rules:
- PHPStan\Rules\Classes\UnusedConstructorParametersRule
- PHPStan\Rules\DeadCode\UnusedPrivateMethodRule
- PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule # 类型别名
typeAliases:
UserId: 'int<1, max>'
Email: 'string' # 前沿功能
reportUnmatchedIgnoredErrors: true
checkTooWideReturnTypesInProtectedAndPublicMethods: true
checkUninitializedProperties: true

Psalm:另一个强大的选择

Psalm 是另一个优秀的静态分析工具,有着不同的优势。它特别擅长发现复杂的类型问题,并且有出色的泛型支持。

安装和设置

# 安装 Psalm
composer require --dev vimeo/psalm # 初始化 Psalm
vendor/bin/psalm --init
<!-- psalm.xml -->
<?xml version="1.0"?>
<psalm
errorLevel="3"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="app" />
<directory name="tests" />
<ignoreFiles>
<directory name="vendor" />
<file name="app/Console/Kernel.php" />
</ignoreFiles>
</projectFiles> <issueHandlers>
<LessSpecificReturnType errorLevel="info" />
<MoreSpecificReturnType errorLevel="info" />
<PropertyNotSetInConstructor errorLevel="info" />
</issueHandlers> <plugins>
<pluginClass class="Psalm\LaravelPlugin\Plugin"/>
</plugins>
</psalm>

Psalm 的 Laravel 插件

# 安装 Laravel 插件
composer require --dev psalm/plugin-laravel # 启用插件
vendor/bin/psalm-plugin enable psalm/plugin-laravel

血的教训:那些差点要命的 Bug

类型错误 - 差点出大事的支付 Bug

就是下面这种写法,当时我在算购物车总价,想当然地以为数组里都是数字。PHPStan 一眼就看出来了,数组里可能有各种乱七八糟的类型,这要是上线了,支付金额算错了还得了?

// 我原来的危险代码
function calculateTotal(array $items): float
{
$total = 0;
foreach ($items as $item) {
$total += $item; // PHPStan: Cannot add array|string to int
}
return $total; // 可能返回完全错误的金额!
} // PHPStan 强制我明确类型
function calculateTotal(array $items): float
{
$total = 0.0;
foreach ($items as $item) {
if (is_numeric($item)) {
$total += (float) $item;
} else {
throw new InvalidArgumentException('All items must be numeric');
}
}
return $total;
}

空指针问题

// PHPStan 捕获潜在的空指针
function getUserEmail(int $userId): string
{
$user = User::find($userId); // 返回 User|null
return $user->email; // 错误:无法访问 null 上的属性
} // 修复版本
function getUserEmail(int $userId): ?string
{
$user = User::find($userId);
return $user?->email;
} // 或者显式空值检查
function getUserEmail(int $userId): string
{
$user = User::find($userId);
if ($user === null) {
throw new UserNotFoundException("User {$userId} not found");
}
return $user->email;
}

无法到达的代码

// PHPStan 检测无法到达的代码
function processPayment(float $amount): bool
{
if ($amount <= 0) {
return false;
} if ($amount > 1000000) {
throw new InvalidArgumentException('Amount too large');
} return true;
echo "Payment processed"; // 无法到达的代码
}

高级类型注解

泛型类型

/**
* @template T
* @param class-string<T> $className
* @return T
*/
function createInstance(string $className): object
{
return new $className();
} // 使用
$user = createInstance(User::class); // PHPStan 知道这是 User

集合类型

/**
* @param array<int, User> $users
* @return array<int, string>
*/
function extractUserEmails(array $users): array
{
return array_map(fn(User $user) => $user->email, $users);
} /**
* @param Collection<int, Product> $products
* @return Collection<int, Product>
*/
function getActiveProducts(Collection $products): Collection
{
return $products->filter(fn(Product $product) => $product->isActive());
}

复杂类型定义

/**
* @param array{name: string, age: int, email: string} $userData
* @return User
*/
function createUser(array $userData): User
{
return new User($userData['name'], $userData['age'], $userData['email']);
} /**
* @param array<string, int|string|bool> $config
* @return void
*/
function configure(array $config): void
{
// 实现
}

自定义 PHPStan 规则

为你的特定需求创建自定义规则:

// CustomRule.php
use PHPStan\Rules\Rule;
use PHPStan\Analyser\Scope;
use PhpParser\Node; class NoDirectDatabaseQueryRule implements Rule
{
public function getNodeType(): string
{
return Node\Expr\StaticCall::class;
} public function processNode(Node $node, Scope $scope): array
{
if ($node->class instanceof Node\Name &&
$node->class->toString() === 'DB' &&
$node->name instanceof Node\Identifier &&
in_array($node->name->name, ['select', 'insert', 'update', 'delete'])) { return ['Direct database queries are not allowed. Use repositories instead.'];
} return [];
}
}

与 CI/CD 集成

GitHub Actions

# .github/workflows/static-analysis.yml
name: Static Analysis on: [push, pull_request] jobs:
phpstan:
runs-on: ubuntu-latest steps:
- uses: actions/checkout@v2 - name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2' - name: Install dependencies
run: composer install --no-dev --optimize-autoloader - name: Run PHPStan
run: vendor/bin/phpstan analyze --error-format=github - name: Run Psalm
run: vendor/bin/psalm --output-format=github

Pre-commit 钩子

# 安装 pre-commit
pip install pre-commit
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: phpstan
name: phpstan
entry: vendor/bin/phpstan analyze --no-progress
language: system
types: [php]
pass_filenames: false - id: psalm
name: psalm
entry: vendor/bin/psalm --no-progress
language: system
types: [php]
pass_filenames: false

代码质量工具集成

PHP CS Fixer

# 安装 PHP CS Fixer
composer require --dev friendsofphp/php-cs-fixer
# .php-cs-fixer.php
<?php
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => true,
'no_unused_imports' => true,
'declare_strict_types' => true,
])
// 遵循 PSR 标准提高代码质量:
// https://mycuriosity.blog/php-psr-standards-writing-interoperable-code
->setFinder(
PhpCsFixer\Finder::create()
->in('app')
->in('tests')
);

PHPMD (PHP Mess Detector)

# 安装 PHPMD
composer require --dev phpmd/phpmd
# phpmd.xml
<?xml version="1.0"?>
<ruleset name="Custom PHPMD ruleset">
<rule ref="rulesets/cleancode.xml">
<exclude name="StaticAccess" />
</rule>
<rule ref="rulesets/codesize.xml" />
<rule ref="rulesets/controversial.xml" />
<rule ref="rulesets/design.xml" />
<rule ref="rulesets/naming.xml" />
<rule ref="rulesets/unusedcode.xml" />
</ruleset>

性能优化

静态分析在大型代码库上可能很慢。以下是优化方法:

基线文件

# 生成基线以忽略现有问题
vendor/bin/phpstan analyze --generate-baseline # 这会创建 phpstan-baseline.neon
parameters:
includes:
- phpstan-baseline.neon

并行处理

# phpstan.neon
parameters:
parallel:
maximumNumberOfProcesses: 4
processTimeout: 120.0

结果缓存

# phpstan.neon
parameters:
tmpDir: var/cache/phpstan
resultCachePath: var/cache/phpstan/resultCache.php

IDE 集成

PHPStorm

PHPStorm 对 PHPStan 和 Psalm 都有出色的内置支持:

  1. 转到 Settings > PHP > Quality Tools
  2. 配置 PHPStan 和 Psalm 路径
  3. 在 Editor > Inspections 中启用检查

VS Code

// .vscode/settings.json
{
"php.validate.enable": false,
"php.suggest.basic": false,
"phpstan.enabled": true,
"phpstan.path": "vendor/bin/phpstan",
"phpstan.config": "phpstan.neon"
}

实际实施策略 - 团队采用的经验教训

让我的团队采用静态分析比我自己学习它更困难。开发者讨厌被告知他们的代码有 800+ 个错误,特别是当它"运行得很好"的时候。以下是真正有效的方法,遵循清洁代码原则以获得更好的团队采用:

第一阶段:基础(第 1-2 周)

  • 在级别 0 安装 PHPStan
  • 修复基本问题
  • 设置 CI/CD 集成

第二阶段:渐进改进(第 3-4 周)

  • 提升到级别 3
  • 添加 Laravel/框架特定规则
  • 培训团队注解

第三阶段:高级功能(第 5-6 周)

  • 达到级别 5-6
  • 添加自定义规则
  • 为遗留代码实施基线

第四阶段:精通(持续进行)

  • 新代码达到级别 8-9
  • 添加 Psalm 以获得额外覆盖
  • 持续改进

常见陷阱和解决方案

过度抑制

// 不好 - 抑制过于宽泛
/** @phpstan-ignore-next-line */
$user = User::find($id); // 好 - 具体抑制并说明原因
/** @phpstan-ignore-next-line User::find() can return null but we know ID exists */
$user = User::find($validatedId);

类型注解过载

// 不好 - 过度注解明显类型
/** @var string $name */
$name = 'John'; // 好 - 注解复杂类型
/** @var array<string, mixed> $config */
$config = json_decode($jsonString, true);

衡量成功

跟踪这些指标来衡量静态分析的成功。理解 PHP 性能分析有助于将静态分析改进与应用程序性能相关联:

// 要跟踪的指标
class StaticAnalysisMetrics
{
public function getMetrics(): array
{
return [
'phpstan_errors' => $this->countPhpStanErrors(),
'psalm_errors' => $this->countPsalmErrors(),
'code_coverage' => $this->getCodeCoverage(),
'type_coverage' => $this->getTypeCoverage(),
'bugs_prevented' => $this->getBugsPrevented(),
];
} private function countPhpStanErrors(): int
{
// 解析 PHPStan 输出
$output = shell_exec('vendor/bin/phpstan analyze --error-format=json');
$data = json_decode($output, true);
return count($data['files'] ?? []);
}
}

总结:从黑粉到真香

PHPStan 抓到的那个支付 bug 彻底改变了我写 PHP 的方式。从一开始的被迫使用,到后来的真心喜欢,这个过程挺有意思的。

最大的变化不是抓 bug,而是心态。以前上线代码心里都没底,祈祷别出事。现在上线前心里有数,该抓的错误都抓了,踏实多了。

写代码的思路也变了:以前是写完了碰运气,现在是边写边考虑类型安全。PHPStan 不光帮我找 bug,还教会我怎么更严谨地思考代码逻辑。

给做 Laravel 的兄弟们几个建议:

别急着装逼:第一天就想跳级别 9?醒醒吧。老老实实从 0 → 3 → 5 → 8 这么来,一步一个脚印。

别怕报错:看到 847 个错误别慌,这不是说你菜,而是给你学习的机会。每修一个错误,你对类型安全的理解就深一分。

让团队看到好处:光说静态分析有用没人信,得拿实际抓到的 bug 说话。一个具体的例子胜过千言万语。

强制执行:把静态分析加到 CI/CD 里,让它变成必须的步骤。代码过不了静态分析就别想合并,这样大家就不会偷懒了。

静态分析不只是让代码写得更好,更重要的是让你晚上睡得安稳。知道有工具帮你把关,用户看到 bug 之前你就能发现,这种踏实感一旦体验过就回不去了。配合好的 PHP 内存管理和安全认证,静态分析就是写出靠谱 PHP 应用的基石。

PHP 静态分析工具实战 PHPStan 和 Psalm 完全指南的更多相关文章

  1. PHP代码静态分析工具PHPStan

    最近发现自己写的PHP代码运行结果总跟自己预想的不一样,排查时发现大多是语法错误,在运行之前错误已经种下.可能是自己粗心大意,或者说php -l检测太简单,不过的确是有一些语法错误埋藏得太深(毕竟PH ...

  2. 代码静态分析工具--PMD,Findbugs,CheckStyle

    最近学习Mybatis的官方文档,看到了[项目文档]一节有很多内容没有见过,做个笔记,理解一下. PMD 扫描Java源代码,查找潜在的问题,如: 可能的bugs,如空的try/catch/final ...

  3. 代码静态分析工具——splint的学习与使用

    引言 最近在项目中使用了静态程序分析工具PC-Lint,体会到它在项目实施中带给开发人员的方便.PC-Lint是一款针对C/C++语言.windows平台的静态分析工具,FlexeLint是针对其他平 ...

  4. Analyze 静态分析工具中显示 大量的CF类型指针 内存leak 问题, Core Foundation 类型指针内存泄漏

    Analyze 静态分析工具中显示 大量的CF类型指针 内存leak 问题   今天使用Analyze 看了下项目,   解决办法,项目中使用了ARC,OC的指针类型我们完全不考虑release的问题 ...

  5. 一款C++静态分析工具 —— CppDepend

    Wrote by mutouyun. (http://darkc.at/cppdepend/) 去年6月份的时候,CppDepend的一位技术社区经理(technical community mana ...

  6. 代码静态分析工具PC-LINT安装配置

    代码静态分析工具PC-LINT安装配置--step by step                             作者:ehui928                             ...

  7. Jenkins集成源码静态分析工具

    1.static code analysis插件说明 Jenkins提供了插件"static code analysis",该插件搜集不同的分析结果,并集合显示出来. 实际上,我们 ...

  8. Crash工具实战-变量解析【转】

    转自:http://blog.chinaunix.net/uid-14528823-id-4358785.html Crash工具实战-变量解析 Crash工具用于解析Vmcore文件,Vmcore文 ...

  9. C++代码静态分析工具splint

    1.引言 最近在项目中使用了静态程序分析工具PC-Lint, 体会到它在项目实施中带给开发人员的方便.PC-Lint是一款针对C/C++语言.windows平台的静态分析工具,FlexeLint是针对 ...

  10. 开源静态分析工具androguard体验

    原文链接:http://blog.csdn.net/xbalien29/article/details/21885297 虽然在windows端免费版的IDA.VTS等工具都可用来静态分析,但相对来说 ...

随机推荐

  1. 使用openOffice将office文件转成pdf

    下载安装openoffice,下载地址:http://www.openoffice.org/download/ 我安装的目录: 输入cmd回车 在命令窗口输入 soffice -headless -a ...

  2. Box与Formatting Context

    一直对css都不够重视,觉得简单,其实也是有些知识的.所以抽空做了些总结 参考文献:mdn的css文档.w3c css规范 w3c是制定标准的,而mdn的目标是做开发者服务的开发文档. 但实际上,大家 ...

  3. 一、webrtc版本接听视频电话-纯js版

    先看效果 用户1--拨打 用户2–接听 前端代码 index.html <!DOCTYPE html> <html lang="en"> <head& ...

  4. SpringBoot--如何给项目添加配置属性及读取属性

    SpringBoot允许使用配置文件对应用程序进行配置,支持以下不同形式的配置源: 属性文件(比如application.properties) yaml文件(后缀可以是yml或者yaml) 环境变量 ...

  5. 前端开发系列022-基础篇之JavaScript和JSON(进阶)

    在[javaScript和JSON](http://wendingding.com/2018/04/16/javaScript%E7%B3%BB%E5%88%97%20[05]-javaScript% ...

  6. wordpress - 上传附件大小更改

    我用的是树莓派4b,需要修改php.ini的路径是/etc/php/7.3/apache2/php.ini.根据安装的php版本来决定所在的路径. 让然了,也可以 sudo find / -name ...

  7. js下载绝对路径的图片

    function down(){ var imgUrl = document.getElementById("img").src; var xmlhttp; xmlhttp = n ...

  8. MCU之Microchip PIC16F17146 Curiosity NANO Evaluation Kit申请与收到有感

    申请到寄到已过去好长时间(三个月): 2023-04-22 在bbs.21ic.com提交发布申请: 2023-07-21 收到批准包裹. 对比十五年前,ADI美国模拟器件公司 与 TI美国德州仪器公 ...

  9. 理论的动态发展完完备与进化:数论Number Theory数域的进化史 与 Infinite Precision无限精度+Infinite Approximation无穷近似

    Infinite Precision: https://blogs.ubc.ca/infiniteseriesmodule/units/unit-1/infinite-series/convertin ...

  10. flink 1.11.2 学习笔记(2)-Source/Transform/Sink

    一.flink处理的主要过程 从上一节wordcount的示例可以看到,flink的处理过程分为下面3个步骤: 1.1 .添加数据源addSource,这里的数据源可以是文件,网络数据流,MQ,Mys ...