8.3.1 基本用法

上一节我们知道了定义在命名空间中的类、函数和常量只是加上了namespace名称作为前缀,既然是这样那么在使用时加上同样的前缀是否就可以了呢?答案是肯定的,比如上面那个例子:在com\aa命名空间下定义了一个常量MY_CONST,那么就可以这么使用:

  1. include 'ns_define.php';
  2. 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名称最后一个域或者别名访问,不需要使用完整的名称,比如:

  1. //ns_define.php
  2. namespace aa\bb\cc\dd;
  3. const MY_CONST = 1234;

可以采用如下几种方式使用:

  1. //方式1:
  2. include 'ns_define.php';
  3. use aa\bb\cc\dd;
  4. echo dd\MY_CONST;
  1. //方式2:
  2. include 'ns_define.php';
  3. use aa\bb\cc;
  4. echo cc\dd\MY_CONST;
  1. //方式3:
  2. include 'ns_define.php';
  3. use aa\bb\cc\dd as DD;
  4. echo DD\MY_CONST;
  1. //方式4:
  2. include 'ns_define.php';
  3. use aa\bb\cc as CC;
  4. 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的名称。
namespace_com.png

use除了上面介绍的用法外还可以导入一个类,导入后再使用类就不需要加namespace了,例如:

  1. //ns_define.php
  2. namespace aa\bb\cc\dd;
  3. class my_class { /* ... */ }
  1. include 'ns_define.php';
  2. //导入一个类
  3. use aa\bb\cc\dd\my_class;
  4. //直接使用
  5. $obj = new my_class();
  6. var_dump($obj);

use的这两种用法实现原理是一样的,都是在编译时通过查找FC(imports)实现的名称补全。从PHP 5.6起,use又提供了两种针对函数、常量的导入,可以通过use function xxxuse const xxx导入一个函数、常量,这种用法的实现原理与上面介绍的实际是相同,只是在编译时没有保存到FC(imports),zend_file_context结构中的另外两个哈希表就是在这种情况下使用的:

  1. typedef struct _zend_file_context {
  2. ...
  3. //用于保存导入的类或命名空间
  4. HashTable *imports;
  5. //用于保存导入的函数
  6. HashTable *imports_function;
  7. //用于保存导入的常量
  8. HashTable *imports_const;
  9. } 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的情况处理
  1. use aa\bb; //导入namespace
  2. use aa\bb\MY_CLASS; //导入类
  3. use function aa\bb\my_func; //导入函数
  4. use const aa\bb\MY_CONST; //导入常量

接下来看下内核的具体实现,首先看下use的编译:

  1. void zend_compile_use(zend_ast *ast)
  2. {
  3. zend_string *current_ns = FC(current_namespace);
  4. //use的类型
  5. uint32_t type = ast->attr;
  6. //根据类型获取存储哈希表:FC(imports)、FC(imports_function)、FC(imports_const)
  7. HashTable *current_import = zend_get_import_ht(type);
  8. ...
  9. //use可以同时导入多个
  10. for (i = 0; i < list->children; ++i) {
  11. zend_ast *use_ast = list->child[i];
  12. zend_ast *old_name_ast = use_ast->child[0];
  13. zend_ast *new_name_ast = use_ast->child[1];
  14. //old_name为use后的namespace名称,new_name为as定义的别名
  15. zend_string *old_name = zend_ast_get_str(old_name_ast);
  16. zend_string *new_name, *lookup_name;
  17. if (new_name_ast) {
  18. //如果有as别名则直接使用
  19. new_name = zend_string_copy(zend_ast_get_str(new_name_ast));
  20. } else {
  21. const char *unqualified_name;
  22. size_t unqualified_name_len;
  23. if (zend_get_unqualified_name(old_name, &unqualified_name, &unqualified_name_len)) {
  24. //按"\"分割,取最后一节为new_name
  25. new_name = zend_string_init(unqualified_name, unqualified_name_len, 0);
  26. } else {
  27. //名称中没有"\":use aa
  28. new_name = zend_string_copy(old_name);
  29. }
  30. }
  31. //如果是use const则大小写敏感,其它用法都转为小写
  32. if (case_sensitive) {
  33. lookup_name = zend_string_copy(new_name);
  34. } else {
  35. lookup_name = zend_string_tolower(new_name);
  36. }
  37. ...
  38. if (current_ns) {
  39. //如果当前是在命名空间中则需要检查名称是否冲突
  40. ...
  41. }
  42. //插入FC(imports/imports_function/imports_const),key为lookup_name,value为old_name
  43. if (!zend_hash_add_ptr(current_import, lookup_name, old_name)) {
  44. ...
  45. }
  46. }
  47. }

