8.2.1 定义语法

命名空间通过关键字namespace 来声明,如果一个文件中包含命名空间,它必须在其它所有代码之前声明命名空间,除了declare关键字以外,也就是说除declare之外任何代码都不能在namespace之前声明。另外,命名空间并没有文件限制,可以在多个文件中声明同一个命名空间,也可以在同一文件中声明多个命名空间。

  1. namespace com\aa;
  2. const MY_CONST = 1234;
  3. function my_func(){ /* ... */ }
  4. class my_class { /* ... */ }

另外也可以通过{}将类、函数、常量封装在一个命名空间下:

  1. namespace com\aa{
  2. const MY_CONST = 1234;
  3. function my_func(){ /* ... */ }
  4. class my_class { /* ... */ }
  5. }

但是同一个文件中这两种定义方式不能混用,下面这样的定义将是非法的:

  1. namespace com\aa{
  2. /* ... */
  3. }
  4. namespace com\bb;
  5. /* ... */

如果没有定义任何命名空间,所有的类、函数和常量的定义都是在全局空间,与 PHP 引入命名空间概念前一样。

8.2.2 内部实现

命名空间的实现实际比较简单,当声明了一个命名空间后,接下来编译类、函数和常量时会把类名、函数名和常量名统一加上命名空间的名称作为前缀存储,也就是说声明在命名空间中的类、函数和常量的实际名称是被修改过的,这样来看他们与普通的定义方式是没有区别的,只是这个前缀是内核帮我们自动添加的,例如:

  1. //ns_define.php
  2. namespace com\aa;
  3. const MY_CONST = 1234;
  4. function my_func(){ /* ... */ }
  5. 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]为通过{}方式定义时包裹的语句。
ast_namespace.png

此节点的编译函数为zend_compile_namespace():

  1. void zend_compile_namespace(zend_ast *ast)
  2. {
  3. zend_ast *name_ast = ast->child[0];
  4. zend_ast *stmt_ast = ast->child[1];
  5. zend_string *name;
  6. zend_bool with_bracket = stmt_ast != NULL;
  7. //检查声明方式,不允许{}与非{}混用
  8. ...
  9. if (FC(current_namespace)) {
  10. zend_string_release(FC(current_namespace));
  11. }
  12. if (name_ast) {
  13. name = zend_ast_get_str(name_ast);
  14. if (ZEND_FETCH_CLASS_DEFAULT != zend_get_class_fetch_type(name)) {
  15. zend_error_noreturn(E_COMPILE_ERROR, "Cannot use '%s' as namespace name", ZSTR_VAL(name));
  16. }
  17. //将命名空间名称保存到FC(current_namespace)
  18. FC(current_namespace) = zend_string_copy(name);
  19. } else {
  20. FC(current_namespace) = NULL;
  21. }
  22. //重置use导入的命名空间符号表
  23. zend_reset_import_tables();
  24. ...
  25. if (stmt_ast) {
  26. //如果是通过namespace xxx { ... }这种方式声明的则直接编译{}中的语句
  27. zend_compile_top_stmt(stmt_ast);
  28. zend_end_namespace();
  29. }
  30. }

从上面的编译过程可以看出,命名空间定义的编译过程非常简单,最主要的操作是把FC(current_namespace)设置为当前定义的命名空间名称,FC()这个宏为:CG(file_context),前面曾介绍过,file_context是在编译过程中使用的一个结构:

  1. typedef struct _zend_file_context {
  2. zend_declarables declarables;
  3. znode implementing_class;
  4. //当前所属namespace
  5. zend_string *current_namespace;
  6. //是否在namespace中
  7. zend_bool in_namespace;
  8. //当前namespace是否为{}定义
  9. zend_bool has_bracketed_namespaces;
  10. //下面这三个值在后面介绍use时再说明,这里忽略即可
  11. HashTable *imports;
  12. HashTable *imports_function;
  13. HashTable *imports_const;
  14. } 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时完成的,下面以函数为例看下具体的处理:

  1. //函数的编译方法
  2. void zend_compile_func_decl(znode *result, zend_ast *ast)
  3. {
  4. ...
  5. //生成函数声明的opcode:ZEND_DECLARE_FUNCTION
  6. zend_begin_func_decl(result, op_array, decl);
  7. //编译参数、函数体
  8. ...
  9. }
  1. static void zend_begin_func_decl(znode *result, zend_op_array *op_array, zend_ast_decl *decl)
  2. {
  3. ...
  4. //获取函数名称
  5. op_array->function_name = name = zend_prefix_with_ns(unqualified_name);
  6. lcname = zend_string_tolower(name);
  7. if (FC(imports_function)) {
  8. //如果通过use导入了其他命名空间则检查函数名称是否已存在
  9. }
  10. ....
  11. //生成一条opcode:ZEND_DECLARE_FUNCTION
  12. opline = get_next_op(CG(active_op_array));
  13. opline->opcode = ZEND_DECLARE_FUNCTION;
  14. //函数名的存储位置记录在op2中
  15. opline->op2_type = IS_CONST;
  16. LITERAL_STR(opline->op2, zend_string_copy(lcname));
  17. ...
  18. }

函数名称通过zend_prefix_with_ns()方法获取:

  1. zend_string *zend_prefix_with_ns(zend_string *name) {
  2. if (FC(current_namespace)) {
  3. //如果当前是在namespace下则拼上namespace名称作为前缀
  4. zend_string *ns = FC(current_namespace);
  5. return zend_concat_names(ZSTR_VAL(ns), ZSTR_LEN(ns), ZSTR_VAL(name), ZSTR_LEN(name));
  6. } else {
  7. return zend_string_copy(name);
  8. }
  9. }

在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帮我们补全了类名、函数名、常量名。