如今的PHP泛型(差不多)

如果您问PHP开发人员他们想在PHP中看到什么样的机会,那么大多数人会称之为泛型。


语言级别的通用支持将是最好的解决方案。 但是,实现它们很困难 。 我们希望有一天本地支持将成为该语言的一部分,但可能要花几年的时间。


本文将展示如何使用现有工具,在某些情况下,只需进行很少的修改,就可以立即获得PHP中泛型的强大功能。


译者:我特意使用英文“泛型”中的描图纸,因为 我从未在通信中听到有人称它为“通用编程”。

内容:



什么是泛型


本节将简要介绍泛型


阅读链接:



最简单的例子


由于当前无法在语言级别定义泛型,因此我们将不得不利用另一个巨大的机会-在码头区中定义泛型。


我们已经在许多项目中使用此选项。 看一下这个例子:


/** * @param string[] $names * @return User[] */ function createUsers(iterable $names): array { ... } 

在上面的代码中,我们尽力在语言级别上做。 我们将$names参数定义为可以列出的内容。 我们还表明该函数将返回一个数组。 如果参数类型和返回值不匹配,PHP将抛出TypeError


Docblock增强了对代码的理解。 $names必须是字符串,并且该函数必须返回User对象的数组。 PHP本身不进行此类检查。 但是诸如PhpStorm之类的IDE会理解这种表示法,并警告开发人员未遵守附加合同。 除此之外,静态分析工具(例如Psalm,PHPStan和Phan)还可以验证与函数之间传输的数据的正确性。


用于确定枚举类型的键和值的泛型


上面是泛型的最简单示例。 更复杂的方法包括能够指定其键的类型以及值的类型。 下面是描述此问题的一种方法:


 /** * @return array<string, User> */ function getUsers(): array { ... } 

这里说的是, getUsers返回的数组具有字符串键和类型User值。


静态分析器(例如Psalm,PHPStan和Phan)了解此注释,并在检查时将其考虑在内。


考虑以下代码:


 /** * @return array<string, User> */ 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支持以下注释:


 /** * @template T */ class Stack { /** * @param T $item */ public function push($item): void; /** * @return T */ public function pop(); } 

docblock用于传达其他类型信息,例如:


 /** @var Stack<User> $userStack */ $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社区已经非正式地采用了这种通用格式(大多数工具都支持它们,并且大多数人都清楚它们的含义):


 /** * @return User[] */ function getUsers(): array { ... } 

但是,对于这样的简单示例,我们会遇到问题:


 /** * @return array<string, User> */ function getUsers(): array { ... } 

Psalm理解它,并且知道键具有什么类型以及返回的数组的值。


在撰写本文时,PhpStorm还不了解这一点。 使用此条目,我想念PhpStorm提供的实时静态分析的功能。


考虑下面的代码。 PhpStorm无法理解$userUser类型,而$name是字符串类型:


 foreach(getUsers() as $name => $user) { ... } 

如果我选择Psalm作为静态分析工具,则可以编写以下代码:


 /** * @return User[] * @psalm-return array<string, User> */ function getUsers(): array { ... } 

诗篇理解所有这些。


PhpStorm知道$user变量的类型为User 。 但是,他仍然不明白数组键是指字符串。 Phan和PHPStan不了解特定的诗篇注解。 他们在此代码中了解的最大值与PhpStorm中的相同: $user的类型


您可能会争辩说PhpStorm应该只接受协议array<keyType, valueType> 。 我不同意你的看法,因为 我认为,对标准的要求是语言和社区的任务,并且仅应遵循这些工具。


我认为上述描述的协议将被大多数PHP社区欢迎。 一个对泛型感兴趣的人。 但是,关于模式,事情变得更加复杂。 PHPStan和PhpStorm当前均不支持模板。 不同于诗篇和藩。 它们的目的是相似的,但是如果您深入研究,您将意识到实现略有不同。


提出的每个选项都是一种折衷。


简而言之,需要就通用记录格式达成协议:


  • 它们改善了开发人员的生活。 开发人员可以在其代码中添加泛型并从中受益。
  • 开发人员可以使用他们最喜欢的工具,并在必要时在它们之间进行切换。
  • 工具制造商可以创建这些工具,从而了解对社区的好处,而不必担心某些事情会发生变化或被指责为“错误的做法”。

工具支援


Psalm具有检查泛型的所有必要功能。 潘也一样。


我敢肯定,一旦社区达成单一格式协议,PhpStorm就会引入泛型。


第三方代码支持


通用难题的最后一部分是添加对第三方库的支持。


希望当通用定义标准出现时,大多数库都将实现它。 但是,这不会立即发生。 使用了某些库,但没有有效的支持。 使用静态分析器验证泛型中的类型时,重要的是定义这些泛型接受或返回的所有函数。


如果您的项目依赖于没有通用支持的第三方库,会发生什么?


幸运的是,这个问题已经解决了,而存根函数就是解决方案。 Psalm, PhanPhpStorm支持存根。


存根是包含功能和方法签名,但不实现它们的普通文件。 通过将桩块添加到存根,静态分析工具可以获得所需的其他信息。 例如,如果您有一个没有类型提示和泛型的堆栈类。


 class Stack { public function push($item) { /* some implementation */ } public function pop() { /* some implementation */ } } 

您可以创建一个存根文件,该存根文件具有相同的方法,但是增加了停靠块并且没有实现功能。


 /** * @template T */ class Stack { /** * @param T $item * @return void */ public function push($item); /** * @return T */ public function pop(); } 

当静态分析器看到堆栈类时,它从存根而不是实际代码中获取类型信息。


仅共享存根代码(例如,通过作曲家)的功能将非常有用,因为 将允许分享完成的工作。


进一步的步骤


社区需要远离协议和制定标准。


也许最好的选择是通用的PSR?


也许主要的静态分析器,PhpStorm,其他IDE的创建者以及任何参与PHP开发(用于控制)的人员都可以开发出每个人都可以使用的标准。


标准出现后,每个人都将能够帮助将泛型添加到现有库和项目中,从而创建请求请求。 并且在不可能的地方,开发人员可以编写和共享存根。


完成所有操作后,我们可以在编写代码时使用PhpStorm等工具实时检查泛型。 我们可以使用静态分析工具作为CI的一部分,以确保安全。


泛型也可以用PHP实现(好吧,差不多)。


局限性


有很多限制。 PHP是一种动态语言,允许您执行许多“神奇”的事情,例如这些 。 如果使用过多的PHP魔术,则可能会发生静态分析器无法准确提取系统中所有类型的情况。 如果未知任何类型,则这些工具将无法在所有情况下正确使用泛型。


但是,此分析的主要应用是验证您的业务逻辑。 如果编写干净的代码,则不应使用过多的魔术。


您为什么不只是在舌头上添加泛型?


那将是最好的选择。 PHP具有开源代码,没有人会打扰您克隆源代码并实现泛型!


如果我不需要泛型怎么办?


只需忽略以上所有内容。 PHP的主要优点之一是,它可以根据您创建的内容灵活地选择适当级别的实现复杂性。 使用一次性代码,您无需考虑诸如键入提示之类的事情。 但是在大型项目中,值得利用这样的机会。


感谢所有阅读这个地方的人。 我将很高兴您在下午的评论。

UPD注释中的ghost404指出版本0.12.x的PHPStan理解psalm注释并支持泛型

Source: https://habr.com/ru/post/zh-CN456466/


All Articles