Хуки свойств, которые в ряде других языков ещё называются «аксессорами свойств», представляют способ перехвата и переопределения поведения объектов при чтении и записи свойства. Функциональность хуков преследует две цели:
Для нестатических свойств доступны два хука: get и set.
Хуки разрешают переопределять поведение чтения и записи свойства.
Хуки доступны как для типизированных, так и для нетипизированных свойств.
Классы поддерживают «реальные» и «виртуальные» свойства. Реальное свойство — то, которое хранит действительное значение. Свойства без хуков — реальные. Виртуальное свойство — то, для которого объявили хуки и эти хуки не взаимодействуют с самим свойством. Хуки виртуальных свойств — практически то же самое, что и методы, и объект не занимает места в памяти для хранения значения такого свойства.
Хуки несовместимы с readonly-свойствами.
Свойствам устанавливают асимметричную область видимости,
когда в дополнение к изменению поведения, которое вносят хуки get и set,
требуется ограничить доступ к операциям чтения или записи.
Замечание: Сведения о версии
Хуки свойств были представлены в PHP 8.4.
Общий синтаксис объявления хука следующий.
Пример #1 Пример полной версии объявления хуков для свойств
<?php
class Example
{
private bool $modified = false;
public string $foo = 'default value' {
get {
if ($this->modified) {
return $this->foo . ' (modified)';
}
return $this->foo;
}
set(string $value) {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
$example = new Example();
$example->foo = 'changed';
print $example->foo;
?>
Объявление свойства $foo заканчивается фигурными скобками {}, а не точкой с запятой.
Это указывает на то, что для свойства определили хуки.
В примере определили оба хука — и get, и set,
хотя разрешается определять отдельно один или другой.
Обоим хуках определили тела, которые также обозначили фигурными скобками {}.
В теле разрешается записывать произвольный код.
Хук set дополнительно разрешает указывать тип и название входящего значения,
тем же синтаксисом, которым в методах объявляют параметры.
Тип значения указывается либо с ограничением как у типа свойства,
либо контравариантным к значению —
с более широким, или слабым, ограничением.
Например, для свойства с типом string
в хуке set разрешается определять параметр, который принимает типы
string|Stringable,
но нельзя определить только тип array.
В примере по крайней мере один хук ссылается на само свойство $this->foo,
поэтому свойство «реально».
При вызове $example->foo = 'changed'
строка вначале приводится к нижнему регистру, затем сохраняется как реальное значение.
При чтении свойства сохранённое значение разрешается условно дополнить новым текстом.
Для обработки частых случаев предусмотрели ряд версий сокращенного синтаксиса.
Фигурные скобки {} опускают и заменяют стрелочным выражением,
если хук get состоит из одного выражения.
Пример #2 Пример свойства с хуком get из одного выражения
Этот пример эквивалентен предыдущему.
<?php
class Example
{
private bool $modified = false;
public string $foo = 'default value' {
get => $this->foo . ($this->modified ? ' (modified)' : '');
set(string $value) {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
?>
Параметр в объявлении хука разрешается опустить, если тип параметра в хуке set совпадает
с типом свойства, что встречается часто.
Тогда значению для установки автоматически присваивается имя $value.
Пример #3 Пример установки значения по умолчанию для свойства
Этот пример эквивалентен предыдущему.
<?php
class Example
{
private bool $modified = false;
public string $foo = 'default value' {
get => $this->foo . ($this->modified ? ' (modified)' : '');
set {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
?>
Хук set также можно упростить до выражения со стрелкой,
если хук только устанавливает изменённую версию значения аргумента.
Значение, которое вычислит выражение, установится как реальное значение свойства.
Пример #4 Пример установки значения свойства выражением в хуке set
<?php
class Example
{
public string $foo = 'default value' {
get => $this->foo . ($this->modified ? ' (modified)' : '');
set => strtolower($value);
}
}
?>
Этот пример не эквивалентен предыдущему,
поскольку он не изменяет значение свойства $this->modified, как предыдущий пример.
В теле хука set пользуются версией синтаксиса с фигурными скобками, когда требуется указать больше одной инструкции.
В свойствах разрешается реализовывать ноль, один или оба хука в зависимости от ситуации. Сокращённые версии синтаксиса хуков не зависят одна от другой. Поэтому допустимы и короткий синтаксис хука get с длинным синтаксисом хука set, и короткий синтаксис хука set с явным типом параметра, и другие комбинации.
Пропуск хука get или set в реальном свойстве означает,
что чтение или запись поведут себя по умолчанию.
Замечание: PHP разрешает определять хуки при продвижении параметров конструктора до свойств класса. При этом в конструктор потребуется передать значения, которые совпадают с типом свойства, независимо от типа, который разрешает хук
set. Посмотрим на следующий пример:Внутренне движок разберёт этот код так:<?php
class Example
{
public function __construct(
public private(set) DateTimeInterface $created {
set (string|DateTimeInterface $value) {
if (is_string($value)) {
$value = new DateTimeImmutable($value);
}
$this->created = $value;
}
},
) {}
}Установка свойства в классе, вне передачи аргументов в конструктор, разрешит значения с типом string или DateTimeInterface, но конструктор разрешит только тип DateTimeInterface. Причина этого состоит в том, что тип DateTimeInterface, который определяет свойство, внутри сигнатуры конструктора определяет тип параметра, независимо от того, что разрешает хук<?php
class Example
{
public private(set) DateTimeInterface $created {
set (string|DateTimeInterface $value) {
if (is_string($value)) {
$value = new DateTimeImmutable($value);
}
$this->created = $value;
}
}
public function __construct(
DateTimeInterface $created,
) {
$this->created = $created;
}
}set. Нельзя продвигать параметры конструктора до свойств класса, если от конструктора требуется такое поведение.
Виртуальными называются свойства без реальных значений.
Свойство станет виртуальным, если ни хук get,
ни хук set не ссылаются на само свойство точным синтаксисом.
Так, свойство с именем $foo, хук которого содержит ссылку на само свойство $this->foo, будет реальным.
Но свойство в следующем примере не относится к реальным и вызовет ошибку:
Пример #5 Пример недопустимого виртуального свойства
<?php
class Example
{
public string $foo {
get {
$temp = __PROPERTY__;
return $this->$temp; // Инструкция return не ссылается на реальное свойство $this->foo,
// поэтому это не считается
}
}
}
?>Попытка выполнить с виртуальным свойством операцию, для которой не объявили хук, вызовет ошибку, поскольку поведение по умолчанию для такой операции останется неопределённым. Виртуальные свойства не занимают места в памяти объекта. Виртуальные свойства помогают создавать «производные» свойства наподобие комбинации значений двух других свойств.
Пример #6 Пример виртуального свойства
<?php
class Rectangle
{
// Виртуальное свойство
public int $area {
get => $this->h * $this->w;
}
public function __construct(public int $h, public int $w) {}
}
$s = new Rectangle(4, 5);
print $s->area; // Выводит 20
$s->area = 30; // Ошибка, поскольку для свойства не определили комбинацию записи
?>
Для виртуального свойства разрешается определять оба хука, как get, так и set.
Хуки действуют в области видимости модифицуируемого объекта. Поэтому хукам доступны открытые, защищённые и закрытые методы и свойства объекта, включая свойства со своими хуками. Доступ к другому свойству из хука не обходит хуки, которые определили для другого свойства.
Следствие этого состоит в том, что нетривиальным хукам доступен вызов метода произвольной сложности, если потребуется.
Пример #7 Пример вызова метода из хука
<?php
class Person
{
public string $phone {
set => $this->sanitizePhone($value);
}
private function sanitizePhone(string $value): string
{
$value = ltrim($value, '+');
$value = ltrim($value, '1');
if (!preg_match('/\d\d\d\-\d\d\d\-\d\d\d\d/', $value)) {
throw new \InvalidArgumentException();
}
return $value;
}
}
?>
Поскольку хуки перехватывают процесс чтения и записи свойств,
они вызывают проблемы при получении ссылки на свойство или при косвенной модификации,
например, $this->arrayProp['key'] = 'value';.
Это связано с тем, что попытка изменения значения по ссылке обойдет хук set,
если его определили.
В редких случаях, когда требуется получить ссылку на свойство,
для которого определили хуки, перед хуком get разрешается добавлять префикс &,
чтобы заставить хук возвращать значение по ссылке.
Определение get и &get
для одного и того же свойства вызовет синтаксическую ошибку.
Нельзя определять одновременно хуки &get и set для реального свойства.
Как уже отмечалось, запись в значение, которое возвращается по ссылке, обойдет хук set.
Виртуальные свойства не хранят обязательного общего значения, которое хуки разделяют между собой,
поэтому хуки для чтения свойства разрешается определять с возвратом по значению или по ссылке.
Значения в индекс свойства-массива также неявно записываются по ссылке.
Поэтому запись в свойство с реальным массивом и хуками разрешается,
если только в свойстве определили единственный хук &get.
В виртуальном свойстве запись в массив, который возвращает хук get
или &get, возможна, но повлияет ли это
на объект, зависит от реализации хука.
Перезапись самого свойства массива возможна и ведёт себя как и другие свойства. Только работа с элементами массива требует осторожности.
Хуки разрешается также объявлять окончательными через ключевое слово final, тогда хуки нельзя переопределять.
Пример #8 Пример с окончательными хуками
<?php
class User
{
public string $username {
final set => strtolower($value);
}
}
class Manager extends User
{
public string $username {
// Это возможно
get => strtoupper($this->username);
// Но это недопустимо, поскольку хук set окончателен в родительском элементе
set => strtoupper($value);
}
}
?>Свойство тоже возможно объявить окончательным через ключевое слово final. Окончательное свойство нельзя повторно объявлять в дочернем классе, что исключает изменение хуков или ослабление доступа к операциям хуков.
Объявление окончательных хуков для окончательных свойств избыточно, и без предупреждения проигнорируется. Это то же поведение, что и у окончательных методов.
В дочернем классе разрешается определять или переопределять отдельные хуки для свойства путём переопределения свойства и только тех хуков, которые классу требуется переопределить. В дочернем классе также возможно добавить хуки к свойству, для которого хуки не определили прежде. Практически, это то же самое, как если бы хуки были методами.
Пример #9 Наследование хуков
<?php
class Point
{
public int $x;
public int $y;
}
class PositivePoint extends Point
{
public int $x {
set {
if ($value < 0) {
throw new \InvalidArgumentException('Число слишком маленькое');
}
$this->x = $value;
}
}
}
?>Каждый хук переопределяет родительские реализации независимо друг от друга. Значения по умолчанию, которые установили для свойств, удалятся и потребуется объявить значение заново, если дочерний класс добавляет хуки. Это согласуется с тем, как наследование работает в свойствах без хуков.
Хук в дочернем классе получает доступ к свойству родительского класса
через ключевое слово parent::$prop, за которым следует хук, к которому требуется доступ.
Например, parent::$propName::get().
Это читается как «получить доступ к свойству prop, которое определили
в родительском классе, а затем выполнить операцию get, или операцию set, в зависимости от ситуации.
Хук родительского класса игнорируется, если только доступ к родительскому хуку не выполняется непосредственно. Такое поведение соответствует работе методов. Синтаксис обращения к родительскому хуку также открывает доступ к хранилищу родительского класса, если родительский класс содержит такое хранилище. Чтение или запись поведут себя по умолчанию, если хук для родительского свойства не определили. Хукам нельзя получать доступ к другим хукам, за исключением своих хуков в родительских классах.
Проблема предыдущего примера состоит в том, что дочерний класс проигнорирует хук,
который добавят в родительском.
Модификация примера разрешит добавить хук set
в классе Point в будущем.
Пример #10 Доступ к родительскому хуку (set)
<?php
class Point
{
public int $x;
public int $y;
}
class PositivePoint extends Point
{
public int $x {
set {
if ($value < 0) {
throw new \InvalidArgumentException('Число слишком маленькое');
}
parent::$x::set($value);
}
}
}
?>Пример переопределения только хука get:
Пример #11 Доступ к родительскому хуку (get)
<?php
class Strings
{
public string $val;
}
class CaseFoldingStrings extends Strings
{
public bool $uppercase = true;
public string $val {
get => $this->uppercase
? strtoupper(parent::$val::get())
: strtolower(parent::$val::get())
;
}
}
?>В PHP предусмотрели ряд способов сериализовать объект в производственной среде или в целях отладки. Поведение хуков зависит от сценария. В одних случаях берётся необработанное реальное значение свойства, а хуки обходятся. В других — свойство считывается или записывается «через» хук, как и другие стандартные операции чтения и записи.