转译 PHP 代码的终极指南

在理想情况下,我们应该为所有网站使用 PHP 8.0(撰写本文时的最新版本),并在新版本发布后立即更新。但是,开发人员通常需要使用以前的 PHP 版本,例如在为 WordPress 创建公共插件或使用阻碍升级网络服务器环境的遗留代码时。

尝试免费演示

在这些情况下,我们可以放弃使用最新的 PHP 代码的希望。但是还有一个更好的选择:我们仍然可以使用 PHP 8.0 编写我们的源代码并将其转换为以前的 PHP 版本——甚至是 PHP 7.1。

在本指南中,我们将教您有关转译 PHP 代码所需的一切知识。

什么是转译?

转译将源代码从一种编程语言转换为相同或不同编程语言的等效源代码。

转译在 Web 开发中并不是一个新概念:客户端开发人员很可能熟悉 Babel,一种 JavaScript 代码转译器。

Babel 将 JavaScript 代码从现代 ECMAScript 2015+ 版本转换为与旧浏览器兼容的旧版本。例如,给定一个 ES2015 箭头函数:

[2, 4, 6].map((n) => n * 2);

…Babel 会将其转换为 ES5 版本:

[2, 4, 6].map(function(n) {
return n * 2;
});

什么是转译 PHP?

Web 开发中潜在的新事物是转换服务器端代码的可能性,尤其是 PHP。

转译 PHP 的工作方式与转译 JavaScript 的方式相同:现代 PHP 版本的源代码被转换为旧 PHP 版本的等效代码。

按照与之前相同的示例,来自 PHP 7.4 的箭头函数:

$nums = array_map(fn($n) => $n * 2, [2, 4, 6]);

…可以转换成其等效的 PHP 7.3 版本:

$nums = array_map(
function ($n) {
return $n * 2;
},
[2, 4, 6]
);

箭头函数可以转译,因为它们是语法糖,即产生现有行为的新语法。这是唾手可得的果实。

但是,也有一些新功能会创建新行为,因此,以前版本的 PHP 将没有等效代码。PHP 8.0 中引入的联合类型就是这种情况:

function someFunction(float|int $param): string|float|int|null
{
// …
}

在这些情况下,只要新功能需要用于开发而不是用于生产,仍然可以进行转译。然后,我们可以简单地从转译的代码中完全删除该功能,而不会造成严重后果。

一个这样的例子是联合类型。此功能用于检查输入类型与其提供的值之间是否不匹配,这有助于防止错误。如果与类型发生冲突,则说明开发中已经存在错误,我们应该在代码到达生产环境之前捕获并修复它。

因此,我们可以从生产代码中删除该功能:

function someFunction($param)
{
// …
}

如果错误仍然在生产中发生,则抛出的错误消息将不如我们有联合类型时那么精确。然而,首先能够使用联合类型克服了这种潜在的缺点。

0 在我们所有的网站上发布,并在新版本发布后立即更新 😌 但情况并非总是如此。在此处了解有关转译 PHP 代码的所有信息👇点击推文

转译 PHP 代码的优势

转译使人们能够使用最新版本的 PHP 编写应用程序,并生成一个也可以在运行旧版本 PHP 的环境中运行的版本。

这对于为遗留内容管理系统 (CMS) 创建产品的开发人员特别有用。例如,WordPress 仍然正式支持 PHP 5.6(即使它推荐 PHP 7.4+)。运行 PHP 5.6 到 7.2 版本的 WordPress 站点的百分比——它们都是生命周期终止 (EOL),这意味着它们不再接收安全更新——高达 34.8%,而那些运行在任何 PHP 版本之外的 PHP 版本8.0 高达 99.5%:

WordPress 版本使用情况

按版本划分的 WordPress 使用统计数据。图片来源:WordPress

因此,针对全球受众的 WordPress 主题和插件很可能会使用旧版本的 PHP 进行编码,以增加其可能的覆盖范围。多亏了转译,这些可以使用 PHP 8.0 进行编码,并且仍会针对较旧的 PHP 版本发布,从而针对尽可能多的用户。

事实上,任何需要支持除最新版本以外的任何 PHP 版本(即使在当前支持的 PHP 版本范围内)的应用程序都可以受益。

