PHP 8 Constructor Property Promotion(构造器属性提升):DTO与值对象的极简定义

PHP 8 Constructor Property Promotion:DTO与值对象的极简定义

大家好,今天我们来聊聊 PHP 8 引入的一个非常棒的特性:Constructor Property Promotion(构造器属性提升)。它极大地简化了数据传输对象(DTO)和值对象(Value Object)的定义,让我们的代码更加简洁易懂。

什么是 Constructor Property Promotion?

在 PHP 8 之前,定义一个 DTO 或值对象通常需要大量的样板代码。我们需要先声明类的属性,然后在构造函数中接收参数,并将参数赋值给对应的属性。这不仅繁琐,还容易出错。

Constructor Property Promotion 允许我们在构造函数的参数列表中直接声明和初始化类的属性。 简单来说,就是把原本的属性声明、构造函数参数声明和赋值这三个步骤合并成一步。

示例(PHP 7 及更早版本):

class User
{
    private int $id;
    private string $name;
    private string $email;

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

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getEmail(): string
    {
        return $this->email;
    }
}

$user = new User(123, 'John Doe', '[email protected]');

使用 Constructor Property Promotion (PHP 8):

class User
{
    public function __construct(
        private int $id,
        private string $name,
        private string $email
    ) {}

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getEmail(): string
    {
        return $this->email;
    }
}

$user = new User(123, 'John Doe', '[email protected]');

可以看到,使用 Constructor Property Promotion 后,代码量减少了很多,可读性也得到了提升。 我们在构造函数的参数列表中,通过 private int $id 这样的方式,同时声明了属性 $id,并指定了其类型为 int,且将其访问权限设置为 private。 构造函数接收参数 $id,并自动将其赋值给属性 $this->id

访问修饰符的影响

Constructor Property Promotion 支持所有的访问修饰符:publicprotectedprivate。这决定了属性的可见性。

  • public: 属性可以在类的内部和外部访问。
  • protected: 属性只能在类的内部和子类中访问。
  • private: 属性只能在类的内部访问。

示例:

class Product
{
    public function __construct(
        public int $id,        // 可在类外部访问
        protected string $name,   // 只能在类内部和子类中访问
        private float $price      // 只能在类内部访问
    ) {}

    public function getName(): string
    {
        return $this->name; // 可以访问 protected 属性
    }

    public function getPrice(): float
    {
        return $this->price; // 可以访问 private 属性
    }
}

$product = new Product(1, 'Awesome Gadget', 99.99);

echo $product->id; // 可以访问 public 属性

// echo $product->name; // 报错:Cannot access protected property Product::$name
echo $product->getName(); // 正确:通过 public 方法访问 protected 属性

// echo $product->price; // 报错:Cannot access private property Product::$price
echo $product->getPrice(); // 正确:通过 public 方法访问 private 属性

DTO (Data Transfer Object) 的应用

DTO 是一种用于在不同层之间传输数据的对象。 它通常只包含数据,不包含业务逻辑。 Constructor Property Promotion 非常适合定义 DTO,因为它简化了类的结构。

示例:

假设我们需要定义一个 Address DTO 来表示地址信息。

class Address
{
    public function __construct(
        public string $street,
        public string $city,
        public string $state,
        public string $zipCode
    ) {}
}

$address = new Address('123 Main St', 'Anytown', 'CA', '91234');

echo $address->street; // 输出:123 Main St
echo $address->city;   // 输出:Anytown

在这个例子中,Address 类是一个简单的 DTO,只包含地址信息。 使用 Constructor Property Promotion,我们可以用非常少的代码来定义这个类。

值对象 (Value Object) 的应用

值对象是一种基于值的对象。 它的相等性不是基于身份(ID),而是基于其属性的值。 值对象通常是不可变的,也就是说,一旦创建,其值就不能被修改。

Constructor Property Promotion 也非常适合定义值对象,因为它简化了类的结构,并且可以方便地实现不可变性。

示例:

假设我们需要定义一个 Money 值对象来表示货币金额。

class Money
{
    public function __construct(
        private float $amount,
        private string $currency
    ) {}

    public function getAmount(): float
    {
        return $this->amount;
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }

    public function equals(Money $other): bool
    {
        return $this->amount === $other->amount && $this->currency === $other->currency;
    }

    // 为了实现不可变性,不提供任何修改属性的方法
}

$money = new Money(100.00, 'USD');
$anotherMoney = new Money(100.00, 'USD');
$differentMoney = new Money(50.00, 'EUR');

echo $money->getAmount(); // 输出:100
echo $money->getCurrency(); // 输出:USD

var_dump($money->equals($anotherMoney)); // 输出:true
var_dump($money->equals($differentMoney)); // 输出:false

// $money->amount = 200; // 报错:Cannot access private property Money::$amount

