发现方法签名更改

PHP 8 中引入了一些方法签名的变化。如果您的代码扩展了本节中描述的任何类或实现了本节中描述的任何方法,那么理解这些签名变化是非常重要的。只要您了解了这些变化,您的代码就能正确运行,从而减少 bug。

PHP 8 中引入的签名更改反映了更新的最佳实践。因此,如果你编写的代码使用了正确的方法签名,那么你就遵循了这些最佳实践。我们将从回顾 PHP 8 对魔法方法签名的修改开始讨论。

管理魔术方法签名

在 PHP 8 中,魔法方法的定义和使用向标准化迈进了一大步。这是通过引入严格参数和返回数据类型形式的精确魔法方法签名来实现的。与 PHP 8 中的大多数改进一样,这一更新也是为了防止魔法方法的滥用。总体结果是代码更好,错误更少。

该增强功能的缺点是,如果您的代码提供了不正确的参数或返回值类型,就会抛出错误。另一方面,如果你的代码提供了正确的参数数据类型和返回值类型,或者如果你的代码根本没有使用参数或返回值数据类型,那么这一改进将不会产生任何不利影响。

下面的代码块总结了 PHP 8 及以上版本中魔法方法的新参数和返回值数据类型:

__call(string $name, array $arguments): mixed;
__callStatic(string $name, array $arguments): mixed;
__clone(): void;
__debugInfo(): ?array;
__get(string $name): mixed;
__invoke(mixed $arguments): mixed;
__isset(string $name): bool;
__serialize(): array;
__set(string $name, mixed $value): void;
__set_state(array $properties): object;
__sleep(): array;
__unserialize(array $data): void;
__unset(string $name): void;
__wakeup(): void;

现在,让我们来看三个简单的例子,说明神奇方法签名更改的影响:

  1. 第一个例子涉及 NoTypes 类,它定义了 __call() 但没有定义任何数据类型:

    // /repo/ch09/php8_bc_break_magic.php
    class NoTypes {
        public function __call($name, $args) {
            return "Attempt made to call '$name' "
                . "with these arguments: '"
                . implode(',', $args) . "'\n";
        }
    }
    $no = new NoTypes();
    echo $no->doesNotExist('A','B','C');
  2. 下面的示例(与前面的示例在同一个文件中)是混合类型类(MixedTypes)的示例,它定义了 __invoke() 但使用的是数组数据类型而不是混合类型:

    class MixedTypes {
        public function __invoke(array $args) : string {
            return "Arguments: '"
                . implode(',', $args) . "'\n";
        }
    }
    $mixed= new MixedTypes();
    echo $mixed(['A','B','C']);

    以下是前面步骤中显示的代码示例的 PHP 7 输出:

    root@php8_tips_php7 [ /repo/ch09 ]#
    php php8_bc_break_magic.php
    Attempt made to call 'doesNotExist' with these arguments:
    'A,B,C'
    Arguments: 'A,B,C'

    这是相同的代码示例,但在 PHP 8 下运行:

    root@php8_tips_php8 [ /repo/ch09 ]#
    php php8_bc_break_magic.php
    Attempt made to call 'doesNotExist' with these arguments:
    'A,B,C'
    Arguments: 'A,B,C'

    正如你所看到的,两组输出完全相同。第一个显示的类 NoTypes 可以工作,因为没有定义数据类型提示。有趣的是,MixedTypes 类在 PHP 8 及以下版本中都能工作,因为新的混合数据类型实际上是所有类型的联合。因此,使用任何特定的数据类型来代替 mixed 都是安全的。

  3. 在最后一个示例中,我们将定义 WrongType 类。在这个类中,我们将定义一个名为 __isset() 的神奇方法,它使用的返回数据类型不符合 PHP 8 的要求。在这里,我们使用的是字符串,而在 PHP 8 中,它的返回类型必须是 bool

    // /repo/ch09/php8_bc_break_magic_wrong.php
    class WrongType {
        public function __isset($var) : string {
            return (isset($this->$var)) ? 'Y' : '';
        }
    }
    $wrong = new WrongType();
    echo (isset($wrong->nothing)) ? 'Set' : 'Not Set';

