跨版本PHP代码转换终极教程

跨版本PHP代码转换的终极指南

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

在这种情况下,我们可能会放弃使用最新PHP代码的希望。但是还有一个更好的选择:我们仍然可以使用PHP8.0编写源代码,并将其转换到以前的PHP版本,甚至是PHP7.1。

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

  1. 什么是Transpiling?
  2. Transpiling PHP的优势
  3. PHP转换器(PHP Transpilers)
  4. 转换到哪一个PHP版本
  5. Transpiling vs Backporting
  6. Transpiled PHP示例
  7. 转换PHP的利弊
  8. 如何转换PHP
  9. 优化转换过程
  10. 转换代码时要避免的坑
  11. 转换和连续集成
  12. 测试转换代码

什么是Transpiling?

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

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

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

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

…Babel将其转换为ES5版本:

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

什么是Transpiling 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)
{
  // ...
}

如果错误仍然发生在生产中,抛出的错误消息将不如使用联合类型时准确。然而,这一潜在的缺点被能够首先使用联合类型所抵消。

Transpiling PHP的优势

Transpiling使您能够使用最新版本的PHP编写应用程序,并生成一个在运行旧版本PHP的环境中也能工作的版本。

这对于为旧式内容管理系统(CMS)创建产品的开发人员特别有用。例如,WordPress仍然官方支持PHP5.6(尽管它推荐PHP7.4+)。运行PHP版本5.6到7.2的WordPress站点的百分比为34.8%,而运行PHP版本(8.0除外)的站点的百分比高达99.5%:

WordPress使用情况统计(按版本)

WordPress使用情况统计(按版本)图像来源:WordPress

因此,面向全球受众的WordPress主题和插件很可能使用旧版本的PHP进行编码,以增加其可能的影响范围。多亏了transpiling,这些代码可以使用PHP8.0进行编码,并且仍然可以针对较旧的PHP版本发布,从而尽可能多地面向用户。

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

Drupal就是这样,它需要PHP7.3。由于transpiling,开发人员可以使用PHP8.0创建公开可用的Drupal模块,并使用PHP7.3发布它们。

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

何时需要转换PHP(Transpile PHP)

PHP代码始终可以转换,除非它包含一些在之前的PHP版本中没有对等的PHP功能。

情况可能就是这样属性,在PHP 8.0中介绍:

#[SomeAttr]
function someFunc() {}

#[AnotherAttr]
class SomeClass {}

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

用于开发的属性,如#[Deprecated],可以用删除联合类型的相同方式删除。但是,不能删除在生产中修改应用程序行为的属性,也不能直接转换这些属性。

到目前为止,没有一个transpiler能够接受具有PHP8.0属性的代码并自动生成其等效PHP7.4代码。因此,如果您的PHP代码需要使用属性,那么转换它将是困难的或不可行的。

可转换PHP功能

这些是PHP7.1及以上版本的特性,目前可以转换。如果您的代码只使用这些特性,那么您可以确信您的转换应用程序将正常工作。否则,您将需要评估转换的代码是否会产生故障。

PHP转换器(PHP Transpilers)

目前,有一个用于转换PHP代码的工具:Rector

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

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

来自Rector的“diff”输出

来自Rector的“diff”输出

转换到哪一个PHP版本

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

今天,Rector库包含PHP8.0到7.1范围内的大多数代码转换规则。因此,我们可以可靠地将PHP代码转换到7.1版。

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

Transpiling vs Backporting

Backporting与Transpiling类似,但更简单。Backporting代码不一定依赖于语言的新特性。相反,只需从新版本的语言复制/粘贴/改编相应的代码,就可以为旧版本的语言提供相同的功能。

例如,PHP 8.0中引入了str_contains函数。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;
    }
  }
}

因为Backporting比Transpiling更简单,所以无论何时进行Backporting,我们都应该选择这种解决方案。

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

这些库支持以下函数、类、常量和接口:

PHP 版本 特征
7.2 功能:

函数:

7.3 功能:

异常处理:

7.4 功能:

8.0 接口:

  • Stringable

类:

  • ValueError
  • UnhandledMatchError

函数:

  • FILTER_VALIDATE_BOOL

功能:

Transpiled PHP示例

让我们一起来看看几个转换PHP代码的示例,以及几个正在完全转换的包。

PHP代码

match 表达式是在PHP8.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)
  {
  }
}

在为PHP7.4转换时,会生成完整的代码:

 class QueryResolver
 {
  protected QueryFormatter $queryFormatter;

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

上面转换的代码包含PHP7.4中引入的类型化属性。将代码向下转换到PHP7.3将其替换为docblocks:

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

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

PHP包

以下库正在转换以用于生产:

库/描述 代码/注释
Rector
PHP重建工具,使转换成为可能
源代码
已转换代码
笔记
易于编码标准
具有PHP代码的工具遵守一组规则
源代码
已转换代码
笔记
GraphQL API for WordPress
一个为WordPress提供GraphQL server的插件
源代码
已转换代码
笔记

转换PHP的利弊

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

这有效地让我们成为更好的开发人员,生成更高质量的代码。这是因为我们的源代码可以使用PHP8.0的联合类型、PHP7.4的类型属性,以及添加到每个新版本PHP中的不同类型和伪类型(混合自PHP8.0,对象来自PHP7.2),以及PHP的其他现代功能。

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

现在,让我们看看缺点。

必须对它进行编码和维护

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

第三方库也必须转换

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

Rector不会通知我们代码何时不能转录

如果源代码包含PHP8.0属性或任何其他无法转换的功能,我们将无法继续。但是,Rector不会检查此条件,因此我们需要手动执行此操作。这对于我们自己的源代码来说可能不是一个大问题,因为我们已经熟悉它了,但它可能成为第三方依赖关系的障碍。

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

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

还必须对已转换的代码进行预缀

我们的转桩项目和其他一些库也安装在生产环境中,可以使用相同的第三方依赖性。此第三方依赖将转入我们的项目,并保留其其他库的原始源代码。因此,转录的版本必须通过PHP-范围,斯特劳斯,或一些其他工具,以避免潜在的冲突。

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

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

因为转换的代码自然会覆盖源代码,所以我们不应该在开发计算机上运行转换过程,否则我们将冒产生副作用的风险。在CI运行期间运行该进程更合适(下面将对此进行详细介绍)。

如何转换PHP(Transpile 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);
};

