PHPStan和Psalm—查找php錯誤的靜態(tài)代碼分析工具
說起來有點丟人,我以前特別討厭靜態(tài)分析,覺得就是瞎折騰。直到有一次,PHPStan 救了我一命,差點讓我丟了飯碗的那種救命。
當(dāng)時我給支付功能寫了一段代碼,自己覺得寫得挺好,手工測試也過了,單元測試也綠了,看起來沒毛病。結(jié)果同事非要我跑一下 PHPStan,我心想這不是多此一舉嗎?沒想到一跑就炸了,發(fā)現(xiàn)了一個類型錯誤,這玩意兒會讓支付金額算錯!
就這么一個 bug,徹底改變了我的想法。以前覺得 IDE 里那些紅色波浪線煩死了,現(xiàn)在覺得它們就是代碼的保鏢?,F(xiàn)在讓我不用靜態(tài)分析寫 PHP,就像讓我不系安全帶開車一樣心慌。
靜態(tài)分析到底有啥用:不只是抓錯字
那次支付的事兒讓我想明白了,靜態(tài)分析不是用來抓拼寫錯誤的,而是用來抓那些你自己看不出來的邏輯問題。寫代碼的時候,你腦子里想的都是正常情況,PHPStan 想的是各種能出錯的地方。
靜態(tài)分析就像個特別較真的代碼審查員,什么都要質(zhì)疑一遍。類型對不上、空指針、死代碼,這些問題它都能揪出來。就好比有個強迫癥同事,專門盯著你累了或者飄了的時候?qū)懙臓€代碼。
PHPStan和Psalm定位與特性
PHPStan
- 采用NEON配置文件,支持規(guī)則級別自定義(如
level: 8表示嚴格模式)1 - 提供多環(huán)境配置能力,可通過
--release參數(shù)指定PHP版本兼容性檢查2 - 典型配置示例:
level: 8 paths: [src/, tests/] ignoreErrors: [ {message: "Undefined method call", count: 3} ]
- 采用NEON配置文件,支持規(guī)則級別自定義(如
Psalm
- 使用XML配置文件(
.psalm.xml),支持類型推斷和PSR標(biāo)準(zhǔn)檢查3 - 內(nèi)置對PHP 8+新特性的支持(如
??運算符版本兼容性檢測)2 - 基礎(chǔ)配置示例:
< psalm.xml > < project > < name >MyApp< /name > < autoloader >vendor/autoload.php< /autoloader > < /project > < / psalm.xml >
- 使用XML配置文件(
PHPStan:我的編程好幫手
自從那次支付的事兒之后,PHPStan 就成了我寫代碼的標(biāo)配。一開始是被逼著用的,后來發(fā)現(xiàn)這玩意兒真香。最牛的地方是它懂 Laravel,Eloquent 關(guān)系、中間件這些 Laravel 的黑魔法它都認識,別的工具經(jīng)常搞不定。
第一次跑 PHPStan 的時候我差點崩潰——我以為挺干凈的代碼庫居然報了 847 個錯誤。不過修這些錯誤的過程中,我學(xué)到的 PHP 類型安全知識比之前幾年加起來都多。
安裝和基本設(shè)置
# 安裝 PHPStan composer require --dev phpstan/phpstan # 創(chuàng)建 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,想證明自己是個"嚴肅的開發(fā)者"。結(jié)果級別 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#'
# 自定義規(guī)則
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: truePsalm:另一個強大的選擇
Psalm 是另一個優(yōu)秀的靜態(tài)分析工具,有著不同的優(yōu)勢。它特別擅長發(fā)現(xiàn)復(fù)雜的類型問題,并且有出色的泛型支持。
安裝和設(shè)置
# 安裝 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
血的教訓(xùn):那些差點要命的 Bug
類型錯誤 - 差點出大事的支付 Bug
就是下面這種寫法,當(dāng)時我在算購物車總價,想當(dāng)然地以為數(shù)組里都是數(shù)字。PHPStan 一眼就看出來了,數(shù)組里可能有各種亂七八糟的類型,這要是上線了,支付金額算錯了還得了?
// 我原來的危險代碼
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 上的屬性
}
// 修復(fù)版本
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());
}復(fù)雜類型定義
/**
* @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
{
// 實現(xiàn)
}自定義 PHPStan 規(guī)則
為你的特定需求創(chuàng)建自定義規(guī)則:
// 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=githubPre-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代碼質(zhì)量工具集成
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 標(biāo)準(zhǔn)提高代碼質(zhì)量:
// 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>性能優(yōu)化
靜態(tài)分析在大型代碼庫上可能很慢。以下是優(yōu)化方法:
基線文件
# 生成基線以忽略現(xiàn)有問題 vendor/bin/phpstan analyze --generate-baseline # 這會創(chuàng)建 phpstan-baseline.neon
parameters:
includes:
- phpstan-baseline.neon并行處理
# phpstan.neon
parameters:
parallel:
maximumNumberOfProcesses: 4
processTimeout: 120.0結(jié)果緩存
# phpstan.neon parameters: tmpDir: var/cache/phpstan resultCachePath: var/cache/phpstan/resultCache.php
IDE 集成
PHPStorm
PHPStorm 對 PHPStan 和 Psalm 都有出色的內(nèi)置支持:
- 轉(zhuǎn)到 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"
}實際實施策略 - 團隊采用的經(jīng)驗教訓(xùn)
讓我的團隊采用靜態(tài)分析比我自己學(xué)習(xí)它更困難。開發(fā)者討厭被告知他們的代碼有 800+ 個錯誤,特別是當(dāng)它"運行得很好"的時候。以下是真正有效的方法,遵循清潔代碼原則以獲得更好的團隊采用:
第一階段:基礎(chǔ)(第 1-2 周)
- 在級別 0 安裝 PHPStan
- 修復(fù)基本問題
- 設(shè)置 CI/CD 集成
第二階段:漸進改進(第 3-4 周)
- 提升到級別 3
- 添加 Laravel/框架特定規(guī)則
- 培訓(xùn)團隊注解
第三階段:高級功能(第 5-6 周)
- 達到級別 5-6
- 添加自定義規(guī)則
- 為遺留代碼實施基線
第四階段:精通(持續(xù)進行)
- 新代碼達到級別 8-9
- 添加 Psalm 以獲得額外覆蓋
- 持續(xù)改進
常見陷阱和解決方案
過度抑制
// 不好 - 抑制過于寬泛 /** @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'; // 好 - 注解復(fù)雜類型 /** @var array<string, mixed> $config */ $config = json_decode($jsonString, true);
衡量成功
跟蹤這些指標(biāo)來衡量靜態(tài)分析的成功。理解 PHP 性能分析有助于將靜態(tài)分析改進與應(yīng)用程序性能相關(guān)聯(lián):
// 要跟蹤的指標(biāo)
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'] ?? []);
}
}總結(jié):從黑粉到真香
PHPStan 抓到的那個支付 bug 徹底改變了我寫 PHP 的方式。從一開始的被迫使用,到后來的真心喜歡,這個過程挺有意思的。
最大的變化不是抓 bug,而是心態(tài)。以前上線代碼心里都沒底,祈禱別出事?,F(xiàn)在上線前心里有數(shù),該抓的錯誤都抓了,踏實多了。
寫代碼的思路也變了:以前是寫完了碰運氣,現(xiàn)在是邊寫邊考慮類型安全。PHPStan 不光幫我找 bug,還教會我怎么更嚴謹?shù)厮伎即a邏輯。
給做 Laravel 的兄弟們幾個建議:
別急著裝逼:第一天就想跳級別 9?醒醒吧。老老實實從 0 → 3 → 5 → 8 這么來,一步一個腳印。
別怕報錯:看到 847 個錯誤別慌,這不是說你菜,而是給你學(xué)習(xí)的機會。每修一個錯誤,你對類型安全的理解就深一分。
讓團隊看到好處:光說靜態(tài)分析有用沒人信,得拿實際抓到的 bug 說話。一個具體的例子勝過千言萬語。
強制執(zhí)行:把靜態(tài)分析加到 CI/CD 里,讓它變成必須的步驟。代碼過不了靜態(tài)分析就別想合并,這樣大家就不會偷懶了。
靜態(tài)分析不只是讓代碼寫得更好,更重要的是讓你晚上睡得安穩(wěn)。知道有工具幫你把關(guān),用戶看到 bug 之前你就能發(fā)現(xiàn),這種踏實感一旦體驗過就回不去了。配合好的 PHP 內(nèi)存管理和安全認證,靜態(tài)分析就是寫出靠譜 PHP 應(yīng)用的基石。
PHPStan:啟用parallel參數(shù)加速多核分析;Psalm:配置cacheDir復(fù)用掃描結(jié)果,減少重復(fù)分析2。通過合理配置,兩者可協(xié)同使用:Psalm處理復(fù)雜類型與安全檢測,PHPStan作為基礎(chǔ)類型檢查層。
到此這篇關(guān)于PHPStan和Psalm—查找php錯誤的靜態(tài)代碼分析工具的文章就介紹到這了,更多相關(guān)php代碼分析工具PHPStan和Psalm內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在mac?OS上進行多個PHP版本之間切換的實現(xiàn)方法
不同項目使用php版本可能不同,需要安裝不同版本php,本文給大家介紹了在macOS上進行多個?PHP?版本之間切換的實現(xiàn)方法,文中有詳細的代碼示例供大家參考,需要的朋友可以參考下2023-10-10
PHP生成不同顏色、不同大小的tag標(biāo)簽函數(shù)
看別人網(wǎng)站上面的tag都是不同顏色,不同大小的tag標(biāo)簽非常不錯,這里就分享兩個函數(shù),方便需要的朋友2013-09-09
php使用pdo連接并查詢sql數(shù)據(jù)庫的方法
這篇文章主要介紹了php使用pdo連接并查詢sql數(shù)據(jù)庫的方法,實例分析了常用的pdo連接方法與改進方法,并針對pdo技術(shù)進行了分析說明,需要的朋友可以參考下2014-12-12

