找回密码
 立即注册
首页 业界区 业界 PHP 静态分析工具实战 PHPStan 和 Psalm 完全指南 ...

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

鸠站 2025-9-23 09:54:48
PHP 静态分析工具实战 PHPStan 和 Psalm 完全指南

说起来有点丢人,我以前特别讨厌静态分析,觉得就是瞎折腾。直到有一次,PHPStan 救了我一命,差点让我丢了饭碗的那种救命。
当时我给支付功能写了一段代码,自己觉得写得挺好,手工测试也过了,单元测试也绿了,看起来没毛病。结果同事非要我跑一下 PHPStan,我心想这不是多此一举吗?没想到一跑就炸了,发现了一个类型错误,这玩意儿会让支付金额算错!
就这么一个 bug,彻底改变了我的想法。以前觉得 IDE 里那些红色波浪线烦死了,现在觉得它们就是代码的保镖。现在让我不用静态分析写 PHP,就像让我不系安全带开车一样心慌。
原文链接-PHP 静态分析工具实战 PHPStan 和 Psalm 完全指南

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

那次支付的事儿让我想明白了,静态分析不是用来抓拼写错误的,而是用来抓那些你自己看不出来的逻辑问题。写代码的时候,你脑子里想的都是正常情况,PHPStan 想的是各种能出错的地方。
静态分析就像个特别较真的代码审查员,什么都要质疑一遍。类型对不上、空指针、死代码,这些问题它都能揪出来。就好比有个强迫症同事,专门盯着你累了或者飘了的时候写的烂代码。
PHPStan:我的编程好帮手

自从那次支付的事儿之后,PHPStan 就成了我写代码的标配。一开始是被逼着用的,后来发现这玩意儿真香。最牛的地方是它懂 Laravel,Eloquent 关系、中间件这些 Laravel 的黑魔法它都认识,别的工具经常搞不定。
第一次跑 PHPStan 的时候我差点崩溃——我以为挺干净的代码库居然报了 847 个错误。不过修这些错误的过程中,我学到的 PHP 类型安全知识比之前几年加起来都多。
安装和基本设置
  1. # 安装 PHPStan
  2. composer require --dev phpstan/phpstan
  3. # 创建 phpstan.neon 配置文件
  4. touch phpstan.neon
复制代码
  1. # phpstan.neon
  2. parameters:
  3.   level: 5
  4.   paths:
  5.     - app
  6.     - tests
  7.   excludePaths:
  8.     - app/Console/Kernel.php
  9.     - app/Http/Kernel.php
  10.   checkMissingIterableValueType: false
  11.   checkGenericClassInNonGenericObjectType: false
  12.   ignoreErrors:
  13.     - '#Unsafe usage of new static#'
复制代码
分析级别:从 0 到 8 的血泪史

PHPStan 有 10 个级别,这玩意儿教会了我什么叫循序渐进。一开始我想装逼,直接跳到级别 9,想证明自己是个"严肃的开发者"。结果级别 3 就把我整懵了,2000 多个错误,差点让我怀疑人生。后来我老实了,按部就班来:
  1. # 级别 0 - 基本检查
  2. vendor/bin/phpstan analyze --level=0
  3. # 级别 5 - 严格性和实用性的良好平衡
  4. vendor/bin/phpstan analyze --level=5
  5. # 级别 9 - 非常严格,几乎捕获所有问题
  6. vendor/bin/phpstan analyze --level=9
复制代码
Laravel 集成
  1. # 安装 Laravel 扩展
  2. composer require --dev nunomaduro/larastan
复制代码
  1. # 为 Laravel 更新的 phpstan.neon
  2. # 更多 Laravel 特定配置,请参见:
  3. # https://mycuriosity.blog/level-up-your-laravel-validation-advanced-tips-tricks
  4. parameters:
  5.   level: 5
  6.   paths:
  7.     - app
  8.   includes:
  9.     - ./vendor/nunomaduro/larastan/extension.neon
