В версии 8.4 наконец‑то появилась одна из тех фич, о которых давно мечтали многие, — хуки свойств. Что это такое? По сути, это встроенные механизмы get/set для свойств объектов, которые позволяют добавить свою логику при чтении или записи значения прямо внутри определения свойства. Никаких больше громоздких геттеров и сеттеров, никаких загадочных get и set, теперь всё можно сделать красиво и понятно на уровне самих свойств.

Вы наверняка успели привыкнуть к двум основным подходам.

Первый — старый добрый джава стиль: приватное свойство плюс парочка методов getSomething() и setSomething().

Второй — более интересный, появившийся с PHP 8.0, когда можно объявлять свойства прямо в конструкторе (constructor property promotion) и обойтись вообще без лишних методов.

Оба подхода работают, но у них есть минус, стоит захотеть добавить дополнительную логику при изменении или чтении свойства — например, валидацию, логирование или lazy‑инициализацию — и сделать этого нормально уже негде. Либо раздувать класс методами, либо прибегать к get/set.

Хуки свойств решили эту проблему. Теперь можно определять код для чтения и записи значения прямо в месте объявления свойства. При обращении к объекту привычным синтаксисом $obj->prop под капотом вызовется ваш код, как будто это не прямой доступ, а спрятанный метод. PHP позволяет перехватывать операции чтения/записи и переопределять их поведение индивидуально для каждого свойства. Класс может иметь как обычные свойства, так и свойства с хуками.

Причем это работает и для типизированных свойств, и для нетипизированных.

Синтаксис

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

class Demo {
    private bool $modified = false;
    public string $text = "Привет" {
        get {
            // если значение было изменено, добавим пометку при выводе
            if ($this->modified) {
                return $this->text . " (modified)";
            }
            return $this->text;
        }
        set(string $value) {
            // при присвоении всегда храним в нижнем регистре
            $this->text = mb_strtolower($value);
            $this->modified = true;
        }
    }
}

$demo = new Demo();
$demo->text = "Пока";          // при записи вызовется наш hook set
echo $demo->text;               // при чтении вызовется hook get
// Output: "пока (modified)"

Все очень просто, объявление свойства $text заканчивается фигурными скобками { } вместо точки с запятой. Это и есть признак того, что у свойства заданы хуки. Внутри определяем get и/или set по необходимости. В выше я реализовал оба хука. Когда присваиваем значение выполняется код внутри set. Когда читаем свойство срабатывает код из get. Если бы какой‑то из хуков не был указан, соответствующая операция просто работала бы как обычно,то есть прямое чтение или запись без дополнительной логики.

Хук set может принимать параметр,фактически это аргумент, представляющий присваиваемое значение. Здесь я явно указал тип string $value.Тип параметра должен быть либо таким же, как тип самого свойства, либо более широким. Например, для свойства типа DateTimeInterface хук set может объявить параметр типа string|DateTimeInterface, это ок, можно принять либо объект, либо строку и внутри сконвертировать. Но если свойство string, параметр не может быть просто array или что‑то совершенно другое, несочетаемое с string.

В нашем примере хук set приводит строку к нижнему регистру и отмечает флаг $modified. Хук get читает текущее значение $this->text и при необходимости дополняет его пометкой. Внутри хуков прежнему можем обращаться к самому свойству через $this->text, это не бесконечная рекурсия, PHP понимает, что вы хотите обратиться к реальному значению. Если хотя бы один из хуков ссылается на само свойство, как у нас, то у свойства всё‑таки есть реальное хранилище значения, и мы его используем.

Сокращённый синтаксис

Если хук короткий, например, просто возвращает или устанавливает значение без дополнительных действий, синтаксис можно сократить с помощью стрелочной нотации. Вместо фигурных скобок можно написать get => ... выражение ....

class DemoShort {
    private bool $modified = false;
    public string $text = "default" {
        get => $this->text . ($this->modified ? " (modified)" : "");
        set {
            // тут оставим ту же ло��ику в теле, поскольку более одной инструкции
            $this->text = mb_strtolower($value);
            $this->modified = true;
        }
    }
}

