Php,为什么__get()魔术方法在试图通过引用设置对象时执行?

ykejflvf  于 2022-11-28  发布在  PHP
关注(0)|答案(4)|浏览(160)

bounty将在21小时后过期。回答此问题可获得+400的声望奖励。John Smith正在寻找来自知名来源的答案

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;

https://onlinephp.io/c/f7f16
这里你可以看到,我创建了一个stdClass对象,试图将它传递给一个不存在的变量,__get()得到了执行--即使我只是试图写它,而不是阅读它。如果我没有die()它,我会得到Indirect modification of overloaded property has no effect异常。
如果我有这个(无参考)

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;

https://onlinephp.io/c/3afef
一切都很好,但为什么呢?

esyap4oy

esyap4oy1#

在内部,PHP有一个“阅读”属性的概念,以执行写/修改操作。虽然乍看起来似乎不连贯,但它允许PHP以更少的开销执行某些就地修改操作,否则必须依赖于读取-修改-写入的往返。使用它的情况包括追加到数组和创建对该属性的新引用。为了使其与动态解析的属性一起工作,__get魔术方法必须返回一个引用,也就是说,一个可以修改其值的位置;否则将触发一个通知,并显示消息“间接修改重载属性无效”。

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

如果从function& __get的声明中删除&,则在上面的示例中将出现此消息。
当对象属性位于引用赋值的 * 左侧 * 时,将触发相同的“reading-in-order-to-modify”代码路径(=&)。当然,如果属性查找是由用户代码动态执行的,(PHP错误地命名为“overloading”),PHP无法保证“在执行$obj->prop =& (expr)之后,访问$obj->prop将解析为与(expr)当时所做的相同的事情',因此PHP抛出一个异常。然而,解释器只有在已经调用了__get魔术方法之后才意识到这一点。
这可以通过检查Zend引擎的源代码看到。Zend引擎调用函数zend_assign_to_property_reference。此函数调用zend_fetch_property_address以确定哪个zval(PHP值的内部表示),以便它成为与右侧表达式相同的zval的别名。它通过调用该对象的get_property_ptr_ptr内部方法槽来实现;如果没有返回任何结果,则返回到read_property位置。对于用户代码对象,这意味着调用zend_std_read_property函数,这是对__get魔术方法的实际调用。
同样,因为zend_std_read_property是用标志调用的,这些标志表示它正在“按顺序读取以修改”,所以如果__get碰巧不返回引用(因为它没有被声明为function& __get),则会引发E_NOTICE错误。但无论如何,zend_std_read_property都将在zend_fetch_property_address为其准备的zval中返回其结果,而不是预先存在的zval。这会导致后者 not 将其 Package 在所谓的间接zval中(如zend_assign_to_property_reference所期望的那样),进而导致该函数抛出异常。
如果你问我的话,这是一个相当巴洛克式的实现。我甚至同意Zend引擎的实现,这样当__get即将被调用时,解释器意识到调用它实际上是没有意义的,并立即抛出一个异常。但考虑到这个代码路径只在不正确的用户代码中被命中,应该重写,也许优化没有什么意义。
如果您列出PHP中没有意义的东西,我不确定这是否能排在前十位;语言就是这么奇怪。习惯它。或者不要:改变语言也是一种选择。

tzdcorbm

tzdcorbm2#

当你通过reference 1赋值时,左手变量需要被定义(它存在),如果它不存在(它是未定义的),PHP将定义它。
但在$t->prop2情况下则不然:
变量的成员名称($t->prop2)是未定义的,但是$t__get() 2魔术方法指示PHP不像通常那样定义它,而是将成员名称作为第一个参数传递给__get(),以获得其重载值。
这是有缺陷的,因为通过引用赋值 * 需要 * 定义变量(或属性)才能成功操作。但未定义(重载)不能满足这一要求。
因此警告:
重载属性stdClass@anonymous::$prop2的间接修改无效
最后是致命错误:
无法按引用分配给重载对象

示例代码3,4

$t = new class() extends stdClass
{
    public function __get(string $name) {
        $x = null;
        return $x;
    }
};

$t->prop2 = &$x1;

1.按引用赋值- PHP手册
1.属性重载- PHP Manual

  1. Example code on 3v4l.org
  2. Backwards compatible example code on 3v4l.org for historic view
x9ybnkn6

x9ybnkn63#

有很多复杂的解释,这里有一个简单的答案,为什么它调用__get()

$obj = new \stdClass();
$test = "hello";
$obj->var = &%test;

以上面的简单例子为例,为了能够将$obj->var设置为$test指向的内存地址,$obj-〉var需要$obj中的内存地址,如果没有__get,PHP将根据上面的代码自己创建一个内存地址,如果使用__get的PHP不需要并且需要您定义它。您唯一可以做该定义的地方是在__get

vlju58qv

vlju58qv4#

根据文件:
__get()用于从不可访问的(受保护的或私有的)或不存在的属性阅读数据。
如果您想通过引用方式使用__get,则应编写以下内容:

public function &__get ( $index )

相关问题