PHPStan和Psalm—查找php错误的静态代码分析工具
作者:墨舞青云
说起来有点丢人,我以前特别讨厌静态分析,觉得就是瞎折腾。直到有一次,PHPStan 救了我一命,差点让我丢了饭碗的那种救命。
当时我给支付功能写了一段代码,自己觉得写得挺好,手工测试也过了,单元测试也绿了,看起来没毛病。结果同事非要我跑一下 PHPStan,我心想这不是多此一举吗?没想到一跑就炸了,发现了一个类型错误,这玩意儿会让支付金额算错!
就这么一个 bug,彻底改变了我的想法。以前觉得 IDE 里那些红色波浪线烦死了,现在觉得它们就是代码的保镖。现在让我不用静态分析写 PHP,就像让我不系安全带开车一样心慌。
静态分析到底有啥用:不只是抓错字
那次支付的事儿让我想明白了,静态分析不是用来抓拼写错误的,而是用来抓那些你自己看不出来的逻辑问题。写代码的时候,你脑子里想的都是正常情况,PHPStan 想的是各种能出错的地方。
静态分析就像个特别较真的代码审查员,什么都要质疑一遍。类型对不上、空指针、死代码,这些问题它都能揪出来。就好比有个强迫症同事,专门盯着你累了或者飘了的时候写的烂代码。
PHPStan和Psalm定位与特性
PHPStan
- 采用NEON配置文件,支持规则级别自定义(如
level: 8
表示严格模式)1 - 提供多环境配置能力,可通过
--release
参数指定PHP版本兼容性检查2 - 典型配置示例:
level: 8 paths: [src/, tests/] ignoreErrors: [ {message: "Undefined method call", count: 3} ]
- 采用NEON配置文件,支持规则级别自定义(如
Psalm
- 使用XML配置文件(
.psalm.xml
),支持类型推断和PSR标准检查3 - 内置对PHP 8+新特性的支持(如
??
运算符版本兼容性检测)2 - 基础配置示例:
< psalm.xml > < project > < name >MyApp< /name > < autoloader >vendor/autoload.php< /autoloader > < /project > < / psalm.xml >
- 使用XML配置文件(
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 都有出色的内置支持:
- 转到 Settings > PHP > Quality Tools
- 配置 PHPStan 和 Psalm 路径
- 在 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 应用的基石。
PHPStan:启用parallel
参数加速多核分析;Psalm:配置cacheDir
复用扫描结果,减少重复分析2。通过合理配置,两者可协同使用:Psalm处理复杂类型与安全检测,PHPStan作为基础类型检查层。
到此这篇关于PHPStan和Psalm—查找php错误的静态代码分析工具的文章就介绍到这了,更多相关php代码分析工具PHPStan和Psalm内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!