使用类型化属性改进代码

在本章第一节 "使用构造函数属性提升" 中,我们讨论了如何使用数据类型来控制作为函数或类方法参数的数据类型。然而,这种方法无法保证数据类型永不改变。在本节中,您将了解在属性级别分配数据类型如何在 PHP 8 中更严格地控制变量的使用。

什么是类型化属性?

这一极其重要的特性在 PHP 7.4 中引入,并在 PHP 8 中继续使用。简单地说,类型化属性 就是预先指定了数据类型的类属性。下面是一个简单的例子:

// /repo/ch01/php8_prop_type_1.php
declare(strict_types=1)
class Test {
    public int $id = 0;
    public int $token = 0;
    public string $name = '';
}
$test = new Test();
$test->id = 'ABC';

在本例中,如果我们试图为 $test->id 赋值,而不是代表 int 数据类型,就会出现致命错误。下面是输出结果:

Fatal error: Uncaught TypeError: Cannot assign string to
property Test::$id of type int in /repo/ch01/php8_prop_type_1.
php:11 Stack trace: #0 {main} thrown in /repo/ch01/php8_prop_type_1.php on line 11

从前面的输出中可以看出,当错误的数据类型被分配给一个类型化的属性时,就会抛出一个致命错误。

你已经接触过一种属性类型:构造函数属性提升。所有使用构造函数属性提升定义的属性都会自动被属性类型化!

为什么属性类型很重要?

类型化属性是 PHP 大趋势的一部分,最早出现在 PHP 7 中。现在的趋势是对语言进行改进,以限制和加强代码的使用。这将带来更好的代码,也就意味着更少的 bug。

下面的示例说明了仅仅依靠属性类型提示来控制属性数据类型的危险性:

// /repo/ch01/php7_prop_danger.php
declare(strict_types=1);
class Test {
    protected $id = 0;
    protected $token = 0;
    protected $name = '';
    public function __construct(
        int $id, int $token, string $name) {
        $this->id = $id;
        $this->token = md5((string) $token);
        $this->name = $name;
    }
}
$test = new Test(111, 123456, 'Fred');
var_dump($test);

在前面的示例中,请注意在 __construct() 方法中,$token 属性被意外转换成了字符串。输出结果如下:

object(Test)#1 (3) {
    ["id":protected]=> int(111)
    ["token":protected]=>
    string(32) "e10adc3949ba59abbe56e057f20f883e"
    ["name":protected]=> string(4) "Fred"
}

任何期望 $token 为整数的后续代码都可能失败或产生意想不到的结果。现在,我们来看看在 PHP 8 中使用类型属性做同样的事情:

// /repo/ch01/php8_prop_danger.php
declare(strict_types=1);
class Test {
    protected int $id = 0;
    protected int $token = 0;
    protected string $name = '';
    public function __construct(
        int $id, int $token, string $name) {
        $this->id = $id;
        $this->token = md5((string) $token);
        $this->name = $name;
    }
}
$test = new Test(111, 123456, 'Fred');
var_dump($test);

属性键入可以防止对预先分配的数据类型进行任何更改,如图所示的输出结果:

Fatal error: Uncaught TypeError: Cannot assign string to
property Test::$token of type int in /repo/ch01/php8_prop_danger.php:12

从前面的输出中可以看出,当错误的数据类型被赋值给一个类型化属性时,就会抛出一个致命错误。这个示例表明,为属性指定数据类型不仅可以防止直接赋值时的误用,还可以防止在类方法中误用属性!

属性类型可以减少代码量

在代码中引入属性类型的另一个有益的副作用是可能减少所需的代码量。举例来说,当前的做法是将属性标记为 privateprotected 的可见性,然后创建一系列 getset 方法来控制访问(也称为 getterssetters

以下是可能出现的情况:

  1. 首先,我们定义一个带有受保护属性的 Test 类,如下所示:

    // /repo/ch01/php7_prop_reduce.php
    declare(strict_types=1);
    class Test {
        protected $id = 0;
        protected $token = 0;
        protected $name = '';o
  2. 接下来,我们定义一系列的 get 和 set 方法来控制对受保护属性的访问,如下:

        public function getId() { return $this->id; }
        public function setId(int $id) { $this->id = $id;
        public function getToken() { return $this->token; }
        public function setToken(int $token) {
            $this->token = $token;
        }
        public function getName() {
            return $this->name;
        }
        public function setName(string $name) {
            $this->name = $name;
        }
    }
  3. 然后我们使用 set 方法来赋值,如下:

    $test = new Test();
    $test->setId(111);
    $test->setToken(999999);
    $test->setName('Fred');
  4. 最后,我们将结果显示在表格中,使用 get 方法检索属性值,如下所示:

    $pattern = '<tr><th>%s</th><td>%s</td></tr>';
    echo '<table width="50%" border=1>';
    printf($pattern, 'ID', $test->getId());
    printf($pattern, 'Token', $test->getToken());
    printf($pattern, 'Name', $test->getName());
    echo '</table>';

这可能是这样的:

Table 1. Table 1.4 – Output using Get methods

ID

111

Token

999999

Name

Fred

将属性标记为 protected(或 private)属性以及定义 getterssetters 的主要目的是控制访问。通常,这意味着需要防止属性的数据类型发生变化。在这种情况下,可以通过指定属性类型来替代整个基础结构。

只需将可见性改为 public,就可以减少对 getset 方法的需求;但这并不能防止属性数据被更改!使用 PHP 8 属性类型可以同时实现这两个目标:既不需要 getset 方法,又能防止数据类型被意外更改。

请注意,在 PHP 8 中使用属性类型实现同样的结果所需的代码量要少得多:

// /repo/ch01/php8_prop_reduce.php
declare(strict_types=1);
class Test {
    public int $id = 0;
    public int $token = 0;
    public string $name = '';
}
// assign values
$test = new Test();
$test->id = 111;
$test->token = 999999;
$test->name = 'Fred';
// display results
$pattern = '<tr><th>%s</th><td>%s</td></tr>';
echo '<table width="50%" border=1>';
printf($pattern, 'ID', $test->id);
printf($pattern, 'Token', $test->token);
printf($pattern, 'Name', $test->name);
echo '</table>';

前面的代码示例产生了与前面示例完全相同的输出结果,而且还实现了对属性数据类型的更好控制。在这个示例中,通过使用类型化属性,产生相同结果所需的代码量减少了 50%!

最佳实践:尽可能使用类型化属性,除非你明确希望允许数据类型发生变化。

总结

在本章中,你将学习如何使用新的 PHP 8 数据类型:混合类型和联合类型编写更好的代码。您还了解了使用命名参数不仅可以提高代码的可读性,还可以帮助防止意外误用类方法和 PHP 函数,以及提供一种跳过默认参数的好方法。

本章还介绍了如何使用新的属性类(Attribute)来最终替代 PHP 文档块(DocBlocks),从而提高代码的整体性能,同时为类、方法和函数的文档化提供可靠的手段。

此外,我们还了解了 PHP 8 如何利用构造函数参数推广和类型化属性,大大减少早期 PHP 版本所需的代码量。

在下一章中,您将了解 PHP 8 在功能和过程层次上的新特性。