马路上的菱形标志是什么意思| 肾痛在什么位置痛| 性早熟有什么症状| 什么是签注| 2014是什么年| 收放自如是什么意思| 嫌疑人是什么意思| 脂肪瘤应该挂什么科| 帽子戏法是什么意思| 女人出汗多是什么原因| 什么时间喝牛奶最佳| 子午相冲是什么意思| 1月8日是什么星座| 指鹿为马是什么意思| 胃轻度肠化是什么意思| 现在去贵州穿什么衣服| 4月15日什么星座| 狗鱼是什么鱼| 什么叫高尿酸血症| 妇科支原体感染吃什么药| runosd是什么牌子的手表| 怕热出汗多是什么原因| sp是什么面料| 无痛人流和普通人流有什么区别| 12月6号是什么星座| 弱水三千是什么意思| 胰腺占位是什么意思| 人参和什么泡酒能壮阳| 女人鼻子大代表什么| 月经推迟半个月是什么原因| 水浒传主要讲了什么| 低密度脂蛋白偏高吃什么食物| 护士是什么专业| 减肥喝什么| 为什么一热身上就痒| 家里消毒杀菌用什么好| 无语凝噎是什么意思| 男生的鸡鸡长什么样| 长癣是什么原因引起的| 乳酸杆菌阳性什么意思| lee是什么牌子中文名| 孕妇牙痛有什么办法| 视觉感受器是什么| 什么叫浮小麦| 吃海参有什么功效| 风心病是什么病| 光斑是什么意思| 月子里生气有什么危害| bdsm什么意思| 中暑是什么感觉| 肾阴虚火旺有什么症状| 小三阳有什么症状表现| 堃是什么意思| 妈妈吃什么帮宝宝排气| 肠道菌群失调有什么症状| 狮子座与什么星座最配| 肾病钾高吃什么食物好| 918是什么日子| 女人舌苔厚白吃什么药| bosco是什么意思| 25岁属什么| 认知障碍是什么意思| 抽血血液偏黑什么原因| 窦性心律过速吃什么药| 什么是义眼| 伤口出水是什么原因| 火眼金睛是什么生肖| 唐僧被封为什么佛| 慢性非萎缩性胃炎伴糜烂是什么意思| 金骏眉茶是什么茶| 老年人生日送什么礼物| 11月17是什么星座| 小狗吃什么| 什么的李子| 为什么说黑鱼是鬼| 曼陀罗是什么意思| 金黄金黄的什么| hoka是什么牌子| 感统失调挂什么科| 高大的动物是什么生肖| 鹅蛋脸适合戴什么眼镜| 检查是否怀孕要做什么检查| 什么是淋巴肿瘤| 为什么闰月| 一起共勉是什么意思| 怀孕期间不能吃什么| 吃什么增强抵抗力| 甲钴胺有什么副作用| 5月22日什么星座| 尖锐湿疣的症状是什么| 二五八万是什么意思| 上海手表什么档次| 爸爸是什么意思| 什么东西助眠| 嬴稷是秦始皇的什么人| 尿胆原弱阳性什么意思| 破气是什么意思| 杰字属于五行属什么| 什么祛斑产品效果好| 衍心念什么| 卖身契是什么意思| 做四维需要准备什么| 脊髓灰质炎是什么病| 欺凌是什么意思| 柿子什么季节成熟| 做梦吃屎有什么预兆| 什么字属金| 牛奶不能和什么一起吃| 尿酸高的人吃什么食物好| 梦到自己长白头发是什么意思| 西地那非有什么副作用| 比基尼是什么意思| 属牛男和什么属相最配| 来月经前胸胀痛什么原因| 松花粉有什么功效| va是什么维生素| 人造石是什么材料做的| 养肝吃什么药| 变蛋吃多了有什么危害| 什么时候可以上环最好的| 黄色配什么颜色最搭| 冬虫夏草生长在什么地方| 梦见看房子是什么预兆| 50pcs是什么意思| 怀不上孕是什么原因| 热血病是什么病| 鳕鱼不能和什么一起吃| 静静地什么| 淋巴吃什么药好| m和s是什么意思| 蔬菜有什么| 课程是什么| 1.4是什么星座| ch4是什么气体| 海鸥手表属于什么档次| 泳帽什么材质的好| 想吃辣椒身体里缺什么| 什么价格| 椎体终板炎是什么病| 豫州是现在的什么地方| 梦见捉蛇是什么意思| 绿豆什么人不能吃| 心肌缺血吃什么药效果最好| 放屁多吃什么药好| 做ct挂什么科| 梓代表什么意思| 红豆薏仁水有什么功效| 吹箫什么意思| 腋窝痒是什么原因| ab型血可以给什么血型输血| 伤口化脓用什么药| 怀孕初期什么症状| 尖锐湿疣挂什么科| 指甲横纹是什么原因| 为什么头皮总是很痒| 小时的单位是什么| bpd是什么意思| 人为什么会近视| 7月18日什么星座| 屁股疼吃什么药| 劳动的反义词是什么| 为什么人会做梦| pa是什么单位| 黄热病是什么病| 什么国家的钱最值钱| 心里害怕紧张恐惧是什么症状| nary是什么牌子的手表| 肝内多发钙化灶是什么意思| 眉毛有什么作用| 看喉咙挂什么科| 槐米是什么| d3什么时候吃效果最好| 右脸麻木是什么原因| 尼维达手表什么档次| 1月21是什么星座| 神气活现是什么意思| 明天是什么生肖| 黄疸肝炎有什么症状| 头晕冒汗是什么原因| 肌酐高说明什么问题| 肠胃不好吃什么| 窈窕淑女君子好逑是什么意思| 菜板什么木材最好| 马黛茶什么味道| hivab是什么检测| 内膜厚吃什么掉内膜| 陪产假什么时候开始休| 为什么乳头会疼| ala是什么氨基酸| 眼角长脂肪粒是什么原因| 颈部淋巴结肿大吃什么药| 包皮是什么意思| 羊水是什么味道| 墨鱼干和什么煲汤最好| 什么水果去湿气效果最好| 什么牌子的手机好| 名什么中什么| 无花果有什么好处| 半夏反什么药| 乌鸦飞进家里什么征兆| 贱是什么意思| 肥胖去医院挂什么科| 大便失禁吃什么药| 勃起功能障碍吃什么药| 后巩膜葡萄肿是什么意思| 胃疼应该吃什么药| 吃什么对眼睛有好处| 下身瘙痒什么原因| 萤火虫为什么越来越少| 大三阳转小三阳意味着什么| 脂肪肝吃什么食物好| 血凝是什么意思| 肺阴不足的症状是什么| 16岁是什么年华| 肋下未及是什么意思| 啤酒花是什么东西| 前位子宫和后位子宫有什么区别| 10月19日什么星座| 纳气是什么意思| 宽字五行属什么| 打喷嚏代表什么| 什么是自慰| 左肺上叶纤维灶是什么意思| 什么花香| 什么是阻生智齿| 2月2日是什么星座| 脚面浮肿是什么原因| 什么病不能吃玉米| 艮五行属什么| 血脉是什么意思| 五行海中金是什么意思| 手指甲没有月牙是什么原因| 盆腔积液有什么症状有哪些| 什么是扬州瘦马| onlycook是什么牌子| 半边脸疼是什么原因引起的| 纨绔子弟什么意思| 鱼泡是鱼的什么器官| 中巴友谊为什么这么好| 什么是2B铅笔| 结婚28年是什么婚| 送女朋友什么礼物好| 尿频尿急尿不尽吃什么药效果最好| aquascutum是什么牌子| 27属什么| 候场是什么意思| oz是什么单位| 翡翠属于什么玉| 发晕是什么原因引起的| 七月十一日是什么日子| 指什么| 男人勃不起是什么原因造成的| 大熊猫生活在什么地方| 复方氨酚苯海拉明片是什么药| 支气管舒张试验阳性说明什么| 狗仔队是什么意思| 胆囊炎属于什么科| 五月份什么星座| 月经两个月没来是什么原因| 智商140是什么水平| app是什么缩写| 星月菩提是什么| 白带黄什么原因| 梦见手机屏幕摔碎了是什么意思| 百度
rfc:property-hooks

