## 8.1 概述
什么是命名空間?從廣義上來說,命名空間是一種封裝事物的方法。在很多地方都可以見到這種抽象概念。例如,在操作系統中目錄用來將相關文件分組,對于目錄中的文件來說,它就扮演了命名空間的角色。具體舉個例子,文件 foo.txt 可以同時在目錄/home/greg 和 /home/other 中存在,但在同一個目錄中不能存在兩個 foo.txt 文件。另外,在目錄 /home/greg 外訪問 foo.txt 文件時,我們必須將目錄名以及目錄分隔符放在文件名之前得到 /home/greg/foo.txt。這個原理應用到程序設計領域就是命名空間的概念。(引用自php.net)
命名空間主要用來解決兩類問題:
* 用戶編寫的代碼與PHP內部的或第三方的類、函數、常量、接口名字沖突
* 為很長的標識符名稱創建一個別名的名稱,提高源代碼的可讀性
PHP命名空間提供了一種將相關的類、函數、常量和接口組合到一起的途徑,不同命名空間的類、函數、常量、接口相互隔離不會沖突,注意:PHP命名空間只能隔離類、函數、常量和接口,不包括全局變量。
接下來的兩節將介紹下PHP命名空間的內部實現,主要從命名空間的定義及使用兩個方面分析。
## 8.2 命名空間的定義
### 8.2.1 定義語法
命名空間通過關鍵字namespace 來聲明,如果一個文件中包含命名空間,它必須在其它所有代碼之前聲明命名空間,除了declare關鍵字以外,也就是說除declare之外任何代碼都不能在namespace之前聲明。另外,命名空間并沒有文件限制,可以在多個文件中聲明同一個命名空間,也可以在同一文件中聲明多個命名空間。
```php
namespace com\aa;
const MY_CONST = 1234;
function my_func(){ /* ... */ }
class my_class { /* ... */ }
```
另外也可以通過{}將類、函數、常量封裝在一個命名空間下:
```php
namespace com\aa{
const MY_CONST = 1234;
function my_func(){ /* ... */ }
class my_class { /* ... */ }
}
```
但是同一個文件中這兩種定義方式不能混用,下面這樣的定義將是非法的:
```php
namespace com\aa{
/* ... */
}
namespace com\bb;
/* ... */
```
如果沒有定義任何命名空間,所有的類、函數和常量的定義都是在全局空間,與 PHP 引入命名空間概念前一樣。
### 8.2.2 內部實現
命名空間的實現實際比較簡單,當聲明了一個命名空間后,接下來編譯類、函數和常量時會把類名、函數名和常量名統一加上命名空間的名稱作為前綴存儲,也就是說聲明在命名空間中的類、函數和常量的實際名稱是被修改過的,這樣來看他們與普通的定義方式是沒有區別的,只是這個前綴是內核幫我們自動添加的,例如:
```php
//ns_define.php
namespace com\aa;
const MY_CONST = 1234;
function my_func(){ /* ... */ }
class my_class { /* ... */ }
```
最終MY_CONST、my_func、my_class在EG(zend_constants)、EG(function_table)、EG(class_table)中的實際存儲名稱被修改為:com\aa\MY_CONST、com\aa\my_func、com\aa\my_class。
下面具體看下編譯過程,namespace語法被編譯為ZEND_AST_NAMESPACE類型的語法樹節點,它有兩個子節點:child[0]為命名空間的名稱、child[1]為通過{}方式定義時包裹的語句。

