在同一个插件中同时包括PHP7.1和8.0的代码...还是不包括?

144 阅读6分钟

我最近写了很多关于转置PHP代码的文章(这里这里,和这里),描述了我们如何使用最新的PHP代码进行开发,但发布我们的软件包/插件/应用程序的传统版本,将我们的代码从PHP 8.0和7.1之间的任何地方转换。

我自己已经把我的WordPress插件从PHP 8.0转换到7.1。我对结果非常满意,因为我的代码库已经提高了它的质量。我现在可以使用类型化的属性和联合类型,这是我在公共WordPress插件中无法承受的。

然而,我仍然不是百分之百的满意。虽然解决了最初的挑战(在为WordPress编码时能够使用PHP 8.0),但转译代码的过程中又产生了一些新问题。

转译代码的问题

通过在PHP8.0中编码我的插件,然后在PHP7.1中发布它,我体会到了以下三个问题。

1.扩展程序需要用PHP7.1编码方法签名,即使它们需要PHP8.0

我的插件,一个WordPress的GraphQL服务器,允许开发者通过创建一个实现TypeResolverInterface 的对象,用他们自己的类型来扩展GraphQL模式。在其他方面,这个接口有函数getID ,有这个签名

interface TypeResolverInterface
{
  public function getID(object $resultItem): string|int;
}

我们可以看到,这个函数使用了PHP 8.0的联合类型来指定返回类型,以及PHP 7.2的object param类型。

当移植到 PHP 7.1 时,这个方法签名被降级为这个代码

interface TypeResolverInterface
{
  /**
   * @param $resultItem object
   * @return string|int
   */
  public function getID($resultItem);
}

这个方法签名是在插件中发布的。

那么,当开发者想为我的插件创建一个扩展,并将其部署在运行于PHP 8.0的应用程序上时,会发生什么?那么,他们仍然需要对方法签名使用PHP 7.1的代码,即删除object 参数类型和string|int 返回类型;否则,PHP将抛出一个错误。

幸运的是,这种情况只限于方法签名。例如,扩展仍然可以使用联合类型来声明其类上的属性。

class IcecreamTypeResolver implements IcecreamTypeResolverInterface
{
  // PHP 8.0 code here is allowed
  private string|int $id = 'vanilla';

  /**
   * PHP 7.1 code in method signature...
   *
   * @param $resultItem object
   * @return string|int
   */
  public function getID($resultItem)
  {
    return $this->id;
  }
}

然而,当我们的应用程序需要PHP 8.0时,却不得不使用PHP 7.1的代码,这还是很恼人的。作为一个插件提供者,强迫我的用户进入这种情况感觉有点悲哀。

(要清楚的是,我并没有创造这种情况;任何支持PHP 7.1的WordPress插件在覆盖方法签名时都会发生同样的情况。但在这种情况下感觉不同,只是因为我从PHP 8.0开始,目的是为我的用户提供一个更好的选择)。

2.必须使用PHP7.1提供文档

因为该插件是在PHP7.1上发布的,所以关于扩展它的文档也必须使用PHP7.1的方法签名,尽管原始源代码是在PHP8.0上。

此外,文档不能指向带有PHP 8.0源代码的 repo,否则我们会让访问者冒着复制/粘贴一段代码而产生PHP错误的风险。

最后,我们开发人员通常以使用最新版本的 PHP 为荣。但该插件的文档不能反映这一点,因为它仍然基于PHP 7.1。

我们可以通过向访问者解释转译过程来解决这些问题,鼓励他们也用 PHP 8.0 来编码他们的扩展,然后再转译到 PHP 7.1。但这样做会增加认知的复杂性,降低他们能够使用我们软件的机会。

3.调试信息使用转译后的代码,而不是源代码

假设插件抛出了一个异常,在某个debug.log 文件上打印出这个信息,我们用堆栈跟踪来定位源代码上的问题。

那么,堆栈跟踪中显示的发生错误的那一行,将指向转译的代码,而在源代码中的行号很可能是不同的。因此,要想从转译的代码转换回原始代码,还有一些额外的工作要做。