奥特曼兄弟大战僵尸小游戏 奥特曼兄弟大战僵

Introduction

百度 然而,至于巴基斯坦、也门、索马里、尼日尔和利比亚等地的反恐,战术从无人机定点清除开始是我们似乎乐于使用的。

Developers often use methods to wrap and guard access to object properties. There are several highly common patterns for such logic, which in practice may be verbose to implement repeatedly. Alternatively, developers may use __get and __set to intercept reads and writes generically, but that is a sledge-hammer approach that intercepts all undefined (and some defined) properties unconditionally. Property hooks provide a more targeted, purpose-built tool for common property interactions.

The combination of this RFC and the Asymmetric Visibility RFC effectively replicate and replace the previous Property Accessors RFC. Much of the implementation is derived from Nikita's original work on that RFC.

The design and syntax below is most similar to Kotlin, although it also draws influence from C# and Swift. Python and JavaScript have similar features via a different syntax, although that syntax would not be viable for PHP. (See the FAQ section below for an explanation.) Ruby treats properties and methods as nearly the same, so achieves this functionality as a side effect. In short, “property accessors” are a very common feature in major, mainstream programming languages.

A primary use case for hooks is actually to not use them, but retain the ability to do so in the future, should it become necessary. In particular, developers often implement getFoo/setFoo methods on a property not because they are necessary, but because they might become necessary in a hypothetical future, and changing from a property to a method at that point becomes an API change.

By allowing most common getFoo/setFoo patterns to be attached to properties directly, such behavior can be added to a property later without an API change and without the extra boilerplate of two mostly-meaningless methods for every property, “just in case.”

Methods that are not just variations on getFoo/setFoo behavior, of course, are still valuable in their own right.

Consider the following class declaration, which might have been considered idiomatic prior to PHP 7.4:

class User 
{
    private $name;
 
    public function __construct(string $name) {
        $this->name = $name;
    }
 
    public function getName(): string {
        return $this->name;
    }
 
    public function setName(string $name): void {
        $this->name = $name;
    }
}

As of PHP 8.3, if type enforcement is the only need, that can be abbreviated all the way down to:

class User 
{
    public function __construct(public string $name) {}
}

That is much nicer, but comes at a cost: If we later want to add additional behavior (such as validation or pre-processing), there's nowhere to do so. That currently leaves two options:

  1. Re-add getName() and setName() methods, making the property private or protected. This would be an API break.
  2. Use __get and __set. As shown below, this is verbose, ugly, error prone, and breaks static analysis tools.
class User 
{
    private string $_name;
 
    public function __construct(string $name) {
        $this->_name = $name;
    }
 
    public function __get(string $propName): mixed {
        return match ($propName) {
            'name' => $this->_name,
            default => throw new Error("Attempt to read undefined property $propName"),
        };
    }
 
    public function __set(string $propName, $value): void {
        switch ($propName) {
            case 'name':
                if (!is_string($value)) {
                    throw new TypeError("Name must be a string");
                }
                if (strlen($value) === 0) {
                    throw new ValueError("Name must be non-empty");
                }
                $this->_name = $value;
                break;
            default:
                throw new Error("Attempt to write undefined property $propName");
        }
    }
 
    public function __isset(string $propName): bool {
        return $propName === 'name';
    }
}

Property hooks allow developers to introduce additional behavior in a way that is specific to a single property while respecting all other existing aspects of PHP and its tooling.

class User 
{
    public string $name {
        set {
            if (strlen($value) === 0) {
                throw new ValueError("Name must be non-empty");
            }
            $this->name = $value;
        }
    }
 
    public function __construct(string $name) {
        $this->name = $name;
    }
}

This code introduces a new non-empty requirement, but does not change the outward syntax of reading or writing to $name, does not hinder static analysis, and does not fold multiple properties into a single hard-to-follow method.

Similarly, using methods may also impose an extra syntax burden on callers in “read and update” situations. For example:

class Foo
{
    private int $runs = 0;
 
    public function getRuns(): int { return $this->runs; }
 
    public function setRuns(int $runs): void
    {
      if ($runs <= 0) throw new Exception();
      $this->runs = $runs;
    }
}
 
$f = new Foo();
 
$f->setRuns($f->getRuns() + 1);

With property hooks, this can be simplified to:

class Foo
{
    public int $runs = 0 {
        set {
            if ($value <= 0) throw new Exception();
            $this->runs = $value;
        }
    }
}
 
$f = new Foo();
 
$f->runs++;

Which is much more ergonomic from the user's point of view. (A incrementRuns() method would also work in this case, but would only support the one single use case of incrementing, not general read and write.)

A note on the approach