复制代码
高级 PHPStan 配置
  1. # phpstan.neon
  2. parameters:
  3.   level: 6
  4.   paths:
  5.     - app
  6.     - tests
  7.   # 忽略特定模式
  8.   ignoreErrors:
  9.     - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder#'
  10.     - '#Method App\\Models\\User::find\(\) should return App\\Models\\User\|null but returns Illuminate\\Database\\Eloquent\\Model\|null#'
  11.   # 自定义规则
  12.   rules:
  13.     - PHPStan\Rules\Classes\UnusedConstructorParametersRule
  14.     - PHPStan\Rules\DeadCode\UnusedPrivateMethodRule
  15.     - PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule
  16.   # 类型别名
  17.   typeAliases:
  18.     UserId: 'int<1, max>'
  19.     Email: 'string'
  20.   # 前沿功能
  21.   reportUnmatchedIgnoredErrors: true
  22.   checkTooWideReturnTypesInProtectedAndPublicMethods: true
  23.   checkUninitializedProperties: true
复制代码
Psalm:另一个强大的选择

Psalm 是另一个优秀的静态分析工具,有着不同的优势。它特别擅长发现复杂的类型问题,并且有出色的泛型支持。
安装和设置
  1. # 安装 Psalm
  2. composer require --dev vimeo/psalm
  3. # 初始化 Psalm
  4. vendor/bin/psalm --init
复制代码
  1. <?xml version="1.0"?>
  2. <psalm
  3.     errorLevel="3"
  4.     resolveFromConfigFile="true"
  5.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  6.     xmlns="https://getpsalm.org/schema/config"
  7.     xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
  8. >
  9.     <projectFiles>
  10.         <directory name="app" />
  11.         <directory name="tests" />
  12.         <ignoreFiles>
  13.             <directory name="vendor" />
  14.             <file name="app/Console/Kernel.php" />
  15.         </ignoreFiles>
  16.     </projectFiles>
  17.     <issueHandlers>
  18.         <LessSpecificReturnType errorLevel="info" />
  19.         <MoreSpecificReturnType errorLevel="info" />
  20.         <PropertyNotSetInConstructor errorLevel="info" />
  21.     </issueHandlers>
  22.     <plugins>
  23.         <pluginClass />
  24.     </plugins>
  25. </psalm>
复制代码
Psalm 的 Laravel 插件
  1. # 安装 Laravel 插件
  2. composer require --dev psalm/plugin-laravel
  3. # 启用插件
  4. vendor/bin/psalm-plugin enable psalm/plugin-laravel
复制代码
血的教训:那些差点要命的 Bug

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

就是下面这种写法,当时我在算购物车总价,想当然地以为数组里都是数字。PHPStan 一眼就看出来了,数组里可能有各种乱七八糟的类型,这要是上线了,支付金额算错了还得了?
  1. // 我原来的危险代码
  2. function calculateTotal(array $items): float
  3. {
  4.     $total = 0;
  5.     foreach ($items as $item) {
  6.         $total += $item; // PHPStan: Cannot add array|string to int
  7.     }
  8.     return $total; // 可能返回完全错误的金额!
  9. }
  10. // PHPStan 强制我明确类型
  11. function calculateTotal(array $items): float
  12. {
  13.     $total = 0.0;
  14.     foreach ($items as $item) {
  15.         if (is_numeric($item)) {
  16.             $total += (float) $item;
  17.         } else {
  18.             throw new InvalidArgumentException('All items must be numeric');
  19.         }
  20.     }
  21.     return $total;
  22. }
复制代码
空指针问题
  1. // PHPStan 捕获潜在的空指针
  2. function getUserEmail(int $userId): string
  3. {
  4.     $user = User::find($userId); // 返回 User|null
  5.     return $user->email; // 错误:无法访问 null 上的属性
  6. }
  7. // 修复版本
  8. function getUserEmail(int $userId): ?string
  9. {
  10.     $user = User::find($userId);
  11.     return $user?->email;
  12. }
  13. // 或者显式空值检查
  14. function getUserEmail(int $userId): string
  15. {
  16.     $user = User::find($userId);
  17.     if ($user === null) {
  18.         throw new UserNotFoundException("User {$userId} not found");
  19.     }
  20.     return $user->email;
  21. }
复制代码
无法到达的代码
  1. // PHPStan 检测无法到达的代码
  2. function processPayment(float $amount): bool
  3. {
  4.     if ($amount <= 0) {
  5.         return false;
  6.     }
  7.     if ($amount > 1000000) {
  8.         throw new InvalidArgumentException('Amount too large');
  9.     }
  10.     return true;
  11.     echo "Payment processed"; // 无法到达的代码
  12. }
复制代码
高级类型注解

泛型类型
  1. /**
  2. * @template T
  3. * @param class-string<T> $className
  4. * @return T
  5. */
  6. function createInstance(string $className): object
  7. {
  8.     return new $className();
  9. }
  10. // 使用
  11. $user = createInstance(User::class); // PHPStan 知道这是 User