Хук get записан в одну строчку через =>. Такой вариант полностью эквивалентен предыдущему примеру, при чтении будет возвращена строка с пометкой, если значение менялось. Еще мы опустили объявление параметра $value в хуке set. Дело в том, что тип свойства и так string, поэтому PHP и сам поймет, что в коде set под этим именем $value доступно присваиваемое значение. Мы могли бы и вовсе сократить хук set до стрелочного вида, если бы он только вычислял новое значение. Например:

public string $foo = 'bar' {
    set => trim($value)
}

Такой set просто применил бы trim() к приходящему значению и сохранил результат в свойство. Но в нашем случае set делает два действия, поэтому мы оставили фигурные скобки.

Виртуальные свойства

Если хотя бы один хук обращается к самому свойству (через $this->имя), то у свойства есть реальное хранилище, и PHP хранит там значение, как обычно. В примере с $text это как раз так, хук get берёт $this->text. А вот если бы ни get, ни set не использовали само свойство, то оно считалось бы виртуальным, у класса не выделяется память под хранение такого значения.

Представим, что не нужно хранить значение свойства вообще, а хочется вычислять его динамически. Например, сделаем виртуальное свойство fullName, которое объединяет два других поля firstName и lastName:

class Person {
    private string $firstName;
    private string $lastName;
    public string $fullName {
        get => $this->firstName . ' ' . $this->lastName;
        set {
            // раскладываем присвоенную строку на две части
            [$this->firstName, $this->lastName] = explode(' ', $value, 2);
        }
    }
}

$person = new Person();
$person->fullName = 'Иван Иванов';  // хук set разобьёт строку на части
echo $person->fullName;            // хук get вернёт склеенное имя
// Output: "Иван Иванов"

Теперь fullName — виртуальное свойство. У него нет своего хранимого значения, оно используется как прослойка для логики. Не делаем $this->fullName = ... нигде внутри хуков, так что PHP не резервирует память под это свойство вовсе. При вызове $person->fullName будет каждый раз выполняться код в хуке get. При присвоении код в set.

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

Хуки и наследование

Хуки свойств прекрасно работают с наследованием. Можно переопределять свойство в потомке, задавая свои хуки или дополняя родительские. Более того, сами хуки можно объявлять как final, чтобы запретить дальнейшее переопределение в наследниках. Если пометить свойство или отдельный его хук ключевым словом final, то ни один потомок не сможет изменить эту логику.

class User {
    public string $username {
        final set => strtolower($value);
    }
}

class Admin extends User {
    public string $username {
        get => strtoupper($this->username);    // можно переопределить get
        set => strtoupper($value);             // ошибка, set был final в родителе
    }
}

В классе User сеттер помечен финальным, значит попытка его изменить в Admin недопустима. Зато геттер мы спокойно переопределили, он не был финальным. Также отметим, что свойство username сделали public и без явного хранения значения, в хуке get обращаемся $this->username, но это же свойство и есть, получается немного рекурсивно. На самом деле, когда мы в потомке обращаемся к $this->username внутри хуков, можно указать parent::$username::get() или ::set($val), чтобы вызвать реализацию хука родителя. Это похоже на parent::метод(), только для свойства.

Нюансы

Хуки свойств нельзя объявлять для readonly свойств, сама идея конфликтует с неизменяемостью. Еще одна интересность в том, что если попытаться изменять значение свойства по ссылке, то такой обход обойдет вызов хука set. PHP тут сдается и выполняет операцию напрямую. Так что имейте в виду,прямое изменение вложенных структур данных по ссылке минует логику хуков.

Также внутренние функции ведут себя по‑разному, некоторые читают/пишут свойства, минуя хуки. Например, var_dump($obj) выведет реальные значения свойств без выполнения ваших get хуков. А вот json_encode($obj) напротив, будет вызывать хуки get для сериализации свойств.


Конечно, главное не увлечься и не начать пихать всю бизнес‑логику в хуки. Но в меру и с умом это мощный инструмент.

Потянете программу курса PHP Pro? Пройдите тест и узнаете.
Потянете программу курса PHP Pro? Пройдите тест и узнаете.

Если идеи вроде хуков свойств вам близки, логичное продолжение — смотреть на PHP как на системный backend-инструмент. На курсе PHP Developer. Professional разбирают архитектуру, паттерны, тестирование и работу с инфраструктурой так, чтобы понимать, почему код масштабируется, а не просто «работает».

Чтобы узнать больше о формате обучения и познакомиться с преподавателями, приходите на бесплатные демо-уроки: