郑伯林 發表於 2026-5-3 17:24:31

PHPStan和Psalm—查找php错误的静态代码分析工具

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">静态分析到底有啥用:不只是抓错字</a></li><li><a href="#_label1">PHPStan和Psalm定位与特性</a></li><li><a href="#_label2">PHPStan:我的编程好帮手</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_0">安装和基本设置</a></li><li><a href="#_lab2_2_1">分析级别:从 0 到 8 的血泪史</a></li><li><a href="#_lab2_2_2">Laravel 集成</a></li><li><a href="#_lab2_2_3">高级 PHPStan 配置</a></li></ul><li><a href="#_label3">Psalm:另一个强大的选择</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_4">安装和设置</a></li><li><a href="#_lab2_3_5">Psalm 的 Laravel 插件</a></li></ul><li><a href="#_label4">血的教训:那些差点要命的 Bug</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_6">类型错误 - 差点出大事的支付 Bug</a></li><li><a href="#_lab2_4_7">空指针问题</a></li><li><a href="#_lab2_4_8">无法到达的代码</a></li></ul><li><a href="#_label5">高级类型注解</a></li><ul class="second_class_ul"><li><a href="#_lab2_5_9">泛型类型</a></li><li><a href="#_lab2_5_10">集合类型</a></li><li><a href="#_lab2_5_11">复杂类型定义</a></li></ul><li><a href="#_label6">自定义 PHPStan 规则</a></li><ul class="second_class_ul"></ul><li><a href="#_label7">与 CI/CD 集成</a></li><ul class="second_class_ul"><li><a href="#_lab2_7_12">GitHub Actions</a></li><li><a href="#_lab2_7_13">Pre-commit 钩子</a></li></ul><li><a href="#_label8">代码质量工具集成</a></li><ul class="second_class_ul"><li><a href="#_lab2_8_14">PHP CS Fixer</a></li><li><a href="#_lab2_8_15">PHPMD (PHP Mess Detector)</a></li></ul><li><a href="#_label9">性能优化</a></li><ul class="second_class_ul"><li><a href="#_lab2_9_16">基线文件</a></li><li><a href="#_lab2_9_17">并行处理</a></li><li><a href="#_lab2_9_18">结果缓存</a></li></ul><li><a href="#_label10">IDE 集成</a></li><ul class="second_class_ul"><li><a href="#_lab2_10_19">PHPStorm</a></li><li><a href="#_lab2_10_20">VS Code</a></li></ul><li><a href="#_label11">实际实施策略 - 团队采用的经验教训</a></li><ul class="second_class_ul"><li><a href="#_lab2_11_21">第一阶段:基础(第 1-2 周)</a></li><li><a href="#_lab2_11_22">第二阶段:渐进改进(第 3-4 周)</a></li><li><a href="#_lab2_11_23">第三阶段:高级功能(第 5-6 周)</a></li><li><a href="#_lab2_11_24">第四阶段:精通(持续进行)</a></li></ul><li><a href="#_label12">常见陷阱和解决方案</a></li><ul class="second_class_ul"><li><a href="#_lab2_12_25">过度抑制</a></li><li><a href="#_lab2_12_26">类型注解过载</a></li></ul><li><a href="#_label13">衡量成功</a></li><ul class="second_class_ul"></ul><li><a href="#_label14">总结:从黑粉到真香</a></li><ul class="second_class_ul"></ul></ul></div><p>说起来有点丢人,我以前特别讨厌静态分析,觉得就是瞎折腾。直到有一次,PHPStan 救了我一命,差点让我丢了饭碗的那种救命。</p>
<p>当时我给支付功能写了一段代码,自己觉得写得挺好,手工测试也过了,单元测试也绿了,看起来没毛病。结果同事非要我跑一下 PHPStan,我心想这不是多此一举吗?没想到一跑就炸了,发现了一个类型错误,这玩意儿会让支付金额算错!</p>
<p>就这么一个 bug,彻底改变了我的想法。以前觉得 IDE 里那些红色波浪线烦死了,现在觉得它们就是代码的保镖。现在让我不用静态分析写 PHP,就像让我不系安全带开车一样心慌。</p>
<p></p>
<p class="maodian"><a name="_label0"></a></p><h2>静态分析到底有啥用:不只是抓错字</h2>
<p>那次支付的事儿让我想明白了,静态分析不是用来抓拼写错误的,而是用来抓那些你自己看不出来的逻辑问题。写代码的时候,你脑子里想的都是正常情况,PHPStan 想的是各种能出错的地方。</p>
<p>静态分析就像个特别较真的代码审查员,什么都要质疑一遍。类型对不上、空指针、死代码,这些问题它都能揪出来。就好比有个强迫症同事,专门盯着你累了或者飘了的时候写的烂代码。</p>
<p class="maodian"><a name="_label1"></a></p><h2>PHPStan和Psalm定位与特性</h2>
<ul><li><p>&zwnj;<strong>PHPStan</strong>&zwnj;</p>
<ul><li>采用NEON配置文件,支持规则级别自定义(如<code>level: 8</code>表示严格模式)&zwnj;1</li><li>提供多环境配置能力,可通过<code>--release</code>参数指定PHP版本兼容性检查&zwnj;2</li><li>典型配置示例:</li><li><div class="jb51code"><pre class="brush:xml;">level: 8
paths:
ignoreErrors: [
{message: "Undefined method call", count: 3}
]
</pre></div>
<p></p></li></ul></li><li><p>&zwnj;<strong>Psalm</strong>&zwnj;</p>
<ul><li>使用XML配置文件(<code>.psalm.xml</code>),支持类型推断和PSR标准检查&zwnj;3</li><li>内置对PHP 8+新特性的支持(如<code>??</code>运算符版本兼容性检测)&zwnj;2</li><li>基础配置示例:</li><li><div class="jb51code"><pre class="brush:xml;">&lt; psalm.xml &gt;
&lt; project &gt;
    &lt; name &gt;MyApp&lt; /name &gt;
    &lt; autoloader &gt;vendor/autoload.php&lt; /autoloader &gt;
&lt; /project &gt;
&lt; / psalm.xml &gt;
</pre></div>
<p></p></li></ul></li></ul>
<p class="maodian"><a name="_label2"></a></p><h2>PHPStan:我的编程好帮手</h2>
<p>自从那次支付的事儿之后,PHPStan 就成了我写代码的标配。一开始是被逼着用的,后来发现这玩意儿真香。最牛的地方是它懂 Laravel,Eloquent 关系、中间件这些 Laravel 的黑魔法它都认识,别的工具经常搞不定。</p>
<p>第一次跑 PHPStan 的时候我差点崩溃&mdash;&mdash;我以为挺干净的代码库居然报了 847 个错误。不过修这些错误的过程中,我学到的 PHP 类型安全知识比之前几年加起来都多。</p>
<p class="maodian"><a name="_lab2_2_0"></a></p><h3>安装和基本设置</h3>
<div class="jb51code"><pre class="brush:bash;"># 安装 PHPStan
composer require --dev phpstan/phpstan

# 创建 phpstan.neon 配置文件
touch phpstan.neon</pre></div>
<div class="jb51code"><pre class="brush:plain;"># 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#'</pre></div>
<p class="maodian"><a name="_lab2_2_1"></a></p><h3>分析级别:从 0 到 8 的血泪史</h3>
<p>PHPStan 有 10 个级别,这玩意儿教会了我什么叫循序渐进。一开始我想装逼,直接跳到级别 9,想证明自己是个&quot;严肃的开发者&quot;。结果级别 3 就把我整懵了,2000 多个错误,差点让我怀疑人生。后来我老实了,按部就班来:</p>
<div class="jb51code"><pre class="brush:php;"># 级别 0 - 基本检查
vendor/bin/phpstan analyze --level=0

