### 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 aa\bb\my_func;`,編譯`my_func()`會在FC(imports_function)中根據"my_func"找到"aa\bb\my_func",從而使用完整的這個名稱。
- 前言
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.3.1 概述
- 1.3.2 基本實現
- 1.3.3 FPM的初始化
- 1.3.4 請求處理
- 1.3.5 進程管理
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 第3章 Zend虛擬機
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 3.2 函數實現
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 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.1 什么是線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.3.1 擴展的構成
- 7.3.2 編譯工具
- 7.3.3 編寫擴展的基本步驟
- 7.3.4 config.m4
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.5.1 全局變量
- 7.5.2 ini配置
- 7.6 函數
- 7.6.1 內部函數注冊
- 7.6.2 函數參數解析
- 7.6.3 引用傳參
- 7.6.4 函數返回值
- 7.6.5 函數調用
- 7.7 zval的操作
- 7.7.1 新生成各類型zval
- 7.7.2 獲取zval的值及類型
- 7.7.3 類型轉換
- 7.7.4 引用計數
- 7.7.5 字符串操作
- 7.7.6 數組操作
- 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.1 概述
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- break/continue按標簽中斷語法實現
- defer推遲函數調用語法的實現
- 一起線上事故引發的對PHP超時控制的思考