PHP 8.4: Property hooks
Итак, получение свойства класса или установка его значения является обычной задачей в объектно-ориентированном программировании. В PHP есть несколько способов сделать это. Давайте сначала обсудим их.
Возьмем, к примеру, следующий класс.
class User
{
private string $name;
}
Как вы можете заметить, у нас в классе есть приватное свойство $
name. Теперь мы можем определить геттеры и сеттеры для чтения и записи значения свойства соответственно, что до PHP 7.4 можно было считать идиоматическим:
class User
{
private string $name;
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
}
$user = new User();
$user->setName('Sergey');
echo $user->getName(); // Sergey
Это обычный традиционный подход, и люди используют его уже давно.
Начиная с PHP 8 мы можем сократить код еще больше, используя объявление свойства в конструкторе:
class User
{
public function __construct(public string $name) {}
}
$user = new User('Sergey');
echo $user->name; // Sergey
Это довольно аккуратный подход, и он немного более краток при использовании методов получения и установки. Это намного приятнее, но за это приходится платить: если позже мы захотим добавить дополнительное поведение (например, проверку или предварительную обработку), то сделать это будет негде.
Мы также можем использовать магические методы __get
и __set
для достижения того же результата. Но это очень многословно, подвержено ошибкам и не подходит для инструментов статического анализа, таких как, например PHPStan.
В PHP 8.4 этот ключевой аспект будет улучшен за счет введения хуков свойств.
Propertry Hooks
Согласно этому RFC, хуки свойств - это новая функция PHP 8.4, которая позволит вам определять собственную логику для доступа к свойствам и их изменения. Это может быть полезно для различных случаев использования, таких как мутация, ведение логов, проверка или кэширование.
По сути, Propertry Hooks позволяют вам определять дополнительное поведение свойств класса, используя в основном два хука:
get
иset
. И это будет индивидуально для определенных свойств.
Приведенный ниже дизайн и синтаксис больше всего похож на Kotlin, хотя он также опирается на влияние C# и Swift. Python и JavaScript имеют схожие функции благодаря разному синтаксису, хотя этот синтаксис и непригоден для PHP. Ruby рассматривает свойства и методы почти одинаково, поэтому эта функциональность достигается как побочный эффект. Короче говоря, "средства доступа к свойствам" - очень распространенная функция в основных популярных языках программирования.
Что интересно, авторы просят не пугаться длинному тексту RFC, а предлагают отнестись к этому как глубоко проработанному материалу, отвечающему на все вопросы) Мы конечно пробежимся по самому главному, но если вы хотите узнать детали разработки, то можете почитать оригинальный RFC.
Хук set
Вот как мы можем написать set
для $
name
свойства в предыдущем примере:
class User
{
public string $name {
set (string $value) {
// валидация имени
if (mb_strlen(value) < 4)) {
throw new InvalidArgumentException(
'Invalid Name'
);
}
// Установка значения
$this->name = $value;
}
}
}
Как вы можете заметить, хуки заключаются в фигурные скобки, которые идут сразу после имени свойства. Затем мы можем определить хуки внутри этого блока кода.
Тело хука set
- это тело метода произвольной сложности, принимающее один аргумент. Если указано, оно должно включать как тип, так и имя параметра. Здесь мы можем проверить или изменить значение свойства до его установки.
Таким образом, когда свойству присвоено значение $
name, будет вызван хук set
, и значение будет проверено перед его установкой.
$user = new User();
$user->name = 'Ser'; // Должно вывести исключение InvalidArgumentException
$user = new User();
$user->name = 'Sergey';
echo $user->name; // Sergey
Существует также сокращенный синтаксис для определения хука set
с помощью оператора =>, похожий на замыкания:
class User
{
public string $name {
set => strtoupper($value);
}
}
Здесь
предполагается, что $value
это значение свойства, и для него будет вызвана функция strtoupper.
Хук get
Хук get
позволяет вам определить собственную логику для доступа к свойствам. Это может быть полезно для свойств, которые необходимо изменить, прежде чем они будут возвращены пользователю.
Например, если в классе User
есть два свойства $name
и $lastName
, мы можем определить get
для свойства $fullName
следующим образом:
class User
{
public function __construct(
public string $name, public string $lastName
) {}
public string $fullName {
get {
return $this->name . " " . $this->lastName;
}
}
}
$user = new User('Sergey', 'Mukhin');
echo $user->fullName; // Sergey Mukhin
Как вы можете заметить, хук get
не принимает никаких аргументов. Он просто возвращает значение свойства. Как и обычный геттер.
Таким образом, при доступе к значению $fullName
будет вызван хук get
, и значение будет возвращено на основе логики, определенной в хуке.
Так же существует сокращенный синтаксис для хука get
с помощью оператора =>
class User
{
public function __construct(
public string $name,
public string $lastName
) {}
public string $fullName {
get => string $this->name . " " . $this->lastName;
}
}
Это эквивалентно предыдущему примеру.
Использование хуков с интерфейсами
Чтобы устранить необходимость в геттерах и сеттерах, интерфейс должен иметь возможность указать, какие свойства он включает и таким образом в довесок этот RFC также добавляет возможность для интерфейсов асимметрично объявлять публичные свойства. Класс реализации может предоставить свойство через обычное свойство или хуки. Любого способа из них достаточно для удовлетворения интерфейса.
Рассмотрим сначала за пример код указанный в RFC:
interface I
{
// Класс, реализующий интерфейс ДОЛЖЕН иметь публичное свойство,
// но независимо от того, является ли оно публичным или нет - ограничений нет.
public string $readable { get; }
// Класс реализации ДОЛЖЕН иметь публичное свойство для записи,
// но независимо от того, доступно ли оно публичному чтению или нет - ограничений нет.
public string $writeable { set; }
// Класс реализации ДОЛЖЕН иметь свойство, которое является публичным и
// доступно для чтения и записи.
public string $both { get; set; }
}
Теперь возьмем что-нибудь из жизни и покороче:
interface User
{
// Объекты, реализующие этот интерфейс, ДОЛЖНЫ иметь возможность
// получить свойство $fullName. Этого можно добиться обычным
// свойством или свойство с хуком get.
public string $fullName { get; }
}
class Customer implements User
{
// Хук get необязателен, если он не указан,
// свойство будет доступно для чтения даже без хука get.
public function __construct(public string $fullName) {}
}
как видите, свойство $fullName
прекрасно читается и без хука get
. Но если мы определим хук get
для свойства, оно будет доступно для чтения именно с помощью хука get
.
Интерфейсы связаны только с публичным доступом, поэтому наличие закрытых свойств не зависит от интерфейса и не может удовлетворить интерфейс. Это та же самая связь, что и для методов. Ключевое слово public свойства необходимо для обеспечения согласованности синтаксиса (или его редко используемый псевдоним var).
Следует отметить, что свойство интерфейса, которое требует только get может удовлетворяться общедоступным readonly свойством, поскольку ограничения readonly применяются только при записи. Однако свойство интерфейса, которое требует set несовместимо с readonly свойством, поскольку публичная запись будет запрещена.
Магическая константа свойства
В хуке свойства специальная константа __PROPERTY__ определяется автоматически. Его значением будет установлено имя свойства. Это в основном полезно для повторения самоссылающегося кода, пример:
class User
{
private array $cache = [];
// ...
public string $fullName { get => $this->cache[__PROPERTY__] ??= $this->name . " " . $this->lastName; }
}
Взаимодействие с Trait'ами
Свойства в трейтах могут объявлять хуки, как и любое другое свойство. Однако, как и в случае с обычными свойствами, здесь нет механизма разрешения конфликтов, который имеется в методах. Если трейт и класс, в котором он используется, объявляют одно и то же свойство с помощью хуков, выдается ошибка. Ожидается, что это очень редкий крайний случай, и поэтому никаких дополнительных механизмов разрешения не требуется.
Работа с рефлексией
Появилось новое глобальное перечисление PropertyHookType. Он имеет поддержку строки, что позволяет при необходимости легко "преобразовать" примитивные значения:
enum PropertyHookType: string
{
case Get = 'get';
case Set = 'set';
}
На что следует обратить внимание
Есть вещи, которые следует учитывать при использовании хуков свойств.
Хуки доступны только в свойствах объекта. Таким образом, статические свойства не могут иметь хуков.
Хуки свойств переопределяют любое поведение свойства при чтении или записи.
Хуки свойств имеют доступ ко всем публичным, приватным или защищенным методам объекта, а также к любым публичным, приватным или защищенным свойствам, включая свойства, которые могут иметь свои собственные хуки свойств.
Установка ссылок на хуки свойств не допускается, поскольку любая попытка изменения значения по ссылке приведет к обходу хука set, если он определен.
Дочерний класс может определять или переопределять индивидуальные хуки свойства путем переопределения свойства и только тех хуков, которые он хочет переопределить. Тип и видимость свойства объекта независимо регулируются собственными правилами. Таким образом, каждый хук переопределяет родительские реализации независимо друг от друга.
В заключение
Хуки свойств - это мощная функция, позволяющая настраивать поведение свойств более понятным, кратким и гибким способом, чем другие подходы. Они особенно полезны, когда вы хотите добавить пользовательскую логику к свойствам, которые читаются или записываются объектом.
В RFC упоминается, что, хотя в настоящее время существует два хука свойств, в будущем есть возможность добавить больше, что сделает хуки свойств еще более мощными.
Можно только поблагодарить Илью Товило и Ларри Гарфилда, в очередной раз создавших столь прекрасный функционал!
Что думаешь?