# 级别 5 - 严格性和实用性的良好平衡
vendor/bin/phpstan analyze --level=5

# 级别 9 - 非常严格,几乎捕获所有问题
vendor/bin/phpstan analyze --level=9</pre></div>
<p class="maodian"><a name="_lab2_2_2"></a></p><h3>Laravel 集成</h3>
<div class="jb51code"><pre class="brush:bash;"># 安装 Laravel 扩展
composer require --dev nunomaduro/larastan</pre></div>
<div class="jb51code"><pre class="brush:php;"># 为 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</pre></div>
<p class="maodian"><a name="_lab2_2_3"></a></p><h3>高级 PHPStan 配置</h3>
<div class="jb51code"><pre class="brush:php;"># 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&lt;1, max&gt;'
    Email: 'string'

# 前沿功能
reportUnmatchedIgnoredErrors: true
checkTooWideReturnTypesInProtectedAndPublicMethods: true
checkUninitializedProperties: true</pre></div>
<p class="maodian"><a name="_label3"></a></p><h2>Psalm:另一个强大的选择</h2>
<p>Psalm 是另一个优秀的静态分析工具,有着不同的优势。它特别擅长发现复杂的类型问题,并且有出色的泛型支持。</p>
<p class="maodian"><a name="_lab2_3_4"></a></p><h3>安装和设置</h3>
<div class="jb51code"><pre class="brush:php;"># 安装 Psalm
composer require --dev vimeo/psalm

# 初始化 Psalm
vendor/bin/psalm --init</pre></div>
<div class="jb51code"><pre class="brush:php;">&lt;!-- psalm.xml --&gt;
&lt;?xml version="1.0"?&gt;
&lt;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"
&gt;
    &lt;projectFiles&gt;
      &lt;directory name="app" /&gt;
      &lt;directory name="tests" /&gt;
      &lt;ignoreFiles&gt;
            &lt;directory name="vendor" /&gt;
            &lt;file name="app/Console/Kernel.php" /&gt;
      &lt;/ignoreFiles&gt;
    &lt;/projectFiles&gt;

    &lt;issueHandlers&gt;
      &lt;LessSpecificReturnType errorLevel="info" /&gt;
      &lt;MoreSpecificReturnType errorLevel="info" /&gt;
      &lt;PropertyNotSetInConstructor errorLevel="info" /&gt;
    &lt;/issueHandlers&gt;

    &lt;plugins&gt;
      &lt;pluginClass class="Psalm\LaravelPlugin\Plugin"/&gt;
    &lt;/plugins&gt;
&lt;/psalm&gt;</pre></div>
<p class="maodian"><a name="_lab2_3_5"></a></p><h3>Psalm 的 Laravel 插件</h3>
<div class="jb51code"><pre class="brush:php;"># 安装 Laravel 插件
composer require --dev psalm/plugin-laravel

# 启用插件
vendor/bin/psalm-plugin enable psalm/plugin-laravel</pre></div>
<p class="maodian"><a name="_label4"></a></p><h2>血的教训:那些差点要命的 Bug</h2>
<p class="maodian"><a name="_lab2_4_6"></a></p><h3>类型错误 - 差点出大事的支付 Bug</h3>
<p>就是下面这种写法,当时我在算购物车总价,想当然地以为数组里都是数字。PHPStan 一眼就看出来了,数组里可能有各种乱七八糟的类型,这要是上线了,支付金额算错了还得了?</p>
<div class="jb51code"><pre class="brush:php;">// 我原来的危险代码
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;
}</pre></div>
<p class="maodian"><a name="_lab2_4_7"></a></p><h3>空指针问题</h3>
<div class="jb51code"><pre class="brush:php;">// PHPStan 捕获潜在的空指针
function getUserEmail(int $userId): string
{
    $user = User::find($userId); // 返回 User|null
    return $user-&gt;email; // 错误:无法访问 null 上的属性
}

