image
PHPStan:无需写测试就能找到代码中的 Bug
每当我看到开发人员从 Java 或 C# 等编译语言切换到 PHP 这样的解释语言时解放了生产力后感到很高兴。除了这些常规的执行模型(发起、处理请求和结束请求)和更短的反馈环(无需等待编译器)外,还有一个能解决开发人员日常问题的开源框架生态系统,因此,PHP 是目前来说 web 开发中最流行的语言。
但它有一个缺点。
你会在什么时候发现错误?
编译型语言需要在程序运行之前了解每个变量的类型,每个方法的返回类型。这就是为什么编译器需要确保程序是没有错误的,并且会在源码中向你指出这些类型的错误,比如调用了未定义的方法或者是向某个函数传递了错误数量的参数。在把应用程序部署到生产环境前,编译器算是第一道防线。
然而 PHP 就不会这样了。如果程序出错,会执行到错误的代码的时候崩溃。在测试 PHP 应用时,不管是自动化测试还是手动测试,开发人员都会花费大量时间去查一些其它编译型语言不会犯的错从而减少测试实际业务逻辑的时间。
我想改变这一点。
欢迎来到 PHPStan 的世界
现阶段 PHP 实践所产生的代码库中,我们可以确定大部分数据的类型,并且转换为静态类型的语言,尽管还保留着一些动态语言的特性。人们把现在的 PHP 代码库变得跟其他语言一样更加有趣。面向对象,依赖注入以及设计模式的使用已经变得非常普遍。
这让我想到了 PHP 的 静态分析工具,它将替代其他语言的编译器角色。我花了很多时间研究它,并且已经使用它的各种开发版本来检查我们的代码库超过一年。
它就是 PHPStan, 开源且免费
它目前校验什么?
有关类中涉及的,对象实例, 错误/异常捕获,类型约束以及其他语言结构的存在性。 PHP 照旧不会检查这些, 但是会展现其中未被使用的代码。
被调用的方法和函数的存在性和可访问性。同样也会检查他们的参数个数。
方法是否返回了它声明的返回值类型。
被访问成员变量的存在性和可见性。它也可指出是否将一个其他的类型的值赋给了既定类型的成员变量。
sprintf/printf 函数基于格式化字符串所应接收的参数个数。
分支和循环范围中的变量的存在性。
无用的形式指定。例如 (string) 'foo' ,以及不同类型变量间的严格比较 (=== 和 !==),因为他们的结果总为 false。
这个清单的内容随着每次发布都在递增。但成就 PHPStan 也不会只仰赖此一技之微。
PHPStan 迅疾如飞...
它设法一次性检查整个代码库。 它无需多次遍历代码。 只需浏览您想要分析的代码,例如 你写的代码。它无需解析和分析第三方依赖项。 相反,它使用反射来找出有关你代码库中引用的他人代码的有用信息。
PHPStan 能在一分钟里检查我们的代码库 (6000 个文件, 600k LOCs) 。它可在一秒内完成自查。
...可扩展性
即便当前正在使用静态类型,开发者也可以合法的使用 PHP 的动态语法特性,例如 **get, **set 和 __call 这些魔术方法。它们可以在运行时去定义新属性和方法。通常,静态分析都会爆出属性和方法未定义,但是有一种机制可以告诉引擎如何创建新的属性和方法。
它得益于对允许用户扩展的原生 PHP 反射的自定义抽象。更多细节可查看 README 中类反射扩展章节。
某些方法返回的类型取决于它的参数。它可以取决于你传递给它的类名,也可能返回与传递的对象相同的类的对象。这就是 动态返回类型扩展 的用途。
压轴语: 如果你想自己出一个 PHPStan 的新的检查项, 你可以自力更生。可以提出基于特定框架的规则,例如检查 DQL查询中引用的实体和字段是否存在,或者你选择的 MVC 框架中生成的链接是否和现存的控制器有关。
选择规范级别
我使用过其他工具,并将之集成进现有的代码库中,这种体验真是往事不堪回首。他们爆出成千上万的错误让你没法使用。
取而代之,我回顾如何集成 PHPStan 到刚进入开发阶段的代码库中。 首个版本的功能不是很强大,这时并未发现多少错误。但从集成的角度来看,它还是非常不错的 --- 有空时,我就为它增加新规则,我修复了它在版本库中找到的错误,并将新代码合并到主分支。我们会使用新版本几周用来发现其找到的错误,并不断重复这件事。这种逐级增加的规范性的做法在实践中看来大有裨益,所以我使用 PHPStan 的现有功能来模拟它。
默认情况下,PHPStan 只检查它确定的代码---常量,实例化,调用$ this的方法,静态调用的方法,函数和各种语言结构中的现有类。 通过增加级别(从默认值0到当前值4),您还可以增加它对代码所做的假设数量以及它检查的规则数量。
如果内建级别无法满足你的要求,你同样也可以自定义规则。
少写单元测试! (披沙拣金)
可能这个建议你闻所未闻。即便是非常细碎的代码,开发者也不得不编写单元测试,因为这方面犯错的几率都是均等的,例如简单的拼写错误或者忘记将结果赋值给变量。为那些经常出现在控制器或者门脸中的转发代码编写单元测试是很不划算的事。
单元测试也有其成本。它们同样也是代码,难逃编写和维护的窠臼。最理想的做法就是在持续集成服务器上,每次更改时都运行 PHPStan,从而在无需单元测试的情况下防止此类错误的产生。实现100%的代码覆盖率真的很难,并且非常昂贵,但你可以静态分析100%的代码。
至于单元测试的重点应当集中在静态分析代码难以察觉的,容易出错的地方。包括:复杂的数据过滤,循环,条件判断,乘除法包含舍入的计算等。
站在巨人的肩膀上
如果不是 Nikita Popov 创建了 PHP Parser。就不会有 PHPStan 的出现。
PHP 在 2016 年开始广泛使用 包管理, 单元测试 和 编码标准 的工具。然而到现在也没有一个广泛使用的工具,可以在不运行代码的情况下检查代码中的错误。所以我创建了一个易于使用,快速,可扩展的版本,既不会对您的代码有严格的要求,你还会从这些检查中受益。查看 GitHub 仓库 ,了解如何将其集成到您的项目中!