面向对象编程,简称OOP,是一种程序设计思想。面向对象把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。面向对象一直是软件开发领域内比较热门的话题,它更符合人类看待事物的一般规律。与Java不同,PHP并非纯面向对象的语言,它同时支持面向过程、面向对象,PHP最初的版本是不支持面向对象的,从PHP5开始全面实现了面向对象。
面向对象的特点:
1.封装:将类中的成员属性和方法结合成一个独立的单位,确保类外的部分不能随意存取类的内部数据。
2.继承:一个类可以继承并拥有另外一个类的成员属性和成员方法。
3.多态:程序能够处理多种类型对象的能力,PHP中可以通过继承和接口两种方式实现多态(多个派生类调用同一基类接口,会有不同效果)。
7.1 类
类是现实世界或思维世界中的实体在计算机中的反映,它将某些具有关联关系的数据以及这些数据上的操作封装在一起,是具有相同属性和服务的一组对象的集合。在面向对象编程中,类是对象的抽象,对象是类的具体实例。
在PHP中类是编译阶段的产物,而对象是运行时产生的,它们归属于不同阶段。一个类可以包含属于自己的常量、变量(称为“属性”)、函数(称为“方法”),PHP中我们这样定义一个类:
class 类名 {
// 常量;
// 成员属性;
// 成员方法;
}
除了在PHP脚本中定义的类,还有一种类是内核、扩展提供的。在内核中,不管是内部类还是用户自定义类,均通过zend_class_entry结构表示,类的常量、成员属性、成员方法均保存在这个结构中,这个结构包含的成员非常多,这里列几个比较重要的:
typedef struct _zend_class_entry zend_class_entry;
struct _zend_class_entry {
// 类的类型:内部类ZEND_INTERNAL_CLASS(1)、用户自定义类ZEND_USER_CLASS(2)
char type;
// 类名,PHP类名不区分大小写,统一为小写
zend_string *name;
// 父类
struct _zend_class_entry *parent;
int refcount;
// 类掩码,如普通类、抽象类、接口,除了这些还有别的含义
uint32_t ce_flags;
// 普通属性数,包括public、private
int default_properties_count;
// 静态属性数
int default_static_members_count;
// 普通属性值数组
zval *default_properties_table;
// 静态属性值数组
zval *default_static_members_table;
zval *static_members_table;
// 成员方法符号表
HashTable function_table;
// 成员属性基本信息哈希表,key为成员名,value为zend_property_info
HashTable properties_info;
// 常量符号表
HashTable constants_table;
// 以下是构造函数、析构函数、魔术方法的指针
union _zend_function *constructor;
union _zend_function *destructor;
union _zend_function *clone;
union _zend_function *__get;
...
// 自定义的钩子函数,通常在定义内部类时使用,可以灵活地进行一些个性化操作,用户自定义类不会用到,可忽略
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);
union _zend_function *(*get_static_method)(zend_class_entry *ce, zend_string *method);
// 序列化调用的接口
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; // 实现的接口
// union info: 类所在文件、起始行号、所属模块等信息
...
};
PHP脚本中定义的类,在编译阶段将被解析为zend_class_entry结构,例如成员方法会按照函数的编译规则编译为zend_function,然后注册到zend_class_entry->function_table中。类编译完成后,将像函数那样注册到一个全局符号表中:EG(class_table)。使用类时根据类名到符号表EG(class_table)中索引,EG(class_table)保存着全部的类,包括用户自定义类和内部类,如图7-1所示:
7.1.1 常量
PHP中可以把在类中始终保持不变的值定义为常量,在定义和使用常量时不需要使用$
符号,常量的值必须是一个定值,比如布尔型、整型、字符串、数组,不能是变量、数学运算的结果(我使用php 8.0.7测试时,常量可以是数学运算的结果,将常量赋值为1+1一切正常)、函数调用,它是只读的,无法进行赋值。类中的常量与PHP中普通的常量(通过define()或const定义的)含义是一样的,只不过类中的常量只属于固定的类,而普通的常量是全局的。
常量通过const关键字定义,常量名通常采用大写,常量通过class_name::CONST_NAME访问,或在类的内部通过self::CONST_NAME访问,例如:
class my_class {
// 定义一个常量
const CONST_NAME = const_value;
public function __construct() {
// 在类内部访问常量
self::CONST_NAME;
}
}
// 在类外访问类的常量
my_class::CONST_NAME;
常量属于类的维度,对于类的所有实例化对象没有任何差异,这一点与成员属性不同。与普通常量相同,类的常量也通过哈希表存储,类的常量保存在zend_class_entry->constants_table中。通常,访问常量时需要根据常量名索引,但有些情况下会在编译阶段将常量直接替换为常量值使用,比如:
// 示例1
echo my_class::A1;
class my_class {
const A1 = "hi";
}
// 示例2
class my_class {
const A1 = "hi";
}
echo my_class:A1;
以上两例唯一的不同就是常量的使用时机:示例1是在定义前使用的,示例2是在定义后使用的。PHP中无论是变量、常量、函数,都不需要提前声明。
具体debug一下上面两例会发现:示例2编译生成的主要指令只有一个ZEND_ECHO,即直接输出值了,并没有涉及常量的查找,进一步查看发现它的操作数为CONST变量,值为“hi”,即my_class::A1的值;而示例1首先执行的指令是ZEND_FETCH_CONSTANT,查找常量,接着才是ZEND_ECHO。
这两种情况内核会有两种不同的处理方式:示例1会在运行时根据变量名索引zend_class_entry_constants_table,取到常量值后再执行echo;示例2中,由于PHP代码的编译顺序是顺序的,示例2在编译到echo my_class::A1时,首先会尝试检索是否已经编译了my_class,如果能在CG(class_table)中找到,则进一步从类的constants_table中查找对应的常量,找到的话会复制其value替换常量,也就是将常量的检索提前到了编译时,通过这种“预处理”优化了耗时的常量检索过程,避免多次执行时的重复检索,同时也可以利用opcache避免放到运行时重复检索常量(看上去可以更进一步优化,在编译完的pass_two函数里将常量全部替换成值)。
7.1.2 成员属性
类的变量成员叫属性。属性声明由关键字public、protected、private三者之一开头,然后跟一个普通的变量声明来组成。属性中的变量可以初始化,但初始化的值必须是固定不变的值,不能是变量,初始化值在编译阶段就可以得到其值,而不依赖于运行时信息才能求值(看起来类似C++ constexpr),比如public $time = time()
,这样定义属性会触发语法错误。
public、protected、private用于限制成员属性、方法的访问权限:public为公有的,可以在任何地方被访问;protected为受保护的,可以被自身及其子类、父类中访问,在类之外无法访问;private为私有的,只能被其定义所在的类访问。按权限大小排序:public>protected>private,类属性必须定义为三者之一,如果用var定义,则被视为公有。
成员属性分为两类:普通属性、静态属性。静态属性通过static声明,通过self::$property
或className::$property
访问;普通属性通过$this->property
或$object->property
访问。例如:
class my_class {
// 普通属性
public $property = "normal property";
// 静态属性
public static $static_property = "static property";
public function __construct() {
// 内部访问
$this->property; // 普通属性
self::$static_property; // 静态属性
}
}
// 外部访问
$obj = new my_class;
$obj->property; // 普通属性
my_class::$static_property; // 静态属性
// 我用PHP 8.0.7 (cli)测试时,静态属性也可以这样访问:
$obj::$static_property;
静态成员属性为各对象共享,与常量类似,而普通成员属性是各对象独享,对象之间对普通成员属性的修改不会相互影响。与常量的存储方式不同,成员属性的初始化值并不直接以属性名作为索引的哈希表存储,而是通过数组保存的,普通属性、静态属性各有一个数组分别存储,即default_properties_table、default_static_members_table。编译后类的成员属性的数组如图7-2所示。
静态属性是共享的,所以运行时各对象操作的就是zend_class_entry->default_static_members_table数组中的值,即静态属性值保存在类中,而不是对象中;但普通成员属性是对象独享的,各对象的普通成员属性的值不会保存在类中,而是存储在对象结构中,即zend_object。类中的default_properties_table数组中的值只是用于初始化对象的,在实例化对象时会拷贝到对象中。
普通成员属性的存储与局部变量的实现类似,它们分布在zend_object上,通过相对zend_object的内存偏移进行访问,各属性的内存偏移值在编译时分配,普通成员属性的存取都是通过这个内存offset完成的。静态属性直接根据数组下标访问。
实际上,default_properties_table、default_static_members_table数组只是用来存储属性值的,并不是保存属性信息的,这里说的属性信息包括属性的访问权限(public、protected、private)、属性名、静态属性值的存储下标、非静态属性的内存offset等,这些信息通过zend_property_info结构存储,该结构通过zend_class_entry->properties_info符号表存储,这是一个哈希表,key就是属性名。
typedef struct _zend_property_info {
uint32_t offset; // 普通成员变量的内存偏移值,静态成员变量的数组索引
uint32_t flags; // 属性掩码,如public、protected、private及是否为静态属性
zend_string *name; // 属性名:并不是原始属性名
zend_string *doc_comment;
zend_class_entry *ce; // 所属类
} zend_property_info;
关键成员含义:
1.name:属性名,注意这里不是原始属性名,private会在原始属性名前加上类名,protected则会加上*作为前缀。
2.offset:普通成员属性的内存偏移值,与CV变量的操作数相同,普通成员属性分配在zend_object结构上,读取时根据zend_object地址 + offset
获取;对于静态成员属性则是default_static_members_table数组索引。
3.flag:属性标识,有两个含义,一是区分是否为静态,静态、非静态属性的结构都存储在这个符号表中;二是属性权限,即public、private、protected。
// flags标识位
#define ZEND_ACC_PUBLIC 0x100
#define ZEND_ACC_PROTECTED 0x200
#define ZEND_ACC_PRIVATE 0x400
#define ZEND_ACC_STATIC 0x01
在编译时,成员属性将根据属性类型按照属性定义的先后顺序分配一个对应的offset,用于运行时索引属性的存储位置。读取成员属性分两步:
1.根据属性名从zend_class_entry.properties_info中索引到属性的zend_property_info结构。
2.根据zend_property_info->offset获取具体的属性值,其中静态成员属性通过zend_class_entry.default_static_members_table[offset]
获取,普通成员属性则通过((char *)zend_object) + offset
获取。
下类作为示例:
class my_class {
public $property_1 = "hi,php~";
public $property_2 = array();
public static $property_3 = 110;
}
上类生成的属性结构如图7-3:
当访问self::$property_3
时,首先根据字符串“property_3”检索properties_info,找到该属性的zend_property_info结构,然后以zend_property_info->offset为下标,在default_static_members_table数组中读取对应数据,即default_static_members_table[0]。
7.1.3 成员方法
每个类可以定义若干个属于本类的函数,称之为成员方法。成员方法与普通的函数相比并没有本质上的差别,只不过成员方法封装在类中,是类专有的函数,不能被别的类调用,所以成员方法保存在类中而不是EG(function_table)
全局函数符号表中。
与成员属性一样,成员方法也有权限控制,也有静态、非静态之分,其中静态成员方法不需要通过对象调用,可以直接根据类名::静态成员方法
调用,或者在类内部通过self::静态成员方法
调用,而普通非静态成员方法则只能通过对象发起调用。另外,成员方法除了static静态之分,还有abstract、final两类,分别表示抽象方法、不可被覆盖方法(即不可被重写方法)。
class my_class {
static public function static_func() {
// ...
}
public function func() {
// ...
}
}
// 静态方法的调用
my_class::static_func();
// 非静态方法的调用
$obj = new my_class;
$obj->func();
成员方法的存储结构也是zend_function,其中zend_function.common->scope(对用户自定义类而言就是zend_op_array->scope)为该方法所属类,zend_function.common->fn_flags用于标识成员方法的权限、类型(如abstract、final、static),除了这些,fn_flags还有很多其他标识,比如成员方法指定了返回值类型,则fn_flags将包含ZEND_ACC_HAS_RETURN_TYPE标识。
union _zend_function {
zend_uchar type; /* MUST be the first element of this struct! */
struct {
zend_uchar type; /* never used */
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags; // 方法标识:finnal/static/abstrct、private/public/protected
zend_string *function_name;
zend_class_entry *scope; // 成员方法所属类
union _zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_arg_info *arg_info;
} common;
zend_op_array op_array;
zend_internal_function internal_function;
};
编译过程与普通函数相同,但成员方法最后被注册到所属类的function_table符号表中,当调用一个成员方法时,将到zend_class_entry.function_table中进行查找。最终编译生成的成员方法符号表如图7-4所示。
7.1.4 类的编译
以下例来看一下类的编译过程:
// 示例:/tmp/user.php
class User {
const TYPE = 110;
static $name = "uuu";
public $uid = 900;
public function getName() {
return $this->uid;
}
}
类定义的语法规则(这段语法规则是使用Bison(或类似的解析器生成器,如Yacc)的语法编写的):
// 类声明规则
class_declaretion_statement:
// 情况1:带有类修饰符(class_modifiers,如abstract、final)的类声明
// T_CLASS是关键字class
// $<num>$ = CG(zend_lineno)记录了当前行号
class_modifiers T_CLASS { $<num>$ = CG(zend_lineno); }
// T_STRING是类名
// extends_from是继承的父类
// implements_list是实现的接口列表
// backup_doc_comment用于保存类的文档注释
// class_statement_list是类体中的成员列表(属性、方法等)
T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}'
// 动作代码:调用zend_ast_create_decl创建类的AST节点,ZEND_AST_CLASS是节点类型
// $1是类修饰符(来自class_modifiers),$<num>3是行号
// 剩下的$+数字的参数都是变量,如父类、实现的接口列表等,其中zend_ast_get_str是获取类名字符串
// 因此$4应该是类名T_STRING
{ $$ = zend_ast_create_decl(ZEND_AST_CLASS, $1, $<num>3, $7, zend_ast_get_str($4),
$5, $6, $9, NULL); }
// 情况2:无修饰符的类,基本同情况1
| 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
class_statement_list class_statement
// 将新成员添加到列表中
{ $$ = zend_ast_list_add($1, $2); }
| /* empty,空类体 */
// 创建一个空的ZEND_AST_STMT_LIST节点
{ $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); }
;
// 类内语法规则:成员属性、成员方法
class_statement:
// 成员属性
// variable_modifiers是属性修饰符(如public、static),property_list是属性列表(如$a、$b)
variable_modifiers property_list ';'
// 动作代码:将修饰符$1赋值给属性列表的attr成员
{ $$ = $2; $$->attr = $1; }
// 常量
// T_CONST是关键字const,class_const_list是常量列表(如NAME = value)
| T_CONST class_const_list ';'
// 重置文档注释
{ $$ = $2; RESET_DOC_COMMENT(); }
...
// 成员方法
// method_modifiers是方法修饰符(如public、final),function是关键字
// returns_ref是是否返回引用,identifier是方法名,backup_doc_comment是方法的文档注释
| method_modifiers function returns_ref identifier backup_doc_comment
// parameter_list是参数列表
'(' parameter_list ')'
// return_type是返回类型(如: void),method_body是方法体
return_type method_body
// 动作代码:调用zend_ast_create_decl创建ZEND_AST_METHOD类型的节点
{ $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1, $2, $5, zend_ast_get_str($4),
$7, NULL, $10, $9); }
;
从语法规则可以看出,类被编译为ZEND_AST_CLASS节点,该节点有3个子节点,分别表示:继承类、实现的接口列表、类的表达式,其中类的表达式是一个list节点,每个常量、成员属性、成员方法对应一个子节点,其节点类型分别为ZEND_AST_CLASS_CONST_DECL、ZEND_AST_PROP_DECL、ZEND_AST_METHOD:
1.ZEND_AST_CLASS_CONST_DECL:类常量声明节点,类型为list,因为可以同时声明多个常量,比如const C1, C2, C3
,list中的每个子节点都是一个常量,类型为ZEND_AST_CONST_ELEM,它有两个子节点,分别用于常量名、常量值,具体规则见语法规则文件中的class_const_list配置。
2.ZEND_AST_PROP_DECL:成员属性声明节点,与常量节点相同,这也是一个list节点,其中可以同时声明多个成员属性,子节点类型为ZEND_AST_PROP_ELEM,它有三个子节点,其中前两个子节点分别表示属性名、属性初始化,第三个子节点用于保存注释。需要注意的是,属性的访问权限、是否为静态的信息没有保存在ZEND_AST_PROP_ELEM节点中,而是通过ZEND_AST_PROP_DECL->attr保存。具体规则见语法规则文件的property_list。
3.ZEND_AST_METHOD:成员方法声明节点,与函数的声明节点ZEND_AST_FUNC_DECL类似。每个ZEND_AST_METHOD节点表示一个成员方法,它有4个子节点,其中第一个子节点是参数列表,第二个子节点没有使用,第三个子节点为函数体,第四个子节点为返回值类型。
示例/tmp/user.php最终生成的抽象语法树如图7-5所示:
ZEND_AST_CLASS节点由zend_compile_class_decl()函数完成编译,类的各个子节点的编译过程相对比较独立,依次进行编译即可。编译步骤如下:
1.分配zend_class_entry结构,如果有继承的父类,则编译一条ZEND_FETCH_CLASS指令。
2.编译一条类声明的ZEND_DECLARE_CLASS指令,该指令的作用是将编译后的类注册到EG(class_table),与函数的ZEND_DECLARE_FUNCTION指令作用相同。同时将类的zend_class_entry注册到CG(class_table)。注意:key并不是类名,而是\0 + 类名 + 文件名 + lex_pos
,这个与函数编译时的处理是一样的,此时注册后的类还不能被使用。
3.将CG(active_class_entry)指向当前类的zend_class_entry结构,然后编译常量、成员属性、成员方法,并把编译结果注册到CG(active_class_entry)对应的符号表中。
// ast参数是类声明的AST节点
void zend_compile_class_decl(zend_ast *ast) {
zend_ast_decl *decl = (zend_ast_decl *)ast;
zend_ast *extends_ast = decl->child[0]; // 父类
zend_ast *implements_ast = decl->child[1]; // 实现的接口节点
zend_ast *stmt_ast = decl->child[2]; // 类中声明的常量、属性、方法
zend_string *name, *lcname;
// 1) 分配zend_class_entry
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的opcode
zend_compile_class_ref(&extends_node, extends_ast, 0);
}
// 2)生成类声明指令
// 获取下一条opcode
opline = get_next_op(CG(active_op_array));
zend_make_var_result(&declare_node, opline);
...
// 操作数2为类名
opline->op2_type = IS_CONST;
// 将类名的小写形式存入op2
LITERAL_STR(opline->op2, lcname);
// 如果是匿名类
if (decl->flags & ZEND_ACC_ANON_CLASS) {
...
} else {
zend_string *key;
// 如果有父类
if (extends_ast) {
// 生成ZEND_DECLARE_INHERITED_CLASS指令
opline->opcode = ZEND_DECLARE_INHERITED_CLASS;
opline->extended_value = extends_node.u.op.var;
} else {
// 无继承类则生成ZEND_DECLARE_CLASS指令
opline->opcode = ZEND_DECLARE_CLASS;
}
// 生成类的临时注册key:\0 + 类名 + 文件名 + lex_pos
key = zend_build_runtime_definition_key(lcname, decl->lex_pos);
opline->op1_type = IS_CONST;
// 将这个临时key保存到操作数1中
LITERAL_STR(opline->op1, key);
// 以临时key为key,将zend_class_entry注册到类符号表CG(class_table)
zend_hash_update_ptr(CG(class_table), key, ce);
}
// 3)编译类的常量、成员属性、成员方法
// 设置当前编译的类上下文
CG(active_class_entry) = ce;
// 递归编译类体内的成员
zend_compile_stmt(stmt_ast);
...
CG(active_class_entry) = original_ce;
}
生成的ZEND_DECLARE_CLASS指令有两个操作数:操作数1记录的是将类注册到CG(class_table)中的那个特殊key(以上代码中的key变量);操作数2记录的是小写的类名。两个操作数的类型都是CONST,如图7-6:
接下来我们分别看下常量、成员属性、成员方法的编译过程。
1.类常量的编译
常量的节点类型为ZEND_AST_CLASS_CONST_DECL,该节点为list节点,如果一个同时声明(一个const声明多个常量的情况,例如const a, b;
)了多个常量,则会有多个子节点。如果通过多个const声明了常量,则会有多个ZEND_AST_CLASS_CONST_DECL节点。编译过程如下:
// ast是类常量声明的语法树节点
void zend_compile_class_const_decl(zend_ast *ast) {
// 将语法树节点类型转换为list节点
zend_ast_list *list = zend_ast_get_list(ast);
// 获取当前编译的类
zend_class_entry *ce = CG(active_class_entry);
uint32_t i;
// 依次编译每个子节点:const C1, C2, C3 = 100;
for (i = 0; i < list->children; ++i) {
// 单个常量的ast节点
zend_ast *const_ast = list->child[i];
// 常量名节点
zend_ast *name_ast = const_ast->child[0];
// 常量值节点
zend_ast *value_ast = const_ast->child[1];
// 从ast节点中取出常量名字符串
zend_string *name = zend_ast_get_str(name_ast);
zval value_zv;
// 从ast节点中取出常量值
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,该节点也是list节点,表示同时声明多个属性的情况(如public $a, $b;
),子节点类型为ZEND_AST_PROP_ELEM。ZEND_AST_PROP_DECL节点的attr保存着要声明的属性的访问权限,即public、protected、private。编译过程如下:
void zend_compile_prop_decl(zend_ast *ast) {
zend_ast_list *list = zend_ast_get_list(ast);
// 属性修饰符:static、public、private、protected
uint32_t flags = list->attr;
zend_class_entry *ce = CG(active_class_entry);
uint32_t i, children = list->children;
for (i = 0; i < children; ++i) {
// 子节点类型为ZEND_AST_PROP_ELEM
zend_ast *prop_ast = list->child[i];
// 属性名节点
zend_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_property_info结构,并将其注册到zend_class_entry->proterties_info符号表中。然后将属性值保存到属性默认值数组中,这一步会分配属性的offset,静态属性的offset为数组下标0、1、2···依次分配,每编译一个静态属性,就将default_static_members_count的值加1,从而记录静态属性的数量;非静态属性的offset为相对zend_object结构的内存偏移,每编译一个非静态属性,就将default_properties_count的值加1,从而记录非静态属性的数量,分配时也是根据这个值进行编号的,然后将编号乘以sizeof(zval)
得到内存偏移值。
3.成员方法的编译
成员方法和函数的编译过程都是由zend_compile_func_decl函数完成的,该函数在zend_compile_stmt函数中被调用。
但编译成员方法时(通过传入zend_compile_func_decl函数的zend_ast抽象语法树节点中的值来判断是否是方法),会调用zend_begin_method_decl,而非zend_begin_func_decl。zend_begin_method_decl函数会将成员方法的zend_op_array注册到zend_class_entry->function_table,另外,如果成员方法是魔术方法,则会将zend_class_entry结构中对应的魔术方法指向该成员方法。
完成以上类常量、成员属性、成员方法的编译后,最后会将类注册到EG(class_table),注册过程与函数的注册过程完全相同,即在zend_compile_top_stmt函数中,如果函数的抽象语法树节点参数的类型是函数或类编译,则进入相同的处理步骤,该步骤中会调用zend_do_early_binding。
类开始编译时曾生成一条ZEND_DECLARE_CLASS指令,它的作用就是注册类,但该指令不是在运行时执行的,而是在zend_do_early_binding函数中执行,该指令会调用do_bind_class注册类,注册完后会将前面临时注册的类的key从EG(class_table)中删除。
do_bind_class函数首先会根据ZEND_DECLARE_CLASS指令的操作数1获取临时key,然后根据该key从CG(class_table)中得到zend_class_entry,然后再以操作数2记录的实际类名为key,将该zend_class_entry注册到CG(class_table)中。之后会将ZEND_DECLARE_CLASS置为空指令,同时删除它的两个CONST操作数。
7.1.5 内部类
内部类是由内核或扩展直接注册的类,它不需要经历编译的过程。内部类的结构也是zend_class_entry,其注册位置和用户自定义类相同(都是EG(class_table)
)。内部类更简单灵活,可进行一些个性化处理,如定义一个创建对象的钩子函数,在对象实例化时调用它。
定义内部类时的操作(如定义常量、定义方法、注册类到符号表等)与用户自定义类的实现相同,只是定义内部类时直接调用相关API完成这些操作。
7.1.6 类的自动加载
PHP中,我们通常会把类定义在一个文件中,使用时通过include加载进来,有时这导致长长的include列表;有时文件名修改了,我们要把每个引用的地方都改一遍。而PHP通过类自动加载功能,在使用未被定义的类时,自动将类加载进来。
PHP通过__autoload()
、spl_autoload_register()
实现自动加载。
类自动加载实际就是内核提供的一个钩子函数,实例化类时如果EG(class_table)中没找到对应的类,则调用该钩子函数,调用完重新查找一次,该钩子函数通过EG(autoload_func)指定。
1.__autoload()
这种方式只需用户提供一个__autoload()
函数,参数是类名,在函数中include类名对应的文件。
__autoload()
是默认的类加载器,我们也可以自定义类加载器,然后将EG(autoload_func)指向自定义的即可。没有自定义的类加载器时,会将PHP用户代码中定义的__autoload()
作为类加载器。类的查找通过zend_loopup_class_ex()完成,其中会先查找EG(class_table),如果找到了就返回类,否则就查看是否定义了__autoload()
,如果定义了就调用它,之后再次在EG(class_table)中查找类。
2.spl_autoload_register()
spl_autoload_register()允许定义多个加载器,而__autoload()
只能定义一个。实现上,spl(Standard PHP Library,标准PHP库)创建了一个队列保存用户注册的所有加载器,然后定义了一个函数到EG(autoload_func),当找不到类时,内核回调spl_autoload_call,该函数会依次调用用户注册的加载器,每调用一个就到EG(class_table)中重新检查类是否被注册了,直到注册成功为止。
spl_autoload_register函数的参数:
bool spl_autoload_register(callable $autoload_function, bool $throw = true,
bool $prepend = false);
参数autoload_function是函数或成员方法;参数throw用于设置注册失败时是否抛异常;参数prepend为true时会将函数添加到队列之首,而非队列之尾。
SPL的实现位于ext/spl目录下。
7.2 对象
对象是类的实例,创建时使用new关键字。PHP内部,对象的结构为zend_object:
成员含义:
1.handle:一次request期间的对象唯一编号,与创建顺序有关,垃圾回收时会用到。
2.ce:对象所属的类。
3.handlers:对象操作的处理函数,如成员属性读写、成员方法获取、对象销毁和克隆等。这些操作有默认的函数,也可通过扩展的自定义处理函数覆盖:
zend_object中的const zend_object_handlers *handlers
成员的const修饰的是handlers指向的对象,因此可以将其指向自定义的handlers,如obj->handlers = xxx
,但不能修改handlers中的const值,如obj->handlers->write_property = xxx
。如果想自定义某个handler,通常复制一份std_object_handlers,然后修改该副本,最后整体替换handlers。
4.properties:创建对象之初此值为NULL,主要用于定义动态属性。
5.properties_table:非静态成员属性数组,用于存储其属性值。非静态成员通过其offset分配在该数组对应位置上,读写非静态成员属性就是操作此数组。该数组的长度上图中是1,它是一个变长数组。它的长度为1而非0的原因为,类中定义了魔术方法__get
、__set
、__unset
、__isset
时,会多分配一个元素,用于防止魔术方法循环调用。
7.2.1 对象的创建
语法:
// 规则名:new_expr
new_expr:
// T_NEW对应new关键字
// class_name_reference是一个规则,表示类名的引用,可以是类名字符串或变量(如$className)
// ctor_arguments是一个规则,表示构造函数的参数列表
T_NEW class_name_reference ctor_arguments
// $$表示当前规则的结果
// zend_ast_create是PHP内部函数,用于创建AST节点
// ZEND_AST_NEW是要创建的AST节点类型
// $2是子规则2(即class_name_reference)对应的AST
// $3是子规则3(即ctor_arguments)对应的AST
{ $$ = zend_ast_create(ZEND_AST_NEW, $2, $3) }
// |表示或,即另一种可能的语法结构
// anonymous_class是一个规则,表示匿名类的完整定义(类体、继承等)
| T_NEW anonymous_class
{ $$ = $2 }
;
以上语法规则中的匿名类的一个例子为:
// 匿名类对象
$obj = new class($arg) extends ParentClass
{
private $prop;
public function method()
{
}
};
创建对象的语句在语法解析阶段被编译为ZEND_AST_NEW节点,之后该节点被zend_compile_new()编译为ZEND_NEW指令,执行时该指令将创建并初始化对象,步骤如下。
1.查找类
现根据类名从EG(class_table)中查找该类,即zend_fetch_class_by_name()操作,如果没有找到会触发类的自动加载机制,如果找到了会返回类的zend_class_entry结构。另外,如果类名是CONST类型(而非$className
变量),则会用到运行时缓存机制,避免下次调用重复查找。
2.创建、初始化对象
ZEND_NEW指令通过object_init_ex函数完成。
该过程分两步:第一步分配zend_object内存,分配时会根据非静态属性的数量zend_class_entry->default_properties_count,将非静态属性的内存一起分配;第二步是初始化非静态成员属性,将非静态成员属性的默认值拷贝至zend_object->properties_table。object_init_ex函数中会调用_object_and_properties_init()
完成这两步。
_object_and_properties_init()
中,如果没有自定义的对象创建handler时的操作:
(1)创建对象
会调用zend_objects_new()分配zend_object,分配时会调用zend_object_properties_size()获取非静态属性占用的内存大小,该大小通过非静态属性的个数乘sizeof(zval)
来确定,如果定义了魔术方法,那么会多分配一个zval的空间,然后还会设置对象处理handlers为默认的handlers(即std_object_handlers)。
(2)初始化非静态成员属性
非静态成员属性的默认值保存在类的default_properties_table数组中,初始化时根据属性的offset将各属性的value复制到对应位置。这里不是深拷贝,两者指向的value还是同一份,之后如果修改属性值,会触发写时复制,从而重新复制一份。但在ZTS(Zend Thread Safety,Zend线程安全)下,数组、普通字符串会发生深拷贝。
_object_and_properties_init()
中,如果自定义了对象创建handler(通过覆盖类的zend_class_entry.create_object接口成员),则需要自己创建对象并初始化非静态成员属性,此时可以自己设置对象处理handlers。
3.调用构造方法
如果类定义了构造方法,则在对象创建完后会调用它。
7.2.2 非静态成员属性的读写
非静态成员属性的读写处理handler分别为zend_object->handlers中的read_property、write_property,默认的处理函数为zend_std_read_proerty、zend_std_write_property。
1.读取
首先根据属性名查找zend_class_entry->properties_info,找到属性的zend_property_info结构,并检查是否有该属性的操作权限,然后根据zend_property_info->offset从zend_object->properties_table数组中获取对应属性。
如果没有找到属性,会检查类是否定义了__get
魔术方法,如果定义了则调用魔术方法进行处理。
如果类定义了__get()
,在实例化对象分配properties_table时会多分配一个zval,类型为HashTable,称其为guard,每次调用__get($var)
时会把$var
存入其中,目的是防止循环引用。
例如,在__get($var)
中又访问了$var
,会再次调用__get($var)
,从而无限递归下去。所以在调用__get
前会先判断要访问的属性是否已在guard哈希表中了,如果存在则不再调用__get
,如果不存在则将其插入哈希表guard。
其他魔术方法也会用到guard,哈希表的值类型为zend_long,不同魔术方法占用不同bit位。这个HashTable只有在zend_class_entry->ce_flags包含ZEND_ACC_USE_GUARDS标识时才会分配,编译类时,如果发现定义了__get
、__set
、__unset
、__isset
时会给ce_flags打上此标识。
2.修改
类似读取,首先查找属性,然后再修改。如果属性没有找到且定义了__set
魔术方法,则会调用它。
7.2.3 对象的复制
PHP普通对象的复制可直接通过赋值完成,如:
$a = array();
$b = $a;
但将对象赋值给另一个变量时,不会发生深拷贝,如果修改其中一个对象,另一个也随之改变。
第4章介绍写时复制机制时,说过zval的type_flag,只有有IS_TYPE_COPYABLE标识的类型的变量在变量赋值时才进行深拷贝,而object类型没有这个标识。
PHP通过clone关键字实现对象的复制:
$copy_of_object = clone $object;
clone时会复制对象的所有属性(不会深拷贝),这样各自的修改就不会互相影响。如果类中定义了__clone
魔术方法,则clone时会调用此函数。clone通过zend_object->clone_obj(默认是zend_objects_clone_obj()
)进行处理。
7.2.4 对象的比较
使用比较运算符==
比较两个对象变量时,如果两个对象的属性和属性值都相等,且两个对象是同一个类的实例,那么这两个对象变量相等。如果使用全等运算符===
,这两个对象变量一定要指向某个类的同一个实例才为true。
对象间的比较运算符通过zend_std_compare_objects()处理,全等运算符通过zend_is_identical()处理。
7.2.5 对象的销毁
销毁对象由zend_objects_store_del()完成,其中主要操作有:清理成员属性、从EG(objects_store).object_buckets中删除、释放zend_object内存。
垃圾回收时因循环引用导致无法正常回收的垃圾类型,一种类型是数组,另一种是对象。减少refcount时如果发现object的引用计数大于0,则将加入垃圾回收器。
7.3 继承
继承是面向对象的三个特性之一,它允许子类继承父类所有公有或受保护的特征和行为。
PHP通过extends关键词继承一个父类,一个类只允许继承一个父类,但可以多级继承:
class parent {}
class child extends parent {}
如果有继承的父类,则该子类的zend_class_entry->parent指向父类的结构。编译含继承的类时,在注册前需先找到父类,因此父类要在子类前注册。含继承的类编译生成的类声明指令是ZEND_DECLARE_INHERITED_CLASS,不含继承的类编译生成的类声明指令是ZEND_DECLARE_CLASS。同时,含继承的类的声明指令前会生成一条ZEND_FETCH_CLASS指令。
接下来编译类常量、成员属性、成员方法的过程同普通类。之后zend_do_early_binding()的类注册环节不同,普通类直接注册到EG(class_table),而含继承的类需要先注册父类,即子类注册时parent指针不能为空。ZEND_FETCH_CLASS指令就是用来获取父类的。
如果父类先注册,即子类在zend_do_early_binding()时能找到父类,那么执行ZEND_DECLARE_INHERITED_CLASS指令时就会调用do_bind_inherited_class继承父类的数据,然后将子类的parent指针指向父类,然后将子类注册到EG(class_table)从而成功编译,子类成功编译后,ZEND_FETCH_CLASS、ZEND_DECLARE_INHERITED_CLASS指令会被置为空指令。此过程ZEND_FETCH_CLASS指令没有起到什么作用。
如果子类先注册,即子类在zend_do_early_bind()时找不到父类,那么ZEND_FETCH_CLASS、ZEND_DECLARE_INHERITED_CLASS指令会保留到运行时执行。
在运行时,由于父类在编译时已经完成注册,所以此时ZEND_FETCH_CLASS能找到父类。成功fetch父类后,继续执行ZEND_DECLARE_INHERITED_CLASS继承父类,这个过程仍然是do_bind_inherited_class函数完成的。
几种不同情况的指令执行顺序:
上图中只有情况2可以正常执行。上图中实际执行顺序与代码顺序不同的原因是,父类定义在编译期完成了,所以在实际执行顺序中相当于第一个执行。情况1报错的原因是,A类由于编译期没找到父类B而没有完成定义,从而没有被放入EG(class_table)。情况3报错的原因是,编译期由于没找到父类,所以A、B类全部未完成定义,从而在运行时定义A类时因找不到B类报错。
do_bind_inherited_class函数与注册非继承类的do_bind_class函数相比,只多了调用zend_do_inheritance这一步,这一步处理的就是父子类继承的逻辑,其中会合并常量、属性、方法。
7.3.1 常量的继承
父类与子类的常量名相同时,用子类的常量,否则将父类的常量合并到子类。
7.3.2 成员属性的继承
步骤如下:
1.合并非静态成员属性
首先申请一个父类+子类非静态属性大小的数组,然后先将父类的非静态属性复制到新数组,后面紧接着将子类的非静态属性数组复制过去,子类的default_properties_table指向合并后的新数组,default_properties_count更新为新数组大小,最后释放子类旧的非静态属性数组。
2.合并静态成员属性
与非静态属性相同,最后更新子类default_static_members_table到新的静态属性数组。
3.更新子类属性offset
每个子类属性的property_info->offset需加上前面父类属性的总大小。静态属性的offet为数组下标,非静态属性的offset为内存偏移值。
4.合并属性信息哈希表
索引属性时,用的是properties_info哈希表,因此需要将父类和子类的属性索引表合并,此过程决定了父类哪些属性可被子类继承和覆盖。
合并的策略在do_inherit_property()中,父类的私有属性无法被子类继承,如子类覆盖了父类中的属性,则只能放大属性的权限而不能缩小,如父类中的public属性,子类中不能覆盖为protected、private的。关于子类覆盖父类的属性:
(1)如果子类中缩小某属性权限会出现的问题:如果一个函数参数是父类类型,且其中用到了父类类型的该属性,如果传入的是父类对象,则可以正常执行,如果传入的实参是子类对象,那么由于此属性不可访问,会报错,这破坏了父类的原有契约,因此不能缩小属性权限。
(2)对于父类的private属性,它属于父类的实现细节,子类完全不可见,因此子类中可以自由定义同名属性,它与父类中的同名属性同存于两个作用域:
class base
{
private $a = 1;
}
class derived extends base
{
protected $a = 2;
}
$obj = new derived;
var_dump($obj);
执行它:
而覆盖子类中的非private属性时,实际子类中只有一个同名属性:
class base
{
protected $a = 1;
}
class derived extends base
{
public $a = 2;
}
$obj = new derived;
var_dump($obj);
执行它:
7.3.3 成员方法的继承
子类可以继承父类公有、受保护的方法。成员方法的继承的实现也是将父类的function_table合并到子类的function_table中:先扩大子类的function_table,以容纳父子类的全部方法,然后遍历父类的function_table,如果可继承,则将该方法插入子类的function_table。
7.4 动态属性
php中可通过以下方式动态创建一个没有明确定义过的成员属性:
class my_class
{
public $id = 123;
}
$obj = new my_class;
$obj->prop_1 = array(1, 2, 3);
print_r($obj);
执行后输出:
动态创建的属性保存在zend_object->properties哈希表中,查找一个属性时会先按普通属性在zend_class_entry->properties_info中查找,找不到时再去zend_object->properties中继续找。
在修改成员属性时,会先按普通属性查找,如果找到了,就根据其offset取出属性值后再修改。如果没找到,说明是动态属性,就将其插入zend_object->properties哈希表。首次创建动态属性时,会通过rebuild_object_properties()初始化zend_object->properties,初始化时会将普通属性也加入properties哈希表中,因为Zend中很多地方需要获取全部属性,如对象转数组、垃圾回收等,此时直接获取properties数组即可,否则还需要合并动态属性和普通属性,对象处理的handlers中,get_properties()用来获取全部属性。初始化zend_object->properties时,加入其中的普通属性并不是增加原zend_value的refcount,而是创建了一个IS_INDIRECT类型的zval,指向原zval:
成员属性的读取通过zend_object->handlers->read_property(默认是zend_std_read_property()
)函数完成,动态属性的查找与修改成员属性(通过write_property函数)时的过程相同。
7.5 魔术方法
魔术方法是PHP提供的一些特殊操作时的钩子函数,与普通方法相同,它也保存在zend_class_entry->function_table中。
编译方法时,如果发现魔术方法,除了会将其加入zend_class_entry->function_table,还会设置zend_class_entry中对应的指针:
如一个类中定义了__construct()
、__get()
,则其zend_class_entry结构如图7-12所示:
__sleep()
、__wakeup()
在zend_class_entry中没有单独的指针指向,它们主要是序列化serialize()、反序列化unserialize()时调用的。
调用serialize时,最终会调用到php_var_serialize_intern函数,这个函数会根据不同类型选择不同序列化方式,如果类型是对象,会先检查zend_class_entry.function_table中是否有__sleep()
,如果有则调用它进行序列化。