references & arrays + hooks

php.internals

Rob Landers

1 year ago
Hello internals: Currently, this is not allowed by PHP: class Foo { public function __construct( public string &$ref { get => $this->ref ?? 'null'; set => $this->ref = $value; } ) {} } $var = 'hello'; $foo = new Foo($var); $var = "world"; echo $foo->ref; https://3v4l.org/JRArK Currently, you have to emulate it like so: class Foo { public string $ref { get => $this->_ref; set => $this->_ref = $value; } public function __construct( private string &$_ref, ) {} } $var = 'hello'; $foo = new Foo($var); $foo->ref = 'world'; echo $var; https://3v4l.org/0cC0S After the release, I'd like to propose an RFC that allows using references with hooks: A hooked reference variable is a callable/closure under the hood (technically, a reference to the actual hook methods) that can be used like a value. Reading from it calls the ::get() method, and setting to it calls the ::set($value) method. If the callable were to be written in PHP, it might look something like the following psuedo code: function hookRef(...$args) use (&$backedValue) { if(empty($args)) return $backedValue::get(); $backedValue = $backedValue::set(array_first($args)); } In simplified terms, the engine, upon detecting a read/write to a hooked reference (eg, a standalone variable pointing to a hooked property), instead of doing typical variable access, it will call the bound closure with the appropriate arguments. This will effectively allow hooks to be references. This would also handle the case for arrays, which appears to require the same plumbing to exist. Downsides: 1. this will probably necessitate a new zval type, determined at runtime (it will not be exposed to users). 2. there are a lot of ways to access/write to a variable (especially once you consider arrays) thus it will touch a lot of the engine. Thoughts? Concerns? Suggestions? — Rob

Ilija Tovilo

1 year ago
Hi Rob On Thu, Jul 31, 2025 at 10:48 AM Rob Landers <rob@bottled.codes> wrote:
> > Currently, this is not allowed by PHP: > > class Foo { > public function __construct( > public string &$ref { get => $this->ref ?? 'null'; set => $this->ref = $value; } > ) {} > } > > After the release, I'd like to propose an RFC that allows using references with hooks:
As we've considered references extensively during the hooks RFC, allow me to add some context.
> this will probably necessitate a new zval type, determined at runtime (it will not be exposed to users).
The impact for new zval types is gigantic, and it's been avoided for decades. I don't think it's technically necessary either. We're already recording zend_reference.sources, which is a list of properties that store the reference. This is used for type coercion when assigning to a reference that may be stored in a typed property. class C { public int $prop; } $c = new C(); $prop = 42; $c->prop = &$prop; $prop = '43'; var_dump($prop); // int(43) You may be able to use the same mechanism for set hooks. This would need to be extended to store the actual object containing the reference, so that the set hook may be invoked. However, there are some issues. For example, what do you do when a reference is assigned to multiple hooked properties? class C { public int $prop1 { set => $value + 1; } public int $prop2 { set => $value + 2; } } $c = new C(); $prop = 42; $c->prop1 = &$prop; $c->prop2 = &$prop; $prop = 43; // What happens now? Typed properties handle type conflicts gracefully [^1], but graceful handling for hooks is less clear because each hook can transform the assigned value. You might allow assignment only when the reference is bound to a single hooked property, but then this isn't a general solution. I suppose you may also require all set hooks to assign the reference to an identical value. Another current limitation is reference assignment. `$c->prop1 = &$prop;` from above doesn't actually work. That's because hooks currently allow &get, i.e. retrieving the reference stored in a hooked property (`$prop = &$c->prop1`), but they don't allow setting the property reference, which is semantically distinct. set(&value) is not sufficient because the hook implementation must know whether to assign to the backing store through a direct assignment, or =&. set(&value, bool $byRef) would work, but is quite clunky. You also mention arrays. This approach does not solve the issues we've outlined in the RFC [^2]. In particular, indirect modification may still hide mutations from set hooks. Even if we can fix this and make every mutation call the set hook, the significant ergonomic issues of understanding _how_ the array changed remain, along with performance issues due to array separation. Disregarding arrays, this approach might not be too complex. However, given that arrays are by far the most used type when it comes to references, them being exempt makes this feature far less useful. That's the main reason we didn't pursue this idea further, and I remain quite convinced that arrays are unworkable. Ilija [^1]: https://3v4l.org/defVI [^2]: https://wiki.php.net/rfc/property-hooks#arrays