This RFC has been designed to be as robust and feature-complete as possible. It is based on analysis of five other languages with similar functionality (Swift, C#, Kotlin, Javascript, and Python), and multiple experiments with PHP itself to find the corner cases. The RFC is, as a result, long and detailed, because we have chosen to make explicit all the necessary details presented and the reasoning behind them. Very little of this RFC could be “split off” to a future RFC (which would not be guaranteed to pass) without greatly undermining the design and capabilities of the remaining features.

A design goal of this RFC has been to make adding hooks to existing properties as transparent as possible, so that consumers of objects don't need to care if a property has hooks or not. In cases where it cannot be perfectly transparent, we have largely opted to follow the pattern of __get and __set, which already provide similar functionality in a far less robust or usable fashion. The result is to minimize the amount of thinking that developers need to do.

None of the decisions or inclusions have been arbitrary; PHP just has a lot of nooks and crannies, which this RFC has attempted to address in the least-edge-casey way possible.

In short, please don't be scared by the length or the number of moving parts. View it as a sign of polish and robustness instead.

Proposal Summary

This RFC introduces two “hooks” to override the default “get” and “set” behavior of a property. Although not included in this initial version, the design includes the ability to support more hooks in the future. (See the Future Scope section below.) Taken together, they allow for a majority of common reasons to add “just in case” methods to a property to be implemented without methods, leading to shorter code and more flexibility to improve the code without a hard API break.

There are two syntax variants supported, a full and a short, similar to closures. The example below shows both. (See the “Abbreviated Syntax” section below.)

class User implements Named
{
    private bool $isModified = false;
 
    public function __construct(private string $first, private string $last) {}
 
    public string $fullName {
        // Override the "read" action with arbitrary logic.
        get => $this->first . " " . $this->last;
 
        // Override the "write" action with arbitrary logic.
        set { 
            [$this->first, $this->last] = explode(' ', $value, 2);
            $this->isModified = true;
        }
    }
}

Additionally, as this functionality makes it natural to use a public property as part of an API, this RFC allows interfaces to declare properties and whether they should be readable, writeable, or both.

interface Named
{
    // Objects implementing this interface must have a readable
    // $fullName property.  That could be satisfied with a traditional
    // property or a property with a "get" hook.
    public string $fullName { get; }
}
 
// The "User" class above satisfies this interface, but so does:
 
class SimpleUser implements Named
{
    public function __construct(public readonly string $fullName) {}
}

Taken together, these behaviors allow for much shorter, more robust code.

Detailed Proposal

This RFC applies to object properties only, not static properties. Static properties are unaffected by this RFC. It applies to both typed and untyped object properties.

For a property to use a hook, it must replace its trailing ; with a code block denoted by { }. Inside the block are one or more hook implementations, for which the order is explicitly irrelevant. It is a compile error to have an empty hook block.

Properties with hooks may not be used in multi-property declarations. Doing so will trigger a syntax error.

The get and set hooks override the PHP default read and write behavior. They may be implemented individually or together.

When a hook is called, inside that hook $this->[propertyName] will refer to the “unfiltered” value of the property, called the “backing value.” When accessed from anywhere else, $this->[propertyName] calls will go through the relevant hook. This is true for all hooks on the same property. This includes, for example, writing to a property from the get hook; that will write to the backing value, bypassing the set hook.

A normal property has a stored “backing value” that is part of the object, and part of the memory layout of the class. However, if a property has at least one hook, and none of them make use of $this->[propertyName], then no backing value will be created and there will be no data stored in the object automatically (just as if there were no property, just methods). Such properties are known as “virtual properties,” as they have no materialized stored value.

Be aware, the detection logic works on $this->[propertyName] directly at compile time, not on dynamic forms of it like $prop = 'beep'; $this->$prop. That will not trigger a backing value.

get

The get hook, if implemented, overrides PHP's default read behavior.

class User
{
    public function __construct(private string $first, private string $last) {}
 
    public string $fullName {
        get { 
            return $this->first . " " . $this->last;
        }
    }
}
 
$u = new User('Larry', 'Garfield');
 
// prints "Larry Garfield"
print $u->fullName;

The get hook body is an arbitrarily complex method body, which MUST return a value that is type compatible with the property.

The example above creates a virtual property, as there is at least one hook and it does not use $this->fullName. Because it is virtual, there is no default set behavior (as there's nowhere to save to). Thus, any attempt to write to the property will result in an Error being thrown.

The following example does make use of $this->[propertyName], however, and thus a backing value will be created, and write operations will simply write to the property as normal.

class Loud
{
    public string $name {
        get {
            return strtoupper($this->name);
        }
    }
}
 
$l = new Loud();
$l->name = 'larry'; // The stored value is "larry"
 
print $l->name; // prints "LARRY"

In this example, $name is a stored property, so it may be freely written to (subject to scope visibility rules, of course). Read accesses, however, will go through the provided hook body, which capitalizes the value.

set

The set hook, if implemented, overrides PHP's default write behavior.

class User
{
    public function __construct(private string $first, private string $last) {}
 
    public string $fullName {
        set (string $value) {
            [$this->first, $this->last] = explode(' ', $value, 2);
        }
    }
 
    public function getFirst(): string {
        return $this->first;
    }
}
 
u = new User('Larry', 'Garfield');
 
$u->fullName = 'Ilija Tovilo';
 
// prints "Ilija"
print $u->getFirst();

The set hook body is an arbitrarily complex method body, which accepts one argument. If specified, it must include both the type and parameter name.

The above example creates a virtual property. As there is no get hook, no read operation from $fullName is allowed and will throw an Error. This particular usage pattern is not common, but valid.

More commonly, a virtual property will either be get only, or symmetric:

class User
{
    public function __construct(public string $first, public string $last) {}
 
    public string $fullName {
        get {
            return "$this->first $this->last";
        }
        set (string $value) {
            [$this->first, $this->last] = explode(' ', $value, 2);
        }
    }
 
}
 
u = new User('Larry', 'Garfield');
 
$u->fullName = 'Ilija Tovilo';
 
// prints "Ilija"
print $u->first;

Alternatively, the following example creates a stored property, and thus read actions will proceed as normal.

class User {
    public string $username {
        set(string $value) {
            if (strlen($value) > 10) throw new \InvalidArgumentException('Too long');
            $this->username = strtolower($value);
        }
    }
}
 
$u = new User();
$u->username = "Crell"; // the set hook is called
print $u->username; // prints "crell", no hook is called
 
$u->username = "something_very_long"; // the set hook throws \InvalidArgumentException.

We expect this “validate on set” use case to be particularly common.

A set hook on a typed property must declare a parameter type that is the same as or contravariant (wider) from the type of the property. That allows the set body to accept a more permissive set of values. The type of the value written to the backing value and returned by get must still conform to the declared type.

A set hook on an untyped property must not specify a parameter type.

That allows, for example, behavior like this:

use Symfony\Component\String\UnicodeString;
 
class Person
{
    public UnicodeString $name {
        set(string|UnicodeString $value) {
            $this->name = $value instanceof UnicodeString ? $value : new UnicodeString($value);        
        }
    }
}

That allows both strings and UnicodeString objects to be passed in, but normalizes the value to UnicodeString to enforce a consistent and reliable type when reading it (either internally or externally).

The set hook's return type is unspecified, and will silently be treated as void.

Although it is not often used, the = assignment operator is an expression that returns a value. The value returned is already slightly inconsistent, however. In the case of typed properties, that is the value the property holds after the assignment, which may include type coercion. For a property assignment that triggers __set, there is no reasonably defined “value the property holds”, so the value returned is always the right-hand-side of the expression. The set hook has the same behavior as __set, for the same reason.

class C {
    public array $_names;
    public string $names {
        set {
            $this->_names = explode(',', $value, 2);
        }
    }
}
$c = new C();
var_dump($c->names = 'Ilija,Larry'); // 'Ilija,Larry'
var_dump($c->_names); // ['Ilija', 'Larry']

In strict type mode, that means the only case where the result of the = operator changes is when assigning an int to a float. In weak mode, there are additional cases where implicit type casting would change the type, but not the value. These same changes already happen today with __set, using the evaluated value of = is rare, and at most can change the type of the resulting value in a coercion-compatible way. For that reason we consider that an acceptable edge case.

Abbreviated syntax

The syntax shown above is the “full-featured” version. There are several short-hand options available as well to cover the typical cases more easily.

Short-get

If the get hook is a single-expression, then the { } and return statement may be omitted and replaced with =>, just like with arrow functions. That is, the following two examples are equivalent:

class User
{
    public function __construct(private string $first, private string $last) {}
 
    public string $fullName {
        get { 
            return $this->first . " " . $this->last;
        }
    }
 
    public string $fullName {
        get => $this->first . " " . $this->last;
    }
}

Implicit ''set'' parameter

If the write-type of a property is the same as its defined type (this is the common case), then the argument may be omitted entirely. That is, the following two examples are equivalent:

public string $fullName {
    set (string $value) {
        [$this->first, $this->last] = explode(' ', $value, 2);
    }
}
 
public string $fullName {
    set {
        [$this->first, $this->last] = explode(' ', $value, 2);
    }
}

If the parameter is not specified, it defaults to $value. (This is the same variable name used by Kotlin and C#.)

Short-set

The set hook may also be shortened to a single expression using =>. In this case, the value the expression evaluates to will be assigned to the backing property. That is, the following two examples are equivalent:

class User {
    public string $username {
        set(string $value) {
            $this->username = strtolower($value);
        }
    }
 
 
    public string $username {
        set => strtolower($value);
    }
}

Note that, by implication, the short-set syntax implies a backing property. It is therefore incompatible with virtual properties. Using this syntax will always result in a backing property being defined.

Scoping

All hooks operate in the scope of the object being modified. That means they have access to all public, private, or protected methods of the object, as well as any public, private, or protected properties, including properties that may have their own property hooks. Accessing another property from within a hook does not bypass the hooks defined on that property.

The most notable implication of this is that non-trivial hooks may sub-call to an arbitrarily complex method if they wish. For example:

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

If a hook calls a method that in turn tries to read or write from the property again, that would normally result in an infinite loop. To prevent that, accessing the backing value of a property from a method called from a hook on that property will throw an Error. That is somewhat different than the existing behavior of __get and __set, where such sub-called methods would bypass the magic methods. However, as valid use cases for such circular logic are difficult to identify and there is added risk of confusion with dynamic properties, we have elected to simply block that access entirely.

References

Because the presence of hooks intercept the read and write process for properties, they cause issues when acquiring a reference to a property or with indirect modification (such as $this->arrayProp['key'] = 'value';).

That is because any attempted modification of the value by reference would bypass a set hook, if one is defined. For that reason, the presence of a set hook must necessarily also disallow acquiring a reference to a property or indirect modification on a property. For the vast majority of properties this causes no issue, as reading or writing to properties by reference is extremely rare.

class Foo
{
    public string $bar;
 
    public string $baz {
        get => $this->baz;
        set => strtoupper($value);
    }
}
 
$x = 'beep';
 
$foo = new Foo();
// This is fine; as $bar is a normal property.
$foo->bar = &$x;
 
// This will error, as $baz is a 
// set-hooked property and so references are not allowed.
$foo->baz = &$x;

If there is no set hook, however, there is nothing to bypass, so obtaining a reference via get is not inherently problematic. Returning by reference for a get-only property is therefore allowed. To do so, prefix the hook name with &:

class Foo
{
    public string $baz {
        &get {
          if ((!isset($this->baz)) {
            $this->baz = $this->computeBaz();
          }
          return $this->baz;
        }
    }
}
 
$foo = new Foo();
 
// This triggers the get hook, which lazily computes and caches the string.
// It then returns it by reference.
print $foo->baz;
 
// This obtains a reference to the baz property.
$temp =& $foo->baz;
 
// $foo->baz is updated to "update".
$temp = 'update';

A caller may only obtain a reference to a property that has declared &get. Attempting to get a reference on a get property will trigger an error.

The get and &get operations need to be separated to allow hooked properties to “opt-in” to sharing the underlying reference and allowing “spooky action at a distance” (by modifying the property through the reference). A get hook is protected from that leak automatically. If the property does use &get, it implies the class author is aware of that exposure situation and sees it as a feature, not a bug.

Implementing both get and &get simultaneously is a compile error.

There is one exception to the above: if a property is virtual, then there is no possible connection between the reference returned from &get and the property's backing value (given it doesn't have any). This makes it no different from a set of &getProp() + setProp() accessor methods that allow a reference to the underlying property to leak. We leave this possibility open as an opt-in way to achieve a higher degree of backwards compatibility, may it be needed, with the caveat that the class may not being aware of every change that may happen to the property.

class Foo
{
    private string $_baz;
 
    public string $baz {
        &get => $this->_baz;
        set {
            $this->_baz = strtoupper($value);
        }
    }
}
 
$foo = new Foo();
 
// This invokes "set", and sets $_baz to "BEEP".
$foo->baz = 'beep';
 
// This assigns $x to be a reference directly to $_baz
$x =& $foo->baz;
 
// This assigns "boop" to $_baz, bypassing the set hook.
$x = 'boop';

Setting by reference, however, is not supported. It may be possible to add in the future, but its complexity is too large to handle here. See the “Assignment by Reference” section under Future Scope for more details.

This behavior mirrors how the magic methods __get() and __set() handle references. (They are, in a sense, generic virtual properties.)

To summarize:

  • Backed properties
    • get - Legal, may not assign by reference.
    • get/set - Legal, may not assign by reference.
    • &get - Legal, may assign by reference.
    • &get/set - Illegal, compile error.
    • set - Legal, may not assign by reference
  • Virtual properties
    • get - Legal, may not assign by reference.
    • get/set - Legal, may not assign by reference.
    • &get - Legal, may assign by reference.
    • &get/set - Legal, may assign by reference.
    • set - Legal, but not particularly useful.

Be aware that a &get hook may return a value by reference that does not correspond to a property of the object. This is true for both backed and virtual properties. In that case, writing to the returned value may not have the expected effect.

class C {
    public string $a { 
        &get { 
            $b = $this->a;
            return $b;
        }
}
$c = new C();
$c->a = 'beep';
// $c is unchanged.

This concern is the same as for a &getA() method, however, so it is not an edge case limited to hooks.

Additionally, iterating an object's properties by reference will throw an error if it encounters a property that has a hook defined. (It will not error until it reaches that property.)

foreach ($someObjectWithHooks as $key => $value) {
    // Iterates all in-scope properties, using the 'get' operation if defined. 
}
 
foreach ($someObjectWithHooks as $key => &$value) {
    // Throws an error if any in-scope property has a hook.
}

Arrays

There is an additional caveat regarding arrays. Modifications to arrays stored in properties may happen “in-place”, meaning without causing a copy of the array. However, there's no way to achieve this behavior with by-value getter and setter methods.

class Test {
    public $array = [];
 
    public function getArray() {
        echo "getArray()\n";
        return $this->array;
    }
 
    public function setArray($array) {
        echo "setArray()\n";
        $this->array = $array;
    }
}
 
$test = new Test();
 
// This is what we actually want. The array is modified directly, without any performance overhead.
$test->array[] = 'foo';
 
// getArray() returns a temporary value, modifying it has no effect. This approach does not work.
$test->getArray()[] = 'foo';
 
// Storing the value from getArray() in a temporary variable, modifying it and assigning it back
// works as expected.  However, there's an implicit copy on line 2, because the array is referenced
// from both $array and $test->array. The array is copied, just for the copy to immediately
// overwrite the original value.
$array = $test->getArray();
$array[] = 'foo';
$test->setArray($array);

The obvious solution to this is to return from getArray by reference.

class Test {
    // ...
    public function &getArray() {
        echo "getArray()\n";
        return $this->array;
    }
    // ...
}
 
// Now it works!
$test->getArray()[] = 'foo';

However, this comes a significant issue: setArray expects to observe changes to $array, but to no avail. An in-place array modification consists of calling &getArray and modifying the array stored in the reference. At no point is setArray invoked.

These problems exist in the exact same way for hooks.

class Test {
    public $array {
        &get {
            echo "getArray()\n";
            return $this->array;
        }
        set {
            echo "setArray()\n";
            $this->array = $value;
        }
    }
}
 
$test = new Test();
// Appending to an array invokes &get and modifies 
// the array stored in the returned reference, bypassing
// the set hook entirely.
$test->array[] = 'foo';

Two mitigations immediately come to mind, but they each come with significant limitations.

  1. We may introduce various hooks for array modification. For example, $test->array[] = 'foo' may invoke an offsetAppend hook. unset($test->array['foo']) may call an offsetUnset hook, etc. However, a complete solution is impossible. Imagine sort($test->array), along with all the other functions that modify the array by-reference. They can make arbitrary changes to the array, which are not directly translatable to a hook.
  2. We may create an implicit copy to the array, and silently pass it back to set. This comes with the aforementioned performance issue. Moreover, assuming a large array is passed to set containing validation, the set hook is burdened with the task of figuring out what in the array has changed, or rechecking it in full.

Neither of these approaches are satisfactory. Because all API solutions that come to mind are bad, it is the opinion of the authors that for arrays, dedicated mutator methods with a narrow contract are always the superior API choice.

class Test {
    private $_array;
    public $array {
        get => $this->_array;
    }
 
    public function addElement($value) {
        // We can validate $value, without re-validating the entire array.
        $this->_array[] = $value;
    }
}
 
$test = new Test();
$test->addElement('foo');

With Asymmetric Visibility that was previously proposed, the example can be further simplified.

// This example not provided by this RFC.
// It's just to show how asymmetric visibility would solve this use case better.
class Test {
    public private(set) $array;
 
    public function addElement($value) {
        $this->array[] = $value;
    }
}

It would also be possible to make addElement() private/protected, in order to simulate private-write, public-read properties.

For these reasons, we have disallowed intrinsically-reference array operations ([] and writing to ['foo']) on array properties when a set hook is present.

Here is a exhaustive list of possible hook combinations and their supported operations.

To summarize:

Property type Hooks Reading index Writing index Write whole array
Example $a->arr[1]; $a->arr[1] = 2 $a->arr = $arr2
Backed get Allowed Illegal Allowed
Backed &get Allowed Allowed Allowed
Backed get/set Allowed Illegal Allowed
Backed &get/set Illegal for any backed property
Backed set Allowed Illegal Allowed
Virtual get Allowed Illegal Illegal
Virtual &get Allowed Allowed Illegal
Virtual get/set Allowed Allowed Allowed
Virtual &get/set Allowed Allowed Allowed
Virtual set? Illegal Illegal Allowed

? A set-only virtual property is allowed, but probably not useful in practice.

Of note, &get-only allows for lazy-initialization of backed properties that still behave “normally” as far as index writing goes. For example, a lazy-initialized array that is then “fully public” thereafter:

class C
{
    public array $list {
        &get {
          $this->list ??= $this->defaultListValue();
          return $this->list;
        }
    }
 
    private function defaultListValue() {
        return ['a', 'b', 'c'];
    }
}
 
$c = new C();
 
print $c->list[1]; // prints b
 
// This calls the &get hook, which returns a reference
// to the backing value.  Then this code modifies that reference
// to append a value.  This is allowed, as there is no set hook.
$c->list[] = 'd';
 
print count($c->list); // prints 4

The array-offset rules are enforced at runtime, as we cannot reliably tell at compile time what the type and hooks of the property will be. (It may be defined in a different file.)

Default values

Default values are supported on properties that have a backing store. Default values are not supported on virtual properties, as there is no natural value for the default to be assigned to, and will be treated as a compile-time error.

Of note, the default value is assigned directly, and not passed through the set hook. All subsequent writes will go through the set hook. This is primarily to avoid confusion or questions about when, exactly, the set hook should run during object initialization, and is consistent with how Kotlin handles it as well.

Default values are listed before the hook block.

class User
{
    public string $role = 'anonymous' {
        set => strlen($value) <= 10 ? $value : throw new \Exception('Too long');
    }
}

Inheritance

A child class may define or redefine individual hooks on a property by redefining the property and just the hooks it wishes to override. The type and visibility of the property are subject to their own rules independently of this RFC.

A child class may also add hooks to a property that had none.

class Point
{
    public int $x;
    public int $y;
}
 
class PositivePoint extends Point
{
    public int $x {
        set {
            if ($value < 0) {
                throw new \InvalidArgumentException('Too small');
            }
            $this->x = $value;
        }
    }
}

Each hook overrides parent implementations independently of each other.

If a child class adds hooks, any default value set on the property is removed. That is consistent with how inheritance works already; if a property is redeclared in a child, its default is removed or must be re-assigned.

Accessing parent hooks

A hook in a child class may access the parent class's property using the parent::$prop keyword, followed by the desired hook. For example, parent::$propName::get(). It may be read as “access the $prop defined on the parent class, and then run its get operation” (or set operation, as appropriate).

If not accessed this way, the parent class's hook is ignored. This behavior is consistent with how all methods work. This also offers a way to access the parent class's storage, if any. If there is no hook on the parent property, its default get/set behavior will be used.

That is, the above example could be rewritten:

class Point
{
    public int $x;
    public int $y;
}
 
class PositivePoint extends Point
{
    public int $x {
        set($x) {
            if ($x < 0) {
                throw new \InvalidArgumentException('Too small');
            }
            parent::$x::set($x);
        }
    }
}

An example of overriding only a get hook could be:

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());
    }
}

As only a get hook is specified, and the parent is a plain property (and thus “backed”), setting the property will still happen normally. Hooks may not access any other hook except their own parent on their own property.

See the FAQ section below for a discussion of why this syntax was chosen.

Final hooks

Hooks may also be declared final, in which case they may not be overridden.

class User 
{
    public string $username {
        final set => strtolower($value);
    }
}
 
class Manager extends User
{
    public string $username {
        // This is allowed
        get => strtoupper($this->username);
 
        // But this is NOT allowed, because set is final in the parent.
        set => strtoupper($value);
    }
}

A property may also be declared final. A final property may not be redeclared by a child class in any way, which precludes altering hooks or widening its access.

Declaring hooks final on a property that is declared final is redundant, and will be silently ignored. This is the same behavior as final methods.

class User 
{
    // Child classes may not add hooks of any kind to this property.
    public final string $name;
 
    // Child classes may not add any hooks or override set,
    // but this set will still apply.
    public final string $username {
        set => strtolower($value);
    }
}

Interfaces

A key goal for property hooks is to obviate the need for getter/setter methods in the majority case. While straightforward for classes, many value objects also conform to an interface. That interface, therefore, also needs to be able to specify what properties it includes.

This RFC therefore also adds the ability for interfaces to declare public properties, asymmetrically. An implementing class may provide the property via a normal property or hooks. Either one is sufficient to satisfy the interface.

interface I
{
    // An implementing class MUST have a publicly-readable property,
    // but whether or not it's publicly settable is unrestricted.
    public string $readable { get; }
 
    // An implementing class MUST have a publicly-writeable property,
    // but whether or not it's publicly readable is unrestricted.
    public string $writeable { set; }
 
    // An implementing class MUST have a property that is both publicly
    // readable and publicly writeable.
    public string $both { get; set; }
}
 
// This class implements all three properties as traditional, un-hooked
// properties. That's entirely valid.
class C1 implements I
{
    public string $readable;
 
    public string $writeable;
 
    public string $both;
}
 
// This class implements all three properties using just the hooks
// that are requested.  This is also entirely valid.
class C2 implements I
{
    private string $written = '';
    private string $all = '';
 
    // Uses only a get hook to create a virtual property.
    // This satisfies the "public get" requirement. It is not
    // writeable, but that is not required by the interface.
    public string $readable { get => strtoupper($this->writeable); }
 
    // The interface only requires the property be settable,
    // but also including get operations is entirely valid.
    // This example creates a virtual property, which is fine.
    public string $writeable {
        get => $this->written;
        set {
            $this->written = $value;
        }
    }
 
    // This property requires both read and write be possible,
    // so we need to either implement both, or allow it to have
    // the default behavior.
    public string $both {
        get => $this->all;
        set {
            $this->all = strtoupper($value);
        }
    }
}

Interfaces are only concerned with public access, so the presence of non-public properties is both unaffected by an interface and cannot satisfy an interface. This is the same relationship as for methods. The public keyword on the property is required for syntax consistency (or its rarely-used alias, var).

A get hook in an interface may be satisfied by either a get or &get hook in a class. An interface may alternatively specify a &get hook in its definition, in which case an implementing class must also use a &get hook. This behavior is identical to how methods in interfaces already work.

We have deliberately chosen to not support public string $foo in interfaces, without specifying the required hooks. That is because the most common use case would be a get-only property, but it's unclear if undefined hooks should mean “get only” or “get and set”. It may also imply that the property may be referenced, which may not be the case depending on the implementing class. To avoid ambiguity, the expected operations must be specified explicitly.

Of note, an interface property that only requires get may be satisfied by a public readonly property, as the restrictions of readonly only apply on write. However, an interface property that requires set is incompatible with a readonly property, as public-write would be disallowed.

At this time, it is not possible to specify a covariant (wider) write-type in the interface the way a hook implementation can. That is mainly to reduce moving parts and complexity. In concept, it could be cleanly added without a BC break in the future if desired.

Abstract properties

An abstract class may declare an abstract property, for all the same reasons as an interface. However, abstract properties may also be declared protected, just as with abstract methods. In that case, it may be satisfied by a property that is readable/writeable from either protected or public scope. Abstract private properties are not allowed and will result in a compile-time error, just as with methods.

abstract class A
{
    // Extending classes must have a publicly-gettable property.
    abstract public string $readable { get; }
 
    // Extending classes must have a protected- or public-writeable property.
    abstract protected string $writeable { set; }
 
    // Extending classes must have a protected or public symmetric property.
    abstract protected string $both { get; set; }   
}
 
class C extends A
{
    // This satisfies the requirement and also makes it settable, which is valid.
    public string $readable;
 
    // This would NOT satisfy the requirement, as it is not publicly readable.
    protected string $readable;
 
    // This satisfies the requirement exactly, so is sufficient. It may only
    // be written to, and only from protected scope.    
    protected string $writeable {
        set => $value;
    }
 
    // This expands the visibility from protected to public, which is fine.
    public string $both;
}

An abstract property on an abstract class may provide implementations for any hook, but must have either get or set declared but not defined (as in the example above). A property on an interface may not implement any hooks.

abstract class A
{
    // This provides a default (but overridable) set implementation, and requires 
    // child classes to provide a get implementation.
    abstract public string $foo { 
        get;
        set { $this->foo = $value; }
    }
}

As with interfaces, omitting a hook indicates no requirement for it, and specifying neither hook is not supported, for all the same reasons as interfaces.

As with interfaces, a get-only abstract property may be satisfied by a readonly property. A set-requiring abstract property is incompatible with readonly.

Property type variance

Normal properties are neither covariant nor contravariant; their type may not change in a subclass. The reason for that is “get” operations MUST be covariant, and “set” operations MUST be contravariant. The only way for a property to satisfy both requirements is to be invariant.

With abstract properties (on an interface or abstract class) or virtual properties, it is possible to declare a property that has only a get or set operation. As a result, abstract properties or virtual properties that have only a get operation required MAY be covariant. Similarly, an abstract property or virtual property that has only a set operation required MAY be contravariant.

Once a property has both a get and set operation, however, it is no longer covariant or contravariant for further extension. That is, it is now invariant (as all properties are in 8.3 and earlier).

class Animal {}
class Dog extends Animal {}
class Poodle extends Dog {}
 
interface PetOwner 
{
    // Only a get operation is required, so this may be covariant.
    public Animal $pet { get; }
}
 
class DogOwner implements PetOwner 
{
    // This may be a more restrictive type since the "get" side
    // still returns an Animal.  However, as a native property
    // children of this class may not change the type anymore.
    public Dog $pet;
}
 
class PoodleOwner extends DogOwner 
{
    // This is NOT ALLOWED, because DogOwner::$pet has both
    // get and set operations defined and required.
    public Poodle $pet;
}

Property magic constant

Within a property hook, the special constant __PROPERTY__ is automatically defined. Its value will be set to the name of the property. This is mainly useful for repeating self-referential code. See the “cached derived property” example linked below for a complete use case.

Interaction with traits

Properties in traits may declare hooks, just like any other property. However, as with normal properties, there is no conflict resolution mechanism provided the way methods have. If a trait and a class where it is used both declare the same property with hooks, an error is issued.

We anticipate that being a very rare edge case, and thus no additional resolution machinery is necessary.

Interaction with readonly

readonly properties work by checking if the backing store value is uninitialized. A virtual property has no backing store value to check. While technically an inherited readonly property would allow accessing its parent's stored value, in practice it would be non-obvious when readonly works on properties with hooks. Moreover, providing a get hook on an overridden property would further complicate the notion of a “initialized” value.

For that reason, a readonly property with a get or set hook is disallowed and will throw a compile error. That also means that a child class may not redeclare and add hooks to a readonly property, either.

Interaction with magic methods

PHP 8.3 will invoke the __get(), __set(), __isset(), and __unset() magic methods if a property is accessed and it is either not defined, OR it is defined but not visible from the calling scope. The presence of hooks on a defined property does not change that behavior. Naturally the property will be defined if it has hooks; however, if the property is not visible in the calling scope then the appropriate magic method will be called just as if there were no hooks.

Within the magic methods, the property will be visible and therefore accessible. Reads or writes to a hooked property will behave the same as from any other method, and thus hooks will still be invoked as normal.

class C
{
    private string $name {
        get => $this->name;
        set => ucfirst($value);
    }
 
    public function __set($var, $val)
    {
        print "In __set\n";
        $this->$var = $val;
    }
}
 
$c = new C();
 
$c->name = 'picard';
 
// prints "In __set"
// $c->name now has the value "Picard"

Interaction with isset() and unset()

If a scope-visible property implements get, then isset() will invoke the get hook and return true if the value is non-null. That is, isset($o->foo), where $foo has a get hook, is equivalent to !is_null($o->foo).

In comparison, with an undefined property isset() currently checks both __isset() and __get(), and returns true only if the isset magic method is defined AND returns true AND the get magic method returns non-null. Since a property with a hook is always defined, by definition, there is no need to verify it (the equivalent logic would always return true). The net result is that the behavior is consistent with how isset() interacts with __get/__isset() today.

If a property has a backing value and there is no get hook, it will operate on the property value directly the same as if there were no hooks.

If a property is virtual and has no get hook, calling isset() will throw an Error. (This would only happen on a set-only virtual property, which we anticipate being very rare.)

If a property implements any hook, then unset() is disallowed and will result in an error. unset() is a very narrow-purpose write operation; supporting it directly would involve bypassing any set hook that is defined, which is undesireable. If in the future a compelling need can be found for it, that may justify a dedicated unset hook. (See Future Scope.)

Interaction with constructor property promotion

As of PHP 8.0, properties may be declared inline with the constructor. That creates an interesting potential for complexity if the property also includes hooks, as the hooks may be arbitrarily complex, and therefore long, leading to potentially tens of lines of code technically within the constructor's method signature.

On the other hand, we expect the use of the set hook for validation (as shown in various examples here) to be fairly popular, including validation on promoted properties. Making them incompatible would undercut the value of both tremendously. (Virtual properties make little sense to make promoted.)

After much consideration, the authors have opted to allow hooks to be implemented within constructor property promotion. While pathological examples could certainly be shown, we anticipate in practice that the impact will be far less. In particular, the shorthand version of hook bodies and the ability to call out to private methods if they get complicated partially obviate the concern about syntactic complexity.

For example, we predict the following to be the extent of most combinations of hooks and promotion:

class User
{
    public function __construct(
        public string $username { set => strtolower($value); }
    ) {}
}

Which is, all things considered, pretty good for the level of power it gives.

Interaction with serialization

The behavior of properties with hooks when serialized has been designed to model the behavior of non-hooked properties as closely as possible.

There are several serialization contexts to consider. Their behavior is summarized below, with explanations afterward.

  • var_dump(): Use raw value
  • serialize(): Use raw value
  • unserialize(): Use raw value
  • __serialize()/__unserialize(): Custom logic, uses get/set hook
  • Array casting: Use raw value
  • var_export(): Use get hook
  • json_encode(): Use get hook
  • JsonSerializable: Custom logic, uses get hook
  • get_object_vars(): Use get hook
  • get_mangled_object_vars(): Use raw value

serialize() and var_dump() are both intended to show the internal state of the object. For that reason, for backed properties they will store/display the raw value of the property, without invoking get. Virtual properties, which have no backing store of their own, will be omitted.

Similarly, unserialize() will write to a property's backing value directly, without invoking set. If the input has a value for a virtual property, an error will be thrown.

Note that if the __serialize() or __unserialize() magic methods are used, those will run like any other method and therefore read through the get hook.

When casting an object to an array ($arr = (array) $obj), currently the visibility of properties is ignored; the keys returned may have an extra prefix in them to indicate that they were private, but that's it. As this operation currently reveals internal implementation details, it also will not invoke the get hook.

get_mangled_object_vars() was intended as a long-term replacement for array casting of objects, and therefore its interaction with hooks is identical. No hook will get called.

JsonSerializable is a non-issue; its jsonSerialize method will be called as a normal method and have the same access to properties as any other method (that is, through the get hook if present), and may return whatever value it wishes.

In PHP 8.3, using json_encode() on an object that does not implement JsonSerializable will return a JSON object of key/value pairs of the public properties only, regardless of what scope it is called from. The intent is to serialize the “public face” of the object. For that reason, public properties with a get hook will be included, and the get hook invoked, regardless of whether the property is virtual or not.

get_object_vars() is also scope-aware, and thus is not supposed to have access to internal state. Functionally, it is equivalent in behavior to calling foreach over an object. Its behavior with hooks is therefore the same: any property readable in scope will be included, and a get hook called if defined, regardless of whether the property is virtual or not.

var_export() is an interesting case. Its intent is to create an export of the object's internal state, and it bypasses visibiilty control, but in a way that it may be re-hydrated entirely from user-space code in the __set_state() method. __set_state() necessarily must send any assignments through the set hook, if defined. To minimize asymmetry, therefore, we have chosen to invoke get hooks on properties for var_export().

Reflection

There is a new global enum, PropertyHookType. It is string-backed to allow for easy “upcasting” of primitive values when appropriate.

enum PropertyHookType: string
{
    case Get = 'get';
    case Set = 'set';
}

ReflectionProperty has several new methods to work with hooks.

  • getHooks(): array returns an array of \ReflectionMethod objects keyed by the hook they are for. So for example, a property with both get and set hook will return a 2 element array with keys get and set, each of which are a \ReflectionMethod object. The order in which they are returned is explicitly undefined. If an empty array is returned, it means there are no hooks defined.
  • getHook(PropertyHookType $hook): ?\ReflectionMethod returns the corresponding \ReflectionMethod object or null if it is not defined.
  • isVirtual(): bool, returns true if the property has no backing value, and false if it does. (That is, all existing properties without hooks will return false.)
  • getSettableType(): ?\ReflectionType will return the type definition for the set hook, if defined. If there is no set-type specified, it will return the property type exactly as getType(), including null if the property is untyped. If the property is intrinsically unsettable (because it is virtual and has no set hook), ReflectionType(never) will be returned. (A readonly property is still settable, just once, so in that case the behavior is identical to getType().)
  • getRawValue(object $object): mixed will return the raw backing value of the property, without caling a get hook. If there is no hook, it behaves identically to getValue(). If the property is virtual, it will throw an error. On a static property, this method will always throw an error.
  • setRawValue(object $object, mixed $value): void will, similarly, set the raw value of a property without invoking a set hook. If there is no hook, it behaves identically to setValue(). If the property is virtual, it will throw an error. On a static property, this method will always throw an error.
  • The existing getValue() method will invoke a get hook if one is defined, regardless of whether the property is virtual or not. If a property is write-only (virtual and has only a set hook defined), an error will be thrown.
  • The existing setValue() method will invoke a set hook if one is defined, regardless of whether the property is virtual or not. If a property is get-only (virtual and has only a get hook defined), an error will be thrown.

There is also a \ReflectionProperty::IS_VIRTUAL constant for use in property filters.

The returned \ReflectionMethod objects will have the class the property is on as its declaring class (returned by getDeclaringClass()). Its return and parameter types will be as defined by the rules above in the hooks section. Its getName() method will return ClassName::$prop::get (or set, accordingly).

Hooks defined by a parent class's property will be included and available, the same as if they were defined on the property directly, unless overridden in the child class.

Attributes

Hook implementations are internally implemented as methods. That means hooks may accept method-targeted attributes. They may be accessed via reflection in the usual way, once the \ReflectionMethod object is obtained.

#[Attribute(Attribute::TARGET_METHOD)]
class A {}
 
#[Attribute(Attribute::TARGET_METHOD)]
class B {}
 
class C {
    public $prop { 
        #[A] get {}
        #[B] set {}
    }
}
 
$getAttr = (new ReflectionProperty(C::class, 'prop'))
    ->getHook(PropertyHookType::Get)
    ->getAttributes()[0];
$aAttrib = $getAttr->getInstance();
 
// $aAttrib is an instance of A.

Hook parameters may also accept parameter-targeted attributes, as expected.

class C {
    public int $prop { 
        set(#[SensitiveParameter] int $value) {
            throw new Exception('Exception from $prop');
        }
    }
}
 
$c = new C();
$c->prop = 'secret';
// Exception: Exception from $prop in %s:%d
// Stack trace:
// #0 example.php(4): C->$prop::set(Object(SensitiveParameterValue))
// #1 {main}

Of note, the #[\Override] attribute can be applied to a hook, and it will treat the parent property's hook as its parent and behave accordingly. If the parent property is declared but has no hooks, that is considered “existing” as though it had a trivial hook on it.

Frequently Asked Questions

Why not Python/JavaScript-style accessor methods?

Of the 5 languages we surveyed that had property accessors, C#, Swift, and Kotlin put the accessor/hook logic on the property, as is done here. Python and JavaScript have no property declaration, instead having an annotation on a method that turns it into a getter/setter. Using a JavaScript-inspired syntax, in PHP that might look like:

class Person
{
    public string $firstName;
 
    public function __construct(private string $first, private string $last) {}
 
    public function get firstName(): string
    {
        return $this->first . " " . $this->last;
    }
 
    public function set firstName(string $value): void
    {
        $this->first = $value;
    }
}

While that may seem superficially preferable, it is not workable for a number of reasons.

  1. What is the property type of the $firstName property? Presumably string, but there's nothing inherent that forces, public string $firstName, get firstName()s return and set firstName()s parameter to be the same. Even if we could detect it as a compile error, it means one more thing that the developer has to keep track of and get right, in three different places. Architecture should “make invalid states impossible”, and this does not. (Python and JavaScript are both largely untyped, which is why they don't have this issue.)
  2. What about visibility? Do the get/set methods need to have the same visibility as the property? If not, does that become a way to do asymmetric visibility? But then even if not, the visibility would be repeated multiple times. What about inconsistency between the method's visibility and the property visibility? How is that handled? (Python and JavaScript do not have visibility modifiers in the sense PHP does, which is why they don't have this issue.)
  3. How do you differentiate between virtual and non-virtual properties? Arguably that could be where declaring the property separately has value, but as noted above that introduces a host of additional issues. Without that triple-bookkeeping, however, it's not obvious if a property is virtual or not. (Python and JavaScript both do not pre-define properties, which is why they don't have this issue.)
  4. For non-virtual properties, if you need to triple-enter everything, we're back to constructors pre-promotion. Plus, the accessor methods could be anywhere in the class, potentially hundreds of lines away. That means just looking at the property declaration doesn't tell you what its logic is; the logic may be on line 960, which only makes keeping its type/visibility in sync with the property harder.