本例在 PHP 7 中可以正常运行,因为在本例中,如果变量未设置,则返回空字符串,然后插值为 FALSE 布尔值。下面是 PHP 7 的输出结果:

root@php8_tips_php7 [ /repo/ch09 ]#
php php8_bc_break_magic_wrong.php
Not Set

但是,在 PHP 8 中,由于魔法方法签名已经标准化,因此示例失败,如图所示:

root@php8_tips_php8 [ /repo/ch09 ]#
php php8_bc_break_magic_wrong.php
PHP Fatal error: WrongTypes::__isset(): Return type must be
bool when declared in /repo/ch09/php8_bc_break_magic_wrong.php
on line 6

正如您所看到的,PHP 8 严格执行其神奇的方法签名。

最佳实践:修改任何使用魔法方法的代码,以遵循新的严格方法签名。有关严格魔法方法签名的更多信息,请访问 https://wiki.php.net/rfc/magic-methodssignature

现在你已经知道要注意什么,以及如何纠正涉及魔法方法的潜在代码错误。现在,让我们来看看 Reflection 扩展方法签名的变化。

检查反射方法签名更改

如果应用程序使用 invoke()newInstance() Reflection 扩展方法,可能会出现向后兼容的代码中断。在 PHP 7 及以下版本中,接下来列出的所有三个方法都可以接受数量不限的参数。但在方法签名中,只列出了一个参数,如下所示:

  • ReflectionClass::newInstance($args)

  • ReflectionFunction::invoke($args)

  • ReflectionMethod::invoke($args)

在 PHP 8 中,方法签名准确地反映了现实情况,因为 $args 前面有变量运算符。下面是新的方法签名:

  • ReflectionClass::newInstance(…​$args)

  • ReflectionFunction::invoke(…​$args)

  • ReflectionMethod::invoke($object, …​$args)

只有当您的自定义类扩展了这三个类中的任何一个,并且您的自定义类还覆盖了前面列表中列出的三个方法中的任何一个时,这一更改才会破坏您的代码。

最后,isBuiltin() 方法已从 ReflectionType 移至 ReflectionNamedType。如果您使用 ReflectionType::isBuiltIn() 方法,这可能会导致代码中断。

现在,让我们看看 PDO 扩展中方法签名的变化。

处理 PDO 扩展签名更改

PDO 扩展有两个重要的方法签名变更。这些更改是为了解决不同获取模式下方法调用不一致的问题。下面是 PDO::query() 的新方法签名:

PDO::query(string $query,
    ?int $fetchMode = null, mixed ...$fetchModeArgs)

这是 PDOStatement::setFetchMode() 的新签名:

PDOStatement::setFetchMode(int $mode, mixed ...$args)

PDO::query() 方法签名的修改在 PHP 7.4 到 PHP 8 的迁移指南中有所提及,请点击: https://www.php.net/manual/en/migration80.incompatible.php#migration80.incompatible.pdo

