PHP静态分析:解锁你的代码超能力,从自定义规则开始!
大家好!我是你们今天的代码向导,今天要带大家深入PHP静态分析的世界,特别是如何利用PHPStan和Psalm编写自定义规则和插件,让你的代码质量更上一层楼。
先跟大家打个招呼,今天的旅程略微烧脑,但只要跟着我的节奏,保证你能收获满满,到时候写出让同事惊艳,让bug绕道的代码!
静态分析:代码侦探的秘密武器
首先,咱们来聊聊啥是静态分析。简单来说,静态分析就像一个代码侦探,它不用真正运行你的代码,就能找出潜在的问题,比如:
- 类型错误: 比如你试图把一个字符串当成数字用。
- 未使用的变量: 你定义了一个变量,结果压根没用上。
- 潜在的NullPointerException (虽然PHP里叫TypeError): 你访问了一个可能为null的变量的属性。
- 代码风格问题: 比如命名不规范,代码过于复杂等等。
与动态分析(运行代码然后测试)相比,静态分析的优势在于:
- 更早发现问题: 避免问题上线才暴露,减少修复成本。
- 覆盖更全面: 静态分析可以检查所有可能的代码路径,而测试通常只能覆盖部分。
- 代码质量提升: 帮助你写出更清晰、更易于维护的代码。
PHP世界里,PHPStan和Psalm是两款非常流行的静态分析工具。它们都非常强大,各有特点,大家可以根据自己的需求选择。
PHPStan:严格的守门员
PHPStan以其严格性著称,它会尽力找出代码中的每一个潜在问题。想象一下,它就像一个非常严格的守门员,不放过任何一个可疑的球。
Psalm:灵活的侦察兵
Psalm则更加灵活,它允许你自定义分析的严格程度,并且提供了更丰富的类型推断功能。它就像一个经验丰富的侦察兵,能够深入代码的每一个角落,发现隐藏的风险。
准备工作:磨刀不误砍柴工
在开始编写自定义规则之前,我们需要先准备好环境。
-
安装PHPStan或Psalm:
使用Composer安装:
composer require --dev phpstan/phpstan # 或者 composer require --dev vimeo/psalm
-
配置:
创建
phpstan.neon
或psalm.xml
文件,配置需要分析的文件和目录。一个简单的
phpstan.neon
示例:parameters: level: 7 # 分析级别,数值越高越严格 paths: - src # 需要分析的目录 - tests # 测试目录 excludePaths: - vendor # 排除vendor目录
一个简单的
psalm.xml
示例:<?xml version="1.0"?> <psalm errorLevel="2" 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="src"/> <directory name="tests"/> <ignoreFiles> <directory name="vendor"/> </ignoreFiles> </projectFiles> </psalm>
-
运行:
在命令行中运行:
./vendor/bin/phpstan analyse src # 或者 ./vendor/bin/psalm
自定义规则:让静态分析更懂你的代码
现在,激动人心的时刻到了!让我们开始编写自定义规则,让静态分析工具更懂你的代码。
场景:禁止使用die()
或exit()
在实际项目中,我们通常不希望直接使用die()
或exit()
,因为它们会立即终止脚本的执行,不利于错误处理和程序控制。所以,我们需要一个规则来禁止使用它们。
PHPStan自定义规则
-
创建规则类:
创建一个类,实现
PHPStanRulesRule
接口。<?php namespace MyRules; use PhpParserNode; use PhpParserNodeExprExit_; use PHPStanAnalyserScope; use PHPStanRulesRule; class NoDieOrExitRule implements Rule { public function getNodeType(): string { return Exit_::class; } public function processNode(Node $node, Scope $scope): array { /** @var Exit_ $node */ return [ '不要使用 die() 或 exit(),请使用异常或其他方式处理。', ]; } }
解释一下:
getNodeType()
:指定这个规则要处理的节点类型,这里是Exit_::class
,表示die()
和exit()
。processNode()
:处理节点的逻辑,如果发现使用了die()
或exit()
,就返回一个错误信息。
-
注册规则:
在
phpstan.neon
文件中注册这个规则。parameters: level: 7 paths: - src rules: - MyRulesNoDieOrExitRule
-
运行PHPStan:
运行
./vendor/bin/phpstan analyse src
,如果你的代码中使用了die()
或exit()
,就会看到错误信息。
Psalm自定义规则
-
创建插件类:
创建一个类,实现
PsalmPluginPluginEntryPointInterface
接口。<?php namespace MyRules; use PsalmPluginPluginEntryPointInterface; use PsalmPluginRegistrationInterface; class MyPsalmPlugin implements PluginEntryPointInterface { public function __invoke(RegistrationInterface $api, ?SimpleXMLElement $config = null): void { $api->registerHooksFromClass(NoDieOrExitAnalyzer::class); } }
-
创建分析器类:
创建一个类,实现
PsalmPluginHookAfterEveryFunctionCallHookInterface
接口。<?php namespace MyRules; use PhpParserNodeExprFuncCall; use PsalmCodebase; use PsalmCodeLocation; use PsalmContext; use PsalmIssuePluginIssue; use PsalmPluginHookAfterEveryFunctionCallHookInterface; use PsalmStatementsSource; use PsalmType; class NoDieOrExitAnalyzer implements AfterEveryFunctionCallHookInterface { /** * @param FuncCall $expr * @param Context $context * @param CodeLocation $code_location * * @return null|false */ public static function afterEveryFunctionCall( FuncCall $expr, string $function_id, array $resolved_call_args, TypeUnion $return_type_candidate, Context $context, StatementsSource $statements_source, Codebase $codebase, array &$file_replacements = [], CodeLocation $code_location = null ) : ?bool { if (in_array($function_id, ['die', 'exit'], true)) { $statements_source->addIssue( new PluginIssue( 'NoDieOrExit', '不要使用 die() 或 exit(),请使用异常或其他方式处理。', $code_location ?? new CodeLocation($statements_source, $expr) ) ); } return null; } }
解释一下:
MyPsalmPlugin
是插件入口,负责注册hook。NoDieOrExitAnalyzer
实现了AfterEveryFunctionCallHookInterface
接口,会在每次函数调用后被调用。afterEveryFunctionCall()
方法会检查调用的函数是否是die()
或exit()
,如果是,就添加一个issue。
-
注册插件:
在
psalm.xml
文件中注册这个插件。<?xml version="1.0"?> <psalm errorLevel="2" 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="src"/> <directory name="tests"/> <ignoreFiles> <directory name="vendor"/> </ignoreFiles> </projectFiles> <plugins> <pluginClass class="MyRulesMyPsalmPlugin"/> </plugins> </psalm>
-
运行Psalm:
运行
./vendor/bin/psalm
,如果你的代码中使用了die()
或exit()
,就会看到错误信息。
自定义规则进阶:更多可能性
上面的例子只是一个简单的入门,自定义规则的可能性是无限的。你可以根据自己的需求,编写更复杂的规则,比如:
- 检查代码复杂度: 避免函数过于冗长,提高代码可读性。
- 强制使用特定的设计模式: 确保代码结构符合规范。
- 防止SQL注入: 检查SQL语句是否经过安全处理。
- 检查API调用是否正确: 确保API调用的参数和返回值符合预期。
一些常用的接口和类
工具 | 接口/类 | 说明 |
---|---|---|
PHPStan | PHPStanRulesRule |
定义一个规则 |
PHPStan | PhpParserNode |
表示代码中的一个节点,比如一个变量、一个表达式、一个函数调用等等 |
PHPStan | PHPStanAnalyserScope |
提供代码的上下文信息,比如当前类、当前方法、变量类型等等 |
Psalm | PsalmPluginPluginEntryPointInterface |
定义一个插件的入口 |
Psalm | PsalmPluginRegistrationInterface |
用于注册hook |
Psalm | PsalmPluginHook* |
定义各种类型的hook,比如在函数调用前后、类定义前后等等 |
Psalm | PsalmCodebase |
提供代码库的信息,比如类、函数、常量等等 |
Psalm | PsalmContext |
提供代码的上下文信息,比如当前类、当前方法、变量类型等等 |
Psalm | PsalmIssuePluginIssue |
用于报告一个插件发现的问题 |
测试你的规则:确保万无一失
编写完自定义规则后,一定要进行测试,确保它能够正确地工作。
-
创建测试用例:
编写一些包含符合规则和违反规则的代码的测试用例。
-
运行测试:
使用PHPUnit或其他测试框架运行测试用例,检查规则是否能够正确地识别出问题。
分享你的规则:让更多人受益
如果你编写了一些非常有用的自定义规则,不妨分享出来,让更多人受益。
-
创建开源项目:
将你的规则发布到GitHub或其他代码托管平台上。
-
编写文档:
提供清晰的文档,说明如何安装和使用你的规则。
-
积极维护:
及时修复bug,并根据用户的反馈进行改进。
总结:代码质量的守护者
通过自定义规则和插件,我们可以让PHPStan和Psalm更好地理解我们的代码,从而提高代码质量,减少bug,让我们的项目更加健壮。
希望今天的讲座能够帮助大家解锁代码超能力,成为代码质量的守护者!下次再见!