Essentially, the tagged-method approach can work well in languages without explicit typed properties, where all objects are really just dictionaries with funny syntax (Python and JavaScript). In languages with explicit typed properties, that becomes vastly more cumbersome and un-ergonomic. Of note, all three languages surveyed that have explicit typed properties (C#, Swift, and Kotlin) use on-the-property accessor definitions instead. As PHP is also a language with explicit typed properties, following suit makes logical sense.

Why isn't asymmetric visibility included, like in C#?

Kotlin, Swift, and C#, all of which have a similar accessor model to that shown here, all support asymmetric visibility in addition to property hooks (by whatever name). However, they all use different syntaxes. In some but not all cases, the visibility is placed on the get or set hook.

That would cause a problem for PHP. As noted above, property hooks are incompatible with array properties. However, there is no conceptual reason for asymmetric visibility to be incompatible with array properties, and there are ample use cases for wanting to support that.

However, using the hook-bound syntax for visibility would either inherently forbid asymmetric visibility on arrays (undesirable), or necessitate more complex syntax to determine if references should or should not be disabled on a property. Both are poor options.

For that reason, any concept of asymmetric visibility has been omitted from this RFC. Should asymmetric visibility be determined a desirable feature in the future, a left-side syntax as used by Swift and as demonstrated in the original Asymmetric Visibility RFC would be a complementary addition, and the best option in practice.

What's with the weird syntax for accessing a parent property?

The syntax for accessing a parent property through hooks was designed to minimize confusion with other syntax. It's not ideal, but has the fewest trade-offs. The seemingly-obvious alternative, using parent::$x, has several problems.

First, there's then no way to differentiate between “access parent hook” and “read the static property $x on the parent”. Arguably that's not a common case, but it is a point of confusion.

The larger issue is that parent::$x can't be just a stand-in for the backing value in all cases. While supporting that for the = operator is straightforward enough, it wouldn't give us access to ++, --, <=, and the dozen or so other operators that could conceivably apply. In theory those could all be implemented manually, but we estimate that would be “hundreds to thousands of lines of code” to do, which... is not time or code well spent. :-) Especially as this is a very edge-case situation to begin with.

