3.4.4 动态属性
前面介绍的成员属性都是在类中明确的定义过的,这些属性在实例化时会被拷贝到对象空间中去,PHP中除了显示的在类中定义成员属性外,还可以动态的创建非静态成员属性,这种属性不需要在类中明确定义,可以直接通过:$obj->property_name=xxx、$this->property_name = xxx为对象设置一个属性,这种属性称之为动态属性,举个例子:
class my_class { public $id = 123; public function test($name, $value){ $this->$name = $value; } } $obj = new my_class; $obj->test("prop_1", array(1,2,3)); //或者直接: //$obj->prop_1 = array(1,2,3); print_r($obj);
在test()方法中直接操作了没有定义的成员属性,上面的例子将输出:
my_class Object ( [id] => 123 [prop_1] => Array ( [0] => 1 [1] => 2 [2] => 3 ) )
前面类、对象两节曾介绍,非静态成员属性值在实例化时保存到了对象中,属性的操作按照编译时按顺序编好的序号操作,各对象对其非静态成员属性的操作互不干扰,那么动态属性是在运行时创建的,它是如何存储的呢?
与普通非静态属性不同,动态创建的属性保存在zend_object->properties哈希表中,查找的时候首先按照普通属性在zend_class_entry.properties_info找,没有找到再去zend_object->properties继续查找。动态属性的创建过程(即:修改属性的操作):
//zend_object->handlers->write_property: ZEND_API void zend_std_write_property(zval *object, zval *member, zval *value, void **cache_slot) { ... zobj = Z_OBJ_P(object); //先在zend_class_entry.properties_info查找此属性 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)) { //普通属性,直接根据根据属性ofsset取出属性值 } else if (EXPECTED(zobj->properties != NULL)) { //有动态属性 ... //从动态属性中查找 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; } } } if (zobj->ce->__set) { //定义了__set()魔法函数 }else if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)){ if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) { ... } else { //首次创建动态属性将在这里完成 if (!zobj->properties) { rebuild_object_properties(zobj); } //将动态属性插入properties zend_hash_add_new(zobj->properties, Z_STR_P(member), value); } } }
上面就是成员属性的修改过程,普通属性根据其offset再从对象中取出属性值进行修改,而首次创建动态属性将通过rebuild_object_properties()初始化zend_object->properties哈希表,后面再创建动态属性直接插入此哈希表,rebuild_object_properties()过程并不仅仅是创建一个HashTable,还会将普通成员属性值插入到这个数组中,与动态属性不同,这里的插入并不是增加原zend_value的refcount,而是创建了一个IS_INDIRECT类型的zval,指向原属性值zval,具体结构如下图。
Note: 这里不清楚将原有属性也插入properties的用意,已知用到的一个地方是在GC垃圾回收获取对象所有属性时(zend_std_get_gc()),如果有动态属性则直接返回properties给GC遍历,假如不把普通的显式定义的属性"拷贝"进来则需要返回、遍历两个数组。
另外一个地方需要注意,把原属性"转移"到properties并不仅仅是创建动态属性时触发的,调用对象的get_properties(即:zend_std_get_properties())也会这么处理,比如将一个object转为array时就会触发这个动作: $arr = (array)$object,通过foreach遍历一个对象时也会调用get_properties获取属性数组进行遍历。
成员属性的读取通过zend_object->handlers->read_property(默认zend_std_read_property())函数完成,动态属性的查找过程实际与write_property中相同:
zval *zend_std_read_property(zval *object, zval *member, int type, void **cache_slot, zval *rv) { ... zobj = Z_OBJ_P(object); //首先查找zend_class_entry.properties_info,普通属性可以在这里找到 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)) { //普通属性 retval = OBJ_PROP(zobj, property_offset); } else if (EXPECTED(zobj->properties != NULL)) { //动态属性从zend_object->properties中查找 retval = zend_hash_find(zobj->properties, Z_STR_P(member)); if (EXPECTED(retval)) goto exit; } } ... }