标签: Transpiling PHP

  • 跨版本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);
    [2, 4, 6].map((n) => n * 2);
    [2, 4, 6].map((n) => n * 2);
    

    …Babel将其转换为ES5版本:

    [2, 4, 6].map(function(n) {
    return n * 2;
    });
    [2, 4, 6].map(function(n) {
    return n * 2;
    });
    [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]);
    $nums = array_map(fn($n) => $n * 2, [2, 4, 6]);
    $nums = array_map(fn($n) => $n * 2, [2, 4, 6]);
    

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

    $nums = array_map(
    function ($n) {
    return $n * 2;
    },
    [2, 4, 6]
    );
    $nums = array_map(
    function ($n) {
    return $n * 2;
    },
    [2, 4, 6]
    );
    $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(float|int $param): string|float|int|null
    {
    // …
    }
    function someFunction(float|int $param): string|float|int|null
    {
      // ...
    }
    

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

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

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

    function someFunction($param)
    {
    // …
    }
    function someFunction($param)
    {
    // …
    }
    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 {}
    #[SomeAttr]
    function someFunc() {}

    #[AnotherAttr]
    class SomeClass {}

    #[SomeAttr]
    function someFunc() {}
    
    #[AnotherAttr]
    class SomeClass {}

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

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

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

    可转换PHP功能

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

    PHP 版本 特征
    7.1 所有都可以
    7.2 – object type
    – parameter type widening
    – PREG_UNMATCHED_AS_NULL flag in preg_match
    7.3 – Reference assignments in list() / array destructuring (Except inside foreach — #4376)
    – Flexible Heredoc and Nowdoc syntax
    – Trailing commas in functions calls
    – set(raw)cookie accepts $option argument
    7.4 – Typed properties
    – 
    Arrow functions
    – 
    Null coalescing assignment operator
    – 
    Unpacking inside arrays
    – 
    Numeric literal separator
    – 
    strip_tags() with array of tag names
    – 
    covariant return types and contravariant param types
    8.0 – Union types
    – 
    mixed pseudo type
    – 
    static return type
    – 
    ::class magic constant on objects
    – 
    match expressions
    – 
    catch exceptions only by type
    – 
    Null-safe operator
    – 
    Class constructor property promotion
    – 
    Trailing commas in parameter lists and closure use lists

    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;
    }
    }
    }
    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;
    }
    }
    }
    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,
    };
    }
    function getFieldValue(string $fieldName): ?string
    {
    return match($fieldName) {
    ‘foo’ => ‘foofoo’,
    ‘bar’ => ‘barbar’,
    ‘baz’ => ‘bazbaz’,
    default => null,
    };
    }