Новое в журнале. Маленькие pull request: почему их быстрее ревьюят и как резать задачу
PHP и бэкенд

Property hooks и асимметричная видимость в PHP 8.4 на практике

PHP 8.4 добавил property hooks и асимметричную видимость, которые заменяют половину ручных геттеров и сеттеров. Разбираем синтаксис, реальные примеры и ситуации, где старый подход с явными методами всё ещё лучше.

21 апреля 20269 мин чтенияПрактикам
Property hooks и асимметричная видимость в PHP 8.4 на практике
2 новыхвозможности в одном релизе
1 местоописания правила вместо двух методов

PHP 8.4 принёс property hooks и асимметричную видимость, две возможности, которые напрямую отвечают на одну и ту же многолетнюю жалобу разработчиков: половина классов в типичном проекте состоит из свойства, геттера и сеттера, где сеттер делает одну проверку, а геттер просто возвращает значение без всякой логики. До 8.4 для этого требовалось писать явные методы, даже когда вся логика укладывалась в одну строку.

Как выглядели свойства до PHP 8.4

Классический способ защитить свойство от недопустимого значения выглядел одинаково в сотнях проектов: приватное поле, публичный геттер и публичный сеттер с проверкой внутри. Код предсказуем, но многословен, и большая часть такого кода одинакова от класса к классу.

// До PHP 8.4
final class Product
{
    private float $price;

    public function getPrice(): float
    {
        return $this->price;
    }

    public function setPrice(float $price): void
    {
        if ($price < 0) {
            throw new \InvalidArgumentException('Цена не может быть отрицательной');
        }

        $this->price = $price;
    }
}
Главная мысльProperty hooks не добавляют магии в PHP, они убирают ритуал из кода, который раньше требовал писать геттер и сеттер только чтобы проверить одно условие.

Property hooks: логика прямо на свойстве

Property hooks позволяют описать get и set логику прямо внутри объявления свойства, без отдельных методов и без скрытого приватного поля-дублёра. Снаружи класс выглядит так, как будто у него обычное публичное свойство, но при обращении к нему выполняется написанный код.

// PHP 8.4
final class Product
{
    public float $price {
        set(float $value) {
            if ($value < 0) {
                throw new \InvalidArgumentException('Цена не может быть отрицательной');
            }

            $this->price = $value;
        }
    }
}

$product = new Product();
$product->price = 9.99; // проходит через set-хук
echo $product->price;   // обычное обращение к свойству

Снаружи ничего не изменилось: вызывающий код обращается к $product->price как к обычному свойству, не зная, что внутри сработала проверка. Это главное преимущество хуков перед классическими геттерами и сеттерами: синтаксис вызова остаётся простым, а логика защиты никуда не исчезает. Для вычисляемых свойств хук get позволяет вернуть производное значение без отдельного поля вообще.

Например, у класса Rectangle с полями width и height свойство area можно объявить только через хук get, без собственного поля: оно вычисляется как width * height при каждом обращении, и отдельный метод getArea() больше не нужен.

Асимметричная видимость: читать всем, писать только изнутри

Вторая возможность PHP 8.4, асимметричная видимость, решает другую частую задачу: свойство должно быть публично читаемым, но изменяться только внутри самого класса. До 8.4 это требовало отдельного приватного поля и публичного геттера, формально дублирующего видимость. Теперь видимость для чтения и записи указывается раздельно прямо на объявлении.

final class Order
{
    public private(set) string $status = 'new';

    public function markAsPaid(): void
    {
        $this->status = 'paid'; // изменение изнутри разрешено
    }
}

$order = new Order();
echo $order->status;   // чтение снаружи разрешено
$order->status = 'x'; // Error: запись снаружи запрещена

Синтаксис public private(set) читается как «публично доступно для чтения, приватно доступно для записи», и в этом нет противоречия: видимость для получения и изменения значения теперь две независимые настройки, а не одна общая. Для сравнения с prop hooks: асимметричная видимость подходит, когда правило изменения простое, «только изнутри», а property hooks нужны, когда при изменении требуется выполнить дополнительную проверку или побочный эффект.

ЗадачаРешение в PHP 8.4Решение до PHP 8.4
Публичное чтение, запись только изнутри классаАсимметричная видимость private(set)Приватное поле плюс отдельный публичный геттер
Проверка значения при записиProperty hook setПриватное поле плюс сеттер с условием внутри
Вычисляемое значение без хранения в полеProperty hook get без поддерживающего поляОтдельный метод вроде getArea()
Вы в начале разбораДальше: разбор компромиссов, чек-лист и ответы на частые вопросы.

Подводные камни на практике

Первая ловушка: прямой доступ изнутри хука set к тому же свойству, которое объявляет хук, работает только через так называемое поддерживающее хранилище, backing storage, и в отличие от обычного поля требует внимания к именам, если в классе одновременно есть хук и реальное приватное поле с похожим назначением. Путаница между «свойством с хуком» и «обычным приватным полем плюс отдельное публичное свойство такого же имени» в одном классе легко приводит к трудноуловимым ошибкам.

