PHP 8.4.2 Released!

Хуки свойств

Хуки свойств, которые в ряде других языков ещё называются «аксессорами свойств», представляют способ перехвата и переопределения поведения объектов при чтении и записи свойства. Функциональность хуков преследует две цели:

  1. Записывать и считывать значения свойств объектов напрямую, без методов получения и установки свойств, оставляя при этом открытым добавление дополнительного поведения в будущем. Это делает шаблонные геттеры и сеттеры ненужными, даже без хуков.
  2. Получать доступ к свойствам объекта без непосредственного хранения значений свойств.

Для нестатических свойств доступны два хука: 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;
}
},
) {}
}
Внутренне движок разберёт этот код так:
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;
}
}
Установка свойства в классе, вне передачи аргументов в конструктор, разрешит значения с типом string или DateTimeInterface, но конструктор разрешит только тип DateTimeInterface. Причина этого состоит в том, что тип DateTimeInterface, который определяет свойство, внутри сигнатуры конструктора определяет тип параметра, независимо от того, что разрешает хук 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 предусмотрели ряд способов сериализовать объект в производственной среде или в целях отладки. Поведение хуков зависит от сценария. В одних случаях берётся необработанное реальное значение свойства, а хуки обходятся. В других — свойство считывается или записывается «через» хук, как и другие стандартные операции чтения и записи.

  • Функция var_dump(): берёт необработанное значение
  • Функция serialize(): берёт необработанное значение
  • Функция unserialize(): берёт необработанное значение
  • Магические методы __serialize() и __unserialize(): реализуют пользовательскую логику с хуками чтения или записи
  • Приведение массива: берёт необработанное значение
  • Функция var_export(): задействует хук get
  • Функция json_encode(): задействует хук get
  • Интерфейс JsonSerializable: реализует пользовательскую логику с хуком get
  • Функция get_object_vars(): задействует хук get
  • Функция get_mangled_object_vars(): берёт необработанное значение
Добавить

Примечания пользователей

Пользователи ещё не добавляли примечания для страницы
To Top