3.4.4 动态属性

前面介绍的成员属性都是在类中明确的定义过的,这些属性在实例化时会被拷贝到对象空间中去,PHP中除了显示的在类中定义成员属性外,还可以动态的创建非静态成员属性,这种属性不需要在类中明确定义,可以直接通过:$obj->property_name=xxx$this->property_name = xxx为对象设置一个属性,这种属性称之为动态属性,举个例子:

  1. class my_class {
  2. public $id = 123;
  3. public function test($name, $value){
  4. $this->$name = $value;
  5. }
  6. }
  7. $obj = new my_class;
  8. $obj->test("prop_1", array(1,2,3));
  9. //或者直接:
  10. //$obj->prop_1 = array(1,2,3);
  11. print_r($obj);

test()方法中直接操作了没有定义的成员属性,上面的例子将输出:

  1. my_class Object
  2. (
  3. [id] => 123
  4. [prop_1] => Array
  5. (
  6. [0] => 1
  7. [1] => 2
  8. [2] => 3
  9. )
  10. )

前面类、对象两节曾介绍,非静态成员属性值在实例化时保存到了对象中,属性的操作按照编译时按顺序编好的序号操作,各对象对其非静态成员属性的操作互不干扰,那么动态属性是在运行时创建的,它是如何存储的呢?

与普通非静态属性不同,动态创建的属性保存在zend_object->properties哈希表中,查找的时候首先按照普通属性在zend_class_entry.properties_info找,没有找到再去zend_object->properties继续查找。动态属性的创建过程(即:修改属性的操作):

  1. //zend_object->handlers->write_property:
  2. ZEND_API void zend_std_write_property(zval *object, zval *member, zval *value, void **cache_slot)
  3. {
  4. ...
  5. zobj = Z_OBJ_P(object);
  6. //先在zend_class_entry.properties_info查找此属性
  7. property_offset = zend_get_property_offset(zobj->ce, Z_STR_P(member), (zobj->ce->__set != NULL), cache_slot);
  8. if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)) {
  9. if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) {
  10. //普通属性,直接根据根据属性ofsset取出属性值
  11. } else if (EXPECTED(zobj->properties != NULL)) { //有动态属性
  12. ...
  13. //从动态属性中查找
  14. if ((variable_ptr = zend_hash_find(zobj->properties, Z_STR_P(member))) != NULL) {
  15. found:
  16. zend_assign_to_variable(variable_ptr, value, IS_CV);
  17. goto exit;
  18. }
  19. }
  20. }
  21. if (zobj->ce->__set) {
  22. //定义了__set()魔法函数
  23. }else if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)){
  24. if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) {
  25. ...
  26. } else {
  27. //首次创建动态属性将在这里完成
  28. if (!zobj->properties) {
  29. rebuild_object_properties(zobj);
  30. }
  31. //将动态属性插入properties
  32. zend_hash_add_new(zobj->properties, Z_STR_P(member), value);
  33. }
  34. }
  35. }

上面就是成员属性的修改过程,普通属性根据其offset再从对象中取出属性值进行修改,而首次创建动态属性将通过rebuild_object_properties()初始化zend_object->properties哈希表,后面再创建动态属性直接插入此哈希表,rebuild_object_properties()过程并不仅仅是创建一个HashTable,还会将普通成员属性值插入到这个数组中,与动态属性不同,这里的插入并不是增加原zend_value的refcount,而是创建了一个IS_INDIRECT类型的zval,指向原属性值zval,具体结构如下图。

3.4.4 动态属性 - 图1

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中相同:

  1. zval *zend_std_read_property(zval *object, zval *member, int type, void **cache_slot, zval *rv)
  2. {
  3. ...
  4. zobj = Z_OBJ_P(object);
  5. //首先查找zend_class_entry.properties_info,普通属性可以在这里找到
  6. property_offset = zend_get_property_offset(zobj->ce, Z_STR_P(member), (type == BP_VAR_IS) || (zobj->ce->__get != NULL), cache_slot);
  7. if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)) {
  8. if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) {
  9. //普通属性
  10. retval = OBJ_PROP(zobj, property_offset);
  11. } else if (EXPECTED(zobj->properties != NULL)) {
  12. //动态属性从zend_object->properties中查找
  13. retval = zend_hash_find(zobj->properties, Z_STR_P(member));
  14. if (EXPECTED(retval)) goto exit;
  15. }
  16. }
  17. ...
  18. }