在这个例子中,Money 类是一个值对象。 它的相等性基于 amountcurrency 的值。 我们使用 private 访问修饰符来防止外部修改属性,从而实现不可变性。 虽然我们没有直接的setter方法,但是不可变性并不仅仅依赖于此,更重要的是类的设计理念。 如果需要表示不同的金额,我们应该创建一个新的 Money 对象,而不是修改现有的对象。

与默认值结合使用

Constructor Property Promotion 还可以与默认值结合使用,使得类的初始化更加灵活。

示例:

class Product
{
    public function __construct(
        public int $id,
        public string $name,
        public float $price = 0.00, // 默认值为 0.00
        public string $description = '' // 默认值为 空字符串
    ) {}
}

$product1 = new Product(1, 'Awesome Gadget'); // 使用默认值
$product2 = new Product(2, 'Another Gadget', 19.99, 'A really cool gadget'); // 覆盖默认值

echo $product1->price;       // 输出:0
echo $product1->description; // 输出:

echo $product2->price;       // 输出:19.99
echo $product2->description; // 输出:A really cool gadget

构造函数体内的其他逻辑

即使使用了 Constructor Property Promotion,你仍然可以在构造函数体内编写其他的逻辑。 例如,你可以进行参数验证,或者执行一些初始化操作。

示例:

class User
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email
    ) {
        if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email address');
        }
    }
}

try {
    $user = new User(1, 'John Doe', 'invalid-email');
} catch (InvalidArgumentException $e) {
    echo $e->getMessage(); // 输出:Invalid email address
}

$user = new User(2, 'Jane Doe', '[email protected]'); // 正确

在这个例子中,我们在构造函数体内对 email 参数进行了验证。 如果 email 不是一个有效的电子邮件地址,则抛出一个 InvalidArgumentException 异常。

继承与 Constructor Property Promotion

Constructor Property Promotion 在继承中也能很好地工作。 子类可以通过自己的构造函数来扩展父类的属性,并使用 parent::__construct() 来调用父类的构造函数。

示例:

class Animal
{
    public function __construct(
        public string $name,
        public string $species
    ) {}

    public function makeSound(): string
    {
        return 'Generic animal sound';
    }
}

class Dog extends Animal
{
    public function __construct(
        string $name,
        string $breed
    ) {
        parent::__construct($name, 'Dog'); // 调用父类的构造函数
        $this->breed = $breed; // 初始化子类自己的属性,不能用property promotion,因为父类已经声明过了name和species
    }

    public function getBreed(): string
    {
        return $this->breed;
    }

    public function makeSound(): string
    {
        return 'Woof!';
    }
}

$dog = new Dog('Buddy', 'Golden Retriever');

echo $dog->name;     // 输出:Buddy
echo $dog->species;  // 输出:Dog
echo $dog->getBreed(); // 输出:Golden Retriever
echo $dog->makeSound(); // 输出:Woof!

在这个例子中,Dog 类继承自 Animal 类。 Dog 类的构造函数调用了 Animal 类的构造函数,并初始化了自己的属性 breed。 注意,breed 属性 不能 使用 property promotion,因为父类构造函数中已经定义了namespecies,子类不能再用promotion来声明已经存在的属性。

总结:Constructor Property Promotion的优势

总的来说,Constructor Property Promotion 带来了以下优势:

  • 减少样板代码: 简化了 DTO 和值对象的定义。
  • 提高代码可读性: 使类的结构更加清晰。
  • 减少出错的可能性: 避免了手动赋值属性时可能出现的错误。
  • 与默认值和类型声明完美结合: 提供了更大的灵活性。

并非银弹:何时不宜使用

虽然Constructor Property Promotion 优势很多,但也有一些情况下不适合使用:

  • 复杂的初始化逻辑: 如果属性的初始化需要大量的计算或依赖于其他服务,最好将初始化逻辑放在构造函数体内,而不是直接在参数列表中初始化。
  • 需要延迟初始化: 如果属性的值只有在稍后的某个时刻才能确定,那么不适合在构造函数中立即初始化。
  • 与框架或库的兼容性问题: 某些老旧的框架或库可能不支持 Constructor Property Promotion。

更简洁,更易懂的代码

Constructor Property Promotion 是 PHP 8 中一个非常实用的特性,它极大地简化了 DTO 和值对象的定义,提高了代码的可读性和可维护性。 掌握这个特性,可以帮助我们编写更简洁、更易懂的 PHP 代码。

拥抱PHP 8,提升开发效率

希望通过今天的讲解,大家能够对 Constructor Property Promotion 有更深入的了解,并在实际开发中灵活运用。PHP 8 带来了许多令人兴奋的新特性,拥抱新版本,可以显著提升我们的开发效率。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注