PHPStan 静态分析工具
PHPStan 是一个强大的 PHP 静态分析工具,它可以通过分析代码来发现潜在的错误,而无需实际运行代码。Laravel Skeleton 项目集成了 PHPStan 及其 Laravel 专用扩展 Larastan,以提高代码质量和可靠性。
为什么使用 PHPStan?
静态分析可以帮助你:
- 在代码运行前发现潜在错误
- 提高代码质量和可维护性
- 减少生产环境中的意外行为
- 强制执行团队的编码标准
- 在不编写测试的情况下捕获许多常见错误
Laravel Skeleton 中的配置
Laravel Skeleton 项目使用 Larastan,这是一个专为 Laravel 设计的 PHPStan 扩展包,它理解 Laravel 的魔术方法和动态特性。
项目配置文件
项目的 PHPStan 配置文件位于 phpstan.neon.dist
,你可以在 Laravel Skeleton 项目仓库 中查看完整配置。
分析级别
PHPStan 最新版本支持 11 个分析级别(0-10),级别越高,分析越严格。根据 PHPStan 官方文档,各级别检查内容如下:
- 级别 0:基本检查,未知类、未知函数、调用的未知方法等
- 级别 1:可能未定义的变量,带有
__call
和__get
的类上的未知魔术方法和属性 - 级别 2:所有表达式上的未知方法检查(不仅仅是
$this
),验证 PHPDocs - 级别 3:返回类型,分配给属性的类型
- 级别 4:基本死代码检查 - 始终为 false 的
instanceof
和其他类型检查,死else
分支等 - 级别 5:检查传递给方法和函数的参数类型
- 级别 6:报告缺少类型提示
- 级别 7:报告部分错误的联合类型
- 级别 8:报告在可空类型上调用方法和访问属性
- 级别 9:严格处理显式
mixed
类型 - 级别 10:对
mixed
类型更加严格,报告隐式混合类型错误
Laravel Skeleton 项目使用了较高级别的分析,以确保代码质量。
自动化与工作流集成
Laravel Skeleton 项目通过多种方式将 PHPStan 分析集成到开发工作流中,使代码质量检查自动化:
1. Git 提交前检查
项目使用 husky 和 lint-staged 在提交代码前自动运行 PHPStan 分析:
- 当你执行
git commit
时,pre-commit hook 会触发 - 系统会自动对修改的文件运行 PHPStan 分析
- 如果发现问题,提交会被阻止,你需要修复问题后再次提交
这确保了所有提交到仓库的代码都通过了静态分析检查。
2. CI/CD 自动检查
项目配置了 GitHub Actions 工作流,在以下情况自动运行 PHPStan 分析:
- 每次推送代码到仓库时
- 创建或更新 Pull Request 时
你可以在 GitHub Actions 工作流配置 中查看完整配置。
3. 手动运行分析
如果需要手动运行分析,Laravel Skeleton 项目在 composer.json
中定义了便捷的脚本:
# 运行静态分析
composer analyze
# 生成基线文件
composer analyze:baseline
这些自动化配置确保了:
- 编写新代码前:CI/CD 已经确保现有代码库通过了分析
- 提交代码时:Git hooks 自动验证你的更改不会引入新问题
- 代码审查时:GitHub Actions 自动检查 PR 中的代码,确保符合标准
Larastan 特性
Larastan 扩展了 PHPStan,添加了对 Laravel 特定功能的支持:
1. 模型属性和关系
Larastan 理解 Eloquent 模型的属性和关系:
// Larastan 能够理解这些属性和方法
$user->id;
$user->email;
$user->posts()->where('active', true)->get();
2. 门面(Facades)
正确分析 Laravel 门面:
// Larastan 知道这是有效的方法调用
Cache::get('key');
Route::get('/home', [HomeController::class, 'index']);
3. 辅助函数
支持 Laravel 的全局辅助函数:
// Larastan 理解这些辅助函数的返回类型
$value = old('key', 'default');
$path = app_path('Http/Controllers');
4. 集合方法
正确分析 Laravel 集合的方法链:
// Larastan 能够跟踪集合中的类型
$names = User::all()->map(fn ($user) => $user->name)->toArray();
最佳实践
1. 类型注释
为变量和返回值添加类型注释,帮助 PHPStan 更准确地分析:
/**
* @param Collection<int, User> $users
* @return Collection<int, string>
*/
public function getUserNames(Collection $users): Collection
{
return $users->map(fn (User $user) => $user->name);
}
2. 模型注释
为模型添加属性和关系的类型注释:
/**
* @property int $id
* @property string $name
* @property string $email
* @property Carbon $created_at
* @property-read Collection<int, Post> $posts
*/
class User extends Model
{
// ...
}
3. 渐进式采用
如果你的项目有大量遗留代码,可以:
- 从较低级别开始(如级别 0 或 1)
- 使用基线功能忽略现有错误
- 逐步提高分析级别
# 生成基线文件
composer analyze:baseline
# 使用基线运行分析
./vendor/bin/phpstan analyse --baseline=phpstan-baseline.neon
4. 处理常见错误
未定义属性
当 PHPStan 报告"Access to an undefined property"时:
// 错误:Access to an undefined property App\Models\User::$settings
$user->settings;
解决方法:
- 添加
@property
注释 - 使用
@mixin
注释混入特性 - 对于动态属性,使用
__get
方法并添加适当的 PHPDoc
/**
* @property array $settings
*/
class User extends Model
{
// 或者实现 __get 方法
public function __get($key)
{
if ($key === 'settings') {
return $this->getSettings();
}
return parent::__get($key);
}
/**
* @return array<string, mixed>
*/
protected function getSettings(): array
{
// ...
}
}
可能未定义的变量
// 错误:Variable $result might not be defined
if (someCondition()) {
$result = 'success';
}
echo $result; // PHPStan 报错
解决方法:
- 始终初始化变量
- 使用空合并运算符
- 添加适当的条件检查
// 初始化变量
$result = null;
if (someCondition()) {
$result = 'success';
}
echo $result ?? 'default';
类型不匹配
// 错误:Parameter #1 $id of method Repository::find() expects int, string|null given
$repository->find($request->input('id'));
解决方法:
- 使用类型转换
- 添加类型检查
- 使用 Laravel 的验证器确保输入类型正确
// 类型转换
$repository->find((int) $request->input('id'));
// 或者添加类型检查
$id = $request->input('id');
if (is_numeric($id)) {
$repository->find((int) $id);
}
5. 使用 PHPDoc 泛型
为集合、数组和迭代器添加泛型类型信息:
/**
* @param array<string, mixed> $config 配置数组
* @return array<int, User> 用户数组
*/
public function getUsers(array $config): array
{
// ...
}
/**
* @var Collection<int, Post> $posts
*/
$posts = $user->posts;
/**
* @var array<string, array{id: int, name: string}> $data
*/
$data = [
'user1' => ['id' => 1, 'name' => 'John'],
'user2' => ['id' => 2, 'name' => 'Jane'],
];
6. 处理可空类型
明确处理可能为 null 的值:
/**
* @param User|null $user
* @return string
*/
public function getUserName(?User $user): string
{
// 错误:Method call on possibly null value
// return $user->name;
// 正确:添加 null 检查
if ($user === null) {
return 'Guest';
}
return $user->name;
// 或者使用空合并运算符
// return $user?->name ?? 'Guest';
}
7. 使用 PHPStan 忽略注释
对于特定情况下无法修复的错误,可以使用忽略注释:
// 忽略下一行
/** @phpstan-ignore-next-line */
$result = $someComplexExpression;
// 忽略特定错误
/** @phpstan-ignore-line Method someMethod() is not found */
$object->someMethod();
// 在文件顶部忽略整个文件的特定错误
/**
* @phpstan-ignore-next-line
* @phpstan-ignore AccessToUndefinedProperty
*/
8. 创建自定义类型扩展
对于复杂的框架集成或特定的代码模式,可以创建自定义类型扩展:
- 创建扩展类:
namespace App\PHPStan;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
class CustomExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return YourClass::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'yourMethod';
}
public function getTypeFromMethodCall(/* ... */): Type
{
// 返回正确的类型
}
}
- 在
phpstan.neon
中注册扩展:
services:
-
class: App\PHPStan\CustomExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
9. 使用 IDE 集成
将 PHPStan 与你的 IDE 集成,获得实时反馈:
- PhpStorm: 使用 PHPStan 插件
- VS Code: 使用 PHP Intelephense 或 PHP IntelliSense 插件
- Sublime Text: 使用 SublimeLinter-phpstan 插件
10. 处理第三方库
对于缺少类型信息的第三方库:
- 使用 stub 文件提供缺失的类型信息:
// stubs/SomeLibrary/SomeClass.stub
<?php
namespace SomeLibrary;
class SomeClass
{
/**
* @param string $key
* @return mixed
*/
public function get(string $key)
{
}
}
- 在
phpstan.neon
中配置 stub 文件:
parameters:
stubFiles:
- stubs/SomeLibrary/SomeClass.stub