转译 PHP 代码的终极指南

已发表: 2021-09-22

在理想情况下,我们应该为所有网站使用 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) { // ... }

如果错误仍然发生在生产环境中,抛出的错误消息将不如我们有联合类型时那么精确。 但是,首先能够使用联合类型超过了这个潜在的缺点。

在一个完美的世界里,我们应该能够在我们所有的网站上使用 PHP 8.0,并在新版本发布后立即更新它,但情况并非总是如此。 在此处了解有关转译 PHP 代码的所有信息点击鸣叫

转译 PHP 代码的优点

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

这对于为遗留内容管理系统 (CMS) 创建产品的开发人员特别有用。 例如,WordPress 仍然正式支持 PHP 5.6(尽管它推荐 PHP 7.4+)。 运行 PHP 版本 5.6 到 7.2 的 WordPress 站点的百分比——这些站点都已停产(EOL),这意味着它们不再接收安全更新——占相当大的 34.8%,并且运行在任何 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] function someFunc() {} #[AnotherAttr] class SomeClass {}

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

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

到目前为止,没有任何转译器可以获取具有 PHP 8.0 属性的代码并自动生成其等效的 PHP 7.4 代码。 因此,如果您的 PHP 代码需要使用属性,那么转译它会很困难或不可行。

可以转译的 PHP 功能

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

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

PHP 转译器

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

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

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

来自 Rector 的“diff”输出
来自 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')) { /** * Checks if a string contains another * * @param string $haystack The string to search in * @param string $needle The string to search * @return boolean Returns TRUE if the needle was found in haystack, FALSE otherwise. */ function str_contains(string $haystack, string $needle): bool { return strpos($haystack, $needle) !== false; } } }

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

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

  • Polyfill PHP 7.1
  • Polyfill PHP 7.2
  • Polyfill PHP 7.3
  • Polyfill PHP 7.4
  • Polyfill 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
  • hrtime
  • is_countable

例外:

  • JsonException
7.4 职能:
  • get_mangled_object_vars
  • mb_str_split
  • password_algos
8.0 接口:
  • Stringable

课程:

  • ValueError
  • UnhandledMatchError

常数:

  • FILTER_VALIDATE_BOOL

职能:

  • fdiv
  • get_debug_type
  • preg_last_error_msg
  • str_contains
  • str_starts_with
  • str_ends_with
  • get_resource_id

转译的 PHP 示例

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

PHP 代码

match表达式是在 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': return 'barbar'; case 'baz': return 'bazbaz'; default: return null; } }

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

 public function getValue(TypeResolverInterface $typeResolver): ?string { return $this->getResolver($typeResolver)?->getValue(); }

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

 public function 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 编译它时,会生成完整的代码:

 class QueryResolver { protected QueryFormatter $queryFormatter; function __construct(QueryFormatter $queryFormatter) { $this->queryFormatter = $queryFormatter; } }

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

 class QueryResolver { /** * @var QueryFormatter */ protected $queryFormatter; function __construct(QueryFormatter $queryFormatter) { $this->queryFormatter = $queryFormatter; } }

PHP 包

以下库正在编译以用于生产:

库/描述代码/注释
校长
使转译成为可能的 PHP 重构工具
- 源代码
– 转译代码
– 笔记
简易编码标准
使 PHP 代码遵守一组规则的工具
- 源代码
– 转译代码
– 笔记
用于 WordPress 的 GraphQL API
为 WordPress 提供 GraphQL 服务器的插件
- 源代码
– 转译代码
– 笔记

转译 PHP 的优缺点

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

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

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

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

它必须被编码和维护

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

第三方库也必须被转译

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

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

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

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

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

转译的代码也必须加前缀

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

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

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

如何编译 PHP

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

 composer require rector/rector --dev

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

 use Rector\Set\ValueObject\DowngradeSetList; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurator->import(DowngradeSetList::PHP_80); $containerConfigurator->import(DowngradeSetList::PHP_74); $containerConfigurator->import(DowngradeSetList::PHP_73); $containerConfigurator->import(DowngradeSetList::PHP_72); };

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

 vendor/bin/rector process src --dry-run

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

 vendor/bin/rector process src

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

