普通成员属性的读写处理handler分别为zend_object.handlers
中的:read_property、write_property,默认对应的函数为:zend_std_read_property()、zend_std_write_property(),访问获取修改一个普通成员属性时就是由这两个函数完成的。
(1)读取属性:
通过对象或方法内通过$this访问属性,比如:echo $obj->name;
,具体的实现:
zval *zend_std_read_property(zval *object, zval *member, int type, void **cache_slot, zval *rv)
{
zend_object *zobj;
uint32_t property_offset;
zobj = Z_OBJ_P(object);
//根据属性名在zend_class.zend_property_info中查找zend_property_info,得到属性值在zend_object中的存储offset
//注意:zend_get_property_offset()会对属性的可见性(public、private、protected)进行验证
property_offset = zend_get_property_offset(zobj->ce, Z_STR_P(member), (type == BP_VAR_IS) || (zobj->ce->__get != NULL), cache_slot);
if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)) {
if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) {
//普通属性,直接根据offset取到属性值:((zval*)((char*)(zobj) + offset))
retval = OBJ_PROP(zobj, property_offset);
} else if (EXPECTED(zobj->properties != NULL)) {
//动态属性的情况,没有在类中显式定义的属性,后面一节会单独介绍
....
}
} else if (UNEXPECTED(EG(exception))) {
...
}
//没有找到属性
//调用魔术方法:__isset()
if ((type == BP_VAR_IS) && zobj->ce->__isset) {
...
}
//调用魔术方法:__get()
if (zobj->ce->__get) {
zend_long *guard = zend_get_property_guard(zobj, Z_STR_P(member));
...
if(!((*guard) & IN_ISSET)){
*guard |= IN_ISSET;
zend_std_call_issetter(&tmp_object, member, &tmp_result);
*guard &= ~IN_ISSET;
...
}
}
...
}
普通成员属性的查找比较容易理解,首先是从zend_class的属性信息哈希表中找到zend_property_info,并判断其可见性(public、private、protected),如果可以访问则直接根据属性的offset在zend_object.properties_table数组中取到属性值,如果没有在属性哈希表中找到且定义了get()魔术方法则会调用get()方法处理。
> Note: 如果类存在get()方法,则在实例化对象分配属性内存(即:properties_table)时会多分配一个zval,类型为HashTable,每次调用get($var)时会把输入的$var名称存入这个哈希表,这样做的目的是防止循环调用,举个例子: > > ***public function get($var) { return $this->$var; }** > > 这种情况是调用get()时又访问了一个不存在的属性,也就是会在get()方法中递归调用,如果不对请求的$var作判断则将一直递归下去,所以在调用get()前首先会判断当前$var是不是已经在get()中了,如果是则不会再调用__get(),否则会把$var作为key插入那个HashTable,然后将哈希值设置为:guard |= IN_ISSET,调用完get()再把哈希值设置为:*guard &= ~IN_ISSET。 > > 这个HashTable不仅仅是给__get()用的,其它魔术方法也会用到,所以其哈希值类型是zend_long,不同的魔术方法占不同的bit位;其次,并不是所有的对象都会额外分配这个HashTable,在对象创建时会根据 zend_class_entry.ce_flags 是否包含 ZEND_ACC_USE_GUARDS 确定是否分配,在类编译时如果发现定义了get()、set()、unset()、isset()方法则会将ce_flags打上这个掩码。
(2)设置属性:
与读取属性不同,设置属性是对属性的修改操作,比如:$obj->name = "pangudashu";
,看下具体的实现过程:
ZEND_API void zend_std_write_property(zval *object, zval *member, zval *value, void **cache_slot)
{
zend_object *zobj;
uint32_t property_offset;
zobj = Z_OBJ_P(object);
//与读取属性相同
property_offset = zend_get_property_offset(zobj->ce, Z_STR_P(member), (zobj->ce->__set != NULL), cache_slot);
if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)) {
if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) {
//普通属性
variable_ptr = OBJ_PROP(zobj, property_offset);
if (Z_TYPE_P(variable_ptr) != IS_UNDEF) {
goto found;
}
} else if (EXPECTED(zobj->properties != NULL)) {
//动态属性哈希表已经初始化,直接插入zobj->properties哈希表,后面单独介绍
...
if ((variable_ptr = zend_hash_find(zobj->properties, Z_STR_P(member))) != NULL) {
found:
//赋值操作,与普通变量的操作相同
zend_assign_to_variable(variable_ptr, value, IS_CV);
goto exit;
}
}
} else if (UNEXPECTED(EG(exception))) {
...
}
//没有找到属性
//如果定义了__set()则调用
if (zobj->ce->__set) {
//与__get()相同,也会判断set的变量名是否已经在__set()中
...
ZVAL_COPY(&tmp_object, object);
(*guard) |= IN_SET; //防止循环__set()
if (zend_std_call_setter(&tmp_object, member, value) != SUCCESS) {
}
(*guard) &= ~IN_SET;
}else if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)) {
...
}
}
首先与读取属性的操作相同:先找到zend_property_info,判断其可见性,然后根据offset取到具体的属性值,最后对其进行赋值修改。
> Note: 属性读写操作的函数中有一个cache_slot的参数,它的作用涉及PHP的一个缓存机制:运行时缓存,后面会单独介绍。