Drupal 就是这种情况,它需要 PHP 7.3。由于转译,开发人员可以使用 PHP 8.0 创建公开可用的 Drupal 模块,并使用 PHP 7.3 发布它们。

另一个示例是为由于某种原因而无法在其环境中运行 PHP 8.0 的客户创建自定义代码时。尽管如此,多亏了转译,开发人员仍然可以使用 PHP 8.0 编写他们的可交付成果,并在这些遗留环境中运行它们。

何时转译 PHP

PHP 代码始终可以被转译,除非它包含某些 PHP 功能,而这些功能在以前的 PHP 版本中是没有的。

PHP 8.0 中引入的属性可能就是这种情况:

#[SomeAttr]
函数 someFunc() {}

#[AnotherAttr]
class SomeClass {}

在前面使用箭头函数的例子中,代码可以被转译,因为箭头函数是语法糖。相比之下,属性会创建全新的行为。这种行为也可以在 PHP 7.4 及以下版本中重现,但只能通过手动编码,即不能根据工具或流程自动进行(AI 可以提供解决方案,但我们还没有)。

可以像删除联合类型一样删除用于开发用途的属性,例如 #[Deprecated]。但是在生产中修改应用程序行为的属性不能删除,也不能直接转换。

截至今天,没有任何转译器可以采用具有 PHP 8.0 属性的代码并自动生成其等效的 PHP 7.4 代码。因此,如果您的 PHP 代码需要使用属性,那么对其进行转译将是困难的或不可行的。

可以转译的 PHP 特性

这些是 PHP 7.1 及更高版本的功能,目前可以转译。如果您的代码只使用这些功能,您可以确信您的转译应用程序将正常工作。否则,您需要评估转译的代码是否会产生故障。