此節點的編譯函數為zend_compile_namespace():
```c
void zend_compile_namespace(zend_ast *ast)
{
zend_ast *name_ast = ast->child[0];
zend_ast *stmt_ast = ast->child[1];
zend_string *name;
zend_bool with_bracket = stmt_ast != NULL;
//檢查聲明方式,不允許{}與非{}混用
...
if (FC(current_namespace)) {
zend_string_release(FC(current_namespace));
}
if (name_ast) {
name = zend_ast_get_str(name_ast);
if (ZEND_FETCH_CLASS_DEFAULT != zend_get_class_fetch_type(name)) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use '%s' as namespace name", ZSTR_VAL(name));
}
//將命名空間名稱保存到FC(current_namespace)
FC(current_namespace) = zend_string_copy(name);
} else {
FC(current_namespace) = NULL;
}
//重置use導入的命名空間符號表
zend_reset_import_tables();
...
if (stmt_ast) {
//如果是通過namespace xxx { ... }這種方式聲明的則直接編譯{}中的語句
zend_compile_top_stmt(stmt_ast);
zend_end_namespace();
}
}
```
從上面的編譯過程可以看出,命名空間定義的編譯過程非常簡單,最主要的操作是把FC(current_namespace)設置為當前定義的命名空間名稱,FC()這個宏為:CG(file_context),前面曾介紹過,file_context是在編譯過程中使用的一個結構:
```c
typedef struct _zend_file_context {
zend_declarables declarables;
znode implementing_class;
//當前所屬namespace
zend_string *current_namespace;
//是否在namespace中
zend_bool in_namespace;
//當前namespace是否為{}定義
zend_bool has_bracketed_namespaces;
//下面這三個值在后面介紹use時再說明,這里忽略即可
HashTable *imports;
HashTable *imports_function;
HashTable *imports_const;
} zend_file_context;
```
編譯完namespace聲明語句后接著編譯下面的語句,此后定義的類、函數、常量均屬于此命名空間,直到遇到下一個namespace的定義,接下來繼續分析下這三種類型編譯過程中有何不同之處。
__(1)編譯類、函數__
前面章節曾詳細介紹過函數、類的編譯過程,總結下主要分為兩步:第1步是編譯函數、類,這個過程將分別生成一條ZEND_DECLARE_FUNCTION、ZEND_DECLARE_CLASS的opcode;第2步是在整個腳本編譯的最后執行zend_do_early_binding(),這一步相當于執行ZEND_DECLARE_FUNCTION、ZEND_DECLARE_CLASS,函數、類正是在這一步注冊到EG(function_table)、EG(class_table)中去的。
在生成ZEND_DECLARE_FUNCTION、ZEND_DECLARE_CLASS兩條opcode時會把函數名、類名的存儲位置通過操作數記錄下來,然后在zend_do_early_binding()階段直接獲取函數名、類名作為key注冊到EG(function_table)、EG(class_table)中,定義在命名空間中的函數、類的名稱修改正是在生成ZEND_DECLARE_FUNCTION、ZEND_DECLARE_CLASS時完成的,下面以函數為例看下具體的處理:
```c
//函數的編譯方法
void zend_compile_func_decl(znode *result, zend_ast *ast)
{
...
//生成函數聲明的opcode:ZEND_DECLARE_FUNCTION
zend_begin_func_decl(result, op_array, decl);
//編譯參數、函數體
...
}
```
```c
static void zend_begin_func_decl(znode *result, zend_op_array *op_array, zend_ast_decl *decl)
{
...
//獲取函數名稱
op_array->function_name = name = zend_prefix_with_ns(unqualified_name);
lcname = zend_string_tolower(name);
if (FC(imports_function)) {
//如果通過use導入了其他命名空間則檢查函數名稱是否已存在
}
....
//生成一條opcode:ZEND_DECLARE_FUNCTION
opline = get_next_op(CG(active_op_array));
opline->opcode = ZEND_DECLARE_FUNCTION;
//函數名的存儲位置記錄在op2中
opline->op2_type = IS_CONST;
LITERAL_STR(opline->op2, zend_string_copy(lcname));
...
}
```
函數名稱通過zend_prefix_with_ns()方法獲取:
```c
zend_string *zend_prefix_with_ns(zend_string *name) {
if (FC(current_namespace)) {
//如果當前是在namespace下則拼上namespace名稱作為前綴
zend_string *ns = FC(current_namespace);
return zend_concat_names(ZSTR_VAL(ns), ZSTR_LEN(ns), ZSTR_VAL(name), ZSTR_LEN(name));
} else {
return zend_string_copy(name);
}
}
```
在zend_prefix_with_ns()方法中如果發現FC(current_namespace)不為空則將函數名加上FC(current_namespace)作為前綴,接下來向EG(function_table)注冊時就使用修改后的函數名作為key,類的情況與函數的處理方式相同,不再贅述。
__(2)編譯常量__
常量的編譯過程與函數、類基本相同,也是在編譯過程獲取常量名時檢查FC(current_namespace)是否為空,如果不為空表示常量聲明在namespace下,則為常量名加上FC(current_namespace)前綴。
總結下命名空間的定義:編譯時如果發現定義了一個namespace,則將命名空間名稱保存到FC(current_namespace),編譯類、函數、常量時先判斷FC(current_namespace)是否為空,如果為空則按正常名稱編譯,如果不為空則將類名、函數名、常量名加上FC(current_namespace)作為前綴,然后再以修改后的名稱注冊。整個過程相當于PHP幫我們補全了類名、函數名、常量名。
## 8.3 命名空間的使用
### 8.3.1 基本用法
上一節我們知道了定義在命名空間中的類、函數和常量只是加上了namespace名稱作為前綴,既然是這樣那么在使用時加上同樣的前綴是否就可以了呢?答案是肯定的,比如上面那個例子:在com\aa命名空間下定義了一個常量MY_CONST,那么就可以這么使用:
```php
include 'ns_define.php';
echo \com\aa\MY_CONST;
```
這種按照實際類名、函數名、常量名使用的方式很容易理解,與普通的類型沒有差別,這種以"\"開頭使用的名稱稱之為:完全限定名稱,類似于絕對目錄的概念,使用這種名稱PHP會直接根據"\"之后的名稱去對應的符號表中查找(namespace定義時前面是沒有加"\"的,所以查找時也會去掉這個字符)。
除了這種形式的名稱之外,還有兩種形式的名稱:
* __非限定名稱:__ 即沒有加任何namespace前綴的普通名稱,比如my_func(),使用這種名稱時如果當前有命名空間則會被解析為:currentnamespace\my_func,如果當前沒有命名空間則按照原始名稱my_func解析
* __部分限定名稱:__ 即包含namespace前綴,但不是以"\"開始的,比如:aa\my_func(),類似相對路徑的概念,這種名稱解析規則比較復雜,如果當前空間沒有使用use導入任何namespace那么與非限定名稱的解析規則相同,即如果當前有命名空間則會把解析為:currentnamespace\aa\my_func,否則解析為aa\my_func,使用use的情況后面再作說明
### 8.3.2 use導入
使用一個命名空間中的類、函數、常量雖然可以通過完全限定名稱的形式訪問,但是這種方式需要在每一處使用的地方都加上完整的namespace名稱,如果將來namespace名稱變更了就需要所有使用的地方都改一遍,這將是很痛苦的一件事,為此,PHP提供了一種命名空間導入/別名的機制,可以通過use關鍵字將一個命名空間導入或者定義一個別名,然后在使用時就可以通過導入的namespace名稱最后一個域或者別名訪問,不需要使用完整的名稱,比如:
```php
//ns_define.php
namespace aa\bb\cc\dd;
const MY_CONST = 1234;
```
可以采用如下幾種方式使用:
```php
//方式1:
include 'ns_define.php';
use aa\bb\cc\dd;
echo dd\MY_CONST;
```
```php
//方式2:
include 'ns_define.php';
use aa\bb\cc;
echo cc\dd\MY_CONST;
```
```php
//方式3:
include 'ns_define.php';
use aa\bb\cc\dd as DD;
echo DD\MY_CONST;
```
```php
//方式4:
include 'ns_define.php';
use aa\bb\cc as CC;
echo CC\dd\MY_CONST;
```
這種機制的實現原理也比較簡單:編譯期間如果發現use語句 ,那么就將把這個use后的命名空間名稱插入一個哈希表:FC(imports),而哈希表的key就是定義的別名,如果沒有定義別名則key使用按"\"分割的最后一節,比如方式2的情況將以cc作為key,即:FC(imports)["cc"] = "aa\bb\cc\dd";接下來在使用類、函數和常量時會把名稱按"\"分割,然后以第一節為key查找FC(imports),如果找到了則將FC(imports)中保存的名稱與使用時的名稱拼接在一起,組成完整的名稱。實際上這種機制是把完整的名稱切割縮短然后緩存下來,使用時再拼接成完整的名稱,也就是內核幫我們組裝了名稱,對內核而言,最終使用的都是包括完整namespace的名稱。