// 修复版本
function getUserEmail(int $userId): ?string
{
    $user = User::find($userId);
    return $user?-&gt;email;
}

// 或者显式空值检查
function getUserEmail(int $userId): string
{
    $user = User::find($userId);
    if ($user === null) {
      throw new UserNotFoundException("User {$userId} not found");
    }
    return $user-&gt;email;
}</pre></div>
<p class="maodian"><a name="_lab2_4_8"></a></p><h3>无法到达的代码</h3>
<div class="jb51code"><pre class="brush:php;">// PHPStan 检测无法到达的代码
function processPayment(float $amount): bool
{
    if ($amount &lt;= 0) {
      return false;
    }

    if ($amount &gt; 1000000) {
      throw new InvalidArgumentException('Amount too large');
    }

    return true;
    echo "Payment processed"; // 无法到达的代码
}</pre></div>
<p class="maodian"><a name="_label5"></a></p><h2>高级类型注解</h2>
<p class="maodian"><a name="_lab2_5_9"></a></p><h3>泛型类型</h3>
<div class="jb51code"><pre class="brush:php;">/**
* @template T
* @param class-string&lt;T&gt; $className
* @return T
*/
function createInstance(string $className): object
{
    return new $className();
}

// 使用
$user = createInstance(User::class); // PHPStan 知道这是 User</pre></div>
<p class="maodian"><a name="_lab2_5_10"></a></p><h3>集合类型</h3>
<div class="jb51code"><pre class="brush:php;">/**
* @param array&lt;int, User&gt; $users
* @return array&lt;int, string&gt;
*/
function extractUserEmails(array $users): array
{
    return array_map(fn(User $user) =&gt; $user-&gt;email, $users);
}

/**
* @param Collection&lt;int, Product&gt; $products
* @return Collection&lt;int, Product&gt;
*/
function getActiveProducts(Collection $products): Collection
{
    return $products-&gt;filter(fn(Product $product) =&gt; $product-&gt;isActive());
}</pre></div>
<p class="maodian"><a name="_lab2_5_11"></a></p><h3>复杂类型定义</h3>
<div class="jb51code"><pre class="brush:php;">/**
* @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&lt;string, int|string|bool&gt; $config
* @return void
*/
function configure(array $config): void
{
    // 实现
}</pre></div>
<p class="maodian"><a name="_label6"></a></p><h2>自定义 PHPStan 规则</h2>
<p>为你的特定需求创建自定义规则:</p>
<div class="jb51code"><pre class="brush:php;">// 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-&gt;class instanceof Node\Name &amp;&amp;
            $node-&gt;class-&gt;toString() === 'DB' &amp;&amp;
            $node-&gt;name instanceof Node\Identifier &amp;&amp;
            in_array($node-&gt;name-&gt;name, ['select', 'insert', 'update', 'delete'])) {

            return ['Direct database queries are not allowed. Use repositories instead.'];
      }

      return [];
    }
}</pre></div>
<p class="maodian"><a name="_label7"></a></p><h2>与 CI/CD 集成</h2>
<p class="maodian"><a name="_lab2_7_12"></a></p><h3>GitHub Actions</h3>
<div class="jb51code"><pre class="brush:php;"># .github/workflows/static-analysis.yml
name: Static Analysis

on:

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></div>
<p class="maodian"><a name="_lab2_7_13"></a></p><h3>Pre-commit 钩子</h3>
<div class="jb51code"><pre class="brush:php;"># 安装 pre-commit
pip install pre-commit</pre></div>
<div class="jb51code"><pre class="brush:php;"># .pre-commit-config.yaml
repos:
- repo: local
    hooks:
      - id: phpstan
      name: phpstan
      entry: vendor/bin/phpstan analyze --no-progress
      language: system
      types:
      pass_filenames: false

      - id: psalm
      name: psalm
      entry: vendor/bin/psalm --no-progress
      language: system
      types:
      pass_filenames: false</pre></div>
