Хуки свойств, которые в ряде других языков ещё называются «аксессорами свойств», представляют способ перехвата и переопределения поведения объектов при чтении и записи свойства. Функциональность хуков преследует две цели:
Для нестатических свойств доступны два хука: get
и set
.
Хуки разрешают переопределять поведение чтения и записи свойства.
Хуки доступны как для типизированных, так и для нетипизированных свойств.
Классы поддерживают «реальные» и «виртуальные» свойства. Реальное свойство — то, которое хранит действительное значение. Свойства без хуков — реальные. Виртуальное свойство — то, для которого объявили хуки и эти хуки не взаимодействуют с самим свойством. Хуки виртуальных свойств — практически то же самое, что и методы, и объект не занимает места в памяти для хранения значения такого свойства.
Хуки свойств несовместимы с readonly
-свойствами.
Свойствам устанавливают асимметричную область видимости,
когда в дополнение к изменению поведения, которое вносят хуки get
и set
,
требуется ограничить доступ к операциям чтения или записи.
Общий синтаксис объявления хука следующий.
Пример #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
. Посмотрим на следующий пример:Внутренне движок разберёт этот код так: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, который определяет свойство, внутри сигнатуры конструктора определяет тип параметра, независимо от того, что разрешает хук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
readonly 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('Too small');
}
$this->x = $value;
}
}
}
?>
Каждый хук переопределяет родительские реализации независимо друг от друга. Значения по умолчанию, которые установили для свойств, удалятся и потребуется объявить значение заново, если дочерний класс добавляет хуки. Это согласуется с тем, как наследование работает в свойствах без хуков.
Хук в дочернем классе получает доступ к свойству родительского класса
через ключевое слово parent::$prop
, за которым следует хук, к которому требуется доступ.
Например, parent::$propName::get()
.
Это читается как «получить доступ к свойству prop, которое определили
в родительском классе, а затем выполнить операцию get», или операцию set, в зависимости от ситуации.
Хук родительского класса игнорируется, если доступ не получают таким способом. Такое поведение согласуется с тем, как работают методы. Такой синтаксис также открывает доступ к хранилищу родительского класса, если в классе содержится такое хранилище. Чтение или запись поведут себя по умолчанию, если в родительском свойстве нет хука. Хукам нельзя получать доступ к другим хукам, за исключением своих хуков в родительских классах.
Перепишем уже приведённый пример эффективнее.
Пример #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('Too small');
}
$this->x = $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 предусмотрели ряд способов сериализовать объект в производственной среде или в целях отладки. Поведение хуков зависит от сценария. В одних случаях берётся необработанное реальное значение свойства, а хуки обходятся. В других — свойство считывается или записывается «через» хук, как и другие стандартные операции чтения и записи.