PHP 8.4 Property Hooks (хуки свойств)
Введение
PHP 8.4 выйдет в ноябре 2024 года и принесёт с собой новую замечательную функцию: 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
, должны иметь:
- Свойство
fullName
, которое, по крайней мере, можно получить публично. Этого можно добиться, определив хукget
или не определяя хук вообще. - Свойство
firstName
, которое, по крайней мере, можно получить публично. - Свойство
lastName
, которое, по крайней мере, можно публично установить. Этого можно добиться, определив свойство, имеющее хукset
, или не определяя хук вообще. Но если класс доступен только для чтения, то свойство должно иметь хукset
.
Этот класс, реализующий интерфейс 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
, должны иметь:
- Свойство
fullName
, которое, по крайней мере, публично можно получить и установить. Этого можно добиться, определив хукиget
иset
или не определяя хуки вообще. Также был определён хукget
для свойстваfullName
в абстрактном классе, поэтому его не нужно определять в дочерних классах, но при необходимости его можно переопределить. - Свойство
firstName
, которое, по крайней мере, можно получить публично. Этого можно добиться, определив хукget
или не определяя хук вообще. - Свойство
lastName
, которое, по крайней мере, можно публично установить. Этого можно добиться, определив свойство, имеющее хукset
, или не определяя хук вообще. Но если класс доступен только для чтения, то свойство должно иметь хукset
.
Затем можно создать класс 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.