PHP版本 特征
7.1 一切
7.2 – 对象类型
– 参数类型扩展
– preg_match 中的 PREG_UNMATCHED_AS_NULL 标志
7.3 – list() 中的引用赋值/数组解构(foreach 内部除外 – #4376)
– 灵活的 Heredoc 和 Nowdoc 语法
– 函数调用中的尾随逗号
– set(raw)cookie 接受 $option 参数
7.4 – 类型化属性
– 箭头函数
– 空合并赋值运算符
– 在数组内解包
– 数字文字分隔符
– 带有标签名称数组的 strip_tags()
– 协变返回类型和逆变参数类型
8.0 – 联合类型
– 混合伪类型
– 静态返回类型
– ::class 对象上的魔术常量
– 匹配表达式
– 仅按类型捕获异常
– 空安全运算符
– 类构造函数属性提升
– 参数列表和闭包使用列表中的尾随逗号

PHP 转译器

目前,有一种用于转译 PHP 代码的工具:Rector。

Rector 是一个 PHP 重构器工具,它根据可编程规则转换 PHP 代码。我们输入源代码和要运行的规则集,Rector 将转换代码。

Rector 通过命令行操作,通过 Composer 安装在项目中。执行时,Rector 将输出转换前后代码的“差异”(绿色添加,红色删除):

来自 Rector 的“diff”输出

转译到哪个版本的 PHP

要跨 PHP 版本转换代码,必须创建相应的规则。

今天,Rector 库包含了 PHP 8.0 到 7.1 范围内的大部分代码转换规则。因此,我们可以可靠地将 PHP 代码转换为 7.1 版。

还有一些从 PHP 7.1 到 7.0 以及从 7.0 到 5.6 的转换规则,但这些规则并不详尽。完成它们的工作正在进行中,因此我们最终可能会将 PHP 代码转换为 5.6 版。

转译与向后移植

向后移植类似于转译,但更简单。向后移植代码不一定依赖于语言的新功能。相反,只需从新版本的语言中复制/粘贴/改编相应的代码,就可以向旧版本的语言提供相同的功能。

例如,函数 str_contains 是在 PHP 8.0 中引入的。PHP 7.4 及以下版本的相同功能可以像这样轻松实现:

if (!defined(‘PHP_VERSION_ID’) || (defined(‘PHP_VERSION_ID’) && PHP_VERSION_ID < 80000)) {
if (!function_exists(‘str_contains’)) {
/**
* 检查字符串是否包含另一个
*
* @param string $haystack 要搜索的字符串
* @param string $needle 要搜索的字符串
* @return boolean 如果在 haystack 中找到了针,则返回 TRUE,否则返回 FALSE。
*/
function str_contains(string $haystack, string $needle): bool
{
return strpos($haystack, $needle) !== false;
}
}
}

因为向后移植比转译更简单,所以只要向后移植完成工作,我们就应该选择这种解决方案。

关于 PHP 8.0 到 7.1 之间的范围,我们可以使用 Symfony 的 polyfill 库:

  • 填充 PHP 7.1
  • 填充 PHP 7.2
  • 填充 PHP 7.3
  • 填充 PHP 7.4
  • 填充 PHP 8.0

这些库向后移植以下函数、类、常量和接口:

PHP版本 特征
7.2 职能:

  • spl_object_id
  • utf8_encode
  • utf8_decode

常数:

  • PHP_FLOAT_*
  • PHP_OS_FAMILY
7.3 职能:

  • array_key_first
  • array_key_last
  • 小时
  • is_countable

例外:

  • 异常
7.4 职能:

  • get_mangled_object_vars
  • mb_str_split
  • 密码算法
8.0 接口:

  • 可串接

课程:

  • 值错误
  • 未处理的匹配错误

常数:

  • FILTER_VALIDATE_BOOL

职能:

  • 股利
  • get_debug_type
  • preg_last_error_msg
  • str_contains
  • str_starts_with
  • str_ends_with
  • get_resource_id

转换后的 PHP 示例

让我们检查一些已转译的 PHP 代码示例,以及一些正在完全转译的包。

PHP代码

匹配表达式是在 PHP 8.0 中引入的。此源代码:

function getFieldValue(string $fieldName): ?string
{
return match($fieldName) {
‘foo’ => ‘foofoo’,
‘bar’ => ‘barbar’,
‘baz’ => ‘bazbaz’,
default => null,
};
}

…将使用 switch 运算符转换为等效的 PHP 7.4 版本:

function getFieldValue(string $fieldName): ?string
{
switch ($fieldName) {
case ‘foo’:
return ‘foofoo’;
case ‘bar’:
返回 ‘​​barbar’;
case ‘baz’:
返回 ‘​​bazbaz’;
默认值:
返回空;
}
}

PHP 8.0 中还引入了 nullsafe 运算符:

公共函数 getValue(TypeResolverInterface $typeResolver): ?string
{
return $this->getResolver($typeResolver)?->getValue();
}

转译后的代码需要先将操作的值赋给一个新的变量,以避免两次执行操作:

公共函数 getValue(TypeResolverInterface $typeResolver): ?string
{
return ($val = $this->getResolver($typeResolver)) ? $val->getValue() : null;
}

PHP 8.0 中也引入了构造函数属性提升功能,允许开发人员编写更少的代码:

class QueryResolver
{
function __construct(protected QueryFormatter $queryFormatter)
{
}
}

在为 PHP 7.4 转译它时,会生成完整的代码段:

类 QueryResolver
{
受保护的 QueryFormatter $queryFormatter;

函数 __construct(QueryFormatter $queryFormatter)
{
$this->queryFormatter = $queryFormatter;
}
}

上面转译的代码包含类型化属性,这些属性是在 PHP 7.4 中引入的。将该代码转换为 PHP 7.3 会用 docblocks 替换它们:

class QueryResolver
{
/**
* @var QueryFormatter
*/
protected $queryFormatter;

函数 __construct(QueryFormatter $queryFormatter)
{
$this->queryFormatter = $queryFormatter;
}
}

PHP 包

正在为生产转译以下库:

库/描述 代码/注释
使转译成为可能的Rector PHP 重建器工具 – 源代码
– 转译的代码
– 注释
使 PHP 代码遵守一组规则的简易编码标准工具 – 源代码
– 转译的代码
– 注释
GraphQL API for WordPress
Plugin为 WordPress提供 GraphQL 服务器
– 源代码
– 转译的代码
– 注释

转译 PHP 的优缺点

已经描述了转译 PHP 的好处:它允许源代码使用 PHP 8.0(即 PHP 的最新版本),该版本将被转换为 PHP 的较低版本,用于生产在遗留应用程序或环境中运行。

这有效地使我们能够成为更好的开发人员,生成更高质量的代码。这是因为我们的源代码可以使用 PHP 8.0 的联合类型、PHP 7.4 的类型化属性,以及添加到每个新版本 PHP(PHP 8.0 的混合,PHP 7.2 的对象)中的不同类型和伪类型,其中PHP 的其他现代特性。

使用这些特性,我们可以更好地捕捉开发过程中的错误并编写更易于阅读的代码。

现在,让我们来看看缺点。

它必须被编码和维护

Rector 可以自动转换代码,但该过程可能需要一些手动输入才能使其与我们的特定设置配合使用。

第三方库也必须被转译

每当转换它们产生错误时,这就会成为一个问题,因为我们必须深入研究它们的源代码以找出可能的原因。如果问题可以解决并且项目是开源的,我们将需要提交拉取请求。如果库不是开源的,我们可能会遇到障碍。

当代码无法转译时,Rector 不会通知我们

如果源代码包含 PHP 8.0 属性或任何其他无法转译的功能,我们将无法继续。但是,Rector 不会检查这种情况,因此我们需要手动进行检查。这对于我们自己的源代码来说可能不是一个大问题,因为我们已经熟悉它,但它可能成为第三方依赖的障碍。

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

当应用程序在生产中产生带有堆栈跟踪的错误消息时,行号将指向转译后的代码。我们需要将转译后的代码转换回原始代码,才能在源代码中找到对应的行号。

转译的代码也必须加前缀

我们转译的项目和其他一些也安装在生产环境中的库可以使用相同的第三方依赖项。此第三方依赖项将为我们的项目转译,并为其他库保留其原始源代码。因此,转译后的版本必须通过 PHP-Scoper、Strauss 或其他一些工具作为前缀,以避免潜在的冲突。

订阅时事通讯

想知道我们是如何将流量增加超过 1000% 的吗?

加入 20,000 多名其他人,他们会收到我们的每周时事通讯,其中包含 WordPress 内幕技巧!

现在订阅

转换必须在持续集成 (CI) 期间进行

因为转译的代码自然会覆盖源代码,所以我们不应该在我们的开发计算机上运行转译过程,否则我们会冒产生副作用的风险。在 CI 运行期间运行该过程更合适(更多内容见下文)。

如何转译 PHP

首先,我们需要在我们的项目中安装Rector进行开发:

作曲家需要校长/校长 –dev

然后我们在项目的根目录中创建一个 rector.php 配置文件,其中包含所需的规则集。要将代码从 PHP 8.0 降级到 7.1,我们使用以下配置:

使用 RectorSetValueObjectDowngradeSetList;
使用 SymfonyComponentDependencyInjectionLoaderConfiguratorContainerConfigurator;

返回静态函数 (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->import(DowngradeSetList::PHP_80);
$containerConfigurator->import(DowngradeSetList::PHP_74);
$containerConfigurator->import(DowngradeSetList::PHP_73);
$containerConfigurator->import(DowngradeSetList::PHP_72);
};

为了确保进程按预期执行,我们可以在干燥模式下运行 Rector 的进程命令,传递要处理的位置(在这种情况下,文件夹 src/ 下的所有文件):

供应商/bin/rector 进程 src –dry-run

为了执行转译,我们运行 Rector 的 process 命令,它将修改现有位置中的文件:

供应商/bin/rector 进程 src

请注意:如果我们在开发计算机上运行 rector 进程,源代码将在 src/ 下就地转换。但是,我们希望在不同的位置生成转换后的代码,以免在降级代码时覆盖源代码。因此,在持续集成期间运行流程最合适。

优化转译过程

要为生产生成转译的可交付成果,只需转换生产代码;可以跳过仅用于开发的代码。这意味着我们可以避免转译所有测试(对于我们的项目及其依赖项)和所有开发依赖项。

关于测试,我们已经知道我们项目的测试所在的位置——例如,在文件夹 tests/ 下。我们还必须找出依赖项的位置——例如,在它们的子文件夹 test/、test/ 和 Test/(针对不同的库)下。然后,我们告诉 Rector 跳过处理这些文件夹:

返回静态函数 (ContainerConfigurator $containerConfigurator): void {
// …

$parameters->set(Option::SKIP, [
// 跳过测试
‘*/tests/*’,
‘*/test/*’,
‘*/Test/*’,
]);
};

关于依赖,Composer 知道哪些是用于开发的(那些在 composer.json 中的 require-dev 条目下),哪些是用于生产的(那些在条目 require 下的)。

要从 Composer 检索所有生产依赖项的路径,我们运行:

作曲家信息 –path –no-dev

此命令将生成一个包含名称和路径的依赖项列表,如下所示:

大脑/皮质/Users/leo/GitHub/leoloso/PoP/vendor/brain/cortex
composer/installers /Users/leo/GitHub/leoloso/PoP/vendor/composer/installers
composer/semver /Users/leo/GitHub/leoloso/ PoP/vendor/composer/semver
guzzlehttp /guzzle /Users/leo/GitHub/leoloso/PoP/vendor/guzzlehttp/guzzle League
/pipeline /Users/leo/GitHub/leoloso/PoP/vendor/league/pipeline

我们可以提取所有路径并将它们提供给 Rector 命令,然后该命令将处理我们项目的 src/ 文件夹以及包含所有生产依赖项的文件夹:

$path=”$(composer info –path –no-dev | cut -d’ ‘ -f2- | sed ‘s/ //g’ | tr ‘n’ ‘ ‘)”
$ vendor/bin/rector process源 $paths

进一步的改进可以防止 Rector 处理那些已经使用目标 PHP 版本的依赖项。如果库已使用 PHP 7.1(或以下任何版本)编码,则无需将其转换为 PHP 7.1。

为此,我们可以获取需要 PHP 7.2 及更高版本的库列表并仅处理这些库。我们将通过 Composer 的为什么不命令获取所有这些库的名称,如下所示:

作曲家为什么不 php “7.1.*” | grep -o “S*/S*”

由于此命令不适用于 –no-dev 标志,为了仅包含生产依赖项,我们首先需要删除开发依赖项并重新生成自动加载器,执行该命令,然后再次添加它们:

$ composer install –no-dev
$ packages=$(composer why-not php “7.1.*” | grep -o “S*/S*”)
$ composer install

Composer 的 info –path 命令检索包的路径,格式如下:

# 执行这个命令
$ composer info psr/cache –path
# 产生这个响应:psr
/cache /Users/leo/GitHub/leoloso/PoP/vendor/psr/cache

我们对列表中的所有项目执行此命令以获取所有转译路径:

对于 $packages 中的包
do
path=$(composer info $package –path | cut -d’ ‘ -f2-)
paths=”$paths $path”
done

最后,我们将此列表提供给 Rector(加上项目的 src/ 文件夹):

需要一个为您提供竞争优势的托管解决方案?Kinsta 为您提供令人难以置信的速度、最先进的安全性和自动缩放功能。查看我们的计划

供应商/bin/rector 进程 src $paths

转译代码时要避免的陷阱

转译代码可以被视为一门艺术,通常需要针对项目进行调整。让我们看看我们可能会遇到的一些问题。

链式规则并不总是被处理

链式规则是指规则需要转换先前规则生成的代码。

例如,库 symfony/cache 包含以下代码:

final class CacheItem 实现 ItemInterface
{
public function tag($tags): ItemInterface
{
// …
return $this;
}
}

当从 PHP 7.4 转译到 7.3 时,函数标签必须经过两个修改:

  • 由于规则 DowngradeCovariantReturnTypeRector,返回类型 ItemInterface 必须首先转换为 self
  • 由于规则 DowngradeSelfTypeDeclarationRector,然后必须删除返回类型 self

最终结果应该是这样的:

final class CacheItem 实现了 ItemInterface
{
public function tag($tags)
{
// …
return $this;
}
}

但是,Rector 只输出中间阶段:

final class CacheItem 实现了 ItemInterface
{
public function tag($tags): self
{
// …
return $this;
}
}

问题是 Rector 无法始终控制应用规则的顺序。

解决方案是确定哪些链式规则未处理,并执行新的 Rector 运行以应用它们。

为了识别链式规则,我们在源代码上运行了两次 Rector,如下所示:

$ vendor/bin/rector 进程src
$ vendor/bin/rector 进程src –dry-run

第一次,我们按预期运行 Rector,以执行转译。第二次,我们使用 –dry-run 标志来发现是否还有要进行的更改。如果存在,该命令将退出并显示错误代码,“diff”输出将指示仍可应用哪些规则。这意味着第一次运行没有完成,一些链式规则没有被处理。

使用 --dry-run 标志运行 Rector

使用 –dry-run 标志运行 Rector

一旦我们确定了未应用的链式规则(或规则),我们就可以创建另一个 Rector 配置文件——例如,rector-chained-rule.php 将执行缺失的规则。这一次,我们可以在需要应用的特定文件上运行特定的缺失规则,而不是为 src/ 下的所有文件处理完整的规则集:

// rector-chained-rule.php
使用 RectorCoreConfigurationOption;
使用 RectorDowngradePhp74RectorClassMethodDowngradeSelfTypeDeclarationRector;
使用 SymfonyComponentDependencyInjectionLoaderConfiguratorContainerConfigurator;

返回静态函数(ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(DowngradeSelfTypeDeclarationRector::class);

$parameters = $containerConfigurator->parameters();
$parameters->set(Option::
PATHS , [ __DIR__ . ‘/vendor/symfony/cache/CacheItem.php’,
]);
};

最后,我们告诉 Rector 在第二遍通过输入 –config 使用新的配置文件:

# 首先通过所有修改
$ vendor/bin/rector process src

# 第二遍修复特定问题
$ vendor/bin/rector process –config=rector-chained-rule.php

Composer 依赖项可能不一致

库可以声明一个要开发的依赖项(即在 composer.json 中的 require-dev 下),但仍然引用它们中的一些代码用于生产(例如在 src/ 下的某些文件,而不是 tests/ 下)。

通常,这不是问题,因为该代码可能不会在生产中加载,因此应用程序上永远不会出现错误。但是,当 Rector 处理源代码及其依赖项时,它会验证所有引用的代码都可以加载。如果任何文件引用来自未安装库的某些代码段,Rector 将抛出错误(因为它被声明为仅用于开发)。

例如,Symfony 的 Cache 组件中的 EarlyExpirationHandler 类实现了 Messenger 组件中的 MessageHandlerInterface 接口:

class EarlyExpirationHandler 实现 MessageHandlerInterface
{
//…
}

然而,symfony/cache 声明 symfony/messenger 是开发的依赖。然后,当在依赖 symfony/cache 的项目上运行 Rector 时,它会抛出一个错误:

[错误] 无法处理“vendor/symfony/cache/Messenger/EarlyExpirationHandler.php”文件,原因是:
“分析错误:未找到类 SymfonyComponentMessengerHandlerMessageHandlerInterface。”。将您的文件包含在“$parameters->set(Option:: AUTOLOAD_PATHS, […]);” 在 “rector.php” 配置中。
见 https://github.com/rectorphp/rector#configuration”。

这个问题有以下三种解决方案:

  1. 在 Rector 配置中,跳过处理引用该代码段的文件:

返回静态函数 (ContainerConfigurator $containerConfigurator): void {
// …

$parameters->set(Option::SKIP, [
__DIR__ . ‘/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php’,
]);
};

  1. 下载缺少的库并添加其路径以供 Rector 自动加载:

返回静态函数 (ContainerConfigurator $containerConfigurator): void {
// …

$parameters->set(Option::AUTOLOAD_PATHS, [
__DIR__ . ‘/vendor/symfony/messenger’,
]);
};

  1. 让您的项目依赖于缺少的生产库:

作曲家需要 symfony/messenger

转译和持续集成

如前所述,在我们的开发计算机中,我们必须在运行 Rector 时使用 –dry-run 标志,否则,源代码将被转译的代码覆盖。因此,更适合在持续集成 (CI) 期间运行实际的转译过程,我们可以在其中启动临时运行器来执行该过程。

执行转译过程的理想时间是为我们的项目生成版本。例如,下面的代码是 GitHub Actions 的工作流程,它创建了 WordPress 插件的发布:

名称:生成可安装插件并上传为发布资产

发布:
类型:[已发布]
工作:
构建:
名称:构建、降级和上传发布
运行:ubuntu-最新
步骤:
-名称:结帐代码
使用:动作/[电子邮件受保护]
– 名称:用于生产的降级代码(到 PHP 7.1)
运行:|
composer install
vendor/bin/rector process
sed -i ‘s/Requires PHP: 7.4/Requires PHP: 7.1/’ graphql-api.php
– name: Build project for production
run: |
composer install –no-dev –optimize-autoloader
mkdir build
– name: Create artifact
uses: montudor/[email protected]
with:
参数: zip -X -r build/graphql-api.zip 。-x *.git* node_modules/* .* “*/.*” CODE_OF_CONDUCT.md CONTRIBUTING.md ISSUE_TEMPLATE.md PULL_REQUEST_TEMPLATE.md rector.php *.dist composer.* dev-helpers** build**
– name: Upload工件
使用:动作/[电子邮件保护]
与:
名称:graphql-api
路径:build/graphql-api.zip
– 名称:上传到发布
用途:JasonEtco/[电子邮件保护]
与:
args:build/graphql-api.zip应用程序/zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

此工作流程包含通过 GitHub 操作发布 WordPress 插件的标准程序。将插件的代码从 PHP 7.4 转换为 7.1 的新增功能发生在以下步骤中:

– 名称:用于生产的降级代码(到 PHP 7.1)
运行:|
vendor/bin/rector process
sed -i ‘s/Requires PHP: 7.4/Requires PHP: 7.1/’ graphql-api.php

总的来说,此工作流现在执行以下步骤:

  1. 从使用 PHP 7.4 编写的存储库中查看 WordPress 插件的源代码
  2. 安装其 Composer 依赖项
  3. 将其代码从 PHP 7.4 转换为 7.1
  4. 将插件主文件头中的“Requires PHP”条目从“7.4”修改为“7.1”
  5. 删除开发所需的依赖项
  6. 创建插件的 .zip 文件,排除所有不需要的文件
  7. 将 .zip 文件作为发布资产上传(此外,作为 GitHub 操作的工件)

测试转换后的代码

一旦代码被转换为 PHP 7.1,我们怎么知道它运行良好?或者,换句话说,我们怎么知道它已经被彻底转换了,并且没有留下任何更高版本的 PHP 代码的残余?

与转译代码类似,我们可以在 CI 过程中实现解决方案。这个想法是使用 PHP 7.1 设置运行器的环境并在转换后的代码上运行 linter。如果有任何代码段与 PHP 7.1 不兼容(例如未转换的 PHP 7.4 类型化属性),则 linter 将抛出错误。

适用于 PHP 的 linter 是 PHP Parallel Lint。我们可以将此库安装为项目中的开发依赖项,或者让 CI 进程将其安装为独立的 Composer 项目:

作曲家创建项目 php-parallel-lint/php-parallel-lint

每当代码包含 PHP 7.2 及更高版本时,PHP Parallel Lint 将抛出如下错误:

运行 php-parallel-lint/parallel-lint layers/ vendor/ –exclude vendor/symfony/polyfill-ctype/bootstrap80.php –exclude vendor/symfony/polyfill-intl-grapheme/bootstrap80.php –exclude vendor/symfony /polyfill-intl-idn/bootstrap80.php –exclude vendor/symfony/polyfill-intl-normalizer/bootstrap80.php –exclude vendor/symfony/polyfill-mbstring/bootstrap80.php
PHP 7.1.33 | 10 个并行作业
………………………………. …… 60/2870 (2 %)
…………………. …………………………2870分之120(4%)

………. ………………………………………………………………………………………………………………………………………………………… 660/2870 (22 %)
………………………….X………………………….. ………………. 720/2870 (25 %)
…………………………………………………… 780/2870 (27 %)

…………………………………………………… 2820/2870 (98 %)
………………………………………….. 2870/2870 (100 %)

在 15.4 秒内检查了 2870 个文件
在 1 个文件中发现了语法错误

————————————————– ———-
解析错误:layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/graphql-api.php:55
53| ‘0.8.0’,
54| __(‘GraphQL API for WordPress’, ‘graphql-api’),
> 55| ))) {
56| $plugin->setup();
57| }
在层意外’)’/ GraphQLAPIForWP /插件/ graphql-API换WP上线55 / graphql-api.php
错误:过程,退出代码1完成。

