### 3.4.1 類
類是現實世界或思維世界中的實體在計算機中的反映,它將某些具有關聯關系的數據以及這些數據上的操作封裝在一起。在面向對象中類是對象的抽象,對象是類的具體實例。
在PHP中類是編譯階段的產物,而對象是運行時產生的,它們歸屬于不同階段。
PHP中我們這樣定義一個類:
```php
class 類名 {
常量;
成員屬性;
成員方法;
}
```
一個類可以包含有屬于自己的常量、變量(稱為“屬性”)以及函數(稱為“方法”),本節將圍繞這三部分具體弄清楚以下幾個問題:
* a.類的存儲及索引
* b.成員屬性的存儲結構
* c.成員方法的存儲結構
* d.成員方法的調用過程及與普通function調用的差別
#### 3.4.1.1 類的結構及存儲
首先我們看下類的數據結構:
```c
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、private
int default_static_members_count; //靜態屬性數,static
zval *default_properties_table; //普通屬性值數組
zval *default_static_members_table; //靜態屬性值數組
zval *static_members_table;
HashTable function_table; //成員方法哈希表
HashTable properties_info; //成員屬性基本信息哈希表,key為成員名,value為zend_property_info
HashTable 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()`處理,自定義時可以參考這個函數的實現:
```c
//注意:此操作并沒有將屬性拷貝到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);
//設置對象操作的handler
object->handlers = &std_object_handlers;
return object;
}
```
舉個例子具體看下,定義一個User類,它繼承了Human類,User類中有一個常量、一個靜態屬性、兩個普通屬性:
```php
//父類
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》](zend_executor.md#3313-zend_executor_globals)已經講過。

在接下來的小節中我們將對類的常量、成員屬性、成員方法的實現具體分析。
#### 3.4.1.2 類常量
PHP中可以把在類中始終保持不變的值定義為常量,在定義和使用常量的時候不需要使用 $ 符號,常量的值必須是一個定值(如布爾型、整形、字符串、數組,php5.*不支持數組),不能是變量、數學運算的結果或函數調用,也就是說它是只讀的,無法進行賦值。
常量通過 __const__ 定義:
```php
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哈希表中索引到具體的常量值即可。
事實上我們的這個猜測并不是完全正確的,因為有的情況確實是我們猜想的那樣,但是還有另外一種情況,比較下兩個例子的不同:
```php
//示例1
echo my_class::A1;
class my_class {
const A1 = "hi";
}
```
```php
//示例2
class 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__ 或 __類名::$property__ 訪問;普通屬性通過 __$this->property__ 或 __$object->property__ 訪問。
```php
class my_class {
//普通屬性
public $property = 初始化值;
//靜態屬性
public static $property_2 = 初始化值;
}
```
與常量的存儲方式不同,成員屬性的 __初始化值__ 并不是 __直接__ 用以"屬性名"作為索引的哈希表存儲的,而是通過數組保存的,普通屬性、靜態屬性各有一個數組分別存儲。

看到這里可能有個疑問:使用時成員屬性是如果找到的呢?
實際只是成員屬性的 __VALUE__ 通過數組存儲的,訪問時仍然是根據以"屬性名"為索引的散列表查找具體VALUE的,這個散列表并沒有按照普通屬性、靜態屬性分為兩個,而是只用了一個:__HashTable properties_info__ 。此哈希表存儲元素的value類型為 __zend_property_info__ 。
```c
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及是否為靜態屬性
所以訪問成員屬性時首先是根據屬性名查找到此屬性的存儲位置,然后再進一步獲取屬性值。
舉個例子:
```php
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,因為其操作的作用域全部都是類的而不是對象的,而非靜態方法中可以通過$this訪問屬于本對象的成員屬性。
靜態方法也是通過static關鍵詞定義:
```php
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引擎執行過程》](zend_executor.md)已經詳細說過,這里不再重復。
#### 3.4.1.5 自定義類的編譯
前面我們先介紹了類的相關組成部分,接下來我們從一個例子簡單看下類的編譯過程,這個過程最終的產物就是zend_class_entry。
```php
//示例
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】 {}
語法規則為:
```c
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個子節點:繼承子節點、實現接口子節點、類中聲明表達式節點,其中child[2](即類中聲明表達式節點)為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節點,這個函數中再針對常量、屬性、方法、繼承、接口等分別處理。
```c
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的opcode
zend_compile_class_ref(&extends_node, extends_ast, 0);
}
//在當前父空間生成一條opcode
opline = 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; //有繼承的類為這個opcode
opline->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_pos
opline->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()`:
```c
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()`:
```c
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、protected
zend_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_ELEM
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_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 {
//默認值為null
ZVAL_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中。
```c
//zend_API.c
ZEND_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`:
```c
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()`。
```c
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__。
```c
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)。
```c
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()`處理了。
```c
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中取出那個key
op1 = 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_entry
if ((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再作說明。

```c
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_DELAYED
opline->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)的過程,這個相當于類的聲明在編譯階段提前"執行"了,也有可能因為父類找不到等原因延至運行時執行,清楚了這個過程你應該能明白下面這些例子為什么有的可以運行而有的則報錯的原因了吧?
```php
//情況1
new A();
class A extends B{}
class B{}
===================
完整opcodes:
1 ZEND_NEW => 執行到這報錯,因為此時A因為找不到B尚未編譯進EG(class_table)
2 ZEND_DO_FCALL
3 ZEND_FETCH_CLASS
4 ZEND_DECLARE_INHERITED_CLASS
5 ZEND_DECLARE_CLASS => 注冊class B
6 ZEND_RETURN
實際執行順序:5->1->2->3->4->6
```
```php
//情況2
class A extends B{}
class B{}
new A();
===================
完整opcodes:
1 ZEND_FETCH_CLASS
2 ZEND_DECLARE_INHERITED_CLASS => 注冊class A,此時已經可以找到B
3 ZEND_DECLARE_CLASS => 注冊class B
4 ZEND_NEW
5 ZEND_DO_FCALL
6 ZEND_RETURN
實際執行順序:3->1->2->4->5->6,執行到4時A都已經注冊,所以可以執行
```
```php
//情況3
class A extends B{}
class B extends C{}
class C{}
new A();
===================
完整opcodes:
1 ZEND_FETCH_CLASS => 找不到B,直接報錯
2 ZEND_DECLARE_INHERITED_CLASS
3 ZEND_FETCH_CLASS
4 ZEND_DECLARE_INHERITED_CLASS => 注冊class B,此時可以找到C,所以注冊成功
5 ZEND_DECLARE_CLASS => 注冊class C
6 ZEND_NEW
7 ZEND_DO_FCALL
8 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接口本節不再介紹,我們將在后面介紹擴展開發一章中再系統說明。
- 目錄
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 第3章 Zend虛擬機
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.2 函數實現
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 6.1 什么是線程安全
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.6 函數
- 7.7 zval的操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- 附錄1:break/continue按標簽中斷語法實現
- 附錄2:defer推遲函數調用語法的實現
- 8.1 概述