3.4.1 类
类是现实世界或思维世界中的实体在计算机中的反映,它将某些具有关联关系的数据以及这些数据上的操作封装在一起。在面向对象中类是对象的抽象,对象是类的具体实例。
在PHP中类是编译阶段的产物,而对象是运行时产生的,它们归属于不同阶段。
PHP中我们这样定义一个类:
class 类名 {常量;成员属性;成员方法;}
一个类可以包含有属于自己的常量、变量(称为“属性”)以及函数(称为“方法”),本节将围绕这三部分具体弄清楚以下几个问题:
- a.类的存储及索引
- b.成员属性的存储结构
- c.成员方法的存储结构
- d.成员方法的调用过程及与普通function调用的差别
3.4.1.1 类的结构及存储
首先我们看下类的数据结构:
struct _zend_class_entry {char type; //类的类型:内部类ZEND_INTERNAL_CLASS(1)、用户自定义类ZEND_USER_CLASS(2)zend_string *name; //类名,PHP类不区分大小写,统一为小写struct _zend_class_entry *parent; //父类int refcount;uint32_t ce_flags; //类掩码,如普通类、抽象类、接口,除了这还有别的含义,暂未弄清int default_properties_count; //普通属性数,包括public、privateint default_static_members_count; //静态属性数,staticzval *default_properties_table; //普通属性值数组zval *default_static_members_table; //静态属性值数组zval *static_members_table;HashTable function_table; //成员方法哈希表HashTable properties_info; //成员属性基本信息哈希表,key为成员名,value为zend_property_infoHashTable constants_table; //常量哈希表,通过constant定义的//以下是构造函授、析构函数、魔术方法的指针union _zend_function *constructor;union _zend_function *destructor;union _zend_function *clone;union _zend_function *__get;union _zend_function *__set;union _zend_function *__unset;union _zend_function *__isset;union _zend_function *__call;union _zend_function *__callstatic;union _zend_function *__tostring;union _zend_function *__debugInfo;union _zend_function *serialize_func;union _zend_function *unserialize_func;zend_class_iterator_funcs iterator_funcs;//自定义的钩子函数,通常是定义内部类时使用,可以灵活的进行一些个性化的操作//用户自定义类不会用到,暂时忽略即可zend_object* (*create_object)(zend_class_entry *class_type);zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref);int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type); /* a class implements this interface */union _zend_function *(*get_static_method)(zend_class_entry *ce, zend_string* method);/* serializer callbacks */int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len, zend_serialize_data *data);int (*unserialize)(zval *object, zend_class_entry *ce, const unsigned char *buf, size_t buf_len, zend_unserialize_data *data);uint32_t num_interfaces; //实现的接口数uint32_t num_traits;zend_class_entry **interfaces; //实现的接口zend_class_entry **traits;zend_trait_alias **trait_aliases;zend_trait_precedence **trait_precedences;union {struct {zend_string *filename;uint32_t line_start;uint32_t line_end;zend_string *doc_comment;} user;struct {const struct _zend_function_entry *builtin_functions;struct _zend_module_entry *module; //所属扩展} internal;} info;}
create_object为实例化对象的操作,可以通过扩展自定义一个函数来接管实例化对象的操作,没有定义这个函数的话将由默认的zend_objects_new()处理,自定义时可以参考这个函数的实现:
//注意:此操作并没有将属性拷贝到zend_object中:由object_properties_init()完成ZEND_API zend_object *zend_objects_new(zend_class_entry *ce){zend_object *object = emalloc(sizeof(zend_object) + zend_object_properties_size(ce));zend_object_std_init(object, ce);//设置对象操作的handlerobject->handlers = &std_object_handlers;return object;}
举个例子具体看下,定义一个User类,它继承了Human类,User类中有一个常量、一个静态属性、两个普通属性:
//父类class Human {}class User extends Human{const type = 110;static $name = "uuu";public $uid = 900;public $sex = 'w';public function __construct(){}public function getName(){return $this->name;}}
其对应的zend_class_entry存储结构如下图。

开始的时候已经提到,类是编译阶段的产物,编译完成后我们定义的每个类都会生成一个zend_class_entry,它保存着类的全部信息,在执行阶段所有类相关的操作都是用的这个结构。
所有PHP脚本中定义的类以及内核、扩展中定义的内部类通过一个以”类名”作为索引的哈希表存储,这个哈希表保存在Zend引擎global变量中:zend_executor_globals.class_table(即:EG(class_table)),与function的存储相同,关于这个global变量前面《3.3.1.3 zend_executor_globals》已经讲过。

在接下来的小节中我们将对类的常量、成员属性、成员方法的实现具体分析。
3.4.1.2 类常量
PHP中可以把在类中始终保持不变的值定义为常量,在定义和使用常量的时候不需要使用 $ 符号,常量的值必须是一个定值(如布尔型、整形、字符串、数组,php5.*不支持数组),不能是变量、数学运算的结果或函数调用,也就是说它是只读的,无法进行赋值。
常量通过 const 定义:
class my_class {const 常量名 = 常量值;}
常量通过 class_name::常量名 访问,或在class内部通过 self::常量名 访问。
常量是类维度的数据(而不是对象的),它们通过zend_class_entry.constants_table进行存储,这是一个哈希结构,通过 常量名 索引,value就是具体定义的常量值。
常量的读取:
根据前面我们对PHP opcode已有的了解,我们可以猜测常量访问的opcode的组成:常量名保存在literals中(其op_type = IS_CONST),执行时先取出常量名,然后去zend_class_entry.constants_table哈希表中索引到具体的常量值即可。
事实上我们的这个猜测并不是完全正确的,因为有的情况确实是我们猜想的那样,但是还有另外一种情况,比较下两个例子的不同:
//示例1echo my_class::A1;class my_class {const A1 = "hi";}
//示例2class my_class {const A1 = "hi";}echo my_class::A1;
唯一的不同就是常量的使用时机:示例1是在定义前使用的,示例2是在定义后使用的。我们都知道PHP变量无需提前声明,这俩会有什么不同呢?
事实上这两种情况内核会有两种不同的处理方式,示例1这种情况的处理与我们上面的猜测相同,而示例2则有另外一种处理方式:PHP代码的编译是顺序的,示例2的情况编译到echo my_class::A1这行时首先会尝试检索下是否已经编译了my_class,如果能在CG(class_table)中找到,则进一步从类的contants_table查找对应的常量,找到的话则会复制其value替换常量,简单的讲就是类似C语言中的宏,编译时替换为实际的值了,而不是在运行时再去检索。
具体debug下上面两个例子会发现示例2的主要的opcode只有一个ZEND_ECHO,也就是直接输出值了,并没有设计类常量的查找,这就是因为编译的时候已经将 my_class::A1 替换为 hi 了,echo my_class::A1;等同于:echo "hi";;而示例1首先的操作则是ZEND_FETCH_CONSTANT,查找常量,接着才是ZEND_ECHO。
3.4.1.3 成员属性
类的变量成员叫做“属性”。属性声明是由关键字 public,protected 或者 private 开头,然后跟一个普通的变量声明来组成,关于这三个关键字这里不作讨论,后面分析可见性的章节再作说明。
【修饰符(public/private/protected/static)】【成员属性名】= 【属性默认值】;
属性中的变量可以初始化,但是初始化的值必须是常数,这里的常数是指 PHP 脚本在编译阶段时就可以得到其值,而不依赖于运行时的信息才能求值,比如public $time = time();这样定义一个属性就会触发语法错误。
成员属性又分为两类:普通属性、静态属性。静态属性通过 static 声明,通过 self::property 访问;普通属性通过 object->property 访问。
class my_class {//普通属性public $property = 初始化值;//静态属性public static $property_2 = 初始化值;}
与常量的存储方式不同,成员属性的 初始化值 并不是 直接 用以”属性名”作为索引的哈希表存储的,而是通过数组保存的,普通属性、静态属性各有一个数组分别存储。

看到这里可能有个疑问:使用时成员属性是如果找到的呢?
实际只是成员属性的 VALUE 通过数组存储的,访问时仍然是根据以”属性名”为索引的散列表查找具体VALUE的,这个散列表并没有按照普通属性、静态属性分为两个,而是只用了一个:HashTable properties_info 。此哈希表存储元素的value类型为 zend_property_info 。
typedef struct _zend_property_info {uint32_t offset; //普通成员变量的内存偏移值//静态成员变量的数组索引uint32_t flags; //属性掩码,如public、private、protected及是否为静态属性zend_string *name; //属性名:并不是原始属性名zend_string *doc_comment;zend_class_entry *ce; //所属类} zend_property_info;//flags标识位#define ZEND_ACC_PUBLIC 0x100#define ZEND_ACC_PROTECTED 0x200#define ZEND_ACC_PRIVATE 0x400#define ZEND_ACC_STATIC 0x01
- name:属性名,特别注意的是这里并不全是原始属性名,private会在原始属性名前加上类名,protected则会加上*作为前缀
- offset:这个值记录的就是上面说的通过数组保存的属性值的索引,也就是说属性值保存在一个数组中,然后将其在数组中的位置保存在offset中,另外需要说明的一点的是普通属性、静态属性这个值用法是不一样的,静态属性是类的范畴,与对象无关,所以其offset为default_static_members_table数组的下标:0,、1、2……,而普通属性归属于对象,每个对象有其各自的属性,所以这个offset记录的实际是 各属性在object中偏移值 (在后面《3.4.2 对象》一节我们再具体说明普通属性的存储方式),其值是:40、56、72……是按照zval的内存大小偏移的
- flags:bit位,标识的是属性的信息,如public、private、protected及是否为静态属性
所以访问成员属性时首先是根据属性名查找到此属性的存储位置,然后再进一步获取属性值。
举个例子:
class my_class {public $property_1 = "aa";public $property_2 = array();public static $property_3 = 110;}
则 default_properties_table、default_static_properties_table、properties_info 关系图:

下面我们再看下普通成员属性与静态成员属性的不同:静态成员变量保存在类中,各对象共享同一份数据,而普通属性属于对象,各对象独享。
成员属性在类编译阶段就已经分配了zval,静态与普通的区别在于普通属性在创建一个对象时还会重新分配zval(这个过程类似zend引擎执行前分配在zend_execute_data后面的动态变量空间),对象对普通属性的操作都是在其自己的空间进行的,各对象隔离,而静态属性的操作始终是在类的空间内,各对象共享。
3.4.1.4 成员方法
每个类可以定义若干属于本类的函数(称之为成员方法),这种函数与普通的function相同,只是以类的维度进行管理,不是全局性的,所以成员方法保存在类中而不是EG(function_table)。

成员方法的定义:
【修饰符(public/private/protected/static/abstruct/final)】function 【&】【成员方法名】(【参数列表】)【返回值类型】{【成员方法】};
成员方法也有静态、非静态之分,静态方法中不能使用this访问属于本对象的成员属性。
静态方法也是通过static关键词定义:
class my_class {static public function test() {$a = "hi~";echo $a;}}//静态方法可以这么调用:my_class::test();//也可以这样:$method = 'test';my_class::$method();
静态方法中调用其它静态方法或静态变量可以通过 self 访问。
成员方法的调用与普通function过程基本相同,根据对象所属类或直接根据类取到method的zend_function,然后执行,具体的过程《3.3 Zend引擎执行过程》已经详细说过,这里不再重复。
3.4.1.5 自定义类的编译
前面我们先介绍了类的相关组成部分,接下来我们从一个例子简单看下类的编译过程,这个过程最终的产物就是zend_class_entry。
//示例class Human {public $aa = array(1,2,3);}class User extends Human{const type = 110;static $name = "uuu";public $uid = 900;public $sex = 'w';public function __construct(){}public function getName(){return $this->name;}}
类的定义组成部分:
【修饰符(abstract/final)】 class 【类名】 【extends 父类】 【implements 接口1,接口2】 {}
语法规则为:
class_declaration_statement:class_modifiers T_CLASS { $<num>$ = CG(zend_lineno); }T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}'{ $$ = zend_ast_create_decl(ZEND_AST_CLASS, $1, $<num>3, $7, zend_ast_get_str($4), $5, $6, $9, NULL); }| T_CLASS { $<num>$ = CG(zend_lineno); }T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}'{ $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $<num>2, $6, zend_ast_get_str($3), $4, $5, $8, NULL); };//整个类内为list,每个成员属性、成员方法都是一个子节点class_statement_list:class_statement_list class_statement{ $$ = zend_ast_list_add($1, $2); }| /* empty */{ $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); };//类内语法规则:成员属性、成员方法class_statement:variable_modifiers property_list ';'{ $$ = $2; $$->attr = $1; }| T_CONST class_const_list ';'{ $$ = $2; RESET_DOC_COMMENT(); }| T_USE name_list trait_adaptations{ $$ = zend_ast_create(ZEND_AST_USE_TRAIT, $2, $3); }| method_modifiers function returns_ref identifier backup_doc_comment '(' parameter_list ')'return_type method_body{ $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1, $2, $5,zend_ast_get_str($4), $7, NULL, $10, $9); };
生成的抽象语法树:

类的语法树根节点为ZEND_AST_CLASS,此节点有3个子节点:继承子节点、实现接口子节点、类中声明表达式节点,其中child2为zend_ast_list,每个常量定义、成员属性、成员方法对应一个节点,比如上面的例子中user类有6个子节点,这些子节点类型有3类:常量声明(ZEND_AST_CLASS_CONST_DECL)、属性声明(ZEND_AST_PROP_DECL)、方法声明(ZEND_AST_METHOD)。
编译为opcodes操作为:zend_compile_class_decl(),它的输入就是ZEND_AST_CLASS节点,这个函数中再针对常量、属性、方法、继承、接口等分别处理。
void zend_compile_class_decl(zend_ast *ast){zend_ast_decl *decl = (zend_ast_decl *) ast;zend_ast *extends_ast = decl->child[0]; //继承类节点,zen_ast_zval节点,存的是父类名zend_ast *implements_ast = decl->child[1]; //实现接口节点zend_ast *stmt_ast = decl->child[2]; //类中声明的常量、属性、方法zend_string *name, *lcname;zend_class_entry *ce = zend_arena_alloc(&CG(arena), sizeof(zend_class_entry));zend_op *opline;...lcname = zend_new_interned_string(lcname);ce->type = ZEND_USER_CLASS; //类型为用户自定义类ce->name = name; //类名zend_initialize_class_data(ce, 1);...if (extends_ast) {...//有继承的父类则首先生成一条ZEND_FETCH_CLASS的opcodezend_compile_class_ref(&extends_node, extends_ast, 0);}//在当前父空间生成一条opcodeopline = get_next_op(CG(active_op_array));zend_make_var_result(&declare_node, opline);...opline->op2_type = IS_CONST;LITERAL_STR(opline->op2, lcname);if (decl->flags & ZEND_ACC_ANON_CLASS) {//暂不清楚这种情况}else{zend_string *key;if (extends_ast) {opline->opcode = ZEND_DECLARE_INHERITED_CLASS; //有继承的类为这个opcodeopline->extended_value = extends_node.u.op.var;} else {opline->opcode = ZEND_DECLARE_CLASS; //无继承的类为这个opcode}key = zend_build_runtime_definition_key(lcname, decl->lex_pos); //这个key并不是类名,而是:类名+file+lex_posopline->op1_type = IS_CONST;LITERAL_STR(opline->op1, key);//将这个临时key保存到操作数1中zend_hash_update_ptr(CG(class_table), key, ce); //将半成品的zend_class_entry插入CG(class_table),注意这里并不是执行时用于索引类的,它的key不是类名!!!}CG(active_class_entry) = ce;zend_compile_stmt(stmt_ast); //将常量、成员属性、方法编译到CG(active_class_entry)中...CG(active_class_entry) = original_ce;}
上面这个过程主要操作是新分配一个zend_class_entry,如果有继承的话首先生成一条ZEND_FETCH_CLASS的opcode,然后生成一条类声明的opcode(这个地方与之前3.2.1.3节介绍函数的编译时相同),接着就是编译常量、属性、成员方法到新分配的zend_class_entry中,这个过程还有一个容易误解的地方:将生成的zend_class_entry插入到CG(class_table)哈希表中,这个操作这是中间步骤,它的key并不是类名,而是类名后面带来一长串其它的字符,也就是这个时候通过类名在class_table是索引不到对应类的,后面我们会说明这样处理的作用。
Human类情况比较简单,不再展开,我们看下User类在zend_compile_class_decl()中执行到zend_compile_stmt(stmt_ast)这步时关键数据结构:

接下来我们分别看下常量、成员属性、方法的编译过程。
(1)常量编译
常量的节点类型为:ZEND_AST_CLASS_CONST_DECL,每个常量对应一个这样的节点,处理函数为:zend_compile_class_const_decl():
void zend_compile_class_const_decl(zend_ast *ast){zend_ast_list *list = zend_ast_get_list(ast);zend_class_entry *ce = CG(active_class_entry);uint32_t i;for (i = 0; i < list->children; ++i) { //const声明了多个常量,遍历编译每个子节点zend_ast *const_ast = list->child[i];zend_ast *name_ast = const_ast->child[0]; //常量名节点zend_ast *value_ast = const_ast->child[1];//常量值节点zend_string *name = zend_ast_get_str(name_ast); //常量名zval value_zv;//取出常量值zend_const_expr_to_zval(&value_zv, value_ast);name = zend_new_interned_string_safe(name);//将常量添加到zend_class_entry.constants_table哈希表中if (zend_hash_add(&ce->constants_table, name, &value_zv) == NULL) {...}...}}
(2)属性编译
属性节点类型为:ZEND_AST_PROP_DECL,对应的处理函数:zend_compile_prop_decl():
void zend_compile_prop_decl(zend_ast *ast){zend_ast_list *list = zend_ast_get_list(ast);uint32_t flags = list->attr; //属性修饰符:static、public、private、protectedzend_class_entry *ce = CG(active_class_entry);uint32_t i, children = list->children;for (i = 0; i < children; ++i) {zend_ast *prop_ast = list->child[i]; //这个节点类型为:ZEND_AST_PROP_ELEMzend_ast *name_ast = prop_ast->child[0]; //属性名节点zend_ast *value_ast = prop_ast->child[1]; //属性值节点zend_ast *doc_comment_ast = prop_ast->child[2];zend_string *name = zend_ast_get_str(name_ast); //属性名zend_string *doc_comment = NULL;zval value_zv;...//检查该属性是否在当前类中已经定义if (zend_hash_exists(&ce->properties_info, name)) {zend_error_noreturn(...);}if (value_ast) {//取出默认值zend_const_expr_to_zval(&value_zv, value_ast);} else {//默认值为nullZVAL_NULL(&value_zv);}name = zend_new_interned_string_safe(name);//保存属性zend_declare_property_ex(ce, name, &value_zv, flags, doc_comment);}}
开始的时候我们已经介绍:属性值是通过 数组 保存的,然后其存储位置通过以 属性名 为key的哈希表保存,使用的时候先从这个哈希表中找到属性信息同时得到属性值的保存位置,然后再进一步取出属性值。
zend_declare_property_ex()这步操作就是来确定属性的存储位置的,它将属性值按静态、非静态分别保存在default_static_members_table、default_properties_table两个数组中,同时将其存储位置保存到属性结构的offset中。
//zend_API.cZEND_API int zend_declare_property_ex(zend_class_entry *ce, zend_string *name, zval *property, int access_type,...){zend_property_info *property_info, *property_info_ptr;if (ce->type == ZEND_INTERNAL_CLASS) {//内部类...}else{property_info = zend_arena_alloc(&CG(arena), sizeof(zend_property_info));}if (access_type & ZEND_ACC_STATIC) {//静态属性...property_info->offset = ce->default_static_members_count++; //分配属性编号,同变量一样,静态属性的就是数组索引ce->default_static_members_table = perealloc(ce->default_static_members_table, sizeof(zval) * ce->default_static_members_count, ce->type == ZEND_INTERNAL_CLASS);ZVAL_COPY_VALUE(&ce->default_static_members_table[property_info->offset], property);if (ce->type == ZEND_USER_CLASS) {ce->static_members_table = ce->default_static_members_table;}}else{//非静态属性...//非静态属性值存储在对象中,所以与静态属性不同,它的offset并不是default_properties_table数组索引//而是相对于zend_object大小的(因为普通属性值数组保存在zend_object结构之后,这个与局部变量、zend_execute_data关系一样)property_info->offset = OBJ_PROP_TO_OFFSET(ce->default_properties_count);ce->default_properties_count++;ce->default_properties_table = perealloc(ce->default_properties_table, sizeof(zval) * ce->default_properties_count, ce->type == ZEND_INTERNAL_CLASS);ZVAL_COPY_VALUE(&ce->default_properties_table[OBJ_PROP_TO_NUM(property_info->offset)], property);}//设置property_info其它的一些值...}
这个操作中重点是offset的计算方式,静态属性这个比较好理解,就是default_static_members_table数组索引;非静态属性zend_class_entry.default_properties_table保存的只是默认属性值,我们在下一篇介绍对象时再具体说明object、class之间属性的存储关系。
(3)成员方法编译
3.4.1.4一节已经介绍过成员方法与普通函数的关系,两者没有很大的区别,实现上是相同,不同的地方在于成员方法保存在各zend_class_entry中,调用时会有一些可见性方面的限制,如private、public、protected,还有一些专有用法,比如this、self等,但在编译、执行、存储结构等方面两者基本是一致的。
成员方法的语法树根节点为ZEND_AST_METHOD:
void zend_compile_stmt(zend_ast *ast){...switch (ast->kind) {...case ZEND_AST_FUNC_DECL: //函数case ZEND_AST_METHOD: //成员方法zend_compile_func_decl(NULL, ast);break;...}}
如果你还记得3.2.1.3函数处理的过程就会发现函数、成员方法的编译是同一个函数:zend_compile_func_decl()。
void zend_compile_func_decl(znode *result, zend_ast *ast){//参数、函数内语法编译等不看了,与函数的相同,不清楚请看3.2.1.3节...if (is_method) {zend_bool has_body = stmt_ast != NULL;zend_begin_method_decl(op_array, decl->name, has_body);} else {//函数是在当前空间生成了一条ZEND_DECLARE_FUNCTION的opcode//然后在zend_do_early_binding()中"执行"了这条opcode,即将函数添加到CG(function_table)zend_begin_func_decl(result, op_array, decl);}...}
这个过程之前已经说过,这里不再重复,我们只看下与普通函数处理不同的地方:zend_begin_method_decl(),它的工作也比较简单,最重要的一个地方就是将成员方法的zend_op_array插入 zend_class_entry.function_table。
void zend_begin_method_decl(zend_op_array *op_array, zend_string *name, zend_bool has_body){zend_class_entry *ce = CG(active_class_entry);...op_array->scope = ce;op_array->function_name = zend_string_copy(name);lcname = zend_string_tolower(name);lcname = zend_new_interned_string(lcname);//插入类的function_table中if (zend_hash_add_ptr(&ce->function_table, lcname, op_array) == NULL) {zend_error_noreturn(..);}//后面主要是设置一些构造函数、析构函数、魔术方法指针,以及其它一些可见性、静态非静态的检查...}
上面我们分别介绍了常量、成员属性、方法的编译过程,最后再用一张图总结下整个类的编译过程:

图中还有一步我们没有说到:zend_do_early_binding() ,这是非常关键的一步,如果你看过3.2.1.3一节那么对这个函数应该不陌生,没错,在函数编译的最后一步也会调用这个函数,它的作用是将编译的function以函数名为key添加到CG(function_table)中,同样地上面整个过程中你可能发现所有的操作都是针对zend_class_entry,并没有发现最后把它存到什么位置了,这最后的一步就是把zend_class_entry以类名为key添加到CG(class_table)。
void zend_do_early_binding(void){...switch (opline->opcode) {...case ZEND_DECLARE_CLASS:if (do_bind_class(CG(active_op_array), opline, CG(class_table), 1) == NULL) {return;}table = CG(class_table);break;case ZEND_DECLARE_INHERITED_CLASS://比较长,后面单独摘出来break;}//将那个以(类名+file+lex_pos)为key的值从CG(class_table)中删除//同时删除两个相关的literals:key、类名zend_hash_del(table, Z_STR_P(CT_CONSTANT(opline->op1)));zend_del_literal(CG(active_op_array), opline->op1.constant);zend_del_literal(CG(active_op_array), opline->op2.constant);MAKE_NOP(opline); //将ZEND_DECLARE_CLASS或ZEND_DECLARE_INHERITED_CLASS的opcode置为空,表示已执行}
这个地方会有两种情况,上面我们说过,如果是普通的没有继承的类定义会生成一条ZEND_DECLARE_CLASS的opcode,而有继承的类则会生成ZEND_FETCH_CLASS、ZEND_DECLARE_INHERITED_CLASS两条opcode,这两种有很大的不同,接下来我们具体看下:
(1)无继承类: 这种情况直接调用
do_bind_class()处理了。
ZEND_API zend_class_entry *do_bind_class(const zend_op_array* op_array,const zend_op *opline,HashTable *class_table,zend_bool compile_time){if (compile_time) { //编译时//还记得zend_compile_class_decl()中有一个把zend_class_entry以(类名+file+lex_pos)//为key存入CG(class_table)的操作吗?那个key的存储位置保存在op1中了//这里就是从op_array.literals中取出那个keyop1 = CT_CONSTANT_EX(op_array, opline->op1.constant);//op2为类名op2 = CT_CONSTANT_EX(op_array, opline->op2.constant);} else { //运行时,如果当前类在编译阶段没有编译完成则也有可能在zend_execute执行阶段完成op1 = RT_CONSTANT(op_array, opline->op1);op2 = RT_CONSTANT(op_array, opline->op2);}//从CG(class_table)中取出zend_class_entryif ((ce = zend_hash_find_ptr(class_table, Z_STR_P(op1))) == NULL) {zend_error_noreturn(E_COMPILE_ERROR, ...);return NULL;}ce->refcount++; //这里加1是因为CG(class_table)中多了一个bucket指向这个ce了//以标准类名为key将zend_class_entry插入CG(class_table)//这才是后面要用到的类if (zend_hash_add_ptr(class_table, Z_STR_P(op2), ce) == NULL) {//插入失败return NULL;}else{//插入成功return ce;}}
这个函数就是将类以 正确的类名 为key插入到CG(class_table),这一步完成后
zend_do_early_binding()后面就将ZEND_DECLARE_CLASS这条opcode置为0了,这样在运行时就直接跳过此opcode了,现在清楚为什么执行时会有很多为0的opcode了吧?(2)有继承类: 这种类是有继承的父类,它的定义有两条opcode:
ZEND_FETCH_CLASS、ZEND_DECLARE_INHERITED_CLASS,上面我们一张图画过示例中user类编译的情况,我们先看下它的opcode再作说明。

case ZEND_DECLARE_INHERITED_CLASS:{zend_op *fetch_class_opline = opline-1;zval *parent_name;zend_class_entry *ce;parent_name = CT_CONSTANT(fetch_class_opline->op2); //父类名//在EG(class_table)中查找父类(注意:EG(class_table)与CG(class_table)指向同一个位置)if (((ce = zend_lookup_class_ex(Z_STR_P(parent_name), parent_name + 1, 0)) == NULL) || ...) {//没找到父类,有可能父类没有定义、有可能父类在子类之后定义的......if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {...//将opcode重置为ZEND_DECLARE_INHERITED_CLASS_DELAYEDopline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED;opline->result_type = IS_UNUSED;opline->result.opline_num = -1;}return;}//注册继承类if (do_bind_inherited_class(CG(active_op_array), opline, CG(class_table), ce, 1) == NULL) {return;}//清理无用的opcode:ZEND_FETCH_CLASS,重置为0,执行时直接跳过zend_del_literal(CG(active_op_array), fetch_class_opline->op2.constant);MAKE_NOP(fetch_class_opline);table = CG(class_table);break;}
通过上面的处理我们可以看到,首先是查找父类:
1)如果父类没有找到则将opcode置为
ZEND_DECLARE_INHERITED_CLASS_DELAYED,这种情况下当前类是没有编译到CG(class_table)中去的,也就是这个时候这个类是无法使用的,在执行的时候会再次尝试这个过程,那个时候如果找到父类了则再加入EG(class_table);2)如果找到父类了则与无继承的类处理一样,将zend_class_entry添加到CG(class_table)中,然后将对应的两条opcode删掉,除了这个外还有一个非常重要的操作:
zend_do_inheritance(),这里主要是进行属性、常量、成员方法的合并、拷贝,这个过程这里暂不展开,《3.4.3继承》一节再作具体说明。
总结:
上面我们介绍了类的编译过程,整个流程东西比较但并不复杂,主要围绕zend_class_entry进行的操作,另外我们知道了类插入EG(class_table)的过程,这个相当于类的声明在编译阶段提前”执行”了,也有可能因为父类找不到等原因延至运行时执行,清楚了这个过程你应该能明白下面这些例子为什么有的可以运行而有的则报错的原因了吧?
//情况1new A();class A extends B{}class B{}===================完整opcodes:1 ZEND_NEW => 执行到这报错,因为此时A因为找不到B尚未编译进EG(class_table)2 ZEND_DO_FCALL3 ZEND_FETCH_CLASS4 ZEND_DECLARE_INHERITED_CLASS5 ZEND_DECLARE_CLASS => 注册class B6 ZEND_RETURN实际执行顺序:5->1->2->3->4->6
//情况2class A extends B{}class B{}new A();===================完整opcodes:1 ZEND_FETCH_CLASS2 ZEND_DECLARE_INHERITED_CLASS => 注册class A,此时已经可以找到B3 ZEND_DECLARE_CLASS => 注册class B4 ZEND_NEW5 ZEND_DO_FCALL6 ZEND_RETURN实际执行顺序:3->1->2->4->5->6,执行到4时A都已经注册,所以可以执行
//情况3class A extends B{}class B extends C{}class C{}new A();===================完整opcodes:1 ZEND_FETCH_CLASS => 找不到B,直接报错2 ZEND_DECLARE_INHERITED_CLASS3 ZEND_FETCH_CLASS4 ZEND_DECLARE_INHERITED_CLASS => 注册class B,此时可以找到C,所以注册成功5 ZEND_DECLARE_CLASS => 注册class C6 ZEND_NEW7 ZEND_DO_FCALL8 ZEND_RETURN实际执行顺序:5->1->2->3->4->5->6->7->8,执行到1发现还是找不到父类B,报错
3.4.1.6 内部类
前面我们介绍了类的基本组成以及用户自定义类的编译,除了在PHP代码中可以定义一个类,我们也可以在内核或扩展中定义一个类(与定义内部函数类似),这种类称之为 内部类。
相比于用户自定义类的编译实现,内部类的定义比较简单,也更加灵活,可以进行一些个性化的处理,比如我们可以定义创建对象的钩子函数:create_object,从而在对象实例化时调用我们自己定义的函数完成,这样我们就可以进行很多其它的操作。
内部类的定义简单的概括就是创建一个zend_class_entry结构,然后插入到EG(class_table)中,涉及的操作主要有:
- 注册类到符号表
- 实现继承、接口
- 定义常量
- 定义成员属性
- 定义成员方法
实际这些与用户自定义类的实现相同,只是内部类直接调用相关API完成这些操作,具体的API接口本节不再介绍,我们将在后面介绍扩展开发一章中再系统说明。
3.4.2 对象
对象是类的实例,PHP中要创建一个类的实例,必须使用 new 关键字。类应在被实例化之前定义(某些情况下则必须这样,比如3.4.1最后那几个例子)。
3.4.2.1 对象的数据结构
对象的数据结构非常简单:
typedef struct _zend_object zend_object;struct _zend_object {zend_refcounted_h gc; //引用计数uint32_t handle;zend_class_entry *ce; //所属类const zend_object_handlers *handlers; //对象操作处理函数HashTable *properties;zval properties_table[1]; //普通属性值数组};
几个主要的成员:
(1)handle: 一次request期间对象的编号,每个对象都有一个唯一的编号,与创建先后顺序有关,主要在垃圾回收时用,下面会详细说明。
(2)ce: 所属类的zend_class_entry。
(3)handlers: 这个保存的对象相关操作的一些函数指针,比如成员属性的读写、成员方法的获取、对象的销毁/克隆等等,这些操作接口都有默认的函数。
struct _zend_object_handlers {int offset;zend_object_free_obj_t free_obj; //释放对象zend_object_dtor_obj_t dtor_obj; //销毁对象zend_object_clone_obj_t clone_obj;//复制对象zend_object_read_property_t read_property; //读取成员属性zend_object_write_property_t write_property;//修改成员属性...}//默认值处理handlerZEND_API zend_object_handlers std_object_handlers = {0,zend_object_std_dtor, /* free_obj */zend_objects_destroy_object, /* dtor_obj */zend_objects_clone_obj, /* clone_obj */zend_std_read_property, /* read_property */zend_std_write_property, /* write_property */zend_std_read_dimension, /* read_dimension */zend_std_write_dimension, /* write_dimension */zend_std_get_property_ptr_ptr, /* get_property_ptr_ptr */NULL, /* get */NULL, /* set */zend_std_has_property, /* has_property */zend_std_unset_property, /* unset_property */zend_std_has_dimension, /* has_dimension */zend_std_unset_dimension, /* unset_dimension */zend_std_get_properties, /* get_properties */zend_std_get_method, /* get_method */NULL, /* call_method */zend_std_get_constructor, /* get_constructor */zend_std_object_get_class_name, /* get_class_name */zend_std_compare_objects, /* compare_objects */zend_std_cast_object_tostring, /* cast_object */NULL, /* count_elements */zend_std_get_debug_info, /* get_debug_info */zend_std_get_closure, /* get_closure */zend_std_get_gc, /* get_gc */NULL, /* do_operation */NULL, /* compare */}
Note: 这些handler用于操作对象(如:设置、读取属性),std_object_handlers是PHP定义的默认、标准的处理函数,在扩展中可以自定义handler,比如:重定义write_property,这样设置一个对象的属性时将调用扩展自己定义的处理函数,让扩展拥有了更高的控制权限。 需要注意的是:const zend_object_handlers handlers,这里的handlers指针加了const修饰符,const修饰的是handlers*指向的对象,而不是handlers指针本身,所以扩展中可以将一个对象的handlers修改为另一个zend_object_handlers指针,但无法修改zend_object_handlers中的值,比如:
obj->handlers->write_property = xxx将报错,而:obj->handlers = xxx则是可以的。
(4)properties: 普通成员属性哈希表,对象创建之初这个值为NULL,主要是在动态定义属性时会用到,与properties_table有一定关系,下一节我们将单独说明,这里暂时忽略。
(5)properties_table: 成员属性数组,还记得我们在介绍类一节时提过非静态属性存储在对象结构中吗?就是这个properties_table!注意,它是一个数组,zend_object是个变长结构体,分配时会根据非静态属性的数量确定其大小。
3.4.2.2 对象的创建
PHP中通过new + 类名创建一个类的实例,我们从一个例子分析下对象创建的过程中都有哪些操作。
class my_class{const TYPE = 90;public $name = "pangudashu";public $ids = array();}$obj = new my_class();
类的定义就不用再说了,我们只看$obj = new my_class();这一句,这条语句包括两部分:实例化类、赋值,下面看下实例化类的语法规则:
new_expr:T_NEW class_name_reference ctor_arguments{ $$ = zend_ast_create(ZEND_AST_NEW, $2, $3); }| T_NEW anonymous_class{ $$ = $2; };
从语法规则可以很直观的看出此语法的两个主要部分:类名、参数列表,编译器在解析到实例化类时就创建一个ZEND_AST_NEW类型的节点,后面编译为opcodes的过程我们不再细究,这里直接看下最终生成的opcodes。

你会发现实例化类产生了两条opcode(实际可能还会更多):ZEND_NEW、ZEND_DO_FCALL,除了创建对象的操作还有一条函数调用的,没错,那条就是调用构造方法的操作。
根据opcode、操作数类型可知ZEND_NEW对应的处理handler为ZEND_NEW_SPEC_CONST_HANDLER():
static int ZEND_NEW_SPEC_CONST_HANDLER(zend_execute_data *execute_data){zval object_zval;zend_function *constructor;zend_class_entry *ce;...//第1步:根据类名查找zend_class_entryce = zend_fetch_class_by_name(Z_STR_P(EX_CONSTANT(opline->op1)), ...);...//第2步:创建&初始化一个这个类的对象if (UNEXPECTED(object_init_ex(&object_zval, ce) != SUCCESS)) {HANDLE_EXCEPTION();}//第3步:获取构造方法//获取构造方法函数,实际就是直接取zend_class_entry.constructor//get_constructor => zend_std_get_constructor()constructor = Z_OBJ_HT(object_zval)->get_constructor(Z_OBJ(object_zval));if (constructor == NULL) {...//此opcode之后还有传参、调用构造方法的操作//所以如果没有定义构造方法则直接跳过这些操作ZEND_VM_JMP(OP_JMP_ADDR(opline, opline->op2));}else{//定义了构造方法//初始化调用构造函数的zend_execute_datazend_execute_data *call = zend_vm_stack_push_call_frame(...);call->prev_execute_data = EX(call);EX(call) = call;...}}
从上面的创建对象的过程看整个流程主要分为三步:首先是根据类名在EG(class_table)中查找对应zend_class_entry、然后是创建并初始化一个对象、最后是初始化调用构造函数的zend_execute_data。
我们再具体看下第2步创建、初始化对象的操作,object_init_ex(&object_zval, ce)最终调用的是_object_and_properties_init()。
//zend_API.cZEND_API int _object_and_properties_init(zval *arg, zend_class_entry *class_type, ...){//检查类是否可以实例化...//用户自定义的类create_object都是NULL//只有PHP几个内部的类有这个值,比如exception、error等if (class_type->create_object == NULL) {//分配一个对象ZVAL_OBJ(arg, zend_objects_new(class_type));...//初始化成员属性object_properties_init(Z_OBJ_P(arg), class_type);} else {//调用自定义的创建object的钩子函数ZVAL_OBJ(arg, class_type->create_object(class_type));}return SUCCESS;}
还记得上一节介绍zend_class_entry时有几个自定义的钩子函数吗?如果定义了create_object这个地方就会调用自定义的函数来创建zend_object,这种情况通常发生在内核或扩展中定义的内部类(当然用户自定义类也可以修改,但一般不会那样做);用户自定义类在这个地方又具体分了两步:分配对象结构、初始化成员属性,我们继续看下这里面的处理。
(1)分配对象结构:zend_object
//zend_objects.cZEND_API zend_object *zend_objects_new(zend_class_entry *ce){//分配zend_objectzend_object *object = emalloc(sizeof(zend_object) + zend_object_properties_size(ce));zend_object_std_init(object, ce);//设置对象的操作handler为std_object_handlersobject->handlers = &std_object_handlers;return object;}
有个地方这里需要特别注意:分配对象结构的内存并不仅仅是zend_object的大小。我们在3.4.2.1介绍properties_table时说过这是一个变长数组,它用来存放非静态属性的值,所以分配zend_object时需要加上非静态属性所占用的内存大小:zend_object_properties_size(),根据普通非静态属性个数确定,如果没有定义get()、set()等魔术方法则占用内存就是: 属性数*sizeof(zval) ,如果定义了这些魔术方法那么会多分配一个zval的空间,这个多出来zval的用途下面介绍成员属性的读写时再作说明。
另外这里还有一个关键操作:将object编号并插入EG(objects_store).object_buckets数组。zend_object有个成员:handle,这个值在一次request期间所有实例化对象的编号,每调用zend_objects_new()实例化一个对象就会将其插入到object_buckets数组中,其在数组中的下标就是handle。这个过程是在zend_objects_store_put()中完成的。
//zend_objects_API.cZEND_API void zend_objects_store_put(zend_object *object){int handle;if (EG(objects_store).free_list_head != -1) {//这种情况主要是gc中会将中间一些object销毁,空出一些bucket位置//然后free_list_head就指向了第一个可用的bucket位置//后面可用的保存在第一个空闲bucket的handle中handle = EG(objects_store).free_list_head;EG(objects_store).free_list_head = GET_OBJ_BUCKET_NUMBER(EG(objects_store).object_buckets[handle]);} else {if (EG(objects_store).top == EG(objects_store).size) {//扩容}//递增加1handle = EG(objects_store).top++;}object->handle = handle;//存入object_buckets数组EG(objects_store).object_buckets[handle] = object;}typedef struct _zend_objects_store {zend_object **object_buckets; //对象数组uint32_t top; //当前全部object数uint32_t size; //object_buckets大小int free_list_head; //第一个可用object_buckets位置} zend_objects_store;
将所有的对象保存在EG(objects_store).object_buckets中的目的是用于垃圾回收(不确定是不是还有其它的作用),防止出现循环引用而导致内存泄漏的问题,这个机制后面章节会单独介绍,这里只要记得有这么个东西就行了。
(2)初始化成员属性
ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type){if (class_type->default_properties_count) {zval *src = class_type->default_properties_table;zval *dst = object->properties_table;zval *end = src + class_type->default_properties_count;//将非静态属性值从://zend_class_entry.default_properties_table复制到zend_object.properties_tabledo {ZVAL_COPY(dst, src);src++;dst++;} while (src != end);object->properties = NULL;}}
这一步操作是将非静态属性的值从zend_class_entry.default_properties_table -> zend_object.properties_table,当然这里不是硬拷贝,而是浅复制(增加引用),两者当前指向的value还是同一份,除非对象试图改写指向的属性值,那时将触发写时复制机制重新拷贝一份。
上面那个例子,类有两个普通属性:ids,假如我们实例化了两个对象,那么zend_class_entry与zend_object中普通属性值的关系如下图所示。

以上就是实例化一个对象的过程,总结一下具体的步骤:
- step1: 首先根据类名去EG(class_table)中找到具体的类,即zend_class_entry
- step2: 分配zend_object结构,一起分配的还有普通非静态属性值的内存
- step3: 初始化对象的非静态属性,将属性值从zend_class_entry浅复制到对象中
- step4: 查找当前类是否定义了构造函数,如果没有定义则跳过执行构造函数的opcode,否则为调用构造函数的执行进行一些准备工作(分配zend_execute_data)
- step5: 实例化完成,返回新实例化的对象(如果返回的对象没有变量使用则直接释放掉了)
3.4.2.3 成员属性的读写
普通成员属性的读写处理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(
%E6%97%B6%E4%BC%9A%E6%8A%8A%E8%BE%93%E5%85%A5%E7%9A%84#card=math&code=var%29%E6%97%B6%E4%BC%9A%E6%8A%8A%E8%BE%93%E5%85%A5%E7%9A%84)var名称存入这个哈希表,这样做的目的是防止循环调用,举个例子: *public function __get($var) { return
var; } 这种情况是调用get()时又访问了一个不存在的属性,也就是会在get()方法中递归调用,如果不对请求的var是不是已经在get()中了,如果是则不会再调用get(),否则会把$var作为key插入那个HashTable,然后将哈希值设置为:_guard |= IN_ISSET,调用完__get()再把哈希值设置为:_guard &= ~IN_ISSET。 这个HashTable不仅仅是给get()用的,其它魔术方法也会用到,所以其哈希值类型是zendlong,不同的魔术方法占不同的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的一个缓存机制:运行时缓存,后面会单独介绍。
3.4.2.4 对象的复制
PHP中普通变量的复制可以通过直接赋值完成,比如:
$a = array();$b = $a;
但是对象无法这么进行复制,仅仅通过赋值传递对象,它们指向的都是同一个对象,修改时也不会发生硬拷贝。比如上面这个例子,我们把$a赋值给$b,然后如果我们修改$b的内容,那么这时候会进行value分离,$a的内容是不变的,但是如果是把一个对象赋值给了另一个变量,这俩对象不管哪一个修改另外一个都随之改变。
class my_class{public $arr = array();}$a = new my_class;$b = $a;$b->arr[] = 1;var_dump($a === $b);====================输出:bool(true)
还记得我们在《2.1.3.2 写时复制》一节讲过zval有个类型掩码: type_flag 吗?其中有个是否可复制的标识:IS_TYPE_COPYABLE ,copyable的意思是当value发生duplication时是否需要或能够copy,而object的类型是不能复制(不清楚的可以翻下前面的章节),所以我们不能简单的通过赋值语句进行对象的复制。
PHP提供了另外一个关键词来实现对象的复制:clone。
$copy_of_object = clone $object;
clone出的对象就与原来的对象完全隔离了,各自修改都不会相互影响,另外如果类中定义了__clone()魔术方法,那么在clone时将调用此函数。
clone的实现比较简单,通过zend_object.clone_obj(即:zend_objects_clone_obj())完成。
//zend_objects.cZEND_API zend_object *zend_objects_clone_obj(zval *zobject){zend_object *old_object;zend_object *new_object;old_object = Z_OBJ_P(zobject);//重新分配一个zend_objectnew_object = zend_objects_new(old_object->ce);//浅复制properties_table、properties//如果定义了__clone()则调用此方法zend_objects_clone_members(new_object, old_object);return new_object;}
3.4.2.5 对象比较
当使用比较运算符()比较两个对象变量时,比较的原则是:如果两个对象的属性和属性值 都相等,而且两个对象是同一个类的实例,那么这两个对象变量相等;而如果使用全等运算符(=),这两个对象变量一定要指向某个类的同一个实例(即同一个对象)。
PHP中对象间的”==”比较通过函数zend_std_compare_objects()处理。
static int zend_std_compare_objects(zval *o1, zval *o2){...if (zobj1->ce != zobj2->ce) {return 1; /* different classes */}if (!zobj1->properties && !zobj2->properties) {//逐个比较properties_table...}else{//比较propertiesreturn zend_compare_symbol_tables(zobj1->properties, zobj2->properties);}}
“===”的比较通过函数zend_is_identical()处理,比较简单,这里不再展开。
3.4.2.6 对象的销毁
object与string、array等类型不同,它是个复合类型,所以它的销毁过程更加复杂,赋值、函数调用结束或主动unset等操作中如果发现object引用计数为0则将触发销毁动作。
//情况1$obj1 = new my_function();$obj1 = 123; //此时将断开对zend_object的引用,如果refcount=0则销毁//情况2function xxxx(){$obj1 = new my_function();...return null; //清理局部变量时如果发现$obj1引用为0则销毁}//情况3$obj1 = new my_function();//整个脚本结束,清理全局变量时//情况4$obj1 = new my_function();unset($obj1);
上面这几个都是比较常见的会进行变量销毁的情况,销毁一个对象由zend_objects_store_del()完成,销毁的过程主要是清理成员属性、从EG(objects_store).object_buckets中删除、释放zend_object内存等等。
//zend_objects_API.cZEND_API void zend_objects_store_del(zend_object *object){//这个函数if嵌套写的很挫......if (GC_REFCOUNT(object) > 0) {GC_REFCOUNT(object)--;return;}...//调用dtor_obj,默认zend_objects_destroy_object()//接着调用free_obj,默认zend_object_std_dtor()object->handlers->dtor_obj(object);object->handlers->free_obj(object);...ptr = ((char*)object) - object->handlers->offset;efree(ptr);}
另外,在减少refcount时如果发现object的引用计数大于0那么并不是什么都不做了,还记得2.1.3.4介绍的垃圾回收吗?PHP变量类型有的会因为循环引用导致正常的gc无法生效,这种类型的变量就有可能成为垃圾,所以会对这些类型的zval.u1.type_flag打上IS_TYPE_COLLECTABLE标签,然后在减少引用时即使refcount大于0也会启动垃圾检查,目前只有object、array两种类型会使用这种机制。
3.4.3 继承
继承是面向对象编程技术的一块基石,它允许创建分等级层次的类,它允许子类继承父类所有公有或受保护的特征和行为,使得子类对象具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
继承对于功能的设计和抽象是非常有用的,而且对于类似的对象增加新功能就无须重新再写这些公用的功能。
PHP中通过extends关键词继承一个父类,一个类只允许继承一个父类,但是可以多级继承。
class 父类 {}class 子类 extends 父类 {}
前面的介绍我们已经知道,类中保存着成员属性、方法、常量等,父类与子类之间通过zend_class_entry.parent建立关联,如下图所示。

问题来了:每个类都有自己独立的常量、成员属性、成员方法,那么继承类父子之间的这些信息是如何进行关联的呢?接下来我们将带着这个疑问再重新分析一下类的编译过程中是如何处理继承关系的。
3.4.1.5一节详细介绍了类的编译过程,这里再简单回顾下:首先为类分配一个zend_class_entry结构,如果没有继承类则生成一条类声明的opcode(ZEND_DECLARE_CLASS),有继承类则生成两条opcode(ZEND_FETCH_CLASS、ZEND_DECLARE_INHERITED_CLASS),然后再继续编译常量、成员属性、成员方法注册到zend_class_entry中,最后编译完成后调用zend_do_early_binding()进行 父子类关联 以及 注册到EG(class_table)符号表。
如果父类在子类之前定义的,那么父子类之间的关联就是在zend_do_early_binding()中完成的,这里不考虑子类在父类前定义的情况,实际两者没有本质差别,区别在于在哪一个阶段执行。有继承类的情况在zend_do_early_binding()中首先是查找父类,然后调用do_bind_inherited_class()处理,最后将ZEND_FETCH_CLASS、ZEND_DECLARE_INHERITED_CLASS两条opcode删除,这些过程前面已经介绍过了,下面我们重点看下do_bind_inherited_class()的处理过程。
ZEND_API zend_class_entry *do_bind_inherited_class(const zend_op_array *op_array, //这个是定义类的地方的const zend_op *opline, //类声明的opcode:ZEND_DECLARE_INHERITED_CLASSHashTable *class_table, //CG(class_table)zend_class_entry *parent_ce, //父类zend_bool compile_time) //是否编译时{zend_class_entry *ce;zval *op1, *op2;if (compile_time) {op1 = CT_CONSTANT_EX(op_array, opline->op1.constant);op2 = CT_CONSTANT_EX(op_array, opline->op2.constant);}else{...}...//父子类关联zend_do_inheritance(ce, parent_ce);//注册到CG(class_table)...}
上面这个函数的处理与注册非继承类的do_bind_class()几乎完全相同,只是多了一个zend_do_inheritance()一步,此函数输入很直观,只一个类及父类。
//zend_inheritance.c #line:758ZEND_API void zend_do_inheritance(zend_class_entry *ce, zend_class_entry *parent_ce){zend_property_info *property_info;zend_function *func;zend_string *key;zval *zv;//interface、trait、final类检查...ce->parent = parent_ce;zend_do_inherit_interfaces(ce, parent_ce);//下面就是继承属性、常量、方法}
下面的操作我们根据一个示例逐个来看。
//示例class A {const A1 = 1;public $a1 = array(1);private $a2 = 120;public function get() {echo "A::get()";}}class B extends A {const B1 = 2;public $b1 = "ddd";public function get() {echo "B::get()";}}
3.4.3.1 继承属性
前面我们已经介绍过:属性按静态、非静态分别保存在两个数组中,各属性按照定义的先后顺序编号(offset),同时按照这个编号顺序存储排列,而这些编号信息通过zend_property_info结构保存,全部静态、非静态属性的zend_property_info保存在一个以属性名为key的HashTable中,所以检索属性时首先根据属性名找到此属性的zend_property_info,然后拿到其属性值的offset,再根据静态、非静态分别到default_static_members_count、default_properties_table数组中取出属性值。
当类存在继承关系时,操作方式是:将属性从父类复制到子类 。子类会将父类的公共、受保护的属性值数组全部合并到子类中,然后将全部属性的zend_property_info哈希表也合并到子类中。
合并的步骤:
(1)合并非静态属性default_properties_table: 首先申请一个父类+子类非静态属性大小的数组,然后先将父类非静态属性复制到新数组,然后再将子类的非静态数组接着父类属性的位置复制过去,子类的default_properties_table指向合并后的新数组,default_properties_count更新为新数组的大小,最后将子类旧的数组释放。
if (parent_ce->default_properties_count) {zval *src, *dst, *end;...zval *table = pemalloc(sizeof(zval) * (ce->default_properties_count + parent_ce->default_properties_count), ...);ce->default_properties_table = table;//复制父类、子类default_properties_tabledo {...}while(dst != end);//更新default_properties_count为合并后的大小ce->default_properties_count += parent_ce->default_properties_count;}
示例合并后的情况如下图。
(2)合并静态属性default_static_members_table: 与非静态属性相同,新申请一个父类+子类静态属性大小的数组,依次将父类、子类静态属性复制到新数组,然后更新子类default_static_members_table指向新数组。
(3)更新子类属性offset: 因为合并后原子类属性整体向后移了,所以子类属性的编号offset需要加上前面父类属性的总大小。
ZEND_HASH_FOREACH_PTR(&ce->properties_info, property_info) {if (property_info->ce == ce) {if (property_info->flags & ZEND_ACC_STATIC) {//静态属性offset为数组下标,直接加上父类default_static_members_count即可property_info->offset += parent_ce->default_static_members_count;} else {//非静态属性offset为内存偏移值,按zval大小递增property_info->offset += parent_ce->default_properties_count * sizeof(zval);}}} ZEND_HASH_FOREACH_END();
(4)合并properties_info哈希表: 这也是非常关键的一步,上面只是将父类的属性值合并到了子类,但是索引属性用的是properties_info哈希表,所以需要将父类的属性索引表与子类的索引表合并。在合并的过程中就牵扯到父子类属性的继承、覆盖问题了,各种情况具体处理如下:
- 父类属性不与子类冲突 且 父类属性是私有: 即父类属性为private,且子类中没有重名的,则将此属性插入子类properties_info,但是更新其flag为ZEND_ACC_SHADOW,这种属性将不能被子类使用;
- 父类属性不与子类冲突 且 父类属性是公有: 这种比较简单,子类可以继承使用,直接插入子类properties_info;
- 父类属性与子类冲突 且 父类属性为私有: 不继承父类的,以子类原属性为准,但是打上
ZEND_ACC_CHANGED的flag,这种属性父子类隔离,互不干扰; - 父类属性与子类冲突 且 父类属性是公有或受保护的:
- 父子类属性一个是静态一个是非静态: 编译错误;
- 父子类属性都是非静态: 用父类的offset,但是值用子类的,父子类共享;
- 父子类属性都是静态: 不继承父类属性,以子类原属性为准,父子类隔离,互不干扰;
这个地方相对比较复杂,具体的合并策略在do_inherit_property()中,这里不再罗列代码。
所以,继承类实际上是把父类的属性、常量、方法合并到了子类里面,上一节介绍实例化时会将普通成员属性值复制到对象中去,这样在实例化时子类就与普通的类的操作没有任何差别了。
3.4.3.2 继承常量
常量的合并策略比较简单,如果父类与子类冲突时用子类的,不冲突时则将父类的常量合并到子类。
static void do_inherit_class_constant(zend_string *name, zval *zv, zend_class_entry *ce, zend_class_entry *parent_ce){//父类定义的常量在子类中没有定义if (!zend_hash_exists(&ce->constants_table, name)) {..._zend_hash_append(&ce->constants_table, name, zv);}}
3.4.3.3 继承方法
与属性一样,子类可以继承父类的公有、受保护的方法,方法的继承比较复杂,因为会有访问控制、抽象类、接口、Trait等多种限制条件。实现上与前面几种相同,即父类的function_table合并到子类的function_table中。
首先是将子类function_table扩大,以容纳父子类全部方法,然后遍历父类function_table,逐个判断是否可被子类继承,如果可被继承则插入到子类function_table中。
if (zend_hash_num_elements(&parent_ce->function_table)) {//扩展子类的function_table哈希表大小zend_hash_extend(&ce->function_table,zend_hash_num_elements(&ce->function_table) +zend_hash_num_elements(&parent_ce->function_table), 0);//遍历父类function_table,检查是否可被子类继承ZEND_HASH_FOREACH_STR_KEY_PTR(&parent_ce->function_table, key, func) {zend_function *new_func = do_inherit_method(key, func, ce);if (new_func) {_zend_hash_append_ptr(&ce->function_table, key, new_func);}} ZEND_HASH_FOREACH_END();}
在合并的过程中需要对父类的方法进行一系列检查,最简单的情况就是父类中定义的方法在子类中不存在,这种情况比较简单,直接将父类的zend_function复制一份给子类。
static zend_function *do_inherit_method(zend_string *key, zend_function *parent, zend_class_entry *ce){zval *child = zend_hash_find(&ce->function_table, key);if(child){//方法与子类冲突...}//父子类方法不冲突,直接复制return zend_duplicate_function(parent, ce);}
当然这里不完全是复制:如果继承的父类是内部类则会硬拷贝一份zend_function结构(此结构的指针成员不复制);如果父类是用户自定义的类,且继承的方法没有静态变量则不会硬拷贝,而是增加zend_function的引用计数(zend_op_array.refcount)。
//func是父类成员方法,ce是子类static zend_function *zend_duplicate_function(zend_function *func, zend_class_entry *ce){zend_function *new_function;if (UNEXPECTED(func->type == ZEND_INTERNAL_FUNCTION)) {//内部函数//如果子类也是内部类则会调用malloc分配内存(不会被回收),否则在zend内存池分配...}else{if (func->op_array.refcount) {(*func->op_array.refcount)++;}if (EXPECTED(!func->op_array.static_variables)) {return func;}//硬拷贝new_function = zend_arena_alloc(&CG(arena), sizeof(zend_op_array));memcpy(new_function, func, sizeof(zend_op_array));}}
合并时另外一个比较复杂的情况是父类与子类中的方法冲突了,即子类重写了父类的方法,这种情况需要对父子类以及要合并的方法进行一系列检查,这一步在do_inheritance_check_on_method()中完成,具体情况如下:
static void do_inheritance_check_on_method(zend_function *child, zend_function *parent){uint32_t child_flags;uint32_t parent_flags = parent->common.fn_flags;...}
(1)抽象子类的抽象方法与抽象父类的抽象方法冲突: 无法重写,Fatal错误。
abstract class B extends A {abstract function test();}abstract class A{abstract function test();}============================PHP Fatal error: Can't inherit abstract function A::test() (previously declared abstract in B)
判断逻辑:
//do_inheritance_check_on_method():if ((parent->common.scope->ce_flags & ZEND_ACC_INTERFACE) == 0 //父类非接口&& parent->common.fn_flags & ZEND_ACC_ABSTRACT //父类方法为抽象方法&& parent->common.scope != (child->common.prototype ? child->common.prototype->common.scope : child->common.scope)&& child->common.fn_flags & (ZEND_ACC_ABSTRACT|ZEND_ACC_IMPLEMENTED_ABSTRACT) //子类方法为抽象或实现了抽象方法) {zend_error_noreturn(E_COMPILE_ERROR, "Can't inherit abstract function %s::%s() (previously declared abstract in %s)",...);}
(2)父类方法为final: Fatal错误,final成员方法不得被重写。
判断逻辑:
//do_inheritance_check_on_method():if (UNEXPECTED(parent_flags & ZEND_ACC_FINAL)) {zend_error_noreturn(E_COMPILE_ERROR, "Cannot override final method %s::%s()", ...);}
(3)父子类方法静态属性不一致: 父类方法为非静态而子类的是静态(或相反),Fatal错误。
class A {public function test(){}}class B extends A {static public function test(){}}============================PHP Fatal error: Cannot make non static method A::test() static in class B
判断逻辑:
//do_inheritance_check_on_method():if (UNEXPECTED((child_flags & ZEND_ACC_STATIC) != (parent_flags & ZEND_ACC_STATIC))) {zend_error_noreturn(E_COMPILE_ERROR,...);}
(4)抽象子类的抽象方法覆盖父类非抽象方法: Fatal错误。
class A {public function test(){}}abstract class B extends A {abstract public function test();}============================PHP Fatal error: Cannot make non abstract method A::test() abstract in class B
判断逻辑:
//do_inheritance_check_on_method():if (UNEXPECTED((child_flags & ZEND_ACC_ABSTRACT) > (parent_flags & ZEND_ACC_ABSTRACT))) {zend_error_noreturn(E_COMPILE_ERROR, "Cannot make non abstract method %s::%s() abstract in class %s",...);}
(5)子类方法限制父类方法访问权限: Fatal错误,不允许派生类限制父类方法的访问权限,如父类方法为public,而子类试图重写为protected/private。
class A {public function test(){}}class B extends A {protected function test(){}}============================PHP Fatal error: Access level to B::test() must be public (as in class A)
判断逻辑:
//do_inheritance_check_on_method()://ZEND_ACC_PPP_MASK = (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE)if (UNEXPECTED((child_flags & ZEND_ACC_PPP_MASK) > (parent_flags & ZEND_ACC_PPP_MASK))) {zend_error_noreturn(E_COMPILE_ERROR, "Access level to %s::%s() must be %s (as in class %s)%s", ...);} else if (((child_flags & ZEND_ACC_PPP_MASK) < (parent_flags & ZEND_ACC_PPP_MASK))&& ((parent_flags & ZEND_ACC_PPP_MASK) & ZEND_ACC_PRIVATE)) {child->common.fn_flags |= ZEND_ACC_CHANGED;}
(6)剩余检查情况: 除了上面5中情形下无法重写方法,剩下还有一步对函数参数的检查,这个过程我们整体看一下。
//do_inheritance_check_on_method():if (UNEXPECTED(!zend_do_perform_implementation_check(child, parent))) {...zend_error(error_level, "Declaration of %s %s be compatible with %s", ZSTR_VAL(child_prototype), error_verb, ZSTR_VAL(method_prototype));zend_string_free(child_prototype);zend_string_free(method_prototype);}
实际上zend_do_perform_implementation_check()这个函数是用来检查一个方法是否实现了某抽象方法的,继承的时候遵循的也是这个规则,所以这里可以将父类方法理解为抽象方法,只有子类方法实现了该”抽象方法”才能重写父类方法。
static zend_bool zend_do_perform_implementation_check(const zend_function *fe, const zend_function *proto){...//如果检查的方法是__construct且父类方法不是interface和abstract则子类__construct覆盖父类的if ((fe->common.fn_flags & ZEND_ACC_CTOR)&& ((proto->common.scope->ce_flags & ZEND_ACC_INTERFACE) == 0&& (proto->common.fn_flags & ZEND_ACC_ABSTRACT) == 0)) {return 1;}//如果父类方法为私有方法则子类方法可以覆盖if (proto->common.fn_flags & ZEND_ACC_PRIVATE) {return 1;}//如果父类方法必传参数小于子类的或者父类的总参数大于子类的则不能覆盖//如:// 父类 public function test($a, $b = 3){}// 子类 public function test($a, $b){}if (proto->common.required_num_args < fe->common.required_num_args|| proto->common.num_args > fe->common.num_args) {return 0;}//可变函数,暂未理解这里的可变函数指哪类,忽略...//如果有定义的参数检查参数类型是否匹配,如果显式声明了参数类型则父子类方法必须匹配for (i = 0; i < num_args; i++) {zend_arg_info *fe_arg_info = &fe->common.arg_info[i];if (!zend_do_perform_type_hint_check(fe, fe_arg_info, proto, proto_arg_info)) {return 0;}//是否引用也必须一致if (fe_arg_info->pass_by_reference != proto_arg_info->pass_by_reference) {return 0;}}//如果父类方法声明了返回值类型则子类方法必须声明且类型一致,相反如果子类声明了而父类无要求则可以if (proto->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {if (!(fe->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE)) {return 0;}if (!zend_do_perform_type_hint_check(fe, fe->common.arg_info - 1, proto, proto->common.arg_info - 1)) {return 0;}}}
这个判断过程还是比较复杂的,有些地方很难理解为什么设计,想了解完整过程的可以自行翻下代码。
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);}//将动态属性插入propertieszend_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时就会触发这个动作:
#card=math&code=arr%20%3D%20%28array%29)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;}}...}
3.4.5 魔术方法
PHP在类的成员方法中预留了一些特殊的方法,它们会在一些特殊的时机被调用(比如创建对象之初、访问成员属性时…),这类方法称为:魔术方法,包括:construct()、destruct()、call()、callStatic()、get()、set()、isset()、unset()、sleep()、wakeup()、toString()、invoke()、 set_state()、 clone() 和 __debugInfo(),关于这些方法的用法这里不作说明,不清楚的可以翻下官方文档。
魔术方法实际是PHP提供的一些特殊操作时的钩子函数,与普通成员方法无异,它们只是与一些操作的口头约定,并没有什么字段标识它们,比如我们定义了一个函数:my_function(),我们希望在这个函数处理对象时首先调用其成员方法my_magic(),那么my_magic()也可以认为是一个魔术方法。
魔术方法与普通成员方法一样保存在zend_class_entry.function_table中,另外针对一些内核常用到的成员方法在zend_class_entry中还有一些单独的指针指向具体的成员方法:
struct _zend_class_entry {...union _zend_function *constructor;union _zend_function *destructor;union _zend_function *clone;union _zend_function *__get;union _zend_function *__set;union _zend_function *__unset;union _zend_function *__isset;union _zend_function *__call;union _zend_function *__callstatic;union _zend_function *__tostring;union _zend_function *__debugInfo;...}
在编译成员方法时如果发现与这些魔术方法名称一致,则除了插入zend_class_entry.function_table哈希表以外,还会设置zend_class_entry中对应的指针。

具体在编译成员方法时设置:zend_begin_method_decl()。
void zend_begin_method_decl(zend_op_array *op_array, zend_string *name, zend_bool has_body){...//插入类的function_table中if (zend_hash_add_ptr(&ce->function_table, lcname, op_array) == NULL) {zend_error_noreturn(..);}if (!in_trait && zend_string_equals_ci(lcname, ce->name)) {if (!ce->constructor) {ce->constructor = (zend_function *) op_array;}} else if (zend_string_equals_literal(lcname, ZEND_CONSTRUCTOR_FUNC_NAME)) {ce->constructor = (zend_function *) op_array;} else if (zend_string_equals_literal(lcname, ZEND_DESTRUCTOR_FUNC_NAME)) {ce->destructor = (zend_function *) op_array;} else if (zend_string_equals_literal(lcname, ZEND_CLONE_FUNC_NAME)) {ce->clone = (zend_function *) op_array;} else if (zend_string_equals_literal(lcname, ZEND_CALL_FUNC_NAME)) {ce->__call = (zend_function *) op_array;} else if (zend_string_equals_literal(lcname, ZEND_CALLSTATIC_FUNC_NAME)) {ce->__callstatic = (zend_function *) op_array;} else if (...){...}...}
除了这几个其它魔术方法都没有单独的指针指向,比如:sleep()、wakeup(),这两个主要是serialize()、unserialize()序列化、反序列化时调用的,它们是在这俩函数中写死的,我们简单看下serialize()的实现,这个函数是通过扩展提供的:
//file: ext/standard/var.cPHP_FUNCTION(serialize){zval *struc;php_serialize_data_t var_hash;smart_str buf = {0};if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &struc) == FAILURE) {return;}php_var_serialize(&buf, struc, &var_hash);...}
最终由php_var_serialize_intern()处理,这个函数会根据不同的类型选择不同的处理方式:
static void php_var_serialize_intern(smart_str *buf, zval *struc, php_serialize_data_t var_hash){...switch (Z_TYPE_P(struc)) {case IS_FALSE:...case IS_TRUE:...case IS_NULL:...case IS_LONG:...}}
其中类型是对象时将先检查zend_class_function.function_table中是否定义了__sleep(),如果有的话则调用:
//case IS_OBJEST:...if (ce != PHP_IC_ENTRY && zend_hash_str_exists(&ce->function_table, "__sleep", sizeof("__sleep")-1)) {ZVAL_STRINGL(&fname, "__sleep", sizeof("__sleep") - 1);//调用用户自定义的__sleep()方法res = call_user_function_ex(CG(function_table), struc, &fname, &retval, 0, 0, 1, NULL);if (res == SUCCESS) {if (Z_TYPE(retval) != IS_UNDEF) {if (HASH_OF(&retval)) {php_var_serialize_class(buf, struc, &retval, var_hash);} else {smart_str_appendl(buf,"N;", 2);}zval_ptr_dtor(&retval);}return;}}//后面会走到IS_ARRAY分支继续序列化处理...
其它魔术方法与__sleep()类似,都是在一些特殊操作中固定调用的。
3.4.6 类的自动加载
在实际使用中,通常会把一个类定义在一个文件中,然后使用时include加载进来,这样就带来一个问题:在每个文件的头部都需要包含一个长长的include列表,而且当文件名称修改时也需要把每个引用的地方都改一遍,另外前面我们也介绍过,原则上父类需要在子类定义之前定义,当存在大量类时很难得到保证,因此PHP提供了一种类的自动加载机制,当使用未被定义的类时自动调用类加载器将类加载进来,方便类的同一管理。
在内核实现上类的自动加载实际就是定义了一个钩子函数,实例化类时如果在EG(class_table)中没有找到对应的类则会调用这个钩子函数,调用完以后再重新查找一次。这个钩子函数保存在EG(autoload_func)中。
PHP中提供了两种方式实现自动加载:__autoload()、spl_autoload_register()。
*(1)__autoload():
这种方式比较简单,用户自定义一个__autoload()函数即可,参数是类名,当实例化一个类是如果没有找到这个类则会查找用户是否定义了__autoload()函数,如果定义了则调用此函数,比如:
//文件1:my_class.php<?phpclass my_class {public $id = 123;}//文件2:b.php<?phpfunction __autoload($class_name){//do something...include $class_name . '.php';}$obj = new my_class();var_dump($obj);
(2)spl_autoload_register():
相比__autoload()只能定义一个加载器,spl_autoload_register()提供了更加灵活的注册方式,可以支持任意数量的加载器,比如第三方库加载规则不可能保持一致,这样就可以通过此函数注册自己的加载器了,在实现上spl创建了一个队列来保存用户注册的加载器,然后定义了一个spl_autoload函数到EG(autoload_func),当找不到类时内核回调spl_autoload,这个函数再依次调用用户注册的加载器,没调用一个重新检查下查找的类是否在EG(class_table)中已经注册,仍找不到的话继续调用下一个加载器,直到类成功注册为止。
bool spl_autoload_register ([ callable $autoload_function [, bool $throw = true [, bool $prepend = false ]]] )
参数$autoload_function为加载器,可以是函数名,第2个参数$throw用于设置autoload_function 无法成功注册时, spl_autoload_register()是否抛出异常,最后一个参数如果为true时spl_autoload_register() 会添加函数到队列之首,而不是队列尾部。
function autoload_one($class_name){echo "autoload_one->", $class_name, "\n";}function autoload_two($class_name){echo "autoload_two->", $class_name, "\n";}spl_autoload_register("autoload_one");spl_autoload_register("autoload_two");$obj = new my_class();var_dump($obj);
这个例子执行时就会将autoload_one()、autoload_two()都调一遍,假如第一个函数就成功注册了my_class类则不会再调后面的加载器。
内核查找类通过zend_lookup_class_ex()完成,我们简单看下其处理过程。
//file: zend_execute_API.cZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, const zval *key, int use_autoload){...//从EG(class_table)符号表找类的zend_class_entry,如果找到说明类已经编译,直接返回ce = zend_hash_find_ptr(EG(class_table), lc_name);if (ce) {if (!key) {zend_string_release(lc_name);}return ce;}...//如果没有通过spl注册则看下是否定义了__autoload()if (!EG(autoload_func)) {zend_function *func = zend_hash_str_find_ptr(EG(function_table), "__autoload", sizeof("__autoload") - 1);if (func) {EG(autoload_func) = func;} else {return NULL;}}...fcall_cache.function_handler = EG(autoload_func);...//调用EG(autoload_func)函数,然后再查一次EG(class_table)if ((zend_call_function(&fcall_info, &fcall_cache) == SUCCESS) && !EG(exception)) {ce = zend_hash_find_ptr(EG(class_table), lc_name);}...}
SPL的具体实现比较简单,这里不再介绍。