use除了上面介紹的用法外還可以導入一個類,導入后再使用類就不需要加namespace了,例如:
```php
//ns_define.php
namespace aa\bb\cc\dd;
class my_class { /* ... */ }
```
```php
include 'ns_define.php';
//導入一個類
use aa\bb\cc\dd\my_class;
//直接使用
$obj = new my_class();
var_dump($obj);
```
use的這兩種用法實現原理是一樣的,都是在編譯時通過查找FC(imports)實現的名稱補全。從PHP 5.6起,use又提供了兩種針對函數、常量的導入,可以通過`use function xxx`及`use const xxx`導入一個函數、常量,這種用法的實現原理與上面介紹的實際是相同,只是在編譯時沒有保存到FC(imports),zend_file_context結構中的另外兩個哈希表就是在這種情況下使用的:
```c
typedef struct _zend_file_context {
...
//用于保存導入的類或命名空間
HashTable *imports;
//用于保存導入的函數
HashTable *imports_function;
//用于保存導入的常量
HashTable *imports_const;
} zend_file_context;
```
簡單總結下use的幾種不同用法:
* __a.導入命名空間:__ 導入的名稱保存在FC(imports)中,編譯使用的語句時搜索此符號表進行補全
* __b.導入類:__ 導入的名稱保存在FC(imports)中,與a不同的是不會根據"\"切割后的最后一節檢索,而是直接使用類名查找
* __c.導入函數:__ 通過`use function`導入到FC(imports_function),補全時先查找FC(imports_function),如果沒有找到則繼續按照a的情況處理
* __d.導入常量:__ 通過`use const`導入到FC(imports_const),補全時先查找FC(imports_const),如果沒有找到則繼續按照a的情況處理
```php
use aa\bb; //導入namespace
use aa\bb\MY_CLASS; //導入類
use function aa\bb\my_func; //導入函數
use const aa\bb\MY_CONST; //導入常量
```
接下來看下內核的具體實現,首先看下use的編譯:
```c
void zend_compile_use(zend_ast *ast)
{
zend_string *current_ns = FC(current_namespace);
//use的類型
uint32_t type = ast->attr;
//根據類型獲取存儲哈希表:FC(imports)、FC(imports_function)、FC(imports_const)
HashTable *current_import = zend_get_import_ht(type);
...
//use可以同時導入多個
for (i = 0; i < list->children; ++i) {
zend_ast *use_ast = list->child[i];
zend_ast *old_name_ast = use_ast->child[0];
zend_ast *new_name_ast = use_ast->child[1];
//old_name為use后的namespace名稱,new_name為as定義的別名
zend_string *old_name = zend_ast_get_str(old_name_ast);
zend_string *new_name, *lookup_name;
if (new_name_ast) {
//如果有as別名則直接使用
new_name = zend_string_copy(zend_ast_get_str(new_name_ast));
} else {
const char *unqualified_name;
size_t unqualified_name_len;
if (zend_get_unqualified_name(old_name, &unqualified_name, &unqualified_name_len)) {
//按"\"分割,取最后一節為new_name
new_name = zend_string_init(unqualified_name, unqualified_name_len, 0);
} else {
//名稱中沒有"\":use aa
new_name = zend_string_copy(old_name);
}
}
//如果是use const則大小寫敏感,其它用法都轉為小寫
if (case_sensitive) {
lookup_name = zend_string_copy(new_name);
} else {
lookup_name = zend_string_tolower(new_name);
}
...
if (current_ns) {
//如果當前是在命名空間中則需要檢查名稱是否沖突
...
}
//插入FC(imports/imports_function/imports_const),key為lookup_name,value為old_name
if (!zend_hash_add_ptr(current_import, lookup_name, old_name)) {
...
}
}
}
```
從use的編譯過程可以看到,編譯時的主要處理是把use導入的名稱以別名或最后分節為key存儲到對應的哈希表中,接下來我們看下在編譯使用類、函數、常量的語句時是如何處理的。使用的語法類型比較多,比如類的使用就有new、訪問靜態屬性、調用靜態方法等,但是不管什么語句都會經歷獲取類名、函數名、常量名這一步,類名的補全就是在這一步完成的。
__(1)補全類名__
編譯時通過zend_resolve_class_name()方法進行類名補全,如果沒有任何namespace那么就返回原始的類名,比如編譯`new my_class()`時,首先會把"my_class"傳入該函數,如果查找FC(imports)后發現是一個use導入的類則把補全后的完整名稱返回,然后再進行后續的處理。
```c
zend_string *zend_resolve_class_name(zend_string *name, uint32_t type)
{
char *compound;
//"namespace\xxx\類名"這種用法表示使用當前命名空間
if (type == ZEND_NAME_RELATIVE) {
return zend_prefix_with_ns(name);
}
//完全限定的形式:new \aa\bb\my_class()
if (type == ZEND_NAME_FQ || ZSTR_VAL(name)[0] == '\\') {
if (ZSTR_VAL(name)[0] == '\\') {
name = zend_string_init(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1, 0);
} else {
zend_string_addref(name);
}
...
return name;
}
//如果當前腳本有通過use導入namespace
if (FC(imports)) {
compound = memchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name));
if (compound) {
// 1) 沒有直接導入一個類的情況,用法a
//名稱中包括"\",比如:new aa\bb\my_class()
size_t len = compound - ZSTR_VAL(name);
//根據按"\"分割后的最后一節為key查找FC(imports)
zend_string *import_name =
zend_hash_find_ptr_lc(FC(imports), ZSTR_VAL(name), len);
//如果找到了表示通過use導入了namespace
if (import_name) {
return zend_concat_names(
ZSTR_VAL(import_name), ZSTR_LEN(import_name), ZSTR_VAL(name) + len + 1, ZSTR_LEN(name) - len - 1);
}
} else {
// 2) 通過use導入一個類的情況,用法b
//直接根據原始類名查找
zend_string *import_name
= zend_hash_find_ptr_lc(FC(imports), ZSTR_VAL(name), ZSTR_LEN(name));
if (import_name) {
return zend_string_copy(import_name);
}
}
}
//沒有使用use或沒命中任何use導入的namespace,按照基本用法處理:如果當前在一個namespace下則解釋為currentnamespace\my_class
return zend_prefix_with_ns(name);
}
```
此方法除了類的名稱后還有一個type參數,這個參數是解析語法是根據使用方式確定的,共有三種類型:
* __ZEND_NAME_NOT_FQ:__ 非限定名稱,也就是普通的類名,沒有加namespace,比如:new my_class()
* __ZEND_NAME_RELATIVE:__ 相對名稱,強制按照當前所屬命名空間解析,使用時通過在類前加"namespace\xx",比如:new namespace\my_class(),如果當前是全局空間則等價于:new my_class,如果當前命名空間為currentnamespace,則解析為"currentnamespace\my_class"
* __ZEND_NAME_FQ:__ 完全限定名稱,即以"\"開頭的
__(2)補全函數名、常量名__
函數與常量名稱的補全操作是相同的:
```c
//補全函數名稱
zend_string *zend_resolve_function_name(zend_string *name, uint32_t type, zend_bool *is_fully_qualified)
{
return zend_resolve_non_class_name(
name, type, is_fully_qualified, 0, FC(imports_function));
}
//補全常量名稱
zend_string *zend_resolve_const_name(zend_string *name, uint32_t type, zend_bool *is_fully_qualified)
return zend_resolve_non_class_name(
name, type, is_fully_qualified, 1, FC(imports_const));
}
```
可以看到函數與常量最終調用同一方法處理,不同點在于傳入了各自的存儲哈希表:
```c
zend_string *zend_resolve_non_class_name(
zend_string *name, uint32_t type, zend_bool *is_fully_qualified,
zend_bool case_sensitive, HashTable *current_import_sub
) {
char *compound;
*is_fully_qualified = 0;
//完整名稱,直接返回,不需要補全
if (ZSTR_VAL(name)[0] == '\\') {
*is_fully_qualified = 1;
return zend_string_init(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1, 0);
}
//與類的用法相同
if (type == ZEND_NAME_RELATIVE) {
*is_fully_qualified = 1;
return zend_prefix_with_ns(name);
}
//current_import_sub如果是函數則為FC(imports_function),否則為FC(imports_const)
if (current_import_sub) {
//查找FC(imports_function)或FC(imports_const)
...
}
//查找FC(imports)
compound = memchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name));
...
return zend_prefix_with_ns(name);
}
```
可以看到,函數與常量的的補全邏輯只是優先用原始名稱去FC(imports_function)或FC(imports_const)查找,如果沒有找到再去FC(imports)中匹配。如果我們這樣導入了一個函數:`use function aa\bb\my_func;`,編譯`my_func()`會在FC(imports_function)中根據"my_func"找到"aa\bb\my_func",從而使用完整的這個名稱。
### 8.3.3 動態用法
前面介紹的這些命名空間的使用都是名稱為CONST類型的情況,所有的處理都是在編譯環節完成的,PHP是動態語言,能否動態使用命名空間呢?舉個例子:
```php
$class_name = "\aa\bb\my_class";
$obj = new $class_name;
```
如果類似這樣的用法只能只用完全限定名稱,也就是按照實際存儲的名稱使用,無法進行自動名稱補全。
- 目錄
- 第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 概述