So we have the choice between making $a = parent::$prop and parent::$prop = $a work, but *nothing else*, inexplicably (creating confusion) or the slightly longer syntax of $a = parent::$prop::get() and parent::$prop::set($a) that wouldn't support those other operations anyway so there's no confusion.

We feel the current approach is the better trade off, but if the consensus generally is for the shorter-but-inconsistent syntax, that can be changed.

Why no explicit "virtual" flag?

One change that was suggested was to require virtual properties to be marked with a virtual keyword or similar, rather than relying on auto-detecting if a property should be backed. We considered this approach, and it has its merits for explicitness, but on further investigation determined that it would not be feasiable due to inheritance.

Specifically, for implementation reasons if a child class extends a parent with a backed property and provides hooks that do not use the backing value, the class still has a masked property on it. Conversely, if a parent class has a virtual property a child class may override it and use a backing value, which will then be created. Essentially, a property is “backed” (as far as the engine is concerned) if any class in its hierarchy is backed.

That would mean, however, that the virtual keyword is unreliable, as a parent or child class could trigger different behavior. Consider:

class P {
    public virtual $prop { get => ...; set { ... } }
}
 
class C extends P {
    public virtual $prop { get => strtoupper(parent::$prop::get()); }
}

In this case, C::$prop is marked as virtual, because it technically does not read from a backing store itself. However, if P::$prop is not-virtual, that means the virtual declaration on C is technically wrong, as there is a backing value named $prop. If P::$prop changes between virtual and non, this could end up being a BC break.

