msgbartop
PHP语言, PHP扩展, Zend引擎相关的研究,技术,新闻分享 – 左手代码 右手诗
msgbarbottom

22 Sep 11 回答下在bugs.php上的一个问题

今天在bugs.php.net上, 有一个用QQ邮箱的用户发了一个问题(#55731).

他问, 为什么, 如下的代码, 会调用俩遍getter:

<?php
class Example{
    private $p1;
    private $p2;
    function __construct($a){
        $this->p1=$a;
    }
    function __get($elmname){
        echo "Call_get()";
        return $this->$elmname;
    }
    function __isset($name){
        return isset($this->$name);
    }
    function __unset($name){
        unset($this->$name);
    }
}
$example = new Example("v1");
unset($example->p1);
echo $example->p1;
//输出
//Call_get()Call_get()

一开始, 我只是简单的回答了下, 和他在__get中再次获取$this->elmname有关系. 后来这个同学又要追问原因, 我只好用我那糟糕的英语给他解释.

可能用英语没太讲明白, 我现在用中文解释下吧.

(补充: 文章发出以后, 有不少同学认为我把简单问题搞复杂了, 他们认为unset($example->p1)也会触发一次getter. 所以我先解答下这一点: “非”读”上下文不会触发对__get的调用”. 这一点也很容易验证, 大家可以试试. 要不然我也不会专门写这个文章来讨论这个问题.)

首先. 这个问题的关键原因是, unset掉一个private的变量.

在我们获取一个对象的变量(“读”上下文)的时候, 其实是首先被翻译成ZEND_FETCH_OBJ_R中间指令(opcode), 那么到了真正执行期的时候, 这条opcode会导致最终调用到zend_read_property, 我把关键代码罗列如下:

.....
    /* make zend_get_property_info silent if we have getter - we may want to use it */
    property_info = zend_get_property_info_quick(zobj->ce, member, (zobj->ce->__get != NULL), key TSRMLS_CC);

    if (UNEXPECTED(!property_info) ||
        ((EXPECTED((property_info->flags & ZEND_ACC_STATIC) == 0) &&
         property_info->offset >= 0) ?
            (zobj->properties ?
                ((retval = (zval**)zobj->properties_table[property_info->offset]) == NULL) :
                (*(retval = &zobj->properties_table[property_info->offset]) == NULL)) :
            (UNEXPECTED(!zobj->properties) ||
              UNEXPECTED(zend_hash_quick_find(zobj->properties, property_info->name,
                property_info->name_length+1, property_info->h, (void **) &retval) == FAILURE)))) {
        zend_guard *guard = NULL;

        if (zobj->ce->__get &&
            zend_get_property_guard(zobj, property_info, member, &guard) == SUCCESS &&
            !guard->in_get) {
            /* have getter - try with it! */
            Z_ADDREF_P(object);
            if (PZVAL_IS_REF(object)) {
                SEPARATE_ZVAL(&object);
            }
            guard->in_get = 1; /* prevent circular getting */
            rv = zend_std_call_getter(object, member TSRMLS_CC);
            guard->in_get = 0;

上面的代码解释如下:

1. 首先调用zend_get_property_info_quick, 尝试在对象对应的类(zend_class_entry * zobj->ce)中寻找该属性的声明信息(public, protect, name, hash), zend_get_property_info_quick会在找不到, 或者找到了, 但是不容许访问(外部访问私有,保护变量)的时候, 返回NULL

2. 如果找到对应的属性信息, 则将依照属性信息中的属性名作为接下来查找的属性名(在PHP中, 私有属性的命名为”\0class_name\0property_name\0″, 保护属性的命名为:”\0*\0property_name\0″, 公有属性的命名为”property_name\0″)

2. 如果没有找到相关的声明信息(未定义属性), 则尝试直接从对象的属性集中寻找(zobj_properties, 这是因为PHP是一个很灵活的语言, 你可以动态的给一个对象则加属性), 如果找到, 成功返回.

3. 如果在对象的属性集合中也没有找到, 则判断对象是否申明了__get魔术方法, 如果没有则报告找不到返回失败.

4. 如果有__get魔术方法, 为了避免发生嵌套递归, 首先查询是否已经存在该属性名的guard, 如果有判断guard->in_get是否为真, 如果为真表示发生递归了,则失败返回. 如果没有, 则设置一个名为属性名的guard(请注意这里), 然后调用__get

5. 调用__get如果找到则成功返回, 否则失败结束.

现在, 让我们来看看文章开头的例子.

1. 调用zend_read_property, zobj是$example, member是p1

2. 调用zend_get_property_info_quick查询p1属性信息, 因为此时的作用域是全局作用域, PHP不容许直接访问对象的私有属性, 所以zend_get_property_info_quick返回NULL

3. 尝试从zobj->properties中寻找p1, 因为p1被unset掉了, 所以不存在, 没找到

4. 发现$example有__get魔术方法.

5. 查找是否有为”p1″设置的guard, 没有.

6. 设置一个名为”p1″的guard, 然后调用$example->__get (输出Call_get())

7. 在$example->__get中, 我们尝试获取$this->p1, 于是再来一次::

8. 调用zend_read_property, zobj是$example, member是p1

9.调用zend_get_property_info_quick查询p1属性信息, 因为此时的作用域是example, 所以zend_get_property_info_quick返回成功

10. 将返回的属性信息中的属性名”\0example\0p1\0″作为要查询的属性名

11. 尝试从zobj->properties中寻找p1, 因为p1被unset掉了, 所以不存在, 没找到

12. 发现$example有__get魔术方法.

13. 查找是否有为”\0example\0p1\0″设置的guard, 没有.

14. 设置一个名为”\0example\0p1\0″的guard, 然后调用$example->__get (输出Call_get())

15. 在$example->__get中, 我们尝试获取$this->p1, 于是再来一次::

然后重复8,9,10,11,12.

16, 查找是否有为”\0example\0p1\0″设置的guard, 发现有递归产生, 报告错误, 失败返回.


分享到:



Related Posts:

Tags: , , ,

17 Responses to “回答下在bugs.php上的一个问题”

  1. kamiff |

    call __unset()
    Call_get()
    Notice: Undefined property: Example::$p1

    我的结果是这样并没有调用2次,不知道是不是我环境的问题

  2. Zjmainstay |

    看似简单,原来程序过程还挺复杂。
    我是这么理解的。
    测试源码可分成两种情况去看:
    一种是没有unset,这种情况下,因为echo $example->p1;无法调用private属性,故而调用了__get()方法,通过__get()方法返回了private属性p1的值,因此输出为“Call_get()v1”。
    另一种是有unset,这种情况下,同样是因为echo $example->p1;无法调用private属性,故而调用了__get()方法,而__get()方法中的$this->$elmname($this->p1)应该被unset了,因此又重复调用了__get()方法,这导致了__get()方法的第二次调用,而第二次调用导致了递归产生并终止,因此,最终输出“Call_get()Call_get()”。

  3. Indimanib |

    http://898tele.com – dianhuahaomameiguo

  4. 回答下在bugs.php上的一个问题树林/咖啡 成都专业php网站制作 | 树林/咖啡 成都专业php网站制作 |

    [...] 风雪之隅 » PHP源码分析 Posted in: php / Tagged: 回答下在bugs.php上的一个问题 [...]

  5. newer |

    其实,这个和访问类中不存在的变量会自动创建变量是一个原理吧

  6. Thinking In LAMP Blog » Blog Archive » PHP每月通讯(2011年10月) |

    [...] http://www.laruence.com/2011/09/22/2152.html   回答下在bugs.php上的一个问题 [...]

  7. helloyou |

    嗯,这样彻底清楚了,谢谢

  8. 雪候鸟 |

    @helloyou 因为p1的属性信息是存在类中的, 你unset只是对一个对象实例

  9. helloyou |

    还是有点疑问:
    既然已经unset了,为何第9步中还是返回了”examplep1″?

    比较下面这两个类:
    class Ex1{
    private $var;

    function __construct(){
    unset($this->var);
    }

    function __get($elmname){
    echo “call __get\n”;
    return $this->$elmname;
    }

    }

    class Ex2{

    function __construct(){
    }

    function __get($elmname){
    echo “call __get\n”;
    return $this->$elmname;
    }

    }

    $ex1=new Ex1();
    $ex2=new Ex2();

    var_dump($ex1);
    var_dump($ex2);

    echo $ex2->var;
    echo $ex1->var;

    在var_dump处,两个类给出完全相同的输出,
    但是下面的echo却给出不同的结果(一个两次,一个一次call __get),还是让人觉得奇怪.

  10. 傻子日志 |

    [...] 本文地址: http://www.laruence.com/2011/09/22/2152.html [...]

  11. 風之紫色 |

    楼主说的没错,把unset($this->$name);这行注释掉,就很容易看出来了

  12. 雪候鸟 |

    @rainkid sigh, 发表意见前, 一定动手验证下.

  13. rainkid |

    楼主扩展研究太多了,简单的事情搞复杂了。

    使用一次$example->p1就调用一次__get方法

    unset($example->p1);
    echo $example->p1;

    这里明显使用的两次,当然会输出两次“Call_get”;

  14. 萝卜青菜 |

    没细看,对象的定义中有一个变量是用来防止__set,__get方法递归调用的

  15. wynn |

    楼上这个说法不对,unset的执行过程中不调用__get的,这一点很容易验证,直接沿用正文这段代码,把__get里面的return那行注释掉就可以用来测试了。

  16. 雪候鸟 |

    @showframework 呵呵,你可以试试unset($example->p1), 看看有没有调用…

  17. showframework |

    没这么复杂吧..
    unset($xample->p1);echo $example->p1;
    这本身就是两次__get啊
    unset语句里面那个语句一次__get
    echo 后面一次__get

Leave a Reply

*