面向对象编程(Object Oriented Programming, OOP)是一种计算机编程规范。OOP抽象出对象和类的概念——计算机程序由多个独立对象交互组合而成。
本章会分别从类和对象的源码来分析PHP7中面向对象的实现方式。6.1节和6.2节先回顾一下PHP中类的语法及其特性;6.3节分析类的实现;6.4节会对对象进行详细的阐述;6.5节展开介绍魔术方法和自动加载的实现。相信读者通过本章的阅读,能够对PHP 7面向对象的实现有深入的理解。
6.1 类的种类
PHP 7中支持多种类(class)的实现,包括普通类、抽象类、接口、特性以及final类和匿名类。
6.1.1 普通类
类以关键字“class”定义,后跟类名。类名不能是PHP预留关键字,必须以字母或下划线开头,每个类有属于自己的常量、方法、属性。类的实例叫作对象,可通过关键字“new”来实例化对象。
先编写一个PHP的普通类:
<?php
class A{
const B =1;
static $b = 2;
var $c = 3;
public function b(){
var_dump("public");
}
protected function c(){
var_dump("protected");
}
private function _d(){
var_dump("private");
}
static function e(){
var_dump(self::$b, self::B);
//var_dump(self::$c); //不能通过“::”访问非静态的属性
}
}
$a = new A();
$a->b();
//$a->c(); //Fatal error: Uncaught Error: Call to protected method A::c() from context ''
//$a->d(); //Fatal error: Uncaught Error: Call to undefined method A::d()
A::e();
说明:
- 类的属性和方法有三个访问级别,分别为public(公有), protected(受保护)或private(私有)。类的外部不能直接调用protected(受保护)或private(私有)的方法和属性。
- 类的属性有普通属性和静态、常量属性,静态、常量属性则分别用关键字“static”和“const”声明。
- 类的普通方法、属性被自己的成员函数调用时可使用“$this->”,静态方法及静态属性也可通过这种方式访问。
- 类的常量属性、静态属性、静态方法,通过“self::”调用。非静态的方法也可以通过“::”调用,但是非静态的属性不能通过“::”访问。
6.1.2 抽象类
PHP 5开始支持抽象类和抽象方法,抽象类不能被实例化。
下面编写一个抽象类的示例:
<?php
abstract class A
{
abstract protected function b($name);
abstract protected function c();
// abstract private function c(); /*抽象方法不能设置为私有*/
abstract public function d();
/*PHP Fatal error: Abstract function A::d() cannot contain body
abstract public function d(){}
*/
public function f(){
echo 'A->d()';
}
}
class E extends A{
public function b($name, $name2=''){
echo $name.$name2;
}
public function c(){
echo 'pub_c';
}
publicfunction d(){
echo 'pub_d';
}
/*访问级别不能降低*/
/*protected function d(){
echo 'pri_d';
} */
/*参数个数也必须一致*/
protected function b($name, $name2){
echo 'pri_d';
}*/
}
$obj = new E();
$obj->b('name');
$obj->c();
$obj->d();
说明:
- 抽象方法必须被子类继承实现,所以不能为私有,只能是受保护的或公有的;
- 抽象类的方法访问控制级别必须和子类相等或更为宽松。例如,父类的抽象方法是受保护的,子类实现时则必须为受保护的或者公有的;
- 抽象方法的实现,必传参数的数量及类型必须严格一致;
- 抽象类的非抽象方法,子类可不实现,等同于普通类方法的继承;
- 抽象类中的抽象方法,只能定义,不能实现其方法体;
- 抽象类可定义常量,且可被子类覆盖。
6.1.3 接口
PHP与大多数面向对象编程语言一样,不支持多重继承,每个类只能继承一个父类。
对象接口(interface)里定义的方法子类需全部实现,且接口不能直接被实例化。接口的主要特性和需注意的点如下。
- 接口类可以通过extend继承一个或多个接口类,多个接口之间用逗号分隔,用以实现接口类的扩充。
- 接口类定义的方法必须声明为公有,因此子类的实现方法也只能为公有。接口方法体也必须为空。
- 接口类定义的常量和类常量使用方式一样,但不能被子类或者子接口覆盖。
- 普通类通过关键字“implements”来实现一个或多个接口。
- 继承多个接口,方法不能有重名。
- 普通类继承接口,必须实现接口类里面所有的方法,参数也和接口方法定义相同。可加默认参数,这点和抽象类方法的实现基本一致。
接下来,结合PHP代码一起来看接口的定义与实现。
- 接口定义
interface A{ public function demo1(); /*接口方法不能有方法体,且只能为公有*/ /* PHP Fatal error: Access type for interface method A::demo1() must be omitted*/ //protected function demo1(); } - 接口扩充
interface B{ public function demo2(); } interface C extends A, B{/*只能通过extends实现接口类扩充*/ public function demo3(); } - 普通类对接口的实现
class D implements A{ /* PHP Fatal error: Declaration of D::demo1 ($name) must be compatible with A::demo1 ()*/ //public function demo1($name){ public function demo1($name=1) {/*接口未定义的参数,可通过加默认值实现*/ } } - 普通类实现多个接口
class D implements A, B, C{/*通过implements实现多个接口类,类之间通过逗号分隔*/ /*继承的所有接口方法都必须实现,否则报错*/ public function demo1(){ } public function demo2(){ } public function demo3(){ } }
6.1.4 特性
特性(trait)从PHP5.4.0开始启用,便于在不同层次结构内实现代码复用。特性不能直接被实例化,主要特性和需注意的点如下。
- 特性与普通类相似,有自己的方法、属性等,但是不可通extends继承,也没有类常量。
- 特性的方法如果和当前类方法冲突,会被当前类的方法覆盖。如果和基类方法冲突,特性方法会覆盖基类中的方法。优先级:当前类>特性类>基类。
- 一个类加载了多个特性,当多个特性中方法有重名时,需要在代码中通过关键字“insteadof”设置优先级或者通过“as”关键字重命名处理,否则报错。
结合PHP代码实践特性的定义与用法。
- 特性的定义
trait A{ //定义特性A public $b = 'public'; //定义特性的属性 function demo1(){ //定义特性的普通方法 echo 'A->demo1'; } static function demo2(){ //定义特性的静态方法 echo 'A::demo2'; } abstract public function demo3(); //定义特性的抽象方法 } - 特性扩容
可见,此时B包含了A的所有内容,扩容非常简单。trait B{ use A; //通过关键字use即可拥有特性A全部的方法属性 function demo4(){ echo 'B->demo4'; } } - 普通类使用特性
class ChildClass { use B; /*此时相当于把特性B所有方法打包加载进子类*/ function demo3(){ /*和继承类似,必须实现特性中的抽象方法*/ echo 'trait:abstract:demo3'; } } - 普通类加载多个特性
trait C{ function demo5(){ echo 'C->demo5'; } } - 通过use加逗号加载多个特性
class ChildClass{ use B, C; /*逗号隔开,加载多个特性;也可一个一个地加载use B; use C; */ function demo3(){ echo 'trait:abstract:demo3'; } } - 特性方法优先级(自身>特性>基类)
class ParentClass{ function demo1(){ /*被特性A中demo1覆盖*/ echo 'Parent->demo1'; } } class ChildClass extends ParentClass{ use A; function demo2(){ /*自身类demoe2覆盖了特性A中的demo2*/ echo 'Child->demo2'; } /*避免重复代码,A中抽象方法demo3实现暂时忽略*/ } $obj = new ChildClass(); $obj ->demo1(); /*A->demo1,特性A的demo1覆盖了基类的demo1*/ $obj ->demo2(); /*Child->demo2,自身类的demo2覆盖了特性A中demo2*/ - 优先级冲突解决
当两个特性的方法相同时,需要通过“insteadof”关键字定义谁优先,或通过“as”关键字修改方法名。先定义一个方法与A冲突的特性D:
普通类同时加载A与D:trait D{ function demo1(){ echo 'C->demo1'; } }class ChildClass {//extends ParentClass { //use A, D; /* 方法重名,且没有自定义优先级,则报错:PHP Fatal error: Trait method demo1 has not been applied, because there are collisions with other trait methods*/ //通过insteadof人为地定义优先级,解决冲突,或者通过as给其中一个方法取个别名 use A, D { D::demo1 insteadof A; //D::demo1 AS dDemo1; } /*避免重复代码,A中抽象方法实现暂时忽略*/ } - 修改特性方法的访问控制级别。
在普通类中加载特性,也可通过“as”来修改其访问控制级别,如下示例代码:
class ChildClass { use C { demo5 as private; } }
6.1.5 final类
如果不希望一个类被继承,可以使用“final”来修饰。如果一个方法不想被子类覆盖,也可以这样声明。
结合PHP示例来看。
- final修饰的函数不能被覆盖
class A{ final function b(){ } } class B extends A { function b(){//PHP Fatal error: Cannot override final method A::b() echo 'B->b'; } } - final修饰的类不能被继承
final class A{ function b(){ } } class B extends A {// PHP Fatal error: Class B may not inherit from final class (A) }
注意
类的属性不能定义为final,只有类和类方法才能被定义为final。
6.1.6 匿名类
当我们想快速实例化一个对象,可以通过匿名类来实现。从PHP 7开始支持匿名类,可通过newclass函数创建,不能有类名。
快速创建一个简单匿名对象:
$obj = new class {
public function b($msg){
return $msg;
}
};
var_dump($obj->b('anonymous')); //输出’anonymous'
var_dump($obj); // object(class@anonymous)#1 (0) {}
6.2 类的特性
上一节介绍了抽象类、匿名类、普通类等的基本特性和简单应用,这一节主要介绍类的特性。
6.2.1 类的属性
类的成员变量叫作属性,属性可声明为private、public、protected三种访问级别。
类的成员方法中,可以通过“$this->”访问非静态、非常量属性;通过“self::”来访问常量及静态属性。接下来逐步介绍各个属性的异同。
- 普通属性:普通属性指的是无static、const声明的属性。普通属性通过“->”访问,类实例化成对象后,会把这些属性复制到对象中。
- 静态属性:静态属性指的是通过关键字static声明的属性,访问时通过“::”调用。
- 常量属性:常量属性指的是通过关键字const声明的属性,常量属性不能被修改,访问时通过“::”调用。
- 动态属性:动态属性指的是在程序运行中产生的属性,不是在类中声明的。如以下代码示例:
class D{ public function set($param) { $this->b = $param ; //设置动态属性 } } $obj = new D(); $obj->set('param_str'); var_dump($obj->b);
6.2.2 访问控制
类中的访问控制是通过在方法和属性前面添加关键字public、protected或private来实现的。
注意
① 在普通类、匿名类和final类中,方法及属性的访问控制声明不受限制,可设为public、protected和private三者中的任意一个;
② 在抽象类和接口中,方法及属性的访问控制不能被声明为private;
③ 在特性类中,方法及属性的访问控制只能被声明为public。
6.3 类的实现
前面介绍了PHP 7的类的种类和常用特性。从本节开始,会依次介绍类在PHP 7中的存储数据结构和类的静态属性、常量、方法、接口和特性的源码实现。最后,以继承的源码实现的分析结束本节。
6.3.1 类的结构
面向对象的核心是类,先来看看PHP 7中存储类的数据结构zend_class_entry:
struct _zend_class_entry {
char type;
zend_string *name;
struct _zend_class_entry *parent;
int refcount;
uint32_t ce_flags;
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;
HashTable properties_info;
HashTable constants_table;
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;
/* handlers */
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;
};
此结构体的主要字段有以下几个。
-
type:类的类型,共有两种——1代表内置的类,2代表用户自定义的类。
#define ZEND_INTERNAL_CLASS //内置类 #define ZEND_USER_CLASS //用户自定义的类 -
name:类名。
-
parent:继承的父类指针。
-
refcount:引用计数。
-
ce_flags:位组合标记。其中0x10表示此类有抽象方法,0x20表示此类为抽象类,0x40表示接口,0x80表示特性,0x100表示匿名类,0x400表示其为某个类的子类。 定义如下:
#define ZEND_ACC_IMPLICIT_ABSTRACT_CLASS 0x10 #define ZEND_ACC_EXPLICIT_ABSTRACT_CLASS 0x20 #define ZEND_ACC_INTERFACE 0x40 #define ZEND_ACC_TRAIT 0x80 #define ZEND_ACC_ANON_CLASS 0x100 #define ZEND_ACC_ANON_BOUND 0x200 #define ZEND_ACC_INHERITED 0x400 -
default_properties_count:默认普通属性个数。
-
default_static_members_count:默认静态属性个数。
-
default_properties_table:默认普通属性值数组。
-
default_static_members_table:默认静态属性值数组。
-
static_members_table:静态属性成员。
-
constructor:构造方法。
-
destructor:析构方法。
-
clone:克隆方法。
-
__get:魔术方法__get。
-
__set:魔术方法__set。
-
__unset:魔术方法__unset。
-
__isset:魔术方法__isset。
-
__call:魔术方法__call。
-
__callstatic:魔术方法__callstatic。
-
__tostring:魔术方法__tostring。
-
__debugInfo:魔术方法__debugInfo。
-
serialize_func:对象序列化方法。
-
unserialize_func:对象反序列化方法。
-
iterator_funcs:PHP 5开始,支持接口并且内置了Iterator接口,其为此接口的相关操作方法。
-
create_object:实例化对象时调用的方法,默认为函数zend_objects_new,可以通过扩展或修改源码来改变此值。
-
serialize:序列化方法回调指针。
-
unserialize:反序列化方法回调指针。
-
num_interfaces:类implements的接口个数。
-
num_traits:类use的特性个数。
-
interfaces:类implements的接口指针。
-
traits:类use的traits指针。
-
trait_aliases:类use的特性方法的别名。
-
trait_precedences:类use的特性方法的优先级(用于多个特性有相同名称的方法时,解决冲突,6.1.4节中有代码示例)。
-
info:记录类的其他信息,比如类所在的文件、注释之类。结合PHP代码说明特性相关的字段,示例为如下代码:
trait Win { public function exec(){ echo "I am Win! \n"; } } trait Mac{ public function exec(){ echo "I am Mac! \n"; } } trait Config { public function filename() { echo "php.ini\n"; } } class Php{ use Config { Config::filename as configName; } use Win, Mac{ Win::exec insteadof Mac; } }
上面代码定义了PHP类。
- 它用了3个特性(分别为Win、Mac、Config),所以ce->num_traits为3,3个特性的结构体分别为ce->traits[0]、ce->traits[1]、ce->traits[2]。
- 代码“Config::filename as configName”添加了一条别名信息,存储在ce->trait_aliases[0]。
- 代码“Win::exec insteadof Mac”添加了一条优先级信息,存储在ce->trait_prece-dences[0]。
PHP的类在编译(通过函数zend_compile_class_decl())时生成。每个类对应着一个结构体struct _zend_class_entry,存储在一个以类名字(全部转为小写)为key的HashTable中,也就是全局变量EG(class_table)中。
介绍完了类的存储结构,再来介绍存储属性和方法的相关数据结构zend_property_info:
typedef struct _zend_property_info {
uint32_t offset; /* property offset for object properties or
property index for static properties */
uint32_t flags;
zend_string *name;
zend_string *doc_comment;
zend_class_entry *ce;
} zend_property_info;
各字段含义如下。
- offset:当查找普通属性时,此值为地址偏移量;查找静态属性时,此值为索引。可能大家很难理解,为什么如此相近的属性,却一个用地址偏移量,另一个用索引?因为普通属性存储在对象结构体struct_zend_object的柔性数组properties_table中(详见6.4.1),而静态属性存储在类结构体struct_zend_class_entry的静态属性指针default_static_members_table指向的内存块中。一个结构体中只能有一个柔性数组,而对象结构中, 只有这一个字段properties_table是变长的数组,所以把此字段放在结构体最后面,用柔性数组即可。而类结构中,有三个字段(default_properties_table、default_static_members_table、static_members_table)是变长数组,所以只能通过指针的方式实现。
- flags:属性的访问权限以及是否是静态属性。
#define ZEND_ACC_PUBLIC 0x100 #define ZEND_ACC_PROTECTED 0x200 #define ZEND_ACC_PRIVATE 0x400 #define ZEND_ACC_STATIC 0x01 - name:属性名称。
- doc_comment:注释。
- ce:所属的类指针。
仍然以示例说明:
class Php{
const VERSION_5 = 5; //
const VERSION_7 = 7; //
protected $_version; //
public function version(){
return $this->_version;
}
}
class Php7 extends Php{
protected $_ast; //
public function ast(){
return $this->_ast;
}
}
PHP通过zend_compile_class_decl()函数将AST树转成struct_zend_class_entry结构体,通过gdb打印这段代码生成的两个struct_zend_class_entry结构。
查看PHP类的名称:
(gdb) p *compiler_globals.class_table.arData[157].val.value.ce.name.val@3
$2 = "Php"
示例代码中PHP类在源码中的数据结构存在的地址如下:
(gdb) p compiler_globals.class_table.arData[157].val.value.ce
$19 = (zend_class_entry *) 0x7ffff7c03018
类的数据结构示意图如图6-1所示。

图6-1 类的数据结构示意图
大家知道,一个类的静态属性和静态方法是和类相关的,而普通属性是和对象相关的,所以下节先详细介绍静态属性、常量和方法。
6.3.2 静态属性、常量和方法
-
静态属性:如前文所述,静态属性存储在properties_info和default_static_members_table中。properties_info是一个HashTable,当访问一个静态属性时,以变量名为key,在properties_info中找到对应的value,再取结构体struct _zend_property_info中的字段offset。而default_static_members_table是一个数组,所以default_static_members_table[offset]即为目标属性值。 类的静态属性查找示意图如图6-2所示。

图6-2 类的静态属性查找示意图 -
常量:类常量存储在HashTable类型的constants_table字段中。
-
方法:类的方法(包括类的静态方法和类的普通方法)和普通方法(非成员方法)编译后生成的zend_op_array基本没有区别。唯一区别就是类的方法编译后生成的zend_op_array是存在于类结构体的function_table中,不像普通方法,编译后存储在全局变量CG(function_table)中。
类成员方法的访问权限(private、protected、public)以及是否是静态方法等信息,存储在zend_op_array中的fn_flags字段里。
类的普通方法调用与静态方法调用基本无异。区别在于普通方法可以使用this传入。
因此,如果在一个类的普通方法的实现中,没有用到$this变量,那么把普通方法当成静态方法调用是没有问题的,否则会报语法错误。这和C++语言的实现基本上一样。
<? php
class Php{
protected $_version;
public function version1(){
echo "7.1.1\n";
}
public function version2(){
$this->_version = "7.1.0";
echo $this->_version;
}
}
Php::version1();
Php::version2();
因为PHP没有指针的概念,为了更加深刻地理解类的普通方法的this变量,可通过一段神奇的C++代码来横向对比一下。
#include<iostream>
using namespace std;
class Php{
protected:
std::string _version;
public:
void version()
{
std::cout << "7.1.0" << endl;
this->_version = “7.1.0”; //会报错
}
};
int main(int argc, char* argv[])
{
Php* php = (Php*)0;
php->version();
return 0;
}
上面的代码并没有实例化一个PHP对象,而是直接使用了0这个指针,但通过这个空指针调用这个类的普通方法,程序仍然可以正常运行,和PHP的原理一样,因为我们并没有使用this指针,也没有修改非法内存。
6.3.3 接口和特性
-
接口 PHP只支持单一继承,也就是一个子类只能有一个父类。而为了实现类似于C++的多重继承的功能,PHP引入了接口的概念。接下来介绍接口的实现。
在一个类初始化时,已经确定了此类的接口个数,而关联一个类与其所实现的接口,是通过函数zend_do_implement_interface()来实现的。
关联接口和类时,根据PHP关于接口的语法,可猜测进行了哪些操作。
- ce->num_interfaces加1,将此接口的结构体指针赋值给ce->interfaces[ce->num_interfaces-1]。
- 遍历接口中的constants_table,并依次插入到ce->constants_table。如果类和接口有相同名字的常量,则报错。
- 遍历接口中的function_table,根据继承的逻辑,判断是否可以插入到类的function_table中。如果可以,则继承此方法。否则不进行任何操作。
- 将接口中的interfaces按顺序拷贝到类的interfaces后。
-
特性 前文已介绍了特性的具体定义和代码示例,现在来介绍特性的具体实现。特性与类进行关联通过方法zend_do_bind_traits()来实现:
ZEND_API void zend_do_bind_traits(zend_class_entry *ce) /* {{{ */ { if (ce->num_traits <= 0) { return; } /* complete initialization of trait strutures in ce */ zend_traits_init_trait_structures(ce); /* first care about all methods to be flattened into the class */ zend_do_traits_method_binding(ce); /* Aliases which have not been applied indicate typos/bugs. */ zend_do_check_for_inconsistent_traits_aliasing(ce); /* then flatten the properties into it, to, mostly to notfiy developer about problems */ zend_do_traits_property_binding(ce); /* verify that all abstract methods from traits have been implemented */ zend_verify_abstract_class(ce); /* Emit E_DEPRECATED for PHP 4 constructors */ zend_check_deprecated_constructor(ce); /* now everything should be fine and an added ZEND_ACC_IMPLICIT_ABSTRACT_CLASS should be removed */ if (ce->ce_flags & ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) { ce->ce_flags -= ZEND_ACC_IMPLICIT_ABSTRACT_CLASS; } }
主要完成了如下操作。
- 完成类结构体指针ce的特性初始化。遍历ce的特性优先级变量ce->trait_precedences(假如遍历中的当前变量叫作cur_pre)。校验优先级最高的类和方法的语法,如果优先级中类cur_pre->exclude_from_classes还未编译,则进行编译(类似include的编译);然后保证优先级中用到的特性类cur_pre->trait_method->ce存在于变量ce->traits中,且优先级特性中的方法cur_pre->trait_method->method_name在特性类的方法表cur_pre->trait_method->ce->function_table中,再遍历被排除的类和方法的变量cur_pre->exclude_from_classes,校验其相关语法。遍历类结构体中的特性别名变量ce->trait_aliases,和上面的逻辑一样,编译未编译的类,校验相关语法。
- 将特性的方法拷贝到类中。遍历类结构体中的变量ce->traits,将其方法拷贝到ce->function_table。在拷贝之前执行判断:先将ce->trait_precedences中因为优先级被排除的类的方法排除掉,如果有方法存在别名,则将以别名为新名的方法拷贝到ce->function_table。
- 校验别名的相关语法。遍历类结构体中的特性别名变量ce->trait_aliases,如发现仍然有别名所属的类未找到,则抛出语法错误。
- 拷贝特性的属性到类中。遍历ce->num_traits,即遍历每个特性的属性properties_info,如果此属性在类中存在,且是继承于父类,则将此属性删除,然后将特性中的属性拷贝到类中;若不是继承自父类,则继续遍历。如果此属性在类中不存在,则将特性中的属性拷贝到类中。
- 校验类是否已实现所有抽象方法,没有则报错。
- 校验类是否存在与类同名的方法来做构造方法,有则提示。
6.3.4 继承
继承划分了类的层次,父类代表的是更一般、更泛化的类,而子类则更为具体、细化。继承是实现代码重用、扩展软件功能的重要手段。子类中与父类完全相同的属性和方法不必重写,只需写出新增或改写的内容,不必一切从零开始。
PHP只支持单一继承,实现相对简单。PHP父类和子类是分别编译的。编译完成后,再对父类和子类进行继承。继承操作在函数do_bind_inherited_class()中完成。
-
继承属性 普通属性和静态属性的继承是先后完成的。在类结构中二者的存储十分相近,继承的操作也十分相近,这里只介绍普通属性继承的实现。
- 申请一个元素类型是zval的数组table,大小为父类的普通属性个数(parent_ce->default_properties_count)和子类的普通属性个数(ce->default_properties_count)之和。
- 将父类的普通属性中parent_ce->default_properties_table的元素拷贝到数组table。
- 将子类的普通属性中的ce->default_properties_table的ce->default_properties_count个元素拷贝到table+parent_ce->default_properties_count。
- 释放子类的普通属性指针ce->default_properties_table,将table赋值给ce->default_properties_table。
这样就完成了普通属性的合并,请看如图6-3所示的示意图。

图6-3 普通属性的继承类的静态属性也如此完成合并。 可以看出,子类的静态属性和普通属性在元素中的位置,相对于合并前都有偏移,所以要对其在HashTable中的偏移进行重置,重置的大致步骤如下。
- 遍历properties_info:如果元素是静态属性,则对offset加parent_ce->default_static_members_count。
- 如果元素是普通属性,则对offset加parent_ce->default_properties_count * sizeof(val)。
- 接下来进行子类的properties_info和父类的properties_info的合并。
由于不同的属性可能拥有不同的权限,例如父类和子类有重复的属性,甚至重复属性的类型也不同(这里的类型指普通属性和静态属性),所以这两个HashTable的合并会有很复杂的逻辑,但是基于以上讲的数据结构,实现起来并不复杂,对PHP语法特别熟稔的同学,完全可以自己实现这段代码,这里就不啰嗦了。
-
继承常量 常量存储是用HashTable实现的,两个HashTable的合并比较简单,无非就是遍历父类的constants_table。
- 如果子类中存在此常量,则不进行任何操作。
- 如果子类中不存在此常量,则把此key->val键值对插入到子类的constants_table中。
if (zend_hash_num_elements(&parent_ce->constants_table)) { zend_class_constant *c; zend_hash_extend(&ce->constants_table, zend_hash_num_elements(&ce->constants_table) + zend_hash_num_elements(&parent_ce->constants_table), 0); ZEND_HASH_FOREACH_STR_KEY_PTR(&parent_ce->constants_table, key, c) { do_inherit_class_constant(key, c, ce); } ZEND_HASH_FOREACH_END(); }
-
继承方法 与常量继承的实现类似,方法的继承也是遍历父类的function_table,然后将结果插入到子类的function_table中。不同的是,可能存在方法为private、abstract或final特性,或者同一个方法在父类中为静态方法,而在子类中为普通方法等特殊情况。
if (zend_hash_num_elements(&parent_ce->function_table)) { zend_hash_extend(&ce->function_table, zend_hash_num_elements(&ce->function_table) + zend_hash_num_elements(&parent_ce->function_table), 0); 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(); }
6.4 对象的实现
对象是类的实例,上面讲了类的源码实现,接下来讲对象的源码实现,并介绍和类相关的普通属性。
6.4.1 实现
先来看看对象的存储结构:
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];
};
struct _zend_object_handlers {
/* offset of real object header (usually zero) */
int offset;
/* general object functions */
zend_object_free_obj_t free_obj;
zend_object_dtor_obj_t dtor_obj;
zend_object_clone_obj_t clone_obj;
/* individual object functions */
zend_object_read_property_t read_property;
zend_object_write_property_t write_property;
zend_object_read_dimension_t read_dimension;
zend_object_write_dimension_t write_dimension;
zend_object_get_property_ptr_ptr_t get_property_ptr_ptr;
zend_object_get_t get;
zend_object_set_t set;
zend_object_has_property_t has_property;
zend_object_unset_property_t unset_property;
zend_object_has_dimension_t has_dimension;
zend_object_unset_dimension_t unset_dimension;
zend_object_get_properties_t get_properties;
zend_object_get_method_t get_method;
zend_object_call_method_t call_method;
zend_object_get_constructor_t get_constructor;
zend_object_get_class_name_t get_class_name;
zend_object_compare_t compare_objects;
zend_object_cast_t cast_object;
zend_object_count_elements_t count_elements;
zend_object_get_debug_info_t get_debug_info;
zend_object_get_closure_t get_closure;
zend_object_get_gc_t get_gc;
zend_object_do_operation_t do_operation;
zend_object_compare_zvals_t compare;
};
结构体struct _zend_object各个字段的说明如下。
- gc:gc头部(详见第3章)。
- handle:每生成一个结构体zend_object,会将其首地址存储在全局变量executor_globals.objects_store.object_buckets中,而handle即为此结构体在此全局变量中的索引。
- ce:所属的类结构体指针。
- handlers:初始化时,默认指向全局变量std_object_handlers,存储着包括操作对象属性等的多个指针函数。
- properties:HashTable结构,存储对象的动态普通属性值。
- properties_table:柔性数组,存储对象的普通属性值。在初始化时创建,数组大小为对象所属类的默认普通属性个数default_properties_count+1。
接下来,创建一个对象Php7,其生成的数据结构示意图如图6-4所示。
$obj = new Php7();

图6-4 对象的结构
6.4.2 普通属性
普通属性也存储在对象中。当查找对象的普通属性时,在其所属的类的变量properties_info中,根据属性名key,找到value(类型为zend_property_info结构体),判断属性后,结构体的字段offset即为要查找到的属性值在对象的properties_table中的地址偏移量,如图6-5所示。
看上去顺利地解决了普通属性的存储问题,其实不然。请看下段代码:
$php = new Php7();
$php->filename = "beatles.php";
基于PHP的特性,对象中还存在一种动态普通属性。当然了,由于动态普通属性没有权限问题,也不需要在对象创建时初始化,所以比较简单,直接存储到对象的properties字段就好了。
6.5 其他特性
6.5.1 魔术方法
魔术方法是PHP独有的特性。魔术方法的实现方式与一般的方法几乎无异。区别就是部分魔术方法不只存在于zend_class_entry中的function_table,在zend_class_entry中也会直接存一份,所以会有如图6-6所示的结构图(请注意__get方法的存储位置)。
6.5.2 自动加载
自动加载是依据用户实现的规则来加载PHP文件,实现类的加载。比如现在新建一个PHP 7类型的对象,如果还未加载这个类,且用户没有实现自动加载方法,或是实现的自动加载方法没能把PHP 7这个类加载进来,则会报语法错误。
根据这个逻辑,能迅速猜测到自动加载的实现原理:当用到一个类时,如果在EG(class_table)中没有找到这个类,则去调用用户实现的自动加载方法。调用后到EG(class_table)中查找此类,如果此时仍然没有,则抛出语法错误。
PHP 7中提供了两种自动加载方式:__autoload()和spl_autoload_register()。
- __autoload:这个自动加载方法比较简单,在PHP中实现了此方法后,会存储在EG(autoload_func)中。当需要加载新的类时,内核会调用此方法加载类。 此外,内核自己实现了__autoload的默认版本PHP_FUNCTION(spl_autoload)。
- spl_autoload_register:这种加载方法比较高级。除了可以实现多个加载逻辑之外,还可以设置优先级(把加载逻辑放在最高优先级或者最低优先级)。知名管理工具Composer便主要是靠spl_autoload_register来实现的。 而且,还可以通过spl_autoload_unregister来删除某个加载逻辑。
现在我们知道,通过spl_autoload_register来进行自动加载,大体有以下几个特征:多个、优先级、可查找(查找后删除)。那么之前学到的内核数据结构有没有可以满足这个需求的?答案是有,内核强大的HashTable完全可以满足这些需求。
事实上,内核确实就是用HashTable来实现的。内核使用HashTable存储在全局变量SPL_G(autoload_functions)中。SPL_G(autoload_functions)初始化为NULL,当程序第一次调用spl_autoload_register()方法来增加自动加载逻辑时,内核会对其进行初始化。
由于spl_autoload_register()可以通过方法、类的普通方法甚至是闭包来实现自动加载,所以这个HashTable的value用zend_function或用zval都不能满足需求,为了满足需求,内核定义了一个结构体autoload_func_info,用于存储用户实现的自动加载逻辑。
typedef struct {
zend_function *func_ptr;
zval obj;
zval closure;
zend_class_entry *ce;
} autoload_func_info;
可以看出,此value类型并非联合体,实现起来很简单。
此外,需要说明的是,无论实现了哪种自动加载,都是在PHP_FUNCTION(spl_autoload_call)中对自动加载方法进行调用。如果全局变量SPL_G(autoload_functions)已初始化,则按顺序调用加载逻辑,直到此类正确加载。否则直接调用__autoload实现。
if (SPL_G(autoload_functions)) {
. . .
while (zend_hash_get_current_key_ex(SPL_G(autoload_functions), &func_name,
&num_idx, &pos) == HASH_KEY_IS_STRING) {
alfi = zend_hash_get_current_data_ptr_ex(SPL_G(autoload_functions), &pos);
. . .
if (zend_hash_exists(EG(class_table), lc_name)) {
break;
}
zend_hash_move_forward_ex(SPL_G(autoload_functions), &pos);
}
zend_exception_restore();
zend_string_free(lc_name);
SPL_G(autoload_running) = l_autoload_running;
} else {
/* do not use or overwrite &EG(autoload_func) here */
zend_call_method_with_1_params(NULL, NULL, NULL, "spl_autoload", NULL, class_
name);
}
6.6 本章小结
本章介绍了面向对象的相关实现,包括类的静态属性、普通属性、常量方法、特性和接口,以及对象的创建、继承等,最后又介绍了魔法方法和自动加载。读完本章,想必读者对PHP 7面向对象的实现原理已经有了比较清晰的认识。