这两个新方法签名比旧签名更加统一,而且完全涵盖了使用不同获取模式时的语法差异。一个使用两种不同获取模式执行 PDO::query() 的简单代码示例说明了方法签名需要规范化的原因:

  1. 首先,让我们加入一个包含数据库连接参数的配置文件。由此,我们将创建一个 PDO 实例:

    // /repo/ch09/php8_pdo_signature_change.php
    $config = include __DIR__ . '/../src/config/config.php';
    $db_cfg = $config['db-config'];
    $pdo = new PDO($db_cfg['dsn'],
        $db_cfg['usr'], $db_cfg['pwd']);
  2. 现在,让我们定义一条 SQL 语句并发送它,以便为它做好准备:

    $sql = 'SELECT hotelName, city, locality, '
        . 'country, postalCode FROM hotels '
        . 'WHERE country = ? AND city = ?';
    $stmt = $pdo->prepare($sql);
  3. 接下来,我们将执行准备语句,并将获取模式设置为 PDO::FETCH_ASSOC。请注意,当我们使用这种获取模式时,setFetchMode() 方法只需提供一个参数:

    $stmt->execute(['IN', 'Budhera']);
    $stmt->setFetchMode(PDO::FETCH_ASSOC);
    while ($row = $stmt->fetch()) var_dump($row);
  4. 最后,我们将第二次执行相同的准备语句。这一次,我们将把获取模式设置为 PDO::FETCH_CLASS。请注意,当我们使用这种获取模式时,setFetchMode() 方法将提供两个参数:

    $stmt->execute(['IN', 'Budhera']);
    $stmt->setFetchMode(
        PDO::FETCH_CLASS, ArrayObject::class);
    while ($row = $stmt->fetch()) var_dump($row);

第一个查询的输出是一个关联数组。第二个查询生成一个 ArrayObject 实例。这是输出:

root@php8_tips_php8 [ /repo/ch09 ]#
php php8_pdo_signature_change.php
array(5) {
    ["hotelName"]=> string(10) "Rose Lodge"
    ["city"]=> string(7) "Budhera"
    ["locality"]=> string(7) "Gurgaon"
    ["country"]=> string(2) "IN"
    ["postalCode"]=> string(6) "122505"
}
object(ArrayObject)#3 (6) {
    ["hotelName"]=> string(10) "Rose Lodge"
    ["city"]=> string(7) "Budhera"
    ["locality"]=> string(7) "Gurgaon"
    ["country"]=> string(2) "IN"
    ["postalCode"]=> string(6) "122505"
    ["storage":"ArrayObject":private]=> array(0) { }
}

值得注意的是,即使方法签名发生了变化,您也可以保持现有代码不变:这并不会造成向后兼容的代码中断!

现在,让我们来看看声明为静态的方法。

处理新定义的静态方法

PHP 8 中另一个潜在的重大变化是,有几个方法现在被声明为静态方法。如果您已经将这里描述的类和方法作为直接对象实例使用,那么就没有问题了。

以下方法现在被声明为静态方法:

  • tidy::repairString()

  • tidy::repairFile()

  • XMLReader::open()

  • XMLReader::xml()

如果重载前面提到的某个类,就有可能出现代码错误。在这种情况下,必须将重载方法声明为静态方法。下面是一个简单的示例,可以说明潜在的问题:

  1. 首先,让我们定义一个有不匹配 <div> 标记的字符串:

    // /repo/ch08/php7_tidy_repair_str_static.php
    $str = <<<EOT
    <DIV>
        <Div>Some Content</div>
        <Div>Some Other Content
    </div>
    EOT;
  2. 然后,定义一个扩展 tidy 的匿名类,固定字符串,并返回所有 HTML 标记都小写的字符串:

    $class = new class() extends tidy {
        public function repairString($str) {
            $fixed = parent::repairString($str);
            return preg_replace_callback(
                '/<+?>/',
                function ($item) {
                    return strtolower($item); },
                $fixed);
        }
    };
  3. 最后,回传修复后的字符串:

    echo $class->repairString($str);

如果在 PHP 7 中运行此代码示例,输出结果如下:

root@php8_tips_php7 [ /repo/ch09 ]#
php php7_tidy_repair_str_static.php
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div>
<div>Some Content</div>
<div>Some Other Content</div>
</div>
</body>
</html>

正如你所看到的,不匹配的 <div> 标记已经修复,一个格式正确的 HTML 文档已经生成。您还会注意到,所有的标记都是小写的。

不过,在 PHP 8 中,方法签名出现了问题,如图所示:

root@php8_tips_php8 [ /repo/ch09 ]#
php php7_tidy_repair_str_static.php
PHP Fatal error: Cannot make static method
tidy::repairString() non static in class tidy@anonymous in
/repo/ch09/php7_tidy_repair_str_static.php on line 11

如你所见,在 PHP 8 中,repairString() 方法现在被声明为静态方法。我们之前定义的匿名类中 repairString() 的方法签名需要重写,如下所示:

public static function repairString(
    string $str,
    array|string|null $config = null,
    ?string $encoding = null) { // etc.

改写后的输出结果(未显示)与前面显示的 PHP 7 输出结果相同。另外,请注意最后一行现在也可以写成下面的样子:

echo $class::repairString($str);

在了解了新定义为静态的方法后,让我们来看看一个相关的主题,即静态返回类型。

使用静态返回类型

在 PHP 中,static 关键字有多种用法。其基本用途超出了本节讨论的范围。在本节中,我们将重点讨论 static 作为返回数据类型的新用法。

由于 static 被认为是 self 的子类型,因此它可以用来扩大 self 的返回类型。不过,static 关键字不能用作类型提示,因为它违反了利斯科夫替换原则(Liskov Substitution Principle)。这也会让开发人员感到困惑,因为 static 已经在太多其他上下文中使用过了。

以下文章介绍了引入静态返回类型之前的背景讨论: https://wiki.php.net/rfc/static_return_type 。以下文档引用了晚期静态绑定: https://www.php.net/manual/en/language.oop5.late-static-bindings.php 。利斯科夫替换原则在第 5 章 "发现潜在的 OOP 向后兼容性中断" 中的 "理解扩展的 PHP 8 差异支持" 一节中进行了讨论。

这种新的返回数据类型最常用于使用 流畅接口 的类中。后者是一种对象方法返回当前对象状态实例的技术,从而允许以流畅(可读)的方式使用一连串的方法调用。在下面的示例中,请注意对象是如何构建 SQL SELECT 语句的:

  1. 首先,我们必须定义一个 Where 类,该类可接受数量不限的参数,以形成 SQL WHERE 子句。注意返回的数据类型是 static

    // /src/Php8/Sql/Where.php
    namespace Php8\Sql;
    class Where {
        public $where = [];
        public function where(...$args) : static {
            $this->where = array_merge($this->where, $args);
            return $this;
        }
        // not all code is shown
    }
  2. 现在,让我们定义主类 Select,它提供了构建 SQL SELECT 语句部分的方法。请再次注意,所有显示的方法都返回当前类实例,并且返回数据类型为 static

    // /src/Php8/Sql/Select.php
    namespace Php8\Sql;
    class Select extends Where {
        public $from = '';
        public $limit = 0;
        public $offset = 0;
        public function from(string $table) : static {
            $this->from = $table;
            return $this;
        }
        public function order(string $order) : static {
            $this->order = $order;
            return $this;
        }
        public function limit(int $num) : static {
            $this->limit = $num;
            return $this;
        }
        // not all methods and properties are shown
    }
  3. 最后,我们必须定义一个调用程序,提供创建 SQL 语句所需的值。请注意,echo 语句使用了流畅接口,使以编程方式创建 SQL 语句变得更加容易:

    // /repo/ch09/php8_static_return_type.php
    require_once __DIR__
        . '/../src/Server/Autoload/Loader.php';
    $loader = new \Server\Autoload\Loader();
    use Php8\Sql\Select;
    $start = "'2021-06-01'";
    $end = "'2021-12-31'";
    $select = new Select();
    echo $select->from('events')
        ->cols(['id', 'event_name', 'event_date'])
        ->limit(10)
        ->where('event_date', '>=', $start)
        ->where('AND', 'event_date', '<=', $end)
        ->render();

下面是在 PHP 8 中运行的示例代码的输出结果:

root@php8_tips_php8 [ /repo/ch09 ]#
php php8_static_return_type.php
SELECT id,event_name,event_date FROM events WHERE event_date >=
'2021-06-01' AND event_date <= '2021-12-31' LIMIT 10

当然,这个例子在 PHP 7 中不起作用,因为 static 关键字不能作为返回数据类型。接下来,让我们看看特殊 ::class 常量的扩展用法。

扩展 ::class 常量的使用

特殊的 ::class 常量是一个非常有用的结构,因为它可以无声地扩展为一个完整的命名空间,外加一个类名字符串。了解它的使用方法,以及 PHP 8 中对它的扩展,可以节省很多时间。使用它还能使代码更易读,尤其是在处理冗长的命名空间和类名时。

特殊的 ::class 常量是 作用域解析运算符::)和类关键字的组合。不过,与 ::parent::self::static 不同,::class 结构可以在类定义之外使用。从某种意义上说,::class 结构是一种神奇的常量,它能使与之关联的类神奇地扩展到其完整的命名空间,并加上类名。

