automattic / phpcs-cobalt-standard
一组适用于现代PHP开发的phpcs嗅探器。
Requires (Dev)
This package is not auto-updated.
Last update: 2024-09-15 05:23:31 UTC
README
这是一组现代(PHP >7)的代码检查指南,旨在与WordPress编码标准一起应用于WordPress开发。由于使用了较新的PHP版本,因此不适用于WordPress核心工作,但对于不受PHP 5.2约束的开发者可能很有用。
这些指南主要是在Automattic内部的团队中开发的,任何人都可以自由使用它们、提出修改意见或报告错误。
此项目是一个phpcs "标准"(一组规则或"嗅探器"),可以包含在任何项目中。
安装
要在使用composer设置的项目中使用这些规则,我们建议使用phpcodesniffer-composer-installer库,该库将在运行phpcs时自动使用当前项目中所有安装的标准,使用composer类型phpcodesniffer-standard
。
composer require --dev squizlabs/php_codesniffer dealerdirect/phpcodesniffer-composer-installer
composer require --dev automattic/phpcs-cobalt-standard
如果您需要此标准、WordPress标准、VariableAnalysis标准和其他自定义,您可以安装元标准CobaltRuleset。
composer require --dev squizlabs/php_codesniffer dealerdirect/phpcodesniffer-composer-installer
composer require --dev automattic/phpcs-cobalt-ruleset
配置
在项目中安装嗅探器标准时,您需要编辑包含在ruleset
标签内的rule
标签的phpcs.xml
文件。该标签的ref
属性应指定一个标准、类别、嗅探器或错误代码以启用。您也可以使用这些标签禁用或修改某些规则。官方注释文件说明了如何操作。
<?xml version="1.0"?> <ruleset name="MyStandard"> <description>My library.</description> <rule ref="CobaltStandard"/> </ruleset>
使用
大多数编辑器都提供phpcs插件,但您也可以手动运行phpcs。要在项目中运行phpcs的文件,只需使用以下命令行(-s
会在命令行中显示嗅探器代码,这对于了解错误非常重要)。
vendor/bin/phpcs -s src/MyProject/MyClass.php
指南
本文档中的关键词"必须"、"禁止"、"应该"、"不应该"和"可能"应按照RFC 2119中的描述进行解释。
严格类型
新PHP文件必须包含严格类型指令。
指令如下所示:declare(strict_types=1);
这防止在验证类型时进行自动标量类型转换(所谓的"弱类型")。您可以在这里了解此指令。
提取
新代码不得使用extract函数。
使用extract()
声明变量时没有声明语句,这会隐藏变量定义的位置。
魔术方法
新代码不得使用魔术方法__get、__set或__serialize。
新代码不推荐使用魔术方法__invoke、__call或__callStatic。
魔术方法可能会隐藏行为,这可能会误导未来的开发者。传统的getter和setter通常更容易理解。
我们允许使用"callable"魔术方法的合法用例,这可以提高可读性而不增加复杂性。这是主观的,必须根据每个案例进行考虑。
全局函数
新代码必须不能向全局命名空间添加函数。
全局函数可能会引起命名空间冲突,因此需要在函数名本身中进行显式命名空间限定。由于语言支持命名空间来组织代码,我们可以利用它们来提高可读性并降低风险。
全局函数剩余的优点是可以在不支持命名空间的PHP版本中广泛移植代码。这不应该成为新代码的问题。
静态方法
新代码必须不能引入非纯静态方法。为此,纯方法总是对相同的参数返回相同的结果,没有副作用。
副作用包括任何数据库访问(即使是读取访问)、网络请求、控制台输出、写入文件、更改全局变量、发送电子邮件、或写入IRC或Slack。
静态方法通常直接调用,如MyClass::myMethod()
,这实际上是一个带有命名空间的全局函数调用。这意味着调用静态函数的代码对静态函数及其类有一个隐含的、紧密耦合的依赖关系。它是隐含的,因为对于使用代码的开发者来说可能并不明显,并且它是紧密耦合的,因为它不能被模拟。
例如,如果一个静态方法向Slack发送消息或进行数据库更新,对调用代码的测试没有直接机制来防止发送Slack消息或数据库调用。
如果静态方法是纯的,同样的依赖关系存在,但它变成了与调用代码中的数据相关的实现细节,降低了风险。
命名空间函数
新代码必须不能在类外引入非纯命名空间函数。为此,纯方法总是对相同的参数返回相同的结果,没有副作用。
尽管命名空间函数极大地降低了冲突的风险,但它们仍然是一种全局函数调用。因此,使用它们承担的风险与使用全局函数或静态方法相同。请参阅静态方法部分,了解为什么这会创建不必要的风险。
具有副作用的函数
新代码必须不能在特定目的是产生副作用的类中,调用非纯函数(不是对象的方法)。为此,纯方法总是对相同的参数返回相同的结果,没有副作用。
副作用包括任何数据库访问(即使是读取访问)、网络请求、控制台输出、写入文件、更改全局变量、发送电子邮件、或写入IRC或Slack。
更具体地说,函数不应该直接调用类似update_database()
的调用。相反,它应该调用$this->database->update_database()
。在这个例子中,$this->database
应该是仅用于更新数据库的类的实例。$this->database
应该被注入到使用它的类或函数中。
调用具有副作用的函数会创建对该函数的隐含和紧密耦合的依赖关系。它是隐含的,因为对于使用代码的开发者来说可能并不明显,并且它是紧密耦合的,因为它不能被模拟。
例如,如果一个函数向Slack发送消息或进行数据库更新,对调用代码的测试没有直接机制来防止发送Slack消息或数据库调用。
另一方面,如果正在测试的代码明确位于一个特定目的是发送消息或更新数据库的类中,风险会降低。在这种情况下,可以采取特殊措施来模拟副作用,或者可以假定它们是正确的。这样的类应该尽可能少地包含业务逻辑,以减少自动测试的需要。
如果静态方法是纯的,同样的依赖关系存在,但它变成了与调用代码中的数据相关的实现细节,降低了风险。
全局变量
新代码必须不能在特定目的是产生副作用的类中使用全局变量。
全局变量会在任意数据上创建隐含的、紧密耦合的依赖关系。这些依赖关系难以模拟,并且可能对使用代码的人来说并不明显。
另一方面,如果正在测试的代码明确位于一个唯一目的是使用全局变量的类中,风险会降低。在这种情况下,可以采取特殊措施来模拟副作用,或者假设它们是正确的。这样的类应尽可能少地包含业务逻辑,以减少自动测试的需求。
显式类型
新代码在可能的情况下应使用参数和返回类型。
强类型允许编译器在执行PHP代码时发现错误,通过使使用明确来防止微小的错误。它还允许编译器优化函数调用的执行。最后,它通过使代码库中不同部分之间传递的数据的隐含假设明确化,有助于函数设计。这可以帮助未来的开发者(或我们未来的自己)避免误解这些假设。
这由可能返回多个值的返回类型复杂化。例如,一些函数可以返回 WP_Error
或 string
。
这可以通过使用包装类来解决,但代价是使用值时增加的复杂性。一种可能的实现方式是 Maybe
如这篇博客中描述的那样(在函数式编程中,这个概念更常被称为 Result
[1])。
常量
新代码不得使用define关键字。
通常使用define关键字实际上只是全局变量的一种方式,但更糟糕的是,它们不能通过特殊技巧来更改以进行测试。任何依赖于这些常量的条件都无法进行测试。
替换容易拼错或混淆的字符串、魔法数字等是常量的有效用途。在类中使用const关键字定义这些是一个很好的方法,以确保它们是隔离和命名的。使用define创建全局常量,这在PHP代码中当然很有用,但很少需要创建新的常量。
当尝试测试依赖于这些常量的代码流时,问题就会出现。例如
function doSomething() { if (FOOBAR) { doX(); } else { doY(); } }
在这种情况下,很难为doSomething()
编写测试,因为使用了隐含的全局变量FOOBAR
。当使用某些测试架构(如phpunit)时,甚至在不同测试之间也无法更改常量(因为它们在同一个PHP进程中运行,而常量,按定义,不能更改)。
函数命名
新函数必须以动词开头。
新函数应尽可能明确地描述函数的目的、参数和返回值。
根据需要以get...、is...、does...、update...等开始所有函数。理想情况下,函数名将解释它需要什么参数以及它做什么或它返回什么。这并不总是可能的,但如果发现很难制定合适的名称,那么这可能是一个迹象,表明该函数做得太多,应该拆分。
例如,考虑一个处理WordPress短代码(add_shortcode()
函数的第二个参数)的函数。假设短代码是foobar
,那么我们应该如何命名处理函数呢?
我们可能将其命名为foobar_shortcode
,但这并没有真正说明它做了什么。
process_foobar_shortcode
更好,但仍然模糊地说明了函数返回什么。
get_markup_from_foobar_shortcode
很棒,因为它告诉我们输入和输出将是什么以及函数做了什么。
函数大小
新函数应少于20行,不包括注释和空白。
长函数包含不能一次看到的代码,因此通常需要上下滚动以跟踪执行流程。因此,它们往往类似于一个程序本身,并且可以使用与不使用函数的代码中隐藏错误相同的模式来隐藏错误。
例如,一个50行的函数,在第35行使用了变量$foo
的值,意味着开发者可能需要向上滚动到第1行才能找到它的定义。如果定义被更改或删除,或者变量在定义和使用之间被修改,很容易出现错误。这可以通过使用代码检查工具来帮助,但可读性仍然会被牺牲。
函数中的代码随着时间的推移自然会增长,但在此过程中,重新考虑是否将函数的任何部分移动到它们自己的函数中是很重要的。辅助函数或私有方法是非常好的方式来实现这一点。
数组函数
新代码应尽可能使用PHP数组函数来明确循环的目的。
使用foreach
很方便,但它可能会隐藏循环的目的。这是主观的,因此很难有一个明确的规则,但总的来说,考虑你实际上想通过循环做什么,并看看是否有可能通过使用数组函数使其含义明确,是值得的。
如果一个循环做了多件事情,那么我们必须考虑是否值得将其拆分为多个循环,每个循环只做一件事。这起初可能看起来效率不高,但在许多情况下,相关的数组相当小,多个循环将大大提高可读性,而不会太多影响性能。
那么为什么foreach
一开始就存在呢?在古老的时候,程序执行是从一个语句流向下一个语句的。要多次重复一个代码块,你只需手动调整程序计数器使用goto,这就是当时世界的方式。
goto的问题在于它是一把钝器。它掩盖了意图。goto的语义是“跳转到行N并继续当前状态”。但在实践中,我们往往并不真的想跳转到任意的行。我们可能想要在满足某些条件时分支,或者重复固定次数的代码块,或者在满足某些条件时放弃代码块。因此,诞生了像if、while、switch、try/catch和foreach这样的控制结构。它们提供了一种更精确的词汇来表达意图。这还有一个额外的好处,就是更容易优化,因为编译器/解释器可以对程序的含义做出更强烈的假设。
具体来说,foreach表达的是“为数组的每个条目重复此代码块”的意图。这听起来很像数组map和reduce所做的,除了它们更精确。array_map的意图是“将此函数应用于数组中的每个项目并保留数组形状”,而reduce的意图是“消费数组的条目以获取一个汇总值”。
文件中的副作用
新代码不得在类构造函数中产生副作用。
新PHP文件不得在函数外部产生副作用。
副作用包括任何数据库访问(即使是读取访问)、网络请求、控制台输出、写入文件、更改全局变量、发送电子邮件、或写入IRC或Slack。
类构造函数的目的是初始化一个新对象,设置默认值,并准备任何作为依赖传递的数据。它们也恰好是一个“免费”的函数调用,当实例化类时发生,这意味着当类的目的简单时,它们经常被用来开始执行该类设计要执行的操作。
因为类构造函数通常通过使用new
关键字和类名显式调用,所以这种模式实际上是将类构造函数用作全局或静态函数。如果有副作用,我们会在实例化代码和副作用之间创建隐式和紧密耦合的依赖。
在编写测试时,我们可能想避免副作用(如Slack消息或数据库写入),但如果它们在构造函数中,我们可能不会意识到它们,并且我们可能无法模拟它们。更糟糕的是,创建类的任何代码可能也不会期望它们。
如果类只有一个目的,最好是创建一个单独的实例方法,如run()
或doSomething()
(理想情况下是一个真正描述函数目的的方法),并使用它来激活副作用。
在构造函数中遇到副作用已经具有挑战性,但仅仅引入一个PHP文件就产生副作用则更为棘手。这通常是不预期的,在测试时可能是一个真正的挑战。文件导入应该是一个完全纯净的操作。
数组简写
新代码绝对不能使用 array()
来创建数组原始数据;应使用 []
简写语法。
这只是为了保持一致性。简写可以减少输入字符数,并且在必须支持PHP < 5.4的WordPress代码外部常用。
Yoda条件
新代码可以使用Yoda条件,但不是必须的。
新代码绝对不能在条件中使用赋值而不包括明确的比较运算符。
这取代了WordPress编码规范中的Yoda条件规则。
Yoda条件不易编写,在许多情况下,为了防止意外赋值而无需使用。更准确的保护是在条件语句中强制进行显式的比较。
在PHP中,赋值表达式求值的结果是被赋值的变量的值。其次,没有显式比较运算符的条件表达式检查表达式的“真值”。这允许出现以下常见模式
if ($foo = getFoo())
此模式是以下模式的快捷方式
$foo = getFoo(); if ($foo == true)
这个快捷方式本身没有错误,但引入Yoda条件是为了防止以下情况
if($foo == getFoo())
为了解决这个问题并允许这种快捷方式,我们可以要求任何具有赋值的条件也必须有一个比较运算符。这使得快捷方式变得明确,并且可以很容易地由代码检查器进行检查。因此,上述快捷方式现在必须写成
if(($foo = getFoo()) == true)
继承
新代码不应该在只需要共享方法时使用继承。
新代码应该将继承深度限制在最多一个级别。
类继承是共享代码的机制,但它也隐含了身份。如果继承的目的仅仅是引入辅助函数或纯函数,那么继承可能不是最佳方法。在这些情况下,“组合优于继承”的说法变得相关;可以通过注入其他类的实例来使外部函数对类可用。
在PHP的情况下,这既适用于使用 extends
关键字的直接继承,也适用于使用 trait
和 use
关键字的混入继承。
这并不是说继承是坏事。只是当它不合适时,很容易将其作为一种代码共享机制。
当继承得到适当使用时,重要的是要小心使用,因为它可以大大增加阅读代码的认知负荷。
如果一个类继承自另一个类,而该类又继承自第三个类,那么开始变得难以跟踪行为以及行为在哪里定义。
例如,如果你要阅读以下类,你能猜出 getData()
方法是在哪里定义的吗?
class MyClass extends ClassC { use ClassB; public function doSomething() { echo $this->getData(); } }
它可能是 ClassC
或 ClassB
。但如果 ClassC
继承自 ClassA
呢?在这种情况下,它可能在 ClassA
中。但这只有在 ClassC
没有覆盖该方法的情况下才成立;如果它覆盖了该方法并调用了 parent::getData
,那么我们仍然需要考虑 ClassA
。
偶尔这种继承是必要的,但更常见的是,它是一种将类紧密耦合在一起的格式,这会在以后造成问题。实际上,调用继承方法是一种紧密耦合的形式,因为它在两个类之间创建了一种无法轻易模拟的依赖关系。
传递ID
新代码应该传递完整的对象而不是数据库ID作为函数参数。
WordPress中的一种常见模式是将网站ID或用户ID传递给一个函数,该函数操作网站或用户数据;然后该函数将查询数据库以获取它需要使用的数据。
这种模式的问题在于,它将可能是一个纯数据操作函数变成了依赖于数据库的非纯函数。纯函数更容易测试、更容易移动,通常也更容易理解。
通常可以先从数据库中获取数据(可能在单独的函数中),然后将整个对象注入到操作它的函数中。这使得每个函数只做一件事情,提高了可读性并减少了依赖。
组合根
新代码不应该在除专门用于实例化类的类(所谓“工厂”)之外的地方实例化对象。
新代码应该尽可能少的地方实例化对象。
类的依赖注入意味着在需要它的类外部创建实例,并将实例作为参数传递。这通过将责任上移一级来解耦类与其依赖关系。在类使用其他类的情况下,这形成了一个依赖注入的树。
这样的注入树的顶端是一个“根”类,它启动整个链。这个类必须创建它所调用的函数所使用的所有依赖项。这被称为“组合根”。在现实世界的代码中,通常存在多个这样的根,有时区分并不完全清晰,但尝试将根的数量保持尽可能少,可以使更改依赖关系和找到类的定义变得更简单。
当一个类在多个地方实例化时,它必须在每个地方提供所有依赖项。如果其中任何一个依赖项发生变化,或者添加了新的依赖项,这意味着找到所有创建实例的地方,并更改它们。这是有风险的,也很耗时。
相反,如果使用单个函数来实例化一个类(通常是称为“工厂”的静态函数),那么只需在一个地方进行更改即可。如果需要新的依赖项配置,可以创建一个新的工厂。
换行符
新代码不得有超过一个相邻的空白行。
空白对于分离代码的逻辑部分很有用,但过多的空白会占用太多的屏幕空间。通常一个空行就足以分隔代码的各个部分。
变量函数
新代码不应该调用变量函数。
新代码不得使用 call_user_func 或 call_user_func_array。
具有变量函数名会阻止轻松追踪函数的使用和定义。例如,如果需要更改或删除函数签名,开发者通常会搜索代码库以查找该函数名的使用情况。对于变量函数,函数名可能是由连接字符串或其他方式操作字符串创建的,这使得找到该使用情况变得几乎不可能。即使字符串未被修改,它可能在远离调用位置的地方定义,再次使其难以追踪其使用。最后,使用作为字符串的函数名,字符串可能会意外修改或设置为意外的值,这可能导致致命错误。
相反,我们可以使用映射函数将字符串转换成硬编码的函数调用。例如,这里有三种调用存储在 $myFunction
中的函数的方法;注意第三个选项实际上在调用它的代码中包含了函数名。
这一种使用 call_user_func
。
call_user_func($myFunction, 'hello');
下一个使用新的语法。
$myFunction('hello');
以下版本实际上根本不调用变量函数。
switch($myFunction) { case 'speak': speak('hello'); break; }
为了保持一致性,如果我们确实需要调用变量函数,我们最好使用较新的语法版本。
call_user_func($f, $x, $y, $z)
等于$f($x, $y, $z)
call_user_func_array($f, $args)
等于$f(...$args)