让我们将 linter 添加到 CI 的工作流程中。执行将代码从 PHP 8.0 转译到 7.1 并对其进行测试的步骤是:

  1. 查看源代码
  2. 让环境运行PHP 8.0,以便Rector可以解释源代码
  3. 将代码转换为 PHP 7.1
  4. 安装 PHP linter 工具
  5. 切换环境的PHP版本为7.1
  6. 在转译的代码上运行 linter

这个 GitHub Action 工作流程完成了这项工作:

名称:降级 PHP 测试
作业:
主要:
名称:通过 Rector 将代码降级到 PHP 7.1,并执行测试
运行:ubuntu-最新
步骤:
-名称:结帐代码
使用:动作/[电子邮件保护]

– 名称:设置 PHP
使用:shivammathur/[email protected]
with:
php-version:8.0
覆盖范围:none

– 名称:本地包 – 通过 Rector
运行降级 PHP 代码: |
composer 安装
vendor/bin/rector 进程

# 准备在 PHP 7.1 上进行测试
– 名称:安装 PHP Parallel Lint
运行:composer create-project php-parallel-lint/php-parallel-lint –ansi

– 名称:切换到 PHP 7.1
使用:shivammathur/[email protected]
with:
php-version:7.1
coverage:none

# Lint 转换后的代码
– name: Run PHP Parallel Lint on PHP 7.1
run: php-parallel-lint/parallel-lint src/ vendor/ –exclude vendor/symfony/polyfill-ctype/bootstrap80.php –exclude vendor/symfony /polyfill-intl-grapheme/bootstrap80.php –exclude vendor/symfony/polyfill-intl-idn/bootstrap80.php –exclude vendor/symfony/polyfill-intl-normalizer/bootstrap80.php –exclude vendor/symfony/polyfill -mbstring/bootstrap80.php