从use的编译过程可以看到,编译时的主要处理是把use导入的名称以别名或最后分节为key存储到对应的哈希表中,接下来我们看下在编译使用类、函数、常量的语句时是如何处理的。使用的语法类型比较多,比如类的使用就有new、访问静态属性、调用静态方法等,但是不管什么语句都会经历获取类名、函数名、常量名这一步,类名的补全就是在这一步完成的。

(1)补全类名

编译时通过zend_resolve_class_name()方法进行类名补全,如果没有任何namespace那么就返回原始的类名,比如编译new my_class()时,首先会把”my_class”传入该函数,如果查找FC(imports)后发现是一个use导入的类则把补全后的完整名称返回,然后再进行后续的处理。

  1. zend_string *zend_resolve_class_name(zend_string *name, uint32_t type)
  2. {
  3. char *compound;
  4. //"namespace\xxx\类名"这种用法表示使用当前命名空间
  5. if (type == ZEND_NAME_RELATIVE) {
  6. return zend_prefix_with_ns(name);
  7. }
  8. //完全限定的形式:new \aa\bb\my_class()
  9. if (type == ZEND_NAME_FQ || ZSTR_VAL(name)[0] == '\\') {
  10. if (ZSTR_VAL(name)[0] == '\\') {
  11. name = zend_string_init(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1, 0);
  12. } else {
  13. zend_string_addref(name);
  14. }
  15. ...
  16. return name;
  17. }
  18. //如果当前脚本有通过use导入namespace
  19. if (FC(imports)) {
  20. compound = memchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name));
  21. if (compound) {
  22. // 1) 没有直接导入一个类的情况,用法a
  23. //名称中包括"\",比如:new aa\bb\my_class()
  24. size_t len = compound - ZSTR_VAL(name);
  25. //根据按"\"分割后的最后一节为key查找FC(imports)
  26. zend_string *import_name =
  27. zend_hash_find_ptr_lc(FC(imports), ZSTR_VAL(name), len);
  28. //如果找到了表示通过use导入了namespace
  29. if (import_name) {
  30. return zend_concat_names(
  31. ZSTR_VAL(import_name), ZSTR_LEN(import_name), ZSTR_VAL(name) + len + 1, ZSTR_LEN(name) - len - 1);
  32. }
  33. } else {
  34. // 2) 通过use导入一个类的情况,用法b
  35. //直接根据原始类名查找
  36. zend_string *import_name
  37. = zend_hash_find_ptr_lc(FC(imports), ZSTR_VAL(name), ZSTR_LEN(name));
  38. if (import_name) {
  39. return zend_string_copy(import_name);
  40. }
  41. }
  42. }
  43. //没有使用use或没命中任何use导入的namespace,按照基本用法处理:如果当前在一个namespace下则解释为currentnamespace\my_class
  44. return zend_prefix_with_ns(name);
  45. }

此方法除了类的名称后还有一个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)补全函数名、常量名

函数与常量名称的补全操作是相同的:

  1. //补全函数名称
  2. zend_string *zend_resolve_function_name(zend_string *name, uint32_t type, zend_bool *is_fully_qualified)
  3. {
  4. return zend_resolve_non_class_name(
  5. name, type, is_fully_qualified, 0, FC(imports_function));
  6. }
  7. //补全常量名称
  8. zend_string *zend_resolve_const_name(zend_string *name, uint32_t type, zend_bool *is_fully_qualified)
  9. return zend_resolve_non_class_name(
  10. name, type, is_fully_qualified, 1, FC(imports_const));
  11. }

可以看到函数与常量最终调用同一方法处理,不同点在于传入了各自的存储哈希表:

  1. zend_string *zend_resolve_non_class_name(
  2. zend_string *name, uint32_t type, zend_bool *is_fully_qualified,
  3. zend_bool case_sensitive, HashTable *current_import_sub
  4. ) {
  5. char *compound;
  6. *is_fully_qualified = 0;
  7. //完整名称,直接返回,不需要补全
  8. if (ZSTR_VAL(name)[0] == '\\') {
  9. *is_fully_qualified = 1;
  10. return zend_string_init(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1, 0);
  11. }
  12. //与类的用法相同
  13. if (type == ZEND_NAME_RELATIVE) {
  14. *is_fully_qualified = 1;
  15. return zend_prefix_with_ns(name);
  16. }
  17. //current_import_sub如果是函数则为FC(imports_function),否则为FC(imports_const)
  18. if (current_import_sub) {
  19. //查找FC(imports_function)或FC(imports_const)
  20. ...
  21. }
  22. //查找FC(imports)
  23. compound = memchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name));
  24. ...
  25. return zend_prefix_with_ns(name);
  26. }

可以看到,函数与常量的的补全逻辑只是优先用原始名称去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是动态语言,能否动态使用命名空间呢?举个例子:

  1. $class_name = "\aa\bb\my_class";
  2. $obj = new $class_name;

如果类似这样的用法只能只用完全限定名称,也就是按照实际存储的名称使用,无法进行自动名称补全。