我最近写了很多关于转置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-api 和graphql-api-php80 。
如果这种情况发生,我相信第二个插件的安装会失败,因为在不同的PHP版本中有相同的方法签名应该产生一个PHP错误,使WordPress停止安装。但即使如此,我也不想冒这个险。
第二个建议的解决方案。在同一个插件中同时包含PHP7.1和8.0的代码
既然上面的简单解决方案不是没有瑕疵的,那么就该是迭代的时候了。
在发布插件时,不要只使用转置的 PHP 7.1 代码,而是同时包括 PHP 8.0 源代码,并在运行时根据环境决定是否使用对应于一个 PHP 版本的代码。
让我们来看看这样做的效果如何。我的插件目前在两个文件夹中发送PHP代码,src 和vendor ,这两个文件夹都转为PHP 7.1。用新的方法,它将包括四个文件夹。
src-php71:代码已转译到 PHP 7.1vendor-php71:转写到 PHP 7.1 的代码src:PHP 8.0中的原始代码vendor:PHP 8.0中的原始代码
这些文件夹必须被称为src 和vendor ,而不是src-php80 和vendor-php80 ,这样,如果我们对这些路径下的某个文件有硬编码的引用,仍然可以不做任何修改。
加载vendor 或vendor-php71 文件夹的做法是这样的。
if (PHP_VERSION_ID < 80000) {
require_once __DIR__ . '/vendor-php71/autoload.php';
} else {
require_once __DIR__ . '/vendor/autoload.php';
}
加载src 或src-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.1vendor-php71:代码转写到PHP 7.1src-php72:代码移植到 PHP 7.2vendor-php72:代码移植到 PHP 7.2src-php73: 代码移植到PHP 7.3vendor-php73: 代码移植到PHP 7.3src-php74: 代码移植到PHP 7.4vendor-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文件或图片)只能在vendor 和src 下保留,在vendor-php71 和src-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有几个问题。
- 扩展程序即使需要PHP 8.0,也需要用PHP 7.1编码方法签名
- 文档必须使用 PHP 7.1 提供
- 调试信息使用转置的代码,而不是源代码
第一个提议的解决方案,生产两个版本的插件,效果并不好,因为。
- WordPress只接受每个插件的版本
- 它不能解决文档的问题
- 没有办法防止插件被安装两次
第二种建议的解决方案,在同一个插件中同时包括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上。