请注意,来自 Symfony 的 polyfill 库(不需要转译)的几个 bootstrap80.php 文件必须从 linter 中排除。这些文件包含 PHP 8.0,因此 linter 在处理它们时会抛出错误。但是,排除这些文件是安全的,因为只有在运行 PHP 8.0 或更高版本时才会在生产中加载它们:

if (PHP_VERSION_ID >= 80000) {
return require __DIR__.’/bootstrap80.php’;
}

无论您是为 WordPress 创建公共插件还是更新旧代码,有很多原因可能无法使用最新的 PHP 版本 👩‍💻 在本指南中了解转译如何提供帮助 👇点击推文

概括

这篇文章教我们如何转译我们的 PHP 代码,允许我们在源代码中使用 PHP 8.0 并创建一个适用于 PHP 7.1 的版本。转译是通过一个 PHP 重构工具 Rector 完成的。

转译我们的代码使我们成为更好的开发人员,因为我们可以更好地捕捉开发中的错误并生成自然更易于阅读和理解的代码。

转译还使我们能够将具有特定 PHP 要求的代码与 CMS 分离。如果我们希望使用最新版本的 PHP 来创建公开可用的 WordPress 插件或 Drupal 模块,而不会严重限制我们的用户群,我们现在可以这样做。

您对转换 PHP 有任何疑问吗?请在评价部分留下您的意见!

通过以下方式节省时间、成本并最大限度地提高站点性能:

  • 来自 WordPress 托管专家的即时帮助,24/7。
  • Cloudflare 企业集成。
  • 全球受众覆盖全球 28 个数据中心。
  • 使用我们内置的应用程序性能监控进行优化。

所有这些以及更多,都在一个没有长期合同、协助迁移和 30 天退款保证的计划中。查看我们的计划或与销售人员交谈以找到适合您的计划。

相关文章