如果您问PHP开发人员他们想在PHP中看到什么样的机会,那么大多数人会称之为泛型。
语言级别的通用支持将是最好的解决方案。 但是,实现它们很困难 。 我们希望有一天本地支持将成为该语言的一部分,但可能要花几年的时间。
本文将展示如何使用现有工具,在某些情况下,只需进行很少的修改,就可以立即获得PHP中泛型的强大功能。
译者:我特意使用英文“泛型”中的描图纸,因为 我从未在通信中听到有人称它为“通用编程”。
内容:
什么是泛型
本节将简要介绍泛型 。
阅读链接:
最简单的例子
由于当前无法在语言级别定义泛型,因此我们将不得不利用另一个巨大的机会-在码头区中定义泛型。
我们已经在许多项目中使用此选项。 看一下这个例子:
function createUsers(iterable $names): array { ... }
在上面的代码中,我们尽力在语言级别上做。 我们将$names
参数定义为可以列出的内容。 我们还表明该函数将返回一个数组。 如果参数类型和返回值不匹配,PHP将抛出TypeError
。
Docblock增强了对代码的理解。 $names
必须是字符串,并且该函数必须返回User
对象的数组。 PHP本身不进行此类检查。 但是诸如PhpStorm之类的IDE会理解这种表示法,并警告开发人员未遵守附加合同。 除此之外,静态分析工具(例如Psalm,PHPStan和Phan)还可以验证与函数之间传输的数据的正确性。
用于确定枚举类型的键和值的泛型
上面是泛型的最简单示例。 更复杂的方法包括能够指定其键的类型以及值的类型。 下面是描述此问题的一种方法:
function getUsers(): array { ... }
这里说的是, getUsers
返回的数组具有字符串键和类型User
值。
静态分析器(例如Psalm,PHPStan和Phan)了解此注释,并在检查时将其考虑在内。
考虑以下代码:
function getUsers(): array { ... } function showAge(int $age): void { ... } foreach(getUsers() as $name => $user) { showAge($name); }
静态分析器将在showAge
调用上引发警告,并显示错误,如下所示: Argument 1 of showAge expects int, string provided
。
不幸的是,在撰写本文时,PhpStorm不知道如何。
更复杂的泛型
我们将继续研究泛型主题。 考虑一个栈对象:
class Stack { public function push($item): void { ... } public function pop() { ... } }
堆栈可以接受任何类型的对象。 但是,如果我们想将堆栈限制为仅User
类型的对象,该怎么办?
Psalm和Phan支持以下注释:
class Stack { public function push($item): void; public function pop(); }
docblock用于传达其他类型信息,例如:
$stack = new Stack(); Means that $userStack must only contain Users.
诗篇,分析以下代码时:
$userStack->push(new User()); $userStack->push("hello");
将会抱怨第2行,并带有错误Argument 1 of Stack::push expects User, string(hello) provided.
PhpStorm当前不支持此注释。
实际上,我们仅涵盖了有关泛型的部分信息,但目前就足够了。
如何在没有语言支持的情况下实现泛型
您必须完成以下步骤:
- 在社区级别,在扩展坞中定义通用标准(例如,新的PSR,或还原为PSR-5)
- 在代码中添加Dockblock批注
- 使用了解这些约定的IDE进行实时静态分析,以发现不一致之处。
- 使用静态分析工具(例如Psalm)作为CI的步骤之一来捕获错误。
- 定义一种将类型信息传递给第三方库的方法。
标准化
目前,PHP社区已经非正式地采用了这种通用格式(大多数工具都支持它们,并且大多数人都清楚它们的含义):
function getUsers(): array { ... }
但是,对于这样的简单示例,我们会遇到问题:
function getUsers(): array { ... }
Psalm理解它,并且知道键具有什么类型以及返回的数组的值。
在撰写本文时,PhpStorm还不了解这一点。 使用此条目,我想念PhpStorm提供的实时静态分析的功能。
考虑下面的代码。 PhpStorm无法理解$user
是User
类型,而$name
是字符串类型:
foreach(getUsers() as $name => $user) { ... }
如果我选择Psalm作为静态分析工具,则可以编写以下代码:
function getUsers(): array { ... }
诗篇理解所有这些。
PhpStorm知道$user
变量的类型为User
。 但是,他仍然不明白数组键是指字符串。 Phan和PHPStan不了解特定的诗篇注解。 他们在此代码中了解的最大值与PhpStorm中的相同: $user
的类型
您可能会争辩说PhpStorm应该只接受协议array<keyType, valueType>
。 我不同意你的看法,因为 我认为,对标准的要求是语言和社区的任务,并且仅应遵循这些工具。
我认为上述描述的协议将被大多数PHP社区欢迎。 一个对泛型感兴趣的人。 但是,关于模式,事情变得更加复杂。 PHPStan和PhpStorm当前均不支持模板。 不同于诗篇和藩。 它们的目的是相似的,但是如果您深入研究,您将意识到实现略有不同。
提出的每个选项都是一种折衷。
简而言之,需要就通用记录格式达成协议:
- 它们改善了开发人员的生活。 开发人员可以在其代码中添加泛型并从中受益。
- 开发人员可以使用他们最喜欢的工具,并在必要时在它们之间进行切换。
- 工具制造商可以创建这些工具,从而了解对社区的好处,而不必担心某些事情会发生变化或被指责为“错误的做法”。
Psalm具有检查泛型的所有必要功能。 潘也一样。
我敢肯定,一旦社区达成单一格式协议,PhpStorm就会引入泛型。
第三方代码支持
通用难题的最后一部分是添加对第三方库的支持。
希望当通用定义标准出现时,大多数库都将实现它。 但是,这不会立即发生。 使用了某些库,但没有有效的支持。 使用静态分析器验证泛型中的类型时,重要的是定义这些泛型接受或返回的所有函数。
如果您的项目依赖于没有通用支持的第三方库,会发生什么?
幸运的是,这个问题已经解决了,而存根函数就是解决方案。 Psalm, Phan和PhpStorm支持存根。
存根是包含功能和方法签名,但不实现它们的普通文件。 通过将桩块添加到存根,静态分析工具可以获得所需的其他信息。 例如,如果您有一个没有类型提示和泛型的堆栈类。
class Stack { public function push($item) { } public function pop() { } }
您可以创建一个存根文件,该存根文件具有相同的方法,但是增加了停靠块并且没有实现功能。
class Stack { public function push($item); public function pop(); }
当静态分析器看到堆栈类时,它从存根而不是实际代码中获取类型信息。
仅共享存根代码(例如,通过作曲家)的功能将非常有用,因为 将允许分享完成的工作。
进一步的步骤
社区需要远离协议和制定标准。
也许最好的选择是通用的PSR?
也许主要的静态分析器,PhpStorm,其他IDE的创建者以及任何参与PHP开发(用于控制)的人员都可以开发出每个人都可以使用的标准。
标准出现后,每个人都将能够帮助将泛型添加到现有库和项目中,从而创建请求请求。 并且在不可能的地方,开发人员可以编写和共享存根。
完成所有操作后,我们可以在编写代码时使用PhpStorm等工具实时检查泛型。 我们可以使用静态分析工具作为CI的一部分,以确保安全。
泛型也可以用PHP实现(好吧,差不多)。
局限性
有很多限制。 PHP是一种动态语言,允许您执行许多“神奇”的事情,例如这些 。 如果使用过多的PHP魔术,则可能会发生静态分析器无法准确提取系统中所有类型的情况。 如果未知任何类型,则这些工具将无法在所有情况下正确使用泛型。
但是,此分析的主要应用是验证您的业务逻辑。 如果编写干净的代码,则不应使用过多的魔术。
您为什么不只是在舌头上添加泛型?
那将是最好的选择。 PHP具有开源代码,没有人会打扰您克隆源代码并实现泛型!
如果我不需要泛型怎么办?
只需忽略以上所有内容。 PHP的主要优点之一是,它可以根据您创建的内容灵活地选择适当级别的实现复杂性。 使用一次性代码,您无需考虑诸如键入提示之类的事情。 但是在大型项目中,值得利用这样的机会。
感谢所有阅读这个地方的人。 我将很高兴您在下午的评论。
UPD : 注释中的ghost404指出版本0.12.x的PHPStan理解psalm注释并支持泛型