为了确保流程按预期执行,我们可以在dry mode下运行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中的 require-dev),哪些是用于生产的(条目下的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版本的依赖项。如果一个库是用PHP7.1(或下面的任何版本)编写的,那么就不需要将它转换到PHP7.1。

为了实现这一点,我们可以获得需要PHP7.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/文件夹):

vendor/bin/rector process src $paths

转换代码时需要避免的坑

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

链式规则(Chained Rules)并不总是被处理的

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

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

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

从PHP 7.4转换到7.3时,函数标记必须经过两次修改:

最终结果应该是:

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,如下所示:

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

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

使用–dry-run flag运行Rector

使用–dry-run flag运行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缓存组件中的类 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',
      ]);
    };
  2. 下载缺少的库并添加其路径,以便由Rector自动加载:
    return static function (ContainerConfigurator $containerConfigurator): void {
      // ...
    
      $parameters->set(Option::AUTOLOAD_PATHS, [
        __DIR__ . '/vendor/symfony/messenger',
      ]);
    };
  3. 让您的项目依赖于缺少的库进行生产:
    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/checkout@v2
      - 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/action-zip@v0.1.0
        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/upload-artifact@v2
        with:
            name: graphql-api
            path: build/graphql-api.zip
      - name: Upload to release
        uses: JasonEtco/upload-to-release@master
        with:
          args: build/graphql-api.zip application/zip
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

此工作流包含通过GitHub操作发布WordPress插件的标准过程。新添加的将插件代码从PHP7.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插件的存储库中检查WordPress插件的源代码,该插件是用PHP7.4编写的
  2. 安装其Composer依赖项
  3. 将其代码从PHP7.4转换到7.1
  4. 将插件主文件头中的“Requires PHP”条目从“7.4”修改为“7.1”
  5. 删除开发所需的依赖项
  6. 创建插件的.zip文件,排除所有不需要的文件
  7. 将.zip文件作为发布资产上载(此外,还作为GitHub操作的工件上载)

测试转换代码

一旦代码被转换到 PHP 7.1,我们怎么知道它工作得很好?或者,换句话说,我们怎么知道它已经被彻底转换,并且没有留下更高版本的 PHP 代码的残余?

与转存代码类似,我们可以在 CI 流程中实现解决方案。这个想法是设置与PHP 7.1的亚军的环境,并在转桩代码上运行一个衬垫。如果任何代码与 PHP 7.1 不兼容(例如未转换的 PHP 7.4 中的键入属性),则衬里会抛出错误。

Php 的衬里效果很好PHP 平行林特.我们可以将此库安装为项目开发的依赖项,或者让 CI 过程将其安装为独立的作曲家项目:

一旦代码被转换到PHP7.1,我们如何知道它工作良好?或者,换句话说,我们如何知道它已经被彻底转换,并且没有留下更高版本的PHP代码的残余?

与转换代码类似,我们可以在CI流程中实现解决方案。其想法是使用PHP7.1设置运行者环境,并在转换的代码上运行linter。如果任何代码与PHP7.1不兼容(例如PHP7.4中未转换的类型化属性),那么linter将抛出一个错误。

PHP的一个运行良好的linter是PHP并行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.

让我们将绒毛添加到我们的 CI 工作流中。执行将代码从 PHP 8.0 转换到 7.1 并测试它的步骤是:

  1. 查看源代码
  2. 让环境运行 PHP 8.0,以便校长可以解释源代码
  3. 将代码转换到 PHP 7.1
  4. 安装 PHP 绒毛工具
  5. 将环境的 PHP 版本切换到 7.1
  6. 在转存代码上运行衬垫

GitHub 行动工作流程做这项工作:

让我们将linter添加到CI的工作流中。将代码从PHP 8.0转换到7.1并进行测试的步骤如下:

  1. 查看源代码
  2. 让环境运行PHP8.0,这样校长就可以解释源代码了
  3. 将代码转换到PHP7.1
  4. 安装PHP linter工具
  5. 将环境的PHP版本切换到7.1
  6. 在转换的代码上运行linter

此GitHub操作工作流执行以下任务:

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/checkout@v2

      - name: Set-up PHP
        uses: shivammathur/setup-php@v2
        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/setup-php@v2
        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

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

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

小结

本文教我们如何转换PHP代码,允许我们在源代码中使用PHP8.0,并创建一个适用于PHP7.1的发行版。Transpiling是通过Rector(一种PHP重构工具)完成的。

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

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

评论留言