复制代码
集合类型
  1. /**
  2. * @param array<int, User> $users
  3. * @return array<int, string>
  4. */
  5. function extractUserEmails(array $users): array
  6. {
  7.     return array_map(fn(User $user) => $user->email, $users);
  8. }
  9. /**
  10. * @param Collection<int, Product> $products
  11. * @return Collection<int, Product>
  12. */
  13. function getActiveProducts(Collection $products): Collection
  14. {
  15.     return $products->filter(fn(Product $product) => $product->isActive());
  16. }
复制代码
复杂类型定义
  1. /**
  2. * @param array{name: string, age: int, email: string} $userData
  3. * @return User
  4. */
  5. function createUser(array $userData): User
  6. {
  7.     return new User($userData['name'], $userData['age'], $userData['email']);
  8. }
  9. /**
  10. * @param array<string, int|string|bool> $config
  11. * @return void
  12. */
  13. function configure(array $config): void
  14. {
  15.     // 实现
  16. }
复制代码
自定义 PHPStan 规则

为你的特定需求创建自定义规则:
  1. // CustomRule.php
  2. use PHPStan\Rules\Rule;
  3. use PHPStan\Analyser\Scope;
  4. use PhpParser\Node;
  5. class NoDirectDatabaseQueryRule implements Rule
  6. {
  7.     public function getNodeType(): string
  8.     {
  9.         return Node\Expr\StaticCall::class;
  10.     }
  11.     public function processNode(Node $node, Scope $scope): array
  12.     {
  13.         if ($node->class instanceof Node\Name &&
  14.             $node->class->toString() === 'DB' &&
  15.             $node->name instanceof Node\Identifier &&
  16.             in_array($node->name->name, ['select', 'insert', 'update', 'delete'])) {
  17.             return ['Direct database queries are not allowed. Use repositories instead.'];
  18.         }
  19.         return [];
  20.     }
  21. }
复制代码
与 CI/CD 集成

GitHub Actions
  1. # .github/workflows/static-analysis.yml
  2. name: Static Analysis
  3. on: [push, pull_request]
  4. jobs:
  5.   phpstan:
  6.     runs-on: ubuntu-latest
  7.     steps:
  8.       - uses: actions/checkout@v2
  9.       - name: Setup PHP
  10.         uses: shivammathur/setup-php@v2
  11.         with:
  12.           php-version: '8.2'
  13.       - name: Install dependencies
  14.         run: composer install --no-dev --optimize-autoloader
  15.       - name: Run PHPStan
  16.         run: vendor/bin/phpstan analyze --error-format=github
  17.       - name: Run Psalm
  18.         run: vendor/bin/psalm --output-format=github
复制代码
Pre-commit 钩子
  1. # 安装 pre-commit
  2. pip install pre-commit
复制代码
  1. # .pre-commit-config.yaml
  2. repos:
  3.   - repo: local
  4.     hooks:
  5.       - id: phpstan
  6.         name: phpstan
  7.         entry: vendor/bin/phpstan analyze --no-progress
  8.         language: system
  9.         types: [php]
  10.         pass_filenames: false
  11.       - id: psalm
  12.         name: psalm
  13.         entry: vendor/bin/psalm --no-progress
  14.         language: system
  15.         types: [php]
  16.         pass_filenames: false
复制代码
代码质量工具集成

PHP CS Fixer
  1. # 安装 PHP CS Fixer
  2. composer require --dev friendsofphp/php-cs-fixer
复制代码
  1. # .php-cs-fixer.php
  2. <?php
  3. return (new PhpCsFixer\Config())
  4.     ->setRules([
  5.         '@PSR12' => true,
  6.         'array_syntax' => ['syntax' => 'short'],
  7.         'ordered_imports' => true,
  8.         'no_unused_imports' => true,
  9.         'declare_strict_types' => true,
  10.     ])
  11.     // 遵循 PSR 标准提高代码质量:
  12.     // https://mycuriosity.blog/php-psr-standards-writing-interoperable-code
  13.     ->setFinder(
  14.         PhpCsFixer\Finder::create()
  15.             ->in('app')
  16.             ->in('tests')
  17.     );
复制代码
性能优化

静态分析在大型代码库上可能很慢。以下是优化方法:
基线文件
  1. # 安装 PHPMD
  2. composer require --dev phpmd/phpmd
