class Test
{
public $prop1;
public function __get(string $n)
{
echo '__GET: '.$n.' - but why?<br>';
die;
}
}
$t = new Test();
$x1 = new \stdClass();
$t->prop2 = &$x1;
echo '.';
die;
here you can see, I create an stdClass
object, try to pass it to a non-exists variable, and __get()
gets executed - even though I was just tryting to write it, instead of reading. And if I didn't die()
it, I get Indirect modification of overloaded property has no effect
exception.
If I have this (no reference)
class Test
{
public $prop1;
public function __get(string $n)
{
echo '__GET: '.$n.' - but why?<br>';
die;
}
}
$t = new Test();
$x1 = new \stdClass();
$t->prop2 = $x1;
echo '.';
die;
everything works fine, but why?
Internally, PHP has a concept of ‘reading’ a property in order to perform a write/modify operation. Though it may seem incoherent at first glance, this allows PHP to perform certain in-place modification operations with less overhead, that would otherwise have to rely on read-modify-write round-trips. Situations where it is used include appending to an array and creating a new reference to the property. For this to work with dynamically-resolved properties, the __get
magic method must return a reference, which is to say, a place which can have its value modified; otherwise a notice will be triggered with the message ‘Indirect modification of overloaded property has no effect’.
class Globalses {
public function& __get(string $name) {
echo "{$name} GET!\n";
return $GLOBALS[$name];
}
}
$obj = new Globalses;
$foo = [0, 1, 2, 3];
$obj->foo[] = 4; // foo GET!
echo 'foo='; debug_zval_dump($foo);
$bar = 42;
$var =& $obj->bar; // bar GET!
$var = 69;
echo 'bar='; debug_zval_dump($bar);
You will get this message in the above example if you remove the &
from the declaration of function& __get
, which will make the method return a value, and not a reference.
The same code path of ‘reading-in-order-to-modify’ is triggered when an object property is on the left-hand side of reference-assignment (=&
). But of course, if property lookup is performed dynamically by user code (what PHP misnames ‘overloading’), there is no way PHP can promise that ‘after $obj->prop =& (expr)
is executed, accessing $obj->prop
will resolve to the same thing (expr)
did at that time’, so PHP throws an exception. However, the interpreter realises this only after the __get
magic method has already been invoked.
This can be seen by inspecting the source code of the Zend engine. To evaluate an expression like $obj->prop =& expr
, the Zend engine calls the function zend_assign_to_property_reference
. This function invokes zend_fetch_property_address
in order to determine which zval (an internal representation of a PHP value) to modify so that it becomes an alias for the same zval as the right-hand side expression. It does so by invoking the get_property_ptr_ptr
internal method slot of the object; if that returns nothing, there is a fallback to the read_property
slot. For user-code objects, this means invoking the zend_std_read_property
function, which is where the call to the __get
magic method is actually made.
Again, because zend_std_read_property
is called with flags that signify it is being ‘read-in-order-to-modify’, if __get
happens not to return a reference (because it is not declared function& __get
), an E_NOTICE
error is raised. But either way, zend_std_read_property
will return its result in the zval prepared for it by zend_fetch_property_address
instead of a pre-existing one, which causes the latter not to wrap it in a so-called indirect zval as zend_assign_to_property_reference
expects, which causes that function in turn to throw an exception.
This is a pretty baroque implementation, if you ask me. I even agree that the Zend engine could have been implemented so that by the point __get
is about to be called, the interpreter realises it’s actually pointless to invoke and throws an exception immediately. But given that this code path is hit only in user code that is incorrect and should be rewritten, perhaps it makes little sense to optimise.
If you compiled the list of things in PHP that make no sense, I’m not sure this would even make the top ten; the language is just that bizarre. Get used to it. Or don’t: changing the language is also an option.