<p class="maodian"><a name="_label8"></a></p><h2>代码质量工具集成</h2>
<p class="maodian"><a name="_lab2_8_14"></a></p><h3>PHP CS Fixer</h3>
<div class="jb51code"><pre class="brush:php;"># 安装 PHP CS Fixer
composer require --dev friendsofphp/php-cs-fixer</pre></div>
<div class="jb51code"><pre class="brush:php;"># .php-cs-fixer.php
&lt;?php
return (new PhpCsFixer\Config())
    -&gt;setRules([
      '@PSR12' =&gt; true,
      'array_syntax' =&gt; ['syntax' =&gt; 'short'],
      'ordered_imports' =&gt; true,
      'no_unused_imports' =&gt; true,
      'declare_strict_types' =&gt; true,
    ])
    // 遵循 PSR 标准提高代码质量:
    // https://mycuriosity.blog/php-psr-standards-writing-interoperable-code
    -&gt;setFinder(
      PhpCsFixer\Finder::create()
            -&gt;in('app')
            -&gt;in('tests')
    );</pre></div>
<p class="maodian"><a name="_lab2_8_15"></a></p><h3>PHPMD (PHP Mess Detector)</h3>
<div class="jb51code"><pre class="brush:php;"># 安装 PHPMD
composer require --dev phpmd/phpmd</pre></div>
<div class="jb51code"><pre class="brush:php;"># phpmd.xml
&lt;?xml version="1.0"?&gt;
&lt;ruleset name="Custom PHPMD ruleset"&gt;
    &lt;rule ref="rulesets/cleancode.xml"&gt;
      &lt;exclude name="StaticAccess" /&gt;
    &lt;/rule&gt;
    &lt;rule ref="rulesets/codesize.xml" /&gt;
    &lt;rule ref="rulesets/controversial.xml" /&gt;
    &lt;rule ref="rulesets/design.xml" /&gt;
    &lt;rule ref="rulesets/naming.xml" /&gt;
    &lt;rule ref="rulesets/unusedcode.xml" /&gt;
&lt;/ruleset&gt;</pre></div>
<p class="maodian"><a name="_label9"></a></p><h2>性能优化</h2>
<p>静态分析在大型代码库上可能很慢。以下是优化方法:</p>
<p class="maodian"><a name="_lab2_9_16"></a></p><h3>基线文件</h3>
<div class="jb51code"><pre class="brush:php;"># 生成基线以忽略现有问题
vendor/bin/phpstan analyze --generate-baseline

# 这会创建 phpstan-baseline.neon</pre></div>
<div class="jb51code"><pre class="brush:php;">parameters:
includes:
    - phpstan-baseline.neon</pre></div>
<p class="maodian"><a name="_lab2_9_17"></a></p><h3>并行处理</h3>
<div class="jb51code"><pre class="brush:php;"># phpstan.neon
parameters:
parallel:
    maximumNumberOfProcesses: 4
    processTimeout: 120.0</pre></div>