第一个建议的解决方案。生成两个版本的插件

可以考虑的最简单的解决方案是生成不是一个而是两个版本。

  • 一个是转码后的 PHP 7.1 代码
  • 一个是原始的 PHP 8.0 代码

这很容易实现,因为使用 PHP 8.0 的新版本只包含原始源代码,没有任何修改。

由于第二个插件使用了PHP 8.0的代码,任何在PHP 8.0上运行网站的开发者都可以用这个插件来代替。

制作两个版本的插件的问题

这种方法有几个问题,我认为使它不切实际。

WordPress只接受每个插件的一个版本

对于像我这样的WordPress插件,我们不可能把两个版本都上传到WordPress.org目录中。因此,我们必须在它们之间做出选择,这意味着我们最终会有使用PHP7.1的 "官方 "插件和使用PHP8.0的 "非官方 "插件。

这使问题变得非常复杂,因为官方插件可以上传到(或从)Plugins目录中下载,而非官方插件则不能--除非它作为一个不同的插件发布,这将是一个糟糕的主意。因此,它将不得不从其网站或其 repo 中下载。

此外,建议只从wordpress.org/plugins下载官方插件,这样就不会乱了准则

一个插件的稳定版本必须可以从它的WordPress插件目录页面获得。

WordPress.org分发的插件的唯一版本是目录中的那个。尽管人们可能在其他地方开发他们的代码,但用户将从目录中下载,而不是开发环境。

通过其他方法分发代码,同时不保持这里托管的代码是最新的,可能导致一个插件被删除。

这实际上意味着我们的用户将需要知道有两个不同版本的插件--一个是官方的,一个是非官方的--而且它们在两个不同的地方可以使用。

这种情况可能会让毫无戒心的用户感到困惑,而这是我宁愿避免的事情。

它没有解决文档的问题

因为文档必须说明官方插件,而官方插件将包含PHP 7.1的代码,那么问题 "2.文档必须使用PHP 7.1提供 "仍然会发生。

没有什么能阻止插件被安装两次

转编译插件必须在我们的持续集成过程中完成。因为我的代码托管在GitHub上,所以只要给代码打上标签,就会通过GitHub Actions生成该插件,并作为发布资产上传

不能有两个同名的发布资产。目前,该插件的名称是graphql-api.zip 。如果我也要生成并上传带有PHP 8.0代码的插件,我就得叫它graphql-api-php80.zip

这可能会导致一个潜在的问题:任何人都可以在WordPress中下载并安装这两个版本的插件,由于它们有不同的名字,WordPress会有效地将它们并排安装在文件夹graphql-apigraphql-api-php80

如果这种情况发生,我相信第二个插件的安装会失败,因为在不同的PHP版本中有相同的方法签名应该产生一个PHP错误,使WordPress停止安装。但即使如此,我也不想冒这个险。

第二个建议的解决方案。在同一个插件中同时包含PHP7.1和8.0的代码

既然上面的简单解决方案不是没有瑕疵的,那么就该是迭代的时候了。

在发布插件时,不要只使用转置的 PHP 7.1 代码,而是同时包括 PHP 8.0 源代码,并在运行时根据环境决定是否使用对应于一个 PHP 版本的代码。

让我们来看看这样做的效果如何。我的插件目前在两个文件夹中发送PHP代码,srcvendor ,这两个文件夹都转为PHP 7.1。用新的方法,它将包括四个文件夹。

  • src-php71 :代码已转译到 PHP 7.1
  • vendor-php71 :转写到 PHP 7.1 的代码
  • src :PHP 8.0中的原始代码
  • vendor :PHP 8.0中的原始代码

这些文件夹必须被称为srcvendor ,而不是src-php80vendor-php80 ,这样,如果我们对这些路径下的某个文件有硬编码的引用,仍然可以不做任何修改。

加载vendorvendor-php71 文件夹的做法是这样的。