复制代码
  1. # phpmd.xml
  2. <?xml version="1.0"?>
  3. <ruleset name="Custom PHPMD ruleset">
  4.     <rule ref="rulesets/cleancode.xml">
  5.         <exclude name="StaticAccess" />
  6.     </rule>
  7.     <rule ref="rulesets/codesize.xml" />
  8.     <rule ref="rulesets/controversial.xml" />
  9.     <rule ref="rulesets/design.xml" />
  10.     <rule ref="rulesets/naming.xml" />
  11.     <rule ref="rulesets/unusedcode.xml" />
  12. </ruleset>
复制代码
并行处理
  1. # 生成基线以忽略现有问题
  2. vendor/bin/phpstan analyze --generate-baseline
  3. # 这会创建 phpstan-baseline.neon
复制代码
结果缓存
  1. parameters:
  2.   includes:
  3.     - phpstan-baseline.neon
复制代码
IDE 集成

PHPStorm

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

  • 转到 Settings > PHP > Quality Tools
  • 配置 PHPStan 和 Psalm 路径
  • 在 Editor > Inspections 中启用检查
VS Code
  1. # phpstan.neon
  2. parameters:
  3.   parallel:
  4.     maximumNumberOfProcesses: 4
  5.     processTimeout: 120.0
复制代码
实际实施策略 - 团队采用的经验教训

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


  • 在级别 0 安装 PHPStan
  • 修复基本问题
  • 设置 CI/CD 集成
第二阶段:渐进改进(第 3-4 周)


  • 提升到级别 3
  • 添加 Laravel/框架特定规则
  • 培训团队注解
第三阶段:高级功能(第 5-6 周)


  • 达到级别 5-6
  • 添加自定义规则
  • 为遗留代码实施基线
第四阶段:精通(持续进行)


  • 新代码达到级别 8-9
  • 添加 Psalm 以获得额外覆盖
  • 持续改进
常见陷阱和解决方案

过度抑制
  1. # phpstan.neon
  2. parameters:
  3.   tmpDir: var/cache/phpstan
  4.   resultCachePath: var/cache/phpstan/resultCache.php
复制代码
类型注解过载
  1. // .vscode/settings.json
  2. {
  3.   "php.validate.enable": false,
  4.   "php.suggest.basic": false,
  5.   "phpstan.enabled": true,
  6.   "phpstan.path": "vendor/bin/phpstan",
  7.   "phpstan.config": "phpstan.neon"
  8. }
复制代码
衡量成功

跟踪这些指标来衡量静态分析的成功。理解 PHP 性能分析有助于将静态分析改进与应用程序性能相关联:
  1. // 不好 - 抑制过于宽泛
  2. /** @phpstan-ignore-next-line */
  3. $user = User::find($id);
  4. // 好 - 具体抑制并说明原因
  5. /** @phpstan-ignore-next-line User::find() can return null but we know ID exists */
  6. $user = User::find($validatedId);
复制代码
总结:从黑粉到真香

PHPStan 抓到的那个支付 bug 彻底改变了我写 PHP 的方式。从一开始的被迫使用,到后来的真心喜欢,这个过程挺有意思的。
最大的变化不是抓 bug,而是心态。以前上线代码心里都没底,祈祷别出事。现在上线前心里有数,该抓的错误都抓了,踏实多了。
写代码的思路也变了:以前是写完了碰运气,现在是边写边考虑类型安全。PHPStan 不光帮我找 bug,还教会我怎么更严谨地思考代码逻辑。
给做 Laravel 的兄弟们几个建议:
别急着装逼:第一天就想跳级别 9?醒醒吧。老老实实从 0 → 3 → 5 → 8 这么来,一步一个脚印。
别怕报错:看到 847 个错误别慌,这不是说你菜,而是给你学习的机会。每修一个错误,你对类型安全的理解就深一分。
让团队看到好处:光说静态分析有用没人信,得拿实际抓到的 bug 说话。一个具体的例子胜过千言万语。
强制执行:把静态分析加到 CI/CD 里,让它变成必须的步骤。代码过不了静态分析就别想合并,这样大家就不会偷懒了。
静态分析不只是让代码写得更好,更重要的是让你晚上睡得安稳。知道有工具帮你把关,用户看到 bug 之前你就能发现,这种踏实感一旦体验过就回不去了。配合好的 PHP 内存管理和安全认证,静态分析就是写出靠谱 PHP 应用的基石。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册