PHP 8.4 Property Hooks (хуки свойств)

Источник: «PHP 8.4 Property Hooks»
Узнайте о новой функции property hooks (хуки свойств), которая будет добавлена в PHP 8.4 выходящем в ноябре 2024 г.

Введение

PHP 8.4 вышел и принёс с собой новую замечательную функцию: property hooks (хуки свойств).

В статье рассмотрим, что такое property hooks (хуки свойств) и как их использовать в проектах PHP 8.4.

В качестве примечания, возможно, будет интересно ознакомиться с другой статьёй, рассказывающей о новых функциях массивов, добавленных в PHP 8.4.

Что такое property hooks (хуки свойств) PHP

Хуки свойств позволяют определять пользовательскую логику получения и установки свойств класса без необходимости писать отдельные методы получения и установки. Это означает, что можно определить логику непосредственно в объявлении свойства, чтобы получить прямой доступ к свойству (например, $user->firstName) без необходимости помнить о вызове метода (например, $user->getFirstName() и $user->setFirstName()).

С RFC для этой функции можно ознакомиться по адресу https://wiki.php.net/rfc/property-hooks.

Если вы Laravel разработчик, то, читая эту статью, можете заметить, что хуки очень похожи на аксессоры и мутаторы в моделях Laravel.

Мне очень нравится внешний вид функции хуков свойств, и думаю, что это то, что буду использовать в своих проектах, когда выйдет PHP 8.4.

Чтобы понять, как работают хуки свойств, давайте рассмотрим несколько примеров их использования.

Хук get

Вы можете определить хук get, вызываемый всякий раз, когда пытаетесь получить доступ к свойству.

Например, представьте, что есть простой класс User, принимающий в конструкторе имена FirstName и LastName. Возможно, потребуется определить свойство fullName, объединяющее имя и фамилию. Для этого можно определить хук get для свойства fullName:

readonly class User
{
public string $fullName {
get {
return $this->firstName.' '.$this->lastName;
}
}

public function __construct(
public readonly string $firstName,
public readonly string $lastName
) {
//
}
}

$user = new User(firstName: 'ash', lastName: 'allen');

echo $user->firstName; // ash
echo $user->lastName; // allen
echo $user->fullName; // ash allen

В примере выше видно, что был определён хук get для свойства fullName, возвращающий значение, вычисляемое путём сложения свойств firstName и lastName. Можно ещё немного почистить эту функцию, используя синтаксис, похожий на синтаксис стрелочных функций:

readonly class User
{
public string $fullName {
get => $this->firstName.' '.$this->lastName;
}

public function __construct(
public readonly string $firstName,
public readonly string $lastName,
) {
//
}
}

$user = new User(firstName: 'ash', lastName: 'allen');

echo $user->firstName; // ash
echo $user->lastName; // allen
echo $user->fullName; // ash allen

Совместимость типов

Важно отметить, что возвращаемое значение геттера должно быть совместимо с типом свойства.

Если строгие типы (declare(strict_types=1);) не включены, значение будет приведено к типу свойства. Например, если возвращается целое число из свойства, объявленного как строка, целое число будет преобразовано в строку:

class User
{
public string $fullName {
get {
return 123;
}
}

public function __construct(
public readonly string $firstName,
public readonly string $lastName,
) {
//
}
}

$user = new User(firstName: 'ash', lastName: 'allen');

echo $user->fullName; // "123"

В приведённом выше примере, несмотря на то что значение 123 было указано как целое число, "123" возвращается как строка, поскольку свойство является строкой.

Мы можем добавить declare(strict_types=1); в верхнюю часть кода, включив строгую проверку типов:

declare(strict_types=1);

class User
{
public string $fullName {
get {
return 123;
}
}

public function __construct(
public readonly string $firstName,
public readonly string $lastName,
) {
//
}
}

Это приведёт к ошибке, поскольку возвращаемое значение — целое число, а свойство — строка:

Fatal error: Uncaught TypeError: User::$fullName::get(): Return value must be of type string, int returned

Хук set

Хуки свойств PHP 8.4 также позволяют определить хук set. Он вызывается каждый раз, когда выполняется попытка задать свойство.

Можно выбрать один из двух различных синтаксисов для хука set:

Давайте рассмотрим оба этих подхода. Представим, что нужно выводить первые буквы имени и фамилии в верхнем регистре, когда они задаются в классе User:

declare(strict_types=1);

class User
{
public string $firstName {
// Явно устанавливаем значение свойства
set(string $name) {
$this->firstName = ucfirst($name);
}
}

public string $lastName {
// Используем стрелочную функцию и возвращаем значение,
// которое нужно установить для свойства
set(string $name) => ucfirst($name);
}

public function __construct(
string $firstName,
string $lastName
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}

$user = new User(firstName: 'ash', lastName: 'allen');

echo $user->firstName; // Ash
echo $user->lastName; // Allen

Как видно из приведённого выше примера, для свойства firstName был определён хук set, переводящий первую букву имени в верхний регистр, прежде чем установить её в свойство. Также определили хук set для свойства lastName, использующий стрелочную функцию для возврата значения, которое нужно установить в свойство.

Совместимость типов

Если свойство содержит объявление типа, то и его хук set обязан содержать совместимый тип. Следующий пример вернёт ошибку, потому что хук set для firstName не содержит объявления типа, но само свойство содержит объявление типа string:

class User
{
public string $firstName {
set($name) => ucfirst($name);
}

public string $lastName {
set(string $name) => ucfirst($name);
}

public function __construct(
string $firstName,
string $lastName
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}```

Попытка выполнить приведённый выше код приведёт к возникновению следующей ошибки:

`
``php
Fatal error: Type of parameter $name of hook User::$firstName::set must be compatible with property type

Использование хуков get и set вместе

Вы не ограничены использованием хуков get и set по отдельности. Их можно использовать вместе в одном свойстве.

Рассмотрим простой пример. Представим, что класса User есть свойство fullName. Когда устанавливаем это свойство, то разбиваем полное имя на имя и фамилию. Я знаю, что это наивный подход и есть гораздо лучшие решения, но это чисто для примера, чтобы подчеркнуть хуки свойства.

Код может выглядеть следующим образом:

declare(strict_types=1);

class User
{
public string $fullName {
// Динамическое создание полного имени
// из имени и фамилии
get => $this->firstName.' '.$this->lastName;

// Разделение полного имени на имя и фамилию,
// а затем установка их в соответствующие свойства
set(string $name) {
$splitName = explode(' ', $name);
$this->firstName = $splitName[0];
$this->lastName = $splitName[1];
}
}

public string $firstName {
set(string $name) => $this->firstName = ucfirst($name);
}

public string $lastName {
set(string $name) => $this->lastName = ucfirst($name);
}

public function __construct(string $fullName) {
$this->fullName = $fullName;
}
}

$user = new User(fullName: 'ash allen');

echo $user->firstName; // Ash
echo $user->firstName; // Allen
echo $user->fullName; // Ash Allen

В приведённом выше коде было определено свойство fullName, имеющее оба хука — get и set. Хук get возвращает полное имя, соединяя имя и фамилию вместе. Хук set разбивает полное имя на имя и фамилию и устанавливает их в соответствующие свойства.

Также можно заметить, что мы не задаём значение самого свойства fullName. Вместо этого, если нужно прочитать значение свойства fullName, будет вызван хук get, собирающий полное имя из свойств firstName и lastName. Я сделал это для того, чтобы показать, что может быть свойство, у которого нет значения, установленного непосредственно для него, а вместо этого значение вычисляется из других свойств.

Использование хуков свойств на продвигаемых свойствах

Замечательная особенность хуков свойств заключается в том, что вы можно также использовать их со продвигаемыми свойствами конструктора.

Давайте рассмотрим пример класса, в котором не используются продвигаемые свойства, а затем посмотрим, как он может выглядеть при использовании продвигаемых свойств.

Класс User может выглядеть так:

readonly class User
{
public string $fullName {
get => $this->firstName.' '.$this->lastName;
}

public string $firstName {
set(string $name) => ucfirst($name);
}

public string $lastName {
set(string $name) => ucfirst($name);
}

public function __construct(
string $firstName,
string $lastName,
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}

Можно продвинуть свойства firstName и lastName в конструкторе и определить логику их установки непосредственно на свойстве:

readonly class User
{
public string $fullName {
get => $this->firstName.' '.$this->lastName;
}

public function __construct(
public string $firstName {
set (string $name) => ucfirst($name);
},
public string $lastName {
set (string $name) => ucfirst($name);
}
) {
//
}
}

Свойства с хуками доступные только для записи

Если вы определите свойство с сеттером, который на самом деле не устанавливает значение свойства, то это свойство будет только для записи. Это означает, что нельзя прочитать значение свойства, можно только установить его.

Возьмём класс User из предыдущего примера и изменим свойство fullName так, чтобы оно было доступно только для записи, удалив хук get:

declare(strict_types=1);

class User
{
public string $fullName {
// Определяем сеттер, который не устанавливает значение
// для свойства "fullName". Это сделает его свойством,
// доступным только для записи.
set(string $name) {
$splitName = explode(' ', $name);
$this->firstName = $splitName[0];
$this->lastName = $splitName[1];
}
}

public string $firstName {
set(string $name) => $this->firstName = ucfirst($name);
}

public string $lastName {
set(string $name) => $this->lastName = ucfirst($name);
}

public function __construct(
string $fullName,
) {
$this->fullName = $fullName;
}
}

$user = new User('ash allen');

echo $user->fullName; // Это вызовет ошибку!

Если запустить приведённый выше код, то при попытке получить доступ к свойству fullName возникнет следующая ошибка:

Fatal error: Uncaught Error: Property User::$fullName is write-only

Свойства с хуками доступные только для чтения

Аналогично, свойство может быть доступно только для чтения.

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

class User
{
public string $fullName {
get {
return $this->firstName.' '.$this->lastName;
}
}

public function __construct(
public readonly string $firstName,
public readonly string $lastName,
) {
$this->fullName = 'Invalid'; // Это вызовет ошибку!
}
}

Если попытаться выполнить приведённый выше код, то будет выброшена следующая ошибка, поскольку мы пытаемся установить свойство fullName напрямую:

Uncaught Error: Property User::$fullName is read-only

Использование ключевого слова readonly

Можно сделать PHP классы доступными для чтения, даже если у них есть свойства с хуками. Например, можно сделать класс User доступным только для чтения:

readonly class User
{
public string $firstName {
set(string $name) => ucfirst($name);
}

public string $lastName {
set(string $name) => ucfirst($name);
}

public function __construct(
string $firstName,
string $lastName,
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}

Однако свойство с хуком не может напрямую использовать ключевое слово readonly. Например, этот класс будет недопустим:

class User
{
public readonly string $fullName {
get => $this->firstName.' '.$this->lastName;
}

public function __construct(
string $firstName,
string $lastName,
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}

Приведённый выше код выбросит следующую ошибку:

Fatal error: Hooked properties cannot be readonly

Магическая константа PROPERTY

В PHP 8.4 появилась новая магическая константа __PROPERTY__. Эта константа может быть использована для ссылки на имя свойства внутри хука свойства.

Рассмотрим пример:

class User
{
// ...

public string $lastName {
set(string $name) {
echo __PROPERTY__; // lastName
$this->{__PROPERTY__} = ucfirst($name); // Вызовет ошибку!
}
}

public function __construct(
string $firstName,
string $lastName,
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}

В примере выше видно, что использование __PROPERTY__ в сеттере свойства lastName выведет имя свойства lastName. Однако стоит отметить, что попытка использовать эту константу для установки значения свойства приведёт к ошибке:

Fatal error: Uncaught Error: Must not write to virtual property User::$lastName

Удобный пример использования магической константы __PROPERTY__ можно посмотреть на GitHub: https://github.com/Crell/php-rfcs/blob/master/property-hooks/examples.md.

Свойства с хуками в интерфейсах

PHP 8.4 также позволяет определять публично доступные свойства с хуками в интерфейсах. Это может быть удобно, если необходимо обеспечить реализацию классом определённых свойств с помощью хуков.

Рассмотрим пример интерфейса с объявленными свойствами с хуками:

interface Nameable
{
// Ожидается публичное получаемое свойство 'fullName'
public string $fullName { get; }

// Ожидается публичное получаемое свойство 'firstName'
public string $firstName { get; }

// Ожидается публичное устанавливаемое свойство 'lastName'
public string $lastName { set; }
}

В приведённом выше интерфейсе мы определяем, что все классы, реализующие интерфейс Nameable, должны иметь:

Этот класс, реализующий интерфейс Nameable, будет валидным:

class User implements Nameable
{
public string $fullName {
get => $this->firstName.' '.$this->lastName;
}

public string $firstName {
set(string $name) => ucfirst($name);
}

public string $lastName;

public function __construct(
string $firstName,
string $lastName,
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}

Приведённый выше класс будет валидным, поскольку свойство fullName содержит хук get, что соответствует определению интерфейса. Свойство firstName имеет только хук set, но всё равно является публично доступным, поэтому удовлетворяет критериям. Свойство lastName не имеет хука get, но его можно публично установить, поэтому оно удовлетворяет критериям.

Давайте обновим класс User, чтобы реализовать хук get и set для свойства fullName:

interface Nameable
{
public string $fullName { get; set; }

public string $firstName { get; }

public string $lastName { set; }
}

Класс User больше не соответствует критериям для свойства fullName, потому что у него не определён хук set. Это приведёт к возникновению следующей ошибки:

Fatal error: Class User contains 1 abstract methods and must therefore be declared abstract or implement the remaining methods (Nameable::$fullName::set)

Свойства с хуками в абстрактных классах

Как и в случае с интерфейсами, можно определять свойства с хуками в абстрактных классах. Это может быть удобно, если необходимо создать базовый класс, определяющий свойства, которые должны реализовывать дочерние классы. Также можно определить хуки в абстрактном классе и переопределить их в дочерних классах.

Например, создадим абстрактный класс Model, определяющий свойство name, которое должно быть реализовано дочерними классами:

abstract class Model
{
abstract public string $fullName {
get => $this->firstName.' '.$this->lastName;
set;
}

abstract public string $firstName { get; }

abstract public string $lastName { set; }
}

В приведённом выше абстрактном классе определяем, что все классы, расширяющие класс Model, должны иметь:

Затем можно создать класс User, расширяющий класс Model:

class User extends Model
{
public string $fullName;

public string $firstName {
set(string $name) => ucfirst($name);
}

public string $lastName;

public function __construct(
string $firstName,
string $lastName,
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}

Заключение

Надеюсь, эта статья дала представление, как работают хуки свойств в PHP 8.4 и как их можно использовать в PHP проектах.

Я бы не стал сильно переживать, если эта функция покажется вам немного запутанной. Когда впервые её увидел, я тоже был немного озадачен (особенно тем, как они работают с интерфейсами и абстрактными классами). Но как только начнёте с ними возиться, вскоре всё поймёте.

Не терпится увидеть, как эта функция будет использоваться в природе, и с нетерпением жду возможности использовать её в своих проектах после выхода PHP 8.4.

Дополнительные материалы

Предыдущая Статья

CSS свойство contain

Следующая Статья

Всё о циклах в JavaScript