- 开发工具
【免费下载链接】php-timer
Utility class for timing
phpstan-strict-rules 是 PHPStan 官方出品的"额外严格且有主见"的规则扩展包,它把 PHP 语言中大量"宽松写法"(松散比较、隐式类型转换、动态调用等)变成静态分析错误,是追求防御式编程与强类型安全的 PHP 项目的标配。本文以 php-timer(phpunit/php-timer)仓库中真实使用的 PHPStan 配置与规则实现源码为佐证,完整讲解该扩展的全部 20 余条规则、安装方式、规则开关的精细控制,以及它在 php-timer 中的实际落地配置,读完即可在自己的项目中直接套用。
phpstan-strict-rules 是什么
PHPStan 的核心目标是"在代码中找出 bug"。但 PHP 本身是一门非常宽松的语言——同样的写法可以有多种表达,其中不少会埋下类型相关的隐患。phpstan/phpstan-strict-rules这个仓库提供了一组额外的、围绕"严格且强类型、不进行松散转换"理念设计的自定义规则,面向那些希望在"极度防御式编程"中获取额外安全感的开发者。
在 php-timer 仓库中,这套扩展被用作开发期静态分析工具链的一部分:仓库根目录的 phpstan.neon 以level: 10的极高级别分析src/与tests/,并同时启用了 tools/.phpstan/composer.json 中声明的phpstan/phpstan-strict-rules(版本约束^2.0.12)与ergebnis/phpstan-rules,两条严格规则扩展共同把关。这也从侧面说明:即使像 php-timer 这样只有 4 个源码文件的小型库,作者依然用严格规则来约束自己。
规则总览:22 组检查项速查表
phpstan-strict-rules 提供的所有规则(含其修改的 PHPStan 默认参数)如下表所示。表中的配置参数名即 rules.neon 中strictRules命名空间下的开关键:
| 配置参数 | 规则说明 |
|---|---|
booleansInConditions | 要求if、elseif、三元运算符、!之后以及&&、\|\|两侧的表达式必须是布尔类型 |
booleansInLoopConditions | 要求while与do while循环条件必须是布尔类型 |
uselessCast | 检测无意义的类型转换——例如表达式本身已经是int却仍强转(int) |
numericOperandsInArithmeticOperators | 要求+、-、*、/、%、**、+$var、-$var、$var++、$var--、++$var、--$var的操作数必须是数值类型 |
strictFunctionCalls | 以下内置函数带有$strict参数以保证类型安全,必须将其设为true:• in_array(第 3 个参数)• array_search(第 3 个参数)• array_keys(第 3 个参数,且仅在提供了第 2 个参数$search_value时检查)• base64_decode(第 2 个参数) |
overwriteVariablesWithLoop | • 禁止用foreach的键变量、值变量覆盖已存在的变量• 禁止用 for循环初始化赋值覆盖已有变量 |
switchConditionsMatchingType | switch条件与case值的类型必须匹配(PHP 默认按松散规则比较,容易产生意外结果) |
dynamicCallOnStaticMethod | 检查静态声明的方法必须以静态方式调用 |
disallowedEmpty | 禁止使用empty()——它是非常松散的比较(PHP 手册也如此说明),建议改用更严格的判断 |
disallowedLooseComparison | 禁止通过==与!=做松散比较 |
disallowedShortTernary | 禁止短三元运算符?:——它隐含弱比较语义,建议改用空合并运算符??或带严格条件的长三元 |
noVariableVariables | 禁止可变变量($$foo、$this->$method()等) |
checkAlwaysTrueInstanceof、checkAlwaysTrueCheckTypeFunctionCall、checkAlwaysTrueStrictComparison | 检测恒为true的instanceof、类型检查函数is_*以及严格比较===/!==。这三项可通过同名参数设为false关闭 |
| (无独立开关) | 被引用与调用的函数名必须使用正确的大小写 |
matchingInheritedMethodNames | 继承与实现的方法名必须使用正确的大小写 |
| (无独立开关) | 继承方法中参数类型应满足逆变(contravariance)、返回类型应满足协变(covariance),即里氏替换原则(LSP) |
| (无独立开关) | 即使静态方法也要检查 LSP |
requireParentConstructorCall | 要求子类构造函数调用父类构造函数 |
disallowedBacktick | 禁止反引号运算符($ls = `ls -la`) |
disallowedImplicitArrayCreation | 当变量尚不存在时,禁止通过$var[] =隐式创建数组 |
strictArrayFilter | 要求array_filter()必须传入回调参数,以避免其默认的松散比较语义 |
illegalConstructorMethodCall | 禁止对已存在的对象调用__construct(),也禁止在父类构造函数之外以静态方式调用它 |
closureUsesThis | 闭包应当直接使用$this,而不是通过use引入的变量间接使用$this |
值得说明的是,上表中"无独立开关"的几项(函数名大小写、LSP 检查)在 rules.neon 的strictRules参数体系中并没有对应的布尔开关,它们统一由strictRules.allRules控制,allRules: false时这些规则会一并停用。而checkAlwaysTrueInstanceof等三个参数虽然出现在本扩展的参数体系里,其实修改的是 PHPStan 自身已有的配置项默认值,这一细节在"与 PHPStan 内置参数的联动"一节展开。
安装与启用
在项目根目录执行以下 Composer 命令即可将本扩展加入开发依赖:
composer require --dev phpstan/phpstan-strict-rules如果你同时安装了phpstan/extension-installer,那么扩展会被自动注册,无需任何额外配置。php-timer 仓库正是采用这条路径:tools/.phpstan/composer.json 中声明了"phpstan/extension-installer": "^1.4.3"与"phpstan/phpstan-strict-rules": "^2.0.12",并在config.allow-plugins中放行phpstan/extension-installer,使得后续的 PHPStan 分析直接加载了 strict-rules 提供的规则。
手动安装(不使用 extension-installer)
如果你不希望依赖phpstan/extension-installer,可以在项目的 PHPStan 配置文件中手动 include 扩展自带的rules.neon:
includes: - vendor/phpstan/phpstan-strict-rules/rules.neon对应到本仓库,这个文件位于 tools/.phpstan/vendor/phpstan/phpstan-strict-rules/rules.neon。当项目通过 Composer 安装依赖后,vendor目录下会出现该文件,include 路径即以此为准。
与 PHPStan 内置参数的联动:rules.neon 做了什么
除了引入一批新的自定义规则,phpstan-strict-rules 还会修改 PHPStan 自身某些配置参数的默认值。打开 rules.neon 前 16 行可以看到这套"改默认值"的机制:
parameters: strictRulesInstalled: true polluteScopeWithLoopInitialAssignments: false polluteScopeWithAlwaysIterableForeach: false polluteScopeWithBlock: false checkDynamicProperties: true checkExplicitMixedMissingReturn: true checkFunctionNameCase: true checkInternalClassCaseSensitivity: true reportMaybesInMethodSignatures: true reportStaticMethodSignatures: true reportMaybesInPropertyPhpDocTypes: true reportWrongPhpDocTypeInVarTag: true checkStrictPrintfPlaceholderTypes: true reportNonIntStringArrayKey: %featureToggles.bleedingEdge%这些参数在 PHPStan 官方文档的 "stricter-analysis"(更严格的分析)一节有完整说明。要点如下:
polluteScopeWithLoopInitialAssignments: false意味着不再把for循环初始化表达式中的赋值污染到循环后的作用域,这正对应overwriteVariablesWithLoop规则的底层语义;polluteScopeWithAlwaysIterableForeach: false使foreach不再向作用域"注入"必然遍历的键/值变量,配合overwriteVariablesWithLoop使用;checkDynamicProperties: true、checkExplicitMixedMissingReturn: true等则让 PHPStan 本体层面收紧动态属性、显式mixed返回类型等检查。
关键在于:strictRulesInstalled: true与这些参数由rules.neon无条件写入,因此即使你把strictRules.allRules设为false,上述"改默认值"的部分依然生效——这正是原 README 反复强调"禁用全部规则后本包仍有部分作用"的原因。
精细控制:禁用指定规则
并非所有团队都愿意一步到位接受全部规则。你可以在parameters下用strictRules命名空间逐个关闭规则,例如:
parameters: strictRules: disallowedLooseComparison: false booleansInConditions: false booleansInLoopConditions: false uselessCast: false requireParentConstructorCall: false disallowedBacktick: false disallowedEmpty: false disallowedImplicitArrayCreation: false disallowedShortTernary: false overwriteVariablesWithLoop: false closureUsesThis: false matchingInheritedMethodNames: false numericOperandsInArithmeticOperators: false strictFunctionCalls: false dynamicCallOnStaticMethod: false switchConditionsMatchingType: false noVariableVariables: false strictArrayFilter: false illegalConstructorMethodCall: false注意checkAlwaysTrueInstanceof、checkAlwaysTrueCheckTypeFunctionCall、checkAlwaysTrueStrictComparison这三个开关不属于strictRules命名空间,它们是 PHPStan 顶层参数,需要单独设置false来关闭(例如parameters: checkAlwaysTrueInstanceof: false),这是原 README 中明确区分的两套开关体系。
strictRules下每个开关的合法值可以是布尔值,也可以是布尔值数组(见 rules.neon 中的parametersSchema:anyOf(bool(), arrayOf(bool()))),后者用于按路径/文件精细放行。
按需开启:一次只启用一两条规则
如果不想一次性接受全部严格规则、只想先上其中一两条,可以先用allRules: false关闭rules.neon引入的所有自定义规则,再单独开启需要的规则:
parameters: strictRules: allRules: false然后重新启用某条规则:
parameters: strictRules: allRules: false booleansInConditions: true这种"默认全关 + 显式白名单"的模式非常适合在存量项目上渐进式推行严格检查:先把影响面最小的规则(如disallowedLooseComparison、strictFunctionCalls)打开,修复完一批告警再逐步扩大。
规则如何生效:conditionalTags 注册机制剖析
从源码层面看,rules.neon 中每一条规则都不是"写死在扩展里",而是通过 PHPStan 的conditionalTags机制按开关值动态决定是否注册。例如:
conditionalTags: PHPStan\Rules\DisallowedConstructs\DisallowedLooseComparisonRule: phpstan.rules.rule: %strictRules.disallowedLooseComparison% PHPStan\Rules\BooleansInConditions\BooleanInIfConditionRule: phpstan.rules.rule: %strictRules.booleansInConditions%也就是说,strictRules.xxx: false的本质是把对应规则类的phpstan.rules.rule标签置为假,PHPStan 在加载时就不会把这个规则加入分析管线。这也解释了为什么每个规则类的开关可以独立配置——规则与开关在 neon 配置层面是一一映射的。
再往下看services段(rules.neon),可以发现若干规则还注入了额外的依赖参数:UselessCastRule注入了treatPhpDocTypesAsCertain(决定是否信任 PHPDoc 类型推断,与 PHPStan 顶层同名参数联动);ArrayFilterStrictRule注入了treatPhpDocTypesAsCertain、checkNullables等。这意味着这些规则的行为会随 PHPStan 全局参数微调,而不仅仅是"报不报错"二选一。
深入规则实现:以几条代表规则为例
严格规则的实际判定逻辑全部位于 tools/.phpstan/vendor/phpstan/phpstan-strict-rules/src/Rules 目录,按语义分为BooleansInConditions、Cast、DisallowedConstructs、Operators、StrictCalls、VariableVariables等子目录。下面结合源码看几条典型规则的判定原理。
disallowedEmpty:无条件报错
DisallowedEmptyRule.php 的processNode()只要命中empty()语法节点(Empty_)就直接返回错误:
Construct empty() is not allowed. Use more strict comparison.
它不分析类型、不区分场景,属于"语言层面的武断禁令"——因为empty()会把0、""、null、false、空数组等一视同仁,语义过于含糊。
disallowedLooseComparison:拦截==/!=
DisallowedLooseComparisonRule.php 以BinaryOp(二元运算节点)为检查对象,仅当节点是Equal(==)或NotEqual(!=)时报错,并提示改用===/!==。在启用 bleedingEdge 特性时,错误信息还会附带两侧操作数的具体类型(如Loose comparison via "==" between int and string is not allowed.),帮助开发者理解为什么会命中。
booleansInConditions:条件必须是布尔
BooleanInIfConditionRule.php 借助BooleanRuleHelper判断if条件是否"可以当作布尔通过"(如本身就是bool、!取反、&&/||组合等),否则报告:
Only booleans are allowed in an if condition, %s given.
同目录下还细分了BooleanInBooleanAndRule、BooleanInElseIfConditionRule、BooleanInTernaryOperatorRule、BooleanInWhileConditionRule、BooleanInDoWhileConditionRule等 8 个规则类,分别对应if/elseif/三元/while/do while及&&、||、!的各个位置,全部挂在booleansInConditions(循环类挂在booleansInLoopConditions)开关下。
numericOperandsInArithmeticOperators:算术操作数必须为数值
以 OperandsInArithmeticAdditionRule.php 为例:它同时处理+(BinaryOpPlus)与+=(AssignOpPlus),通过OperatorRuleHelper校验左右操作数是否满足算术运算要求。一个贴心细节是:如果两侧都是数组(count($leftType->getArrays()) > 0 && count($rightType->getArrays()) > 0),规则会放行——因为数组相加在 PHP 中是合法且有意义的操作。同目录下共有 12 个规则类,覆盖+ - * / % **、一元正负号与自增自减的全部变体。
strictFunctionCalls:强制$strict参数
StrictFunctionCallsRule.php 内部维护了一张函数参数位置表:
private array $functionArguments = [ 'in_array' => 2, 'array_search' => 2, 'base64_decode' => 1, 'array_keys' => 2, ];对应"第 3 个参数必须为true"(索引 2)或"第 2 个参数必须为true"(索引 1)。规则首先用ArgumentsNormalizer把命名参数规整为位置参数,再通过ConstantBooleanType(true)做isSuperTypeOf判断——只有确定恒等于true才算通过。array_keys还有个特例:仅当第 2 个参数(搜索值)也提供了时才检查第 3 个参数,因为此时$strict才有意义。
requireParentConstructorCall:强制调用父构造函数
RequireParentConstructCallRule.php 是一个颇有深度的规则:它先沿继承链向上查找"真正声明了构造函数的父类"(跳过抽象、私有、已废弃的构造函数),若存在这样的父类而子类构造函数体中没有parent::__construct(...)调用,则报错。它甚至能透过@错误抑制符(ErrorSuppress)递归查找parent::__construct的调用痕迹,并明确排除 trait、接口、匿名类等不需要检查的场景。
strictArrayFilter:array_filter 必须有回调
ArrayFilterStrictRule.php 处理两种典型情况:一是只传一个参数(数组)时,若元素类型不是明确的布尔/数组/同时含真值假值的联合类型,则要求补充第 2 个回调参数;二是第 2 个参数为null时直接报错——因为array_filter($arr, null)等价于默认的松散真值过滤。规则还受checkNullables参数影响:当回调类型"可能为 null"且checkNullables开启时也会报错。
其余规则的实现要点
- DisallowedShortTernaryRule.php:只有
?:(Ternary节点且if === null)才拦截,长三元不受影响; - DisallowedImplicitArrayCreationRule.php:剥离嵌套的
ArrayDimFetch后检查最底层变量是否"必然不存在"或"可能不存在",命中即报Implicit array creation is not allowed; - MatchingTypeInSwitchCaseConditionRule.php:逐一比对
switch条件类型与每个case值类型,只有isSuperTypeOf(...)->no()即确定不匹配时才报错; - VariableVariablesRule.php:
Variable节点名不是字符串(即$$foo这类动态变量名)即报错;同目录下还有VariablePropertyFetchRule、VariableMethodCallRule等 7 个规则类,覆盖可变属性、可变方法调用、可变静态属性/方法等全部变体; - WrongCaseOfInheritedMethodRule.php:继承/实现方法时,若子类方法名与父类或接口中的原生方法名大小写不一致则报错(PHP 方法名本不区分大小写,但保持一致可避免误用与维护混乱);
- DynamicCallOnStaticMethodsRule.php:通过对象实例
$obj->staticMethod()调用静态方法时报错(并放行 PHPStan 测试框架内的两个特殊类); - DisallowedBacktickRule.php:命中反引号执行运算符(
ShellExec节点)即报错,提示改用shell_exec(); - OverwriteVariablesWithForeachRule.php:
foreach的键/值变量若在作用域中已存在即报Foreach overwrites $x with its key/value variable.,并递归检查解构(list/数组解包)中的每个目标变量; - ClosureUsesThisRule.php:闭包
use列表中若有变量类型是ThisType(即use ($this)的间接写法),提示直接在函数体内使用$this; - IllegalConstructorMethodCallRule.php:对已有对象调用
$obj->__construct()即报错,另有IllegalConstructorStaticCallRule处理静态调用场景。
php-timer 中的真实落地配置
回到本文主角仓库,php-timer 根目录的 phpstan.neon 展示了"全关再逐个开"策略的完整形态:
parameters: level: 10 paths: - src - tests checkTooWideReturnTypesInProtectedAndPublicMethods: true reportAlwaysTrueInLastCondition: true reportPossiblyNonexistentConstantArrayOffset: true reportPossiblyNonexistentGeneralArrayOffset: true treatPhpDocTypesAsCertain: false strictRules: allRules: false booleansInConditions: true closureUsesThis: true disallowedBacktick: true disallowedEmpty: true disallowedImplicitArrayCreation: true disallowedLooseComparison: true disallowedShortTernary: true illegalConstructorMethodCall: true matchingInheritedMethodNames: true noVariableVariables: true numericOperandsInArithmeticOperators: true overwriteVariablesWithLoop: true requireParentConstructorCall: true strictArrayFilter: true strictFunctionCalls: true switchConditionsMatchingType: true uselessCast: true includes: - phar://phpstan.phar/conf/bleedingEdge.neon这份配置有几个值得学习的细节:
allRules: false后显式开启了 17 条规则,唯独没有开启booleansInLoopConditions(该规则同时要求 PHPStan 的 bleedingEdge 特性,见 rules.neon 中[%strictRules.allRules%, %featureToggles.bleedingEdge%]的二元组合写法);- 结合了 PHPStan 的 bleedingEdge 配置(
phar://phpstan.phar/conf/bleedingEdge.neon),让reportNonIntStringArrayKey等前沿特性一并生效; - 同时启用了
ergebnis/phpstan-rules(allRules: false后仅开final与privateInFinalClass两类),把类设计的强制性检查也纳入进来; treatPhpDocTypesAsCertain: false会直接影响UselessCastRule与ArrayFilterStrictRule的判定——它们优先信任原生类型而不是 PHPDoc 声明。
如果你在分析时发现某条规则误报,优先检查是否由"PHPDoc 类型被当成确定类型"导致,可以通过调整treatPhpDocTypesAsCertain或为规则单独关闭开关来解决。
渐进式落地的实践建议
- 从
strictFunctionCalls与disallowedLooseComparison起步:这两类问题语义清晰、修复成本低(补true参数、==改===),且收益立竿见影; - 使用
allRules: false做白名单:在存量项目上先全关,再按周/迭代逐个开启,配合 CI 中的phpstan analyse --error-format=github让告警直接出现在 PR 检查里; - 注意"改默认值"的隐性生效:即便
allRules: false,rules.neon对 PHPStan 顶层参数的修改依然生效(checkDynamicProperties: true、polluteScopeWith*: false等),开启前请先在项目上跑一遍基线(phpstan analyse --generate-baseline)以评估影响面; - 结合 LSP 与大小写规则关注继承设计:
matchingInheritedMethodNames和 LSP 检查(参数逆变、返回协变)能提前暴露继承体系中的隐患,配合requireParentConstructorCall能显著减少构造函数遗漏初始化的问题。
小结
phpstan-strict-rules 通过 20 余条"有主见"的自定义规则 + 对 PHPStan 内置参数的默认值收紧,把 PHP 的宽松语法逐一钉死在静态分析阶段。本文既覆盖了原 README 的完整规则清单、安装(含 extension-installer 与手动 include 两条路径)、禁用与逐个开启的配置语法,也从 rules.neon 与 src/Rules 的源码出发解释了每类规则的判定逻辑,并以 php-timer 仓库根目录的 phpstan.neon 作为真实落地样板。无论你的项目是新工程还是存量代码,都可以参照这份"全关 + 白名单渐进开启"的配置模式,把严格类型检查平稳地引入团队实践。
- 开发工具
【免费下载链接】php-timer
Utility class for timing
相关推荐
GraphQL-WS多平台适配指南:Express、Fastify、uWebSockets对比
GraphQL WS多平台适配指南:Express、Fastify、uWebSockets对比 GraphQL WS是一个零依赖、简单且符合GraphQL ov
GraphQL-WS服务器配置:完整参数详解与最佳实践
GraphQL WS服务器配置:完整参数详解与最佳实践 GraphQL WS是一个零依赖、符合GraphQL over WebSocket协议的服务器和客户端实
PHPStan Strict Rules 严格规则包全解析:从安装配置到 20+ 组规则的源码级原理
PHPStan Strict Rules 严格规则包全解析:从安装配置到 20+ 组规则的源码级原理 phpstan strict rules 是 PHPSta
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考