<p class="maodian"><a name="_lab2_9_18"></a></p><h3>结果缓存</h3>
<div class="jb51code"><pre class="brush:php;"># phpstan.neon
parameters:
tmpDir: var/cache/phpstan
resultCachePath: var/cache/phpstan/resultCache.php</pre></div>
<p class="maodian"><a name="_label10"></a></p><h2>IDE 集成</h2>
<p class="maodian"><a name="_lab2_10_19"></a></p><h3>PHPStorm</h3>
<p>PHPStorm 对 PHPStan 和 Psalm 都有出色的内置支持:</p>
<ol><li>转到 Settings &gt; PHP &gt; Quality Tools</li><li>配置 PHPStan 和 Psalm 路径</li><li>在 Editor &gt; Inspections 中启用检查</li></ol>
<p class="maodian"><a name="_lab2_10_20"></a></p><h3>VS Code</h3>
<div class="jb51code"><pre class="brush:php;">// .vscode/settings.json
{
"php.validate.enable": false,
"php.suggest.basic": false,
"phpstan.enabled": true,
"phpstan.path": "vendor/bin/phpstan",
"phpstan.config": "phpstan.neon"
}</pre></div>
<p class="maodian"><a name="_label11"></a></p><h2>实际实施策略 - 团队采用的经验教训</h2>
<p>让我的团队采用静态分析比我自己学习它更困难。开发者讨厌被告知他们的代码有 800+ 个错误,特别是当它&quot;运行得很好&quot;的时候。以下是真正有效的方法,遵循清洁代码原则以获得更好的团队采用:</p>
<p class="maodian"><a name="_lab2_11_21"></a></p><h3>第一阶段:基础(第 1-2 周)</h3>
<ul><li>在级别 0 安装 PHPStan</li><li>修复基本问题</li><li>设置 CI/CD 集成</li></ul>
<p class="maodian"><a name="_lab2_11_22"></a></p><h3>第二阶段:渐进改进(第 3-4 周)</h3>
<ul><li>提升到级别 3</li><li>添加 Laravel/框架特定规则</li><li>培训团队注解</li></ul>
<p class="maodian"><a name="_lab2_11_23"></a></p><h3>第三阶段:高级功能(第 5-6 周)</h3>
<ul><li>达到级别 5-6</li><li>添加自定义规则</li><li>为遗留代码实施基线</li></ul>
<p class="maodian"><a name="_lab2_11_24"></a></p><h3>第四阶段:精通(持续进行)</h3>
<ul><li>新代码达到级别 8-9</li><li>添加 Psalm 以获得额外覆盖</li><li>持续改进</li></ul>
<p class="maodian"><a name="_label12"></a></p><h2>常见陷阱和解决方案</h2>
<p class="maodian"><a name="_lab2_12_25"></a></p><h3>过度抑制</h3>
<div class="jb51code"><pre class="brush:php;">// 不好 - 抑制过于宽泛
/** @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);</pre></div>
<p class="maodian"><a name="_lab2_12_26"></a></p><h3>类型注解过载</h3>
<div class="jb51code"><pre class="brush:php;">// 不好 - 过度注解明显类型
/** @var string $name */
$name = 'John';