if (PHP_VERSION_ID < 80000) {
  require_once __DIR__ . '/vendor-php71/autoload.php';
} else {
  require_once __DIR__ . '/vendor/autoload.php';
}

加载srcsrc-php71 文件夹是通过相应的autoload_psr4.php 文件完成的。用于 PHP 8.0 的文件保持不变。

<?php

// autoload_psr4.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
  'GraphQLAPI\\GraphQLAPI\\' => array($baseDir . '/src'),
);

但是移植到 PHP 7.1 的文件,在vendor-php71/composer/autoload_psr4.php 下,必须将路径改为src-php71

return array(
  'GraphQLAPI\\GraphQLAPI\\' => array($baseDir . '/src-php71'),
);

基本上就是这样了。现在,该插件可以在两个不同的PHP版本中发布其代码,运行PHP 8.0的服务器可以使用PHP 8.0的代码。

让我们看看这种方法是如何解决这三个问题的。

1.扩展可以使用PHP7.1的方法签名

现在,该插件仍然支持PHP 7.1,但除此之外,当在Web服务器中运行PHP 8.0时,它支持使用原生的PHP 8.0代码。因此,两个PHP版本都是一流的公民。

这样,运行 PHP 8.0 的 web 服务器将从相应的 PHP 8.0 版本加载方法签名。

interface TypeResolverInterface
{
  public function getID(object $resultItem): string|int;
}

开发人员为他们自己的网站扩展GraphQL模式时,就可以使用PHP 8.0的方法签名来编码他们的扩展。

2.2.可以使用PHP8.0提供文档

因为PHP 8.0成为一流的公民,文档将使用PHP 8.0来演示代码。

将源代码复制/粘贴到文档中也可以从原始 repo 中进行。要演示PHP 7.1版本,我们可以简单地在转置的 repo中添加一个指向相应代码的链接。

3.调试信息尽可能地使用原始代码

如果 web 服务器运行的是 PHP 8.0,调试中的堆栈跟踪将正确地打印出原始源代码的行号。

如果不运行 PHP 8.0,这个问题仍然会发生,但至少我们已经对它进行了改进。

为什么只有两个PHP版本?现在可以针对整个范围了。

如果实施这个解决方案,将插件从只使用 PHP 8.0 和 7.1 升级到使用中间的整个 PHP 版本范围是非常容易的。

我们为什么要这样做?为了改进上面的解决方案 "1.扩展程序可以使用 PHP 7.1 的方法签名",但要让开发人员能够使用他们已经在扩展程序中使用的任何一个 PHP 版本。

例如,如果运行 PHP 7.3,前面介绍的getID 的方法签名不能使用联合类型,但可以使用object 参数类型。所以扩展可以使用这段代码。

interface TypeResolverInterface
{
  /**
   * @return string|int
   */
  public function getID(object $resultItem);
}

实现这种升级意味着将所有中间的降级阶段都存储在版本内,像这样。

  • src-php71 :代码转译到 PHP 7.1
  • vendor-php71 :代码转写到PHP 7.1
  • src-php72 :代码移植到 PHP 7.2
  • vendor-php72 :代码移植到 PHP 7.2
  • src-php73: 代码移植到PHP 7.3
  • vendor-php73: 代码移植到PHP 7.3
  • src-php74: 代码移植到PHP 7.4
  • vendor-php74 :代码移植到PHP 7.4中
  • src :PHP 8.0中的原始代码
  • vendor :PHP 8.0中的原始代码

然后,加载一个或另一个版本是这样做的。

if (PHP_VERSION_ID < 72000) {
  require_once __DIR__ . '/vendor-php71/autoload.php';
} elseif (PHP_VERSION_ID < 73000) {
  require_once __DIR__ . '/vendor-php72/autoload.php';
} elseif (PHP_VERSION_ID < 74000) {
  require_once __DIR__ . '/vendor-php73/autoload.php';
} elseif (PHP_VERSION_ID < 80000) {
  require_once __DIR__ . '/vendor-php74/autoload.php';
} else {
  require_once __DIR__ . '/vendor/autoload.php';
}