在了解 PHP 8 如何扩展其用途之前,我们先来看看它的常规用法。

常规 ::class 常量用法

特殊的 ::class 常量常用于命名空间较长的情况,这样不仅可以节省大量不必要的键入,还可以保持源代码的可读性。

在这个简单的例子中,使用 Php7\Image\Strategy 命名空间,我们希望创建一个策略类列表:

  1. 首先,让我们确定命名空间并设置自动加载器:

    // /repo/ch09/php7_class_normal.php
    namespace Php7\Image\Strategy;
    require_once __DIR__
        . '/../src/Server/Autoload/Loader.php';
    $autoload = new \Server\Autoload\Loader();
  2. 在引入特殊的 ::class 常量之前,要生成完整命名空间类名的列表,必须将其全部写成字符串,如图所示:

    $listOld = [
        'Php7\Image\Strategy\DotFill',
        'Php7\Image\Strategy\LineFill',
        'Php7\Image\Strategy\PlainFill',
        'Php7\Image\Strategy\RotateText',
        'Php7\Image\Strategy\Shadow'
    ];
    print_r($listOld);
  3. 如图所示,使用特殊的 ::class 常量可以减少所需的键入量,还可以使代码更易读:

    $listNew = [
        DotFill::class,
        LineFill::class,
        PlainFill::class,
        RotateText::class,
        Shadow::class
    ];
    print_r($listNew);

如果我们运行这个示例代码,就会发现这两个列表在 PHP 7 和 PHP 8 中都是一样的。下面是 PHP 7 的输出结果:

root@php8_tips_php7 [ /repo/ch09 ]#
php php7_class_normal.php
Array (
    [0] => Php7\Image\Strategy\DotFill
    [1] => Php7\Image\Strategy\LineFill
    [2] => Php7\Image\Strategy\PlainFill
    [3] => Php7\Image\Strategy\RotateText
    [4] => Php7\Image\Strategy\Shadow
)
Array (
    [0] => Php7\Image\Strategy\DotFill
    [1] => Php7\Image\Strategy\LineFill
    [2] => Php7\Image\Strategy\PlainFill
    [3] => Php7\Image\Strategy\RotateText
    [4] => Php7\Image\Strategy\Shadow
)

正如您所看到的,特殊的 ::class 常量会导致类名在编译时扩展到其完整命名空间以及类名,从而导致两个列表包含相同的信息。

现在,让我们看看 PHP 8 中特殊的 ::class 常量用法。

扩展特殊 ::class 常量用法

与 PHP 8 在语法统一性方面的其他改进一致,现在可以在活动对象实例上使用特殊的 ::class 常量。虽然效果与使用 get_class() 相同,但使用特殊的 ::class 常量作为从过程式转向 OOP 的一般最佳实践的一部分是有意义的。