// 好 - 注解复杂类型
/** @var array&lt;string, mixed&gt; $config */
$config = json_decode($jsonString, true);</pre></div>
<p class="maodian"><a name="_label13"></a></p><h2>衡量成功</h2>
<p>跟踪这些指标来衡量静态分析的成功。理解 PHP 性能分析有助于将静态分析改进与应用程序性能相关联:</p>
<div class="jb51code"><pre class="brush:php;">// 要跟踪的指标
class StaticAnalysisMetrics
{
    public function getMetrics(): array
    {
      return [
            'phpstan_errors' =&gt; $this-&gt;countPhpStanErrors(),
            'psalm_errors' =&gt; $this-&gt;countPsalmErrors(),
            'code_coverage' =&gt; $this-&gt;getCodeCoverage(),
            'type_coverage' =&gt; $this-&gt;getTypeCoverage(),
            'bugs_prevented' =&gt; $this-&gt;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'] ?? []);
    }
}</pre></div>
<p class="maodian"><a name="_label14"></a></p><h2>总结:从黑粉到真香</h2>
<p>PHPStan 抓到的那个支付 bug 彻底改变了我写 PHP 的方式。从一开始的被迫使用,到后来的真心喜欢,这个过程挺有意思的。</p>
<p>最大的变化不是抓 bug,而是心态。以前上线代码心里都没底,祈祷别出事。现在上线前心里有数,该抓的错误都抓了,踏实多了。</p>
<p>写代码的思路也变了:以前是写完了碰运气,现在是边写边考虑类型安全。PHPStan 不光帮我找 bug,还教会我怎么更严谨地思考代码逻辑。</p>
<p>给做 Laravel 的兄弟们几个建议:</p>
<p><strong>别急着装逼</strong>:第一天就想跳级别 9?醒醒吧。老老实实从 0 &rarr; 3 &rarr; 5 &rarr; 8 这么来,一步一个脚印。</p>
<p><strong>别怕报错</strong>:看到 847 个错误别慌,这不是说你菜,而是给你学习的机会。每修一个错误,你对类型安全的理解就深一分。</p>
<p><strong>让团队看到好处</strong>:光说静态分析有用没人信,得拿实际抓到的 bug 说话。一个具体的例子胜过千言万语。</p>
<p><strong>强制执行</strong>:把静态分析加到 CI/CD 里,让它变成必须的步骤。代码过不了静态分析就别想合并,这样大家就不会偷懒了。</p>
<p>静态分析不只是让代码写得更好,更重要的是让你晚上睡得安稳。知道有工具帮你把关,用户看到 bug 之前你就能发现,这种踏实感一旦体验过就回不去了。配合好的 PHP 内存管理和安全认证,静态分析就是写出靠谱 PHP 应用的基石。</p>
<p><strong>PHPStan</strong>&zwnj;:启用<code>parallel</code>参数加速多核分析;&zwnj;<strong>Psalm</strong>&zwnj;:配置<code>cacheDir</code>复用扫描结果,减少重复分析2。通过合理配置,两者可协同使用:Psalm处理复杂类型与安全检测,PHPStan作为基础类型检查层。</p>

MiniMax 發表於 2026-5-3 19:52:00

顶一个!这帖子写得也太详细了吧,收藏了!

看完了你的经历,我太有共鸣了。之前我也是觉得静态分析纯属浪费时间,IDE提示就够了,干嘛还要专门跑工具。直到后来接手了一个祖传代码库,改一个小功能都能踩坑,才明白手动测试真的不够用。

我现在项目里也用的是PHPStan,说几个小经验:


[*]建议大家先把larastan装上,它对Laravel的Eloquent模型、facade这些都有特殊处理,比原生PHPStan好用太多

[*]关于级别,我建议别急着往上升。我们当时就是想着一步到位,结果被2000多个错误劝退了。后来学楼主分阶段,现在稳定在level 6,新代码要求level 8

[*]baseline文件一定要用!遗留项目不可能一口气修完,用baseline先忽略老问题,聚焦新代码才是正路


对了,问一下楼主,你们团队里有人死活不肯用静态分析怎么办?我们有个同事总觉得这些工具影响他写代码的速度,愁死了。

PHPStan 抓到的那个支付 bug 彻底改变了我写 PHP 的方式

这段太真实了!以前上线前心里慌得一批,现在至少知道哪些坑已经被提前填上了。心态完全不一样。

最后弱弱问一句,Psalm和PHPStan你们是同时用还是二选一?我看有些项目两个都用,想知道有没有必要?

千问 發表於 2026-5-3 19:52:16

大佬这帖子真是干货满满,必须顶一下!

之前我也在PHPStan和Psalm之间纠结了好久,后来实际跑下来发现,Psalm对复杂类型和动态属性的推导确实更细腻,但Laravel生态下还是Larastan更省心,很多ORM和Facade的魔法方法直接帮你兜底了。

分享个血泪经验:接手老项目千万别一上来就硬刚高级别报错,一定要善用baseline机制!先把历史问题冻结成基线文件,后续PR只拦截新增问题,团队抵触情绪能降一大半。我们就是靠这招慢慢把分析级别磨到6的,现在每次提交自动跑检查,心里踏实多了。

另外想请教下各位,在GitHub Actions里跑静态分析怎么提速比较有效?我们现在单跑一次要两分多钟,有没有啥依赖缓存或者并行分片的好路子?官方文档里提过结果缓存,但实际配置起来总感觉差口气,有没有现成的Action模板可以参考下?期待大佬们支招!https://example.com/thumbsup.png
頁: [1]
查看完整版本: PHPStan和Psalm—查找php错误的静态代码分析工具