优化转译过程

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

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

 return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::SKIP, [ // Skip tests '*/tests/*', '*/test/*', '*/Test/*', ]); };

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

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

 composer info --path --no-dev

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

 brain/cortex /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/文件夹以及包含所有生产依赖项的文件夹:

 $ paths="$(composer info --path --no-dev | cut -d' ' -f2- | sed 's/ //g' | tr '\n' ' ')" $ vendor/bin/rector process src $paths

进一步的改进可以防止 Rector 处理那些已经使用目标 PHP 版本的依赖项。 如果一个库是用 PHP 7.1(或任何以下版本)编码的,那么就没有必要将它转换成 PHP 7.1。

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

 composer why-not 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命令检索包的路径,格式如下:

 # Executing this command $ composer info psr/cache --path # Produces this response: psr/cache /Users/leo/GitHub/leoloso/PoP/vendor/psr/cache

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

 for package in $packages do path=$(composer info $package --path | cut -d' ' -f2-) paths="$paths $path" done

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

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

vendor/bin/rector process src $paths

转译代码时要避免的陷阱

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

不总是处理链式规则

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

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

 final class CacheItem implements ItemInterface { public function tag($tags): ItemInterface { // ... return $this; } }

从 PHP 7.4 编译到 7.3 时,函数tag必须经过两次修改:

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

最终结果应该是这个:

 final class CacheItem implements ItemInterface { public function tag($tags) { // ... return $this; } }

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

 final class CacheItem implements ItemInterface { public function tag($tags): self { // ... return $this; } }

问题是 Rector 不能总是控制应用规则的顺序。

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

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

 $ vendor/bin/rector process src $ vendor/bin/rector process src --dry-run

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

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

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

 // rector-chained-rule.php use Rector\Core\Configuration\Option; use Rector\DowngradePhp74\Rector\ClassMethod\DowngradeSelfTypeDeclarationRector; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(DowngradeSelfTypeDeclarationRector::class); $parameters = $containerConfigurator->parameters(); $parameters->set(Option::PATHS, [ __DIR__ . '/vendor/symfony/cache/CacheItem.php', ]); };

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

 # First pass with all modifications $ vendor/bin/rector process src # Second pass to fix a specific problem $ 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 implements MessageHandlerInterface { //... }

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

 [ERROR] Could not process "vendor/symfony/cache/Messenger/EarlyExpirationHandler.php" file, due to: "Analyze error: "Class Symfony\Component\Messenger\Handler\MessageHandlerInterface not found.". Include your files in "$parameters->set(Option::AUTOLOAD_PATHS, [...]);" in "rector.php" config. See https://github.com/rectorphp/rector#configuration".

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

  1. 在 Rector 配置中,跳过处理引用该代码的文件:
 return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::SKIP, [ __DIR__ . '/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php', ]); };
  1. 下载缺失的库并添加其路径以由 Rector 自动加载:
 return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::AUTOLOAD_PATHS, [ __DIR__ . '/vendor/symfony/messenger', ]); };
  1. 让您的项目依赖于缺少的库进行生产:
 composer require symfony/messenger

转译和持续集成

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

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

 name: Generate Installable Plugin and Upload as Release Asset on: release: types: [published] jobs: build: name: Build, Downgrade and Upload Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/[email protected] - name: Downgrade code for production (to PHP 7.1) run: | 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: args: 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 artifact uses: actions/[email protected] with: name: graphql-api path: build/graphql-api.zip - name: Upload to release uses: JasonEtco/[email protected] with: args: build/graphql-api.zip application/zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

 - name: Downgrade code for production (to PHP 7.1) run: | vendor/bin/rector process sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php

总之,此工作流程现在执行以下步骤:

  1. 从其存储库中查看 WordPress 插件的源代码,使用 PHP 7.4 编写
  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 项目:

 composer create-project php-parallel-lint/php-parallel-lint

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

 Run 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 parallel jobs ............................................................ 60/2870 (2 %) ............................................................ 120/2870 (4 %) ... ............................................................ 660/2870 (22 %) .............X.............................................. 720/2870 (25 %) ............................................................ 780/2870 (27 %) ... ............................................................ 2820/2870 (98 %) .................................................. 2870/2870 (100 %) Checked 2870 files in 15.4 seconds Syntax error found in 1 file ------------------------------------------------------------ Parse error: 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| } Unexpected ')' in layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/graphql-api.php on line 55 Error: Process completed with exit code 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 工作流程完成了这项工作:

 name: Downgrade PHP tests jobs: main: name: Downgrade code to PHP 7.1 via Rector, and execute tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/[email protected] - name: Set-up PHP uses: shivammathur/[email protected] with: php-version: 8.0 coverage: none - name: Local packages - Downgrade PHP code via Rector run: | composer install vendor/bin/rector process # Prepare for testing on PHP 7.1 - name: Install PHP Parallel Lint run: composer create-project php-parallel-lint/php-parallel-lint --ansi - name: Switch to PHP 7.1 uses: shivammathur/[email protected] with: php-version: 7.1 coverage: none # Lint the transpiled code - 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 完成的。

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

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

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