Kodokon kodokon.com

现代面向对象:类、构造器属性提升与可见性

编写符合惯用法的 PHP 8 类:属性提升、readonly、命名构造函数以及不可变值对象。

9 分钟 · 3 题

在 Kodokon 中打开本课

你写类已经很久了,但 PHP 8 让好几个旧习惯变得过时。其中最显眼的一个:构造函数中的属性提升。在构造函数参数前加上一个可见性关键字(privateprotectedpublic),就能一步到位地创建该属性并为它赋值。旧写法中那三段重复的代码 - 声明、参数、赋值 - 就此合而为一。

PHP
<?php

declare(strict_types=1);

class LegacyProduct
{
    private string $name;
    private int $priceCents;

    public function __construct(
        string $name,
        int $priceCents,
    ) {
        $this->name = $name;
        $this->priceCents = $priceCents;
    }
}

final class Product
{
    public function __construct(
        private string $name,
        private int $priceCents,
    ) {
    }

    public function label(): string
    {
        return sprintf(
            '%s (%.2f EUR)',
            $this->name,
            $this->priceCents / 100,
        );
    }
}

$product = new Product('Keyboard', 4990);
echo $product->label();
前后对比:属性提升移除了所有样板赋值代码。

可见性仍然是你实现封装的首选工具:默认用 private,只有当子类需要访问时才用 protectedpublic 则作为最后的选择。自 PHP 8.1 起,readonly 提供了更强的保证:该属性只能被写入一次,而且只能在声明它的类的作用域内写入。与属性提升结合,只需几行代码就能造出不可变值对象:任何"修改"都会返回一个新实例,从而消除共享的副作用。

PHP
<?php

declare(strict_types=1);

final class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {
    }

    public function add(Money $other): self
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException(
                'Currency mismatch'
            );
        }

        return new self(
            $this->amount + $other->amount,
            $this->currency,
        );
    }
}

$total = (new Money(500, 'EUR'))
    ->add(new Money(250, 'EUR'));

echo $total->amount;
不可变值对象:add() 返回一个新实例。

在方法这一侧,有两种模式在资深开发者中占据主导。命名构造函数:一个 private 构造函数搭配静态工厂方法,让每个意图都清晰明确,并禁止不一致的实例化。流式接口:返回 $this(可变)或 new self(...)(不可变),让你能够链式调用。其中的取舍众所周知:不可变版本更安全,但每次调用都会分配一个对象 - 这在几乎所有场景下都可忽略不计,只有在热循环中才可测量。

PHP
<?php

declare(strict_types=1);

final class User
{
    private function __construct(
        public readonly string $email,
        public readonly string $role,
    ) {
    }

    public static function admin(string $email): self
    {
        return new self($email, 'admin');
    }

    public static function member(string $email): self
    {
        return new self($email, 'member');
    }
}

$user = User::admin('lea@example.com');
echo $user->role;
私有构造函数 + 静态工厂:只有合法的状态才能被构造出来。

知识检测

确认你已牢记本课的重点内容。

  1. 在 PHP 8 的构造函数中,属性提升做了什么?
    • 它自动把每个属性都变成 public
    • 它一步到位地声明属性并用参数为它赋值
    • 它为每个属性生成访问器(getter)
    • 它把属性转换成类常量
  2. readonly 属性什么时候可以被写入?
    • 永远不行:它必须在声明时就获得一个默认值
    • 恰好一次,且只能在声明它的类的作用域内
    • 任何时候都行,只要写入操作来自类本身
  3. 对于一个只应由类本身及其子类使用的属性,你应该选择哪种可见性?
    • public
    • protected
    • private