8.3.1 基本用法
上一节我们知道了定义在命名空间中的类、函数和常量只是加上了namespace名称作为前缀,既然是这样那么在使用时加上同样的前缀是否就可以了呢?答案是肯定的,比如上面那个例子:在com\aa命名空间下定义了一个常量MY_CONST,那么就可以这么使用:
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名称最后一个域或者别名访问,不需要使用完整的名称,比如:
//ns_define.phpnamespace aa\bb\cc\dd;const MY_CONST = 1234;
可以采用如下几种方式使用:
//方式1:include 'ns_define.php';use aa\bb\cc\dd;echo dd\MY_CONST;
//方式2:include 'ns_define.php';use aa\bb\cc;echo cc\dd\MY_CONST;
//方式3:include 'ns_define.php';use aa\bb\cc\dd as DD;echo DD\MY_CONST;
//方式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了,例如:
//ns_define.phpnamespace aa\bb\cc\dd;class my_class { /* ... */ }
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结构中的另外两个哈希表就是在这种情况下使用的:
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的情况处理
use aa\bb; //导入namespaceuse aa\bb\MY_CLASS; //导入类use function aa\bb\my_func; //导入函数use const aa\bb\MY_CONST; //导入常量
接下来看下内核的具体实现,首先看下use的编译:
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_namenew_name = zend_string_init(unqualified_name, unqualified_name_len, 0);} else {//名称中没有"\":use aanew_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_nameif (!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导入的类则把补全后的完整名称返回,然后再进行后续的处理。
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导入namespaceif (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导入了namespaceif (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_classreturn 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)补全函数名、常量名
函数与常量名称的补全操作是相同的:
//补全函数名称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));}
可以看到函数与常量最终调用同一方法处理,不同点在于传入了各自的存储哈希表:
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是动态语言,能否动态使用命名空间呢?举个例子:
$class_name = "\aa\bb\my_class";$obj = new $class_name;
如果类似这样的用法只能只用完全限定名称,也就是按照实际存储的名称使用,无法进行自动名称补全。
