Kodokon kodokon.com

Современное ООП: классы, продвижение свойств, видимость

Пиши идиоматичные классы PHP 8: продвижение свойств, readonly, именованные конструкторы и неизменяемые объекты-значения.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

Ты давно пишешь классы, но PHP 8 сделал несколько привычек устаревшими. Самая заметная: продвижение свойств в конструкторе. Достаточно поставить модификатор видимости (private, protected, public) перед параметром конструктора - и свойство создаётся и присваивается за один шаг. Три повторяющихся блока старого стиля - объявление, параметр, присваивание - схлопываются в один.

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 по умолчанию, protected только если доступ нужен дочернему классу, public в последнюю очередь. Начиная с 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?
    • Автоматически делает все свойства публичными
    • Объявляет свойство и присваивает ему значение параметра за один шаг
    • Генерирует геттеры для каждого свойства
    • Превращает свойства в константы класса
  2. Когда можно записать свойство readonly?
    • Никогда: оно должно получить значение по умолчанию при объявлении
    • Ровно один раз, изнутри области видимости класса, который его объявляет
    • В любой момент, лишь бы запись шла из самого класса
  3. Какую видимость выбрать для свойства, которое должны использовать только сам класс и его потомки?
    • public
    • protected
    • private