While there are ways that the engine could be made to handle that scenario without crashing, the syntax would still be confusing to the user. We therefore believe that the current approach of “if $this->prop is used, it's backed” is simpler for the user in practice, less misleading, and easier for the engine.

Usage examples

We have collected a series of examples that show what we expect to be typical hook usage. (Or, arguably, the kind of things one could do that wouldn't require adding a method for in case you want to do them in the future.) It is non-normative, but gives a sense of how hooks can be used to improve a code base (or things that can be added later without needing to create methods “just in case”).

In the interest of brevity, we have placed the examples in an external document, available here: Usage examples

Backward Incompatible Changes

There is one subtle BC break due to accessing parent property hooks. Specifically, in this code:

class A {
    public static $prop = 'C';
}
 
class B extends A {
    public function test() {
        return parent::$prop::get();
    }
}
 
class C {
    public static function get() {
        return 'Hello from C::get';
    }
}

Currently, parent::$prop would resolve to "C", and then the C::get() method would be called.

With this RFC, *if* the method name is the same as a hook, then the above code would error out with a message about trying to access a parent hook when not in a hook. If the method is not the same name as a hook, there is no change in behavior.

The previous logic could be achieved by using a temporary variable:

class B extends A {
    public function test() {
        $class = parent::$prop;
        return $class::get();
    }
}

As the above code is very rare in the wild and rather contrived, and easily worked around, we feel this edge case is acceptable.

Open questions

Proposed PHP Version(s)

PHP 8.4.

Future Scope

isset and unset hooks

PHP supports magic methods for __isset and __unset. While it is tempting to allow those as hooks as well, the authors feel their use is limited. They have therefore been omitted. However, it is possible to reintroduce them in a future RFC should valid use cases be shown.

Reusable hooks

Swift has the ability to declare hook “packages” that can be applied to multiple properties, even in separate classes. That further helps reduce boilerplate, without having to pack even more logic into the type system. In a sense, it does for hooks what PHP traits do for methods and properties. While that is potentially useful, it would be a whole big feature unto itself. The authors therefore opted to avoid that for now. It is an addition that could be pursued in the future if it's found to be useful.

Return to assign for long-set

In the current design, a long-form set hook, with body, is always return-void. That does make multi-statement set hooks a bit more awkward than allowing them to return the value to set, rather than assigning it explicitly to the backing property.

The root problem is that it is not curently possible to differentiate between return, return null, and “nothing returned” at runtime, from the call-side. They all get seen as null. It's then impossible to tell if that means “assign null to the backing property” or “I already handled the assignment, do nothing.”

(This issue doesn't exist for short-set, as we can safely just declare “it's always an assignment, period.”)

The proper solution for this problem is to update the engine to differentate between “null was explicitly returned” and “nothing was returned at all.” If that change were made, it would be reasonably straightforward to support optionally returning from the set hook. However, that is a deeper change with potentially other implications, so we feel it is out of scope for this RFC.

A future RFC that makes that engine change and then enables an optional return-to-set feature for set hooks is certainly possible, and the authors would support that. It would have no BC breakage for the hooks feature itself.

Assignment by reference

This RFC does not support assign-by-reference for hooked properties. That is a very rare edge case, and the current dynamic-property mechanism (__set) doesn't support it either, so it is no great loss, even though there is, technically, a very small potential for breakage if someone is trying to write to a public property of another class, and that class's author changes it to add hooks.

It may be possible to allow assign-by-ref in the future, though it would be rather involved, which is why it has been left out of this RFC. We are, however, documenting the moving parts involved for future reference.

Assigning by reference involves a couple of steps:

  • If the value to be assigned (right side of a =& operation) is not already a reference, wrap it into a reference.
  • Assign that (possibly newly created) reference value to the variable specified on the left side of the operator.
  • The result is the left-side variable is now a reference to some pre-existing value, as are any other references that already exist (and increments refcounts as appropriate).
  • If the left-side variable was previously a reference already, the previous reference is lost (and refcounts decremented as appropriate).

To support assign-by-ref, we would need to differentiate between an incoming value and incoming reference, as the presence of a set hook means we could not do the “wrap if it's not already” logic that a normal reference assignment does (as it would need to happen inside the hook body in user space). There are two options: One would be to make the parameter to set by-reference, and provide a boolean flag to indicate if the method body should save it by reference or not. This seems quite ugly and error prone.

The other would be to introduce a by-ref version of set (potentially named setref or bindref) that would get called instead, should the engine encounter $foo->hookedProperty =& $var. If no set hook is defined on a backed property (but not virtual), it MAY be possible to automate the default behavior, but that would have to be determined at that time.

Again, this is a very small edge case so we have opted to not address it now. Such a change could be made in a BC-friendly way in a future RFC.

Accessing sibling hooks

By design, when inside a get hook, references to $this->[propertyName] will skip all hooks, including both get and set. The same applies to set hooks reading from the property. That should be sufficient for the overwhelming majority of cases. However, there may be cases where there is a use for accessing the set hook from the get hook, or vice versa. (Say, on virtual properties.)

If that is shown to be a need in practice, one straightforward way to support that would be a self::$foo::get() syntax, paralleling the syntax for accessing parent hooks. Such a syntax would allow issuing a method call to the specified hook, and would naturally expand to other hooks if they are ever added. However, that would also create opportunities to create infinite loops if the developer is not careful.

This syntax has been omitted from this RFC as we do not think it is necessary in practice, but it would be possible to add in the future without any BC breaks.

Proposed Voting Choices

This is a simple yes-or-no vote to include this feature. 2/3 majority required to pass.

Implement property hooks as described?
Real name Yes No
alcaeus (alcaeus)  
alec (alec)  
beberlei (beberlei)  
brzuchal (brzuchal)  
bukka (bukka)  
bwoebi (bwoebi)  
colinodell (colinodell)  
cpriest (cpriest)  
crell (crell)  
dams (dams)  
davey (davey)  
derick (derick)  
devnexen (devnexen)  
dragoonis (dragoonis)  
ericmann (ericmann)  
galvao (galvao)  
girgias (girgias)  
ilutov (ilutov)  
jimw (jimw)  
josh (josh)  
jwage (jwage)  
kalle (kalle)  
kguest (kguest)  
klaussilveira (klaussilveira)  
levim (levim)  
nicolasgrekas (nicolasgrekas)  
nielsdos (nielsdos)  
patrickallaert (patrickallaert)  
petk (petk)  
pierrick (pierrick)  
ralphschindler (ralphschindler)  
ramsey (ramsey)  
reywob (reywob)  
saki (saki)  
santiagolizardo (santiagolizardo)  
seld (seld)  
sergey (sergey)  
shivam (shivam)  
sirsnyder (sirsnyder)  
theodorejb (theodorejb)  
thorstenr (thorstenr)  
timwolla (timwolla)  
trowski (trowski)  
weierophinney (weierophinney)  
Final result: 42 2
This poll has been closed.

Implementation

References

rfc/property-hooks.txt · Last modified: by 127.0.0.1

?
三叉神经痛看什么科 尿毒症是什么原因导致的 做梦车丢了有什么预兆 农历六月初十是什么日子 土色是什么颜色
湿疹是什么意思 印第安老斑鸠什么意思 筛选是什么意思 鼻窦炎都有什么症状 孕妇梦见洪水是什么意思
睾丸潮湿吃什么药 八月五号是什么星座 红色尿液是什么原因 3月27号是什么星座 胳膊上种花是什么疫苗
什么的天山 张郃字什么 黑加京念什么 智人是什么意思 年柱金舆是什么意思
姓许的女孩取什么名字好听hcv8jop0ns0r.cn 什么的水井hcv9jop2ns3r.cn 崛起是什么意思hcv9jop3ns3r.cn 红苋菜不能和什么一起吃zhongyiyatai.com 微信证件号是什么hcv9jop3ns8r.cn
二月七号是什么星座hcv8jop8ns6r.cn 灭活疫苗是什么意思hcv8jop2ns6r.cn 786是什么意思hcv8jop5ns2r.cn 国家是什么hcv9jop4ns2r.cn 5月什么星座gangsutong.com
什么是狐臭hcv8jop2ns1r.cn 除体内湿热最好的中成药是什么hcv8jop4ns7r.cn 体内湿气重是什么原因造成的xianpinbao.com 舌头发麻什么原因hcv7jop6ns0r.cn 为什么嘴巴老是干hcv8jop8ns3r.cn
女性分泌物像豆腐渣用什么药hcv9jop0ns8r.cn 什么是风水hcv7jop9ns6r.cn 吾矛之利的利什么意思hcv7jop6ns3r.cn 疮痈是什么意思hcv8jop3ns1r.cn 龟头敏感吃什么药hcv7jop9ns7r.cn
百度