PHPStan 自定义规则:针对项目特定业务逻辑的类型检查
大家好,今天我们来聊聊如何利用 PHPStan 的自定义规则,针对项目特定的业务逻辑进行类型检查,提升代码质量和可维护性。
PHPStan 是一款强大的 PHP 静态分析工具,它可以在不运行代码的情况下,检测代码中的潜在错误,例如类型错误、未定义的变量、以及其他潜在的问题。它基于类型推断,可以识别出很多 PHP 运行时可能发生的错误,从而帮助我们提前发现并修复 bug。
虽然 PHPStan 内置了很多规则,能够覆盖大部分常见的错误,但在实际项目中,往往会遇到一些特定的业务逻辑,这些逻辑无法被通用的规则所覆盖。这时,我们就需要编写自定义规则,来针对这些特定的场景进行检查。
1. 为什么要编写自定义规则?
- 强制执行项目特定约束: 确保代码遵循项目约定的规范和最佳实践。例如,强制某些类只能在特定的上下文中实例化,或者某些方法只能接受特定类型的参数。
- 预防业务逻辑错误: 提前发现与业务逻辑相关的类型错误。例如,确保金额计算总是使用
Money对象,而不是浮点数或字符串,避免精度问题。 - 提高代码可读性和可维护性: 通过明确的规则,可以帮助开发者更好地理解代码的意图,减少理解偏差,提高代码的可读性和可维护性。
- 降低运行时错误: 静态分析可以帮助我们在开发阶段发现潜在的运行时错误,减少上线后的 bug 数量。
2. PHPStan 自定义规则的基本结构
一个 PHPStan 自定义规则主要由以下几个部分组成:
- Rule 类: 实现了
PHPStanRulesRule接口,包含getNodeType()和processNode()两个方法。 - getNodeType() 方法: 定义了该规则要处理的 AST 节点类型。例如,
PhpParserNodeExprMethodCall表示方法调用节点。 - processNode() 方法: 实现了具体的规则逻辑,接收一个 AST 节点作为参数,并返回一个包含错误信息的数组。如果没有错误,则返回空数组。
3. 创建第一个自定义规则:禁止直接使用 new DateTime()
假设我们的项目要求所有日期时间都必须通过 Carbon 类来处理,禁止直接使用 new DateTime(),以统一日期时间处理方式。我们可以创建一个自定义规则来实现这个约束。
<?php
namespace MyProjectPHPStanRules;
use PhpParserNode;
use PhpParserNodeExprNew_;
use PHPStanAnalyserScope;
use PHPStanRulesRule;
/**
* 禁止直接使用 new DateTime(),强制使用 Carbon。
*/
class NoDirectDateTimeInstantiationRule implements Rule
{
public function getNodeType(): string
{
return New_::class;
}
public function processNode(Node $node, Scope $scope): array
{
if (!($node instanceof New_)) {
return [];
}
if (!($node->class instanceof NodeName)) {
return [];
}
if ($node->class->toString() === 'DateTime') {
return [
'禁止直接使用 new DateTime(),请使用 Carbon。',
];
}
return [];
}
}
代码解释:
NoDirectDateTimeInstantiationRule类实现了Rule接口。getNodeType()方法返回New_::class,表示该规则要处理new表达式。processNode()方法首先判断节点是否是New_实例,然后判断new的类名是否是DateTime。如果是,则返回一个包含错误信息的数组。
4. 配置 PHPStan 使用自定义规则
我们需要在 phpstan.neon 配置文件中注册自定义规则。
includes:
- vendor/nunomaduro/larastan/extension.neon
parameters:
paths:
- src
rules:
- MyProjectPHPStanRulesNoDirectDateTimeInstantiationRule
代码解释:
includes引入了 Larastan 的扩展,这是一个针对 Laravel 项目的 PHPStan 扩展,包含了许多有用的规则。paths指定了要分析的代码目录。rules列出了要使用的规则,包括我们自定义的NoDirectDateTimeInstantiationRule。
5. 测试自定义规则
我们可以使用 PHPStan 的命令行工具来测试自定义规则。
./vendor/bin/phpstan analyse src
如果 src 目录下有使用 new DateTime() 的代码,PHPStan 就会报告错误。
6. 一个更复杂的例子:确保金额计算使用 Money 对象
假设我们的项目使用 Money 对象来表示金额,以避免浮点数精度问题。我们需要确保所有金额计算都使用 Money 对象,而不是浮点数或字符串。
<?php
namespace MyProjectPHPStanRules;
use PhpParserNode;
use PhpParserNodeExprBinaryOp;
use PhpParserNodeExprMethodCall;
use PhpParserNodeExprPropertyFetch;
use PhpParserNodeExprStaticCall;
use PhpParserNodeName;
use PHPStanAnalyserScope;
use PHPStanRulesRule;
use PHPStanTypeObjectType;
/**
* 确保金额计算使用 Money 对象。
*/
class MoneyCalculationRule implements Rule
{
public function getNodeType(): string
{
return BinaryOp::class; // 监听二元运算符
}
public function processNode(Node $node, Scope $scope): array
{
if (!($node instanceof BinaryOp)) {
return [];
}
$allowedOperators = ['Plus', 'Minus', 'Mul', 'Div'];
// 只关注加减乘除
$operatorClassName = get_class($node);
$operatorName = substr($operatorClassName, strrpos($operatorClassName, '\') + 1);
if (!in_array($operatorName, $allowedOperators, true)) {
return [];
}
$leftType = $scope->getType($node->left);
$rightType = $scope->getType($node->right);
$moneyObjectType = new ObjectType('MoneyMoney');
// 检查左右操作数是否都是 Money 对象,或者其中一个是 Money 对象,另一个是数字
if (
!($moneyObjectType->isSuperTypeOf($leftType)->yes() && $moneyObjectType->isSuperTypeOf($rightType)->yes())
&& !(($moneyObjectType->isSuperTypeOf($leftType)->yes() && $rightType->isInteger()->yes())
|| ($moneyObjectType->isSuperTypeOf($leftType)->yes() && $rightType->isFloat()->yes())
|| ($moneyObjectType->isSuperTypeOf($rightType)->yes() && $leftType->isInteger()->yes())
|| ($moneyObjectType->isSuperTypeOf($rightType)->yes() && $leftType->isFloat()->yes()))
) {
return [
'金额计算必须使用 Money 对象,或者 Money 对象与数字之间的运算。',
];
}
return [];
}
}
代码解释:
MoneyCalculationRule类实现了Rule接口。getNodeType()方法返回BinaryOp::class,表示该规则要处理二元运算符。processNode()方法首先判断节点是否是BinaryOp实例,然后判断运算符是否是加减乘除。$leftType和$rightType分别表示左右操作数的类型。$moneyObjectType表示MoneyMoney对象的类型。- 规则检查左右操作数是否都是 Money 对象,或者其中一个是 Money 对象,另一个是数字。如果不是,则返回一个包含错误信息的数组。
7. 其他自定义规则的示例
- 强制使用依赖注入: 检查类中是否使用了
new关键字来创建对象,如果使用了,则建议使用依赖注入。 - 禁止使用全局变量: 检查代码中是否使用了全局变量,如果使用了,则建议使用依赖注入或配置。
- 强制使用特定的命名约定: 检查类名、方法名、变量名是否符合项目约定的命名规范。
- 限制某些类的使用范围: 检查某些类是否在允许的上下文中使用。
8. 自定义规则的注意事项
- 性能: 自定义规则会影响 PHPStan 的分析速度,因此要尽量避免编写过于复杂的规则。
- 准确性: 自定义规则应该尽可能准确地识别出错误,避免误报。
- 可维护性: 自定义规则应该易于理解和维护,方便后续修改和扩展。
- 测试: 编写自定义规则后,一定要进行充分的测试,确保规则能够正常工作。
- 只处理特定的业务逻辑: 不要试图用自定义规则来替代 PHPStan 内置的规则。自定义规则应该只处理项目特定的业务逻辑。
9. 如何调试自定义规则
调试 PHPStan 自定义规则可能会比较困难,因为错误信息通常比较抽象。以下是一些调试技巧:
- 使用
var_dump()或dump()函数: 在processNode()方法中,可以使用var_dump()或dump()函数来输出 AST 节点的信息,帮助理解节点的结构和内容。 - 使用 PHPStan 的
--debug选项: 使用--debug选项可以输出更详细的调试信息,包括 PHPStan 的类型推断过程。 - 逐步缩小范围: 如果规则无法正常工作,可以逐步缩小规则的范围,例如只处理特定的文件或类,以便更容易定位问题。
- 阅读 PHPStan 的源代码: 如果遇到难以解决的问题,可以阅读 PHPStan 的源代码,了解其内部实现机制。
10. 高级技巧:使用 Reflection API
在某些情况下,我们需要使用 Reflection API 来获取类或方法的更详细的信息。例如,我们可以使用 Reflection API 来判断一个方法是否被标记为 @deprecated。
<?php
namespace MyProjectPHPStanRules;
use PhpParserNode;
use PhpParserNodeStmtClassMethod;
use PHPStanAnalyserScope;
use PHPStanRulesRule;
use ReflectionMethod;
class NoDeprecatedMethodCallRule implements Rule
{
public function getNodeType(): string
{
return ClassMethod::class;
}
public function processNode(Node $node, Scope $scope): array
{
if (!($node instanceof ClassMethod)) {
return [];
}
$methodName = $node->name->toString();
$className = $scope->getClassReflection()->getName();
try {
$reflectionMethod = new ReflectionMethod($className, $methodName);
$docComment = $reflectionMethod->getDocComment();
if ($docComment !== false && strpos($docComment, '@deprecated') !== false) {
return [
sprintf('方法 %s::%s 已被废弃,请使用替代方案。', $className, $methodName),
];
}
} catch (ReflectionException $e) {
// 方法不存在或无法访问,忽略
}
return [];
}
}
代码解释:
NoDeprecatedMethodCallRule类实现了Rule接口。getNodeType()方法返回ClassMethod::class,表示该规则要处理类方法。processNode()方法首先判断节点是否是ClassMethod实例。- 使用
ReflectionMethod获取方法的反射信息。 - 获取方法的文档注释,并判断是否包含
@deprecated标记。
11. 使用第三方扩展
除了编写自定义规则之外,我们还可以使用第三方扩展来扩展 PHPStan 的功能。例如,Larastan 是一个针对 Laravel 项目的 PHPStan 扩展,包含了许多有用的规则,例如模型属性类型检查、路由参数类型检查等。
总结:编写自定义规则,提升代码质量
通过编写 PHPStan 自定义规则,我们可以针对项目特定的业务逻辑进行类型检查,提前发现潜在的错误,提高代码质量和可维护性。自定义规则可以帮助我们强制执行项目规范、预防业务逻辑错误、提高代码可读性、降低运行时错误。在使用自定义规则时,需要注意性能、准确性、可维护性和测试。