## Хуки свойств

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

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

Для нестатических свойств доступны два хука: `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, поскольку тип 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
    {
        private bool $modified = false;

        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;
>                 }
>             },
>         ) {}
>     }

> Внутренне движок разберёт этот код так:

>     <?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;
>         }
>     }

> Установка свойства в классе, вне передачи аргументов в конструктор, разрешит значения с типом 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

    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 предусмотрели ряд способов сериализовать объект в производственной среде или в целях отладки. Поведение хуков зависит от сценария. В одних случаях берётся необработанное реальное значение свойства, а хуки обходятся. В других — свойство считывается или записывается «через» хук, как и другие стандартные операции чтения и записи.

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

`↑` and `↓` to navigate • `Enter` to select • `Esc` to close • `/` to open