在本例中,使用了扩展的 ::class 语法来确定抛出错误的类型:

  1. 当抛出错误或异常时,最好的做法是在错误日志中记录一条信息。在本例中,错误或异常的类名包含在日志信息中,本例同时适用于 PHP 7 和 PHP 8:

    // /repo/ch09/php7_class_and_obj.php
    try {
        $pdo = new PDO();
        echo 'No problem';
    } catch (Throwable $t) {
        $msg = get_class($t) . ':' . $t->getMessage();
        error_log($msg);
    }
  2. 在 PHP 8 中,您可以通过重写示例来获得相同的结果,如下所示:

    // /repo/ch09/php8_class_and_obj.php
    try {
        $pdo = new PDO();
        echo 'No problem';
    } catch (Throwable $t) {
        $msg = $t::class . ':' . $t->getMessage();
        error_log($msg);
    }

从第二段代码可以看出,语法更加简洁,避免了使用过程函数。但我们必须强调,在这个示例中,性能并没有提高。

在了解了特殊 ::class 常量用法的变化后,让我们来快速了解一下逗号。

利用尾随逗号

一直以来,PHP 都允许在定义数组时使用逗号。例如,这里显示的语法并不少见:

$arr = [1, 2, 3, 4, 5,];

然而,在函数或方法签名中做同样的事情是不允许的:

function xyz ($fn, $ln, $mid = '',) { /* code */ }

虽然这并不是什么大不了的事,但在定义数组时可以添加尾逗号,而在函数或方法签名时却不能这样做,这还是挺烦人的!

PHP 8 现在允许在函数和方法签名中使用逗号。新规则也适用于与匿名函数相关的 use() 语句。

为了说明这一变化,请看下面的例子。在这个示例中,定义了一个匿名函数,该函数显示了一个全名:

// /repo/ch09/php8_trailing_comma.php
$full = function ($fn, $ln, $mid = '',) {
    $mi = ($mid) ? strtoupper($mid[0]) . '. ' : '';
    return $fn . ' ' . $mi . $ln;
};
echo $full('Fred', 'Flintstone', 'John');

如您所见,匿名函数的第三个参数后面有一个逗号。下面是 PHP 7 的输出结果:

root@php8_tips_php7 [ /repo/ch09 ]#
php php8_trailing_comma.php
PHP Parse error: syntax error, unexpected ')', expecting
variable (T_VARIABLE) in /repo/ch09/php8_trailing_comma.php on
line 4

在 PHP 8 中,允许使用尾随逗号并出现预期的输出,如下所示:

root@php8_tips_php8 [ /repo/ch09 ]#
php php8_trailing_comma.php
Fred J. Flintstone

虽然在函数或方法定义中使用尾逗号并不一定是最佳做法,但它确实使 PHP 8 对尾逗号的整体处理保持了一致。

现在,让我们把注意力转向那些仍然存在但不再有用的方法。

了解不再需要的方法

主要由于 PHP 8 资源到对象的迁移,一些函数和方法不再需要。在撰写本文时,它们并没有被废弃,但这些函数已不再有任何实际用途。

打个比方,在 PHP 8 之前的 PHP 版本中,使用 fopen() 打开一个文件句柄资源。完成文件操作后,通常会在文件句柄资源上使用 fclose() 关闭连接。

现在,我们假设使用 SplFileObject 代替 fopen()。当文件工作完成后,只需取消设置对象即可。这样做的目的与使用 fclose() 相同,从而使 fclose() 成为多余。

下表总结了在 PHP 8 中仍然存在、仍然可以使用但不再有任何实用价值的函数。标有星号的函数也已废弃:

Table 1. Table 9.1 – Functions that are no longer useful
扩展 不再使用的函数

cURL

curl_close()

curl_multi_close()

curl_share_close()

Exif

read_exif_data()

GD

imagedestory()

OpenSSL

openssl_x509_free() *

Shared Memory

shmop_close() *

XMLParser

xml_parser_free()

在了解了 PHP 8 对方法签名和用法的主要修改之后,让我们来看看在使用接口和特性(traits)时需要考虑的最佳实践。