在同一个插件中同时包含PHP 7.1和8.0代码的问题

这种方法最明显的问题是,我们将重复插件的文件大小。

不过在大多数情况下,这不会是一个关键的问题,因为这些插件是在服务器端运行的,对应用程序的性能没有任何影响(比如重复JS或CSS文件的大小)。最多,下载文件的时间会长一点,在WordPress中安装的时间也会长一点。

此外,只有PHP代码必然会被重复,但资产(如CSS/JS文件或图片)只能在vendorsrc 下保留,在vendor-php71src-php71 下删除,所以插件的文件大小可能不到两倍。

所以在这方面没有什么大问题。

第二个问题更严重:公共扩展也需要用两个PHP版本进行编码。根据软件包/插件/应用程序的性质,这个问题可能是一个障碍。

不幸的是,我的插件就是这种情况,我在下面解释。

公共扩展也需要同时包含PHP 8.0和7.1的代码

那些向所有人公开的扩展会怎样?他们应该使用什么PHP版本?

例如,GraphQL API插件允许用户将GraphQL模式扩展到从任何其他WordPress插件中获取数据。因此,第三方插件能够提供他们自己的扩展(想想 "WooCommerce for GraphQL API "或 "Yoast for GraphQL API")。这些扩展也可以上传到WordPress.org的插件库,供任何人下载并安装到他们的网站上。

现在,这些扩展将不会事先知道用户将使用什么PHP版本。而且他们不能让代码只使用一个版本(要么是PHP7.1,要么是8.0),因为当其他PHP版本被使用时,肯定会产生PHP错误。因此,这些扩展也需要将他们的代码同时包含在 PHP 7.1 和 8.0 中。

从技术角度来说,这当然是可以做到的。但除此之外,这是个糟糕的主意。尽管我很喜欢转译我的代码,但我不能强迫别人也这样做。我怎么能指望一个生态系统在我的插件周围蓬勃发展,而我却强加如此高的要求?

因此,我决定,对于GraphQL API,采用这种方法是不值得的。

那么,解决方案是什么呢?

让我们回顾一下到目前为止的情况。

将代码从PHP 8.0转到7.1有几个问题。

  1. 扩展程序即使需要PHP 8.0,也需要用PHP 7.1编码方法签名
  2. 文档必须使用 PHP 7.1 提供
  3. 调试信息使用转置的代码,而不是源代码

第一个提议的解决方案,生产两个版本的插件,效果并不好,因为。

  1. WordPress只接受每个插件的版本
  2. 它不能解决文档的问题
  3. 没有办法防止插件被安装两次

第二种建议的解决方案,在同一个插件中同时包括PHP7.1和8.0的代码,可能成功也可能失败。

  • 如果该插件可以被第三方扩展,这些扩展也需要转码。这可能会增加进入的门槛,使之不值得。
  • 否则,它应该可以正常工作

在我的案例中,GraphQL API受到了第二个建议的解决方案的影响。然后,我又绕了一圈,回到了我开始的地方--遭受了我试图寻找解决方案的三个问题。

尽管有这样的挫折,我并没有改变我对转译的积极看法。事实上,如果我不转译我的源代码,它就必须使用 PHP 7.1(或者可能是 PHP 5.6),所以我不会好到哪里去。(只有关于调试信息不指向源代码的问题会得到解决)。

总结

我在这篇文章的开头描述了到目前为止我在将我的WordPress插件从PHP8.0移植到7.1时遇到的三个问题。然后我提出了两个解决方案,其中第一个不会很好地工作。

第二种解决方案会工作得很好,除非是可以被第三方扩展的包/插件/应用程序。我的插件就是这种情况,所以我又回到了我的起点,没有解决这三个问题的办法。

所以我对转译还是没有百分之百的满意。只有93%。

The postIncluding both PHP 7.1 and 8.0 code in the same plugin ... or not?第一次出现在LogRocket Blog上。