Вторая ловушка: сериализация. Фреймворки и библиотеки, которые раньше читали приватные поля через рефлексию, например для маппинга в базу данных или в JSON, могут вести себя по-другому со свойствами, объявленными через хуки, особенно если библиотека не обновлена под PHP 8.4. Перед массовым переходом на хуки в проекте, где активно используется ORM или сериализатор, стоит проверить версии зависимостей через composer show и тесты сериализации на реальных классах.

Property hooks не заменяют валидацию на уровне доменной модели целиком. Если правило сложнее одной проверки и требует обращения к другим свойствам объекта или внешним данным, отдельный метод вроде changeStatus() остаётся понятнее, чем попытка упаковать всю логику в один хук.

Когда оставить обычные геттеры и сеттеры

Хуки хорошо подходят для value object и DTO с простыми инвариантами: цена не может быть отрицательной, email должен проходить базовую проверку формата, площадь вычисляется из двух сторон. Для сложных доменных операций, которые меняют несколько свойств согласованно и могут бросать разные исключения в зависимости от текущего состояния объекта, именованный метод вида cancel() или applyDiscount() остаётся понятнее для читателя кода, чем попытка закодировать всю логику внутри хука одного свойства.

  1. Использовать property hook, когда логика свойства укладывается в одну проверку или одно вычисление
  2. Оставлять обычный метод, когда операция затрагивает несколько свойств одновременно или имеет понятное бизнес-имя
  3. Проверить совместимость ORM и сериализаторов проекта с PHP 8.4 перед массовым переходом
  4. Не смешивать в одном классе хук и обычное приватное поле с похожим именем и назначением

Property hooks и асимметричная видимость не меняют возможности языка принципиально, PHP и раньше позволял написать защищённое свойство. Они меняют соотношение многословности и пользы: там, где раньше требовалось пять строк ритуального кода, теперь достаточно одной декларации, и это делает классы с простыми инвариантами заметно короче без потери ясности для читателя.

Интерфейсы и property hooks

Отдельный практический вопрос: как property hooks сочетаются с интерфейсами, которые традиционно описывают только сигнатуры методов. PHP 8.4 позволяет объявлять в интерфейсе требование к свойству с конкретным набором хуков, и класс, реализующий интерфейс, обязан предоставить совместимое свойство. Это полезно, когда нужно гарантировать, что любая реализация какого-то контракта предоставляет читаемое свойство с определённым поведением, а не просится реализовать отдельный метод-геттер вручную.

Например, интерфейс HasDisplayName может требовать публичного свойства displayName только для чтения, а класс Customer реализует его через хук get, который собирает отображаемое имя из firstName и lastName на лету, не храня отдельного поля вообще. Такой подход избавляет от классической проблемы рассинхронизации: если бы значение хранилось отдельно и обновлялось вручную при каждом изменении имени, легко забыть обновить его в одном из мест, где меняется имя клиента.

Производительность и повторные вычисления

Хуки get без поддерживающего поля вычисляют значение заново при каждом обращении к свойству, и для простых арифметических операций, как площадь прямоугольника, это не создаёт заметной нагрузки. Для вычислений, которые реально дороги, например обращение к внешнему сервису или сложная агрегация по коллекции из тысяч элементов, вызывать такую логику неявно через обращение к свойству может быть обманчивым: разработчик, читающий $report->summary, не ожидает, что это обращение запускает тяжёлый расчёт.

В таких случаях разумнее оставить явный метод вроде calculateSummary(), чтобы по виду вызова сразу было понятно, что происходит что-то не мгновенное, либо явно закешировать результат внутри хука при первом обращении и возвращать сохранённое значение при повторных. PHP не кеширует результат хука автоматически, кеширование, если оно нужно, разработчик реализует сам, обычно через приватное поле внутри класса, которое заполняется при первом вызове и проверяется на последующих.

Правило простое: если обращение к свойству внешне выглядит как мгновенное чтение значения, оно должно оставаться мгновенным и внутри. Тяжёлые вычисления лучше прятать за явным методом, а не за хуком, который выглядит как обычное поле.

Частые вопросы

Чем property hook отличается от обычного геттера и сеттера?

Снаружи обращение выглядит как обычное свойство, а не вызов метода. Логика проверки или вычисления описывается прямо в объявлении свойства, без отдельного приватного поля-дублёра и без пары явных методов.

Что такое асимметричная видимость в PHP 8.4?

Возможность указать разную видимость для чтения и записи одного свойства, например публичное чтение и приватную запись, без отдельного приватного поля и геттера.

Нужно ли переписывать все геттеры и сеттеры проекта на property hooks после обновления до PHP 8.4?

Нет. Хуки удобны для простых инвариантов, а для сложных доменных операций, меняющих несколько свойств сразу, именованный метод остаётся понятнее. Переход стоит делать выборочно, проверяя совместимость с ORM и сериализаторами.

Код, который не стыдно передать

Начните с принципов, а дальше двигайтесь по рубрикам. Каждый разбор заканчивается чек-листом.

Случайный разбор