В последний момент исправляю рефлексию в PHP 8.4!На неделе я начал добавлять поддержку PHP 8.4 в Typhoon Reflection, и очень рад, что взялся за это до релиза.
Во-первых, я зарепортил отсутствие обещанной в RFC хуков константы
ReflectionProperty::IS_VIRTUAL
. Но это тривиально, PR уже готов.
А вот что меня реально загрузило, так это поведение новых методов
ReflectionProperty::isPrivateSet()
и
isProtectedSet()
из Asymmetric Visibility RFC. Посудите сами:
final class Foo
{
// isPrivateSet() = true 👌
public private(set) mixed $public_private_set;
// isPrivateSet() = false 🤯
private private(set) mixed $private_private_set;
// isPrivateSet() = false 🤯
private mixed $private;
// isProtectedSet() = true 👌
public readonly mixed $public_readonly;
// isProtectedSet() = false 🤯
protected readonly mixed $protected_readonly;
// isProtectedSet() = false 🤯
protected protected(set) readonly mixed $protected_protected_set_readonly;
// isPrivateSet() = false 👌, isProtectedSet() = false 👌
public bool $virtual_no_set_hook { get => true; }
}
Сначала я подумал, что это баг, и создал тикет php-src#16175. Ilija, автор RFC, объяснил, почему так работает. Дело в том, что у свойств с симметричной видимостью отсутствует флаг ассиметричности. Поэтому для
private
и
private private(set)
isPrivateSet()
возвращают
false
, а не
true
, как было бы логично ожидать.
readonly
свойства без явного
(set)
под капотом получают
protected (set)
(см. "Relationship with readonly" в RFC), поэтому
public readonly
будет ассиметричным с
isProtectedSet() = true
, а
protected readonly
— симметричным с
isProtectedSet() = false
.
Стало понятно, вот только пользоваться такой рефлексией по-прежнему дико неудобно. В API протекли детали реализации, которые, наоборот, должны быть инкапсулированы. Задача рефлексии — предоставить пользователю комфортный способ изучать код, а не продемонстрировать подкапотное устройство языка.
Но это ещё ладно. Сегодня до меня дошло, что в текущем виде рефлексия в PHP 8.4 ломает обратную совместимость! Если раньше проверка
$reflectionProperty->isPublic()
гарантировала, что не
readonly
свойства доступны на запись и чтение из глобального скоупа, то при текущей реализации в 8.4 она будет гарантировать только чтение!
$reflectionProperty = new ReflectionProperty($class, $property);
if ($reflectionProperty->isPublic() && !$reflectionProperty->isReadonly()) {
$object->{$property} = $value;
}
Например, такой код корректно отработает для любых классов в PHP 8.3, но споткнётся на
public private(set) $property
в 8.4, потому что
isPublic()
вернёт
true
, а запись в свойство бросит ошибку.
Вот что я предложил в письме к internals, которое составил в Сапсане по дороге на Live PHP:
•
ReflectionProperty::isPublic()
,
isProtected()
и
isPrivate()
должны сохранить своё поведение и возвращать
true
только если свойство симметричное. Для
public readonly
метод будет возвращать
false
, потому что под капотом оно асимметричное с
protected(set)
. Да, это сломает обратную совместимость, но не так критично, потому что все библиотеки уже учитывают тот факт, что в
readonly
нельзя писать из глобального скоупа.
• Добавить
ReflectionProperty::isPublicGet()
,
isProtectedGet()
и
isPrivateGet()
. Они должны возвращать
true
, если свойство симметрично или асимметрично доступно на чтение.
• Добавить
ReflectionProperty::isPublicSet()
и поменять поведение
isProtectedSet()
и
isPrivateSet()
: они должны возвращать
true
, если свойство симметрично или асимметрично доступно на запись.
Вся проблема в том, что недавно вышел PHP 8.4 RC1, и вносить изменения уже поздно. Надеюсь, что мой посыл про нарушение обратной совместимости всё-таки убедит сделать исключение.