回答下在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, 发现有递归产生, 报告错误, 失败返回.
建议继续学习:
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:雪候鸟 来源: 风雪之隅
- 标签: bug
- 发布时间:2011-09-25 13:35:07
- [70] Twitter/微博客的学习摘要
- [65] IOS安全–浅谈关于IOS加固的几种方法
- [65] 如何拿下简短的域名
- [64] find命令的一点注意事项
- [63] Go Reflect 性能
- [63] android 开发入门
- [61] 流程管理与用户研究
- [59] 图书馆的世界纪录
- [59] 读书笔记-壹百度:百度十年千倍的29条法则
- [59] Oracle MTS模式下 进程地址与会话信