PHP 5.3引入了命名空间,允许不同命名空间下定义同名函数、类,从而解决不同库之间名称冲突问题。
8.1 概述
PHP命名空间只能隔离类、函数、常量、接口,不包括全局变量。
8.2 命名空间的定义
命名空间通过关键字namespace来声明:
php
// 方式一
// file: ns_define.php
namespace com\aa;
const MY_CONST = 1234;
function my_func() { /* ... */ }
class my_class { /* ... */ }
// 方式二
// file: another_ns_define.php
namespace com\aa {
const MY_CONST = 1234;
function my_func() { /* ... */ }
class my_class { /* ... */ }
}
但同一文件中以上两种定义方式不能混用,只能出现一种。
如果一个文件中包含命名空间,那么命名空间必须在除了declare关键字外的所有代码前声明。可以在多个文件中声明一个命名空间,也可在一个文件中声明多个命名空间。
如果没有定义命名空间,则类、函数定义在全局空间。
命名空间的实现只是在名称上进行了补全,当声明一个命名空间后,接下来编译类、函数、常量时,会把其名字前加上命名空间作为前缀来存储。
namespace语法被编译为ZEND_AST_NAMESPACE类型的抽象语法树节点,它有两个子节点:child[0]为命名空间名称;child[1]为大括号定义方式时包裹的语句,如果不是大括号包裹的方式,则此子节点为空。
ZEND_AST_NAMESPACE节点由zend_compile_namespace()编译,其中会把FC(current_namespace)设为当前定义的命名空间名称,FC这个宏是CG(file_context),file_context是一个编译过程的辅助结构,其在编译抽象语法树前分配:
编译完namespace声明语句后,继续编译后面的语句,此后定义的函数、类、常量均属于此命名空间,直到遇到下一个namespace定义。
namespace中的编译过程:
1.类、函数的编译
正常类、函数的编译分两步:第一步生成一条ZEND_DECLARE_FUNCTION、ZEND_DECALRE_CLASS的opcode;第二步在整个脚本编译的最后执行zend_do_early_binding(),其中会执行第一步生成的opcode,正是这一步将类、函数分别注册到了EG(class_table)、EG(function_table)中。
生成ZEND_DECLARE_FUNCTION、ZEND_DECALRE_CLASS时会把函数名、类名的存储地址保存到操作数中,然后在zend_do_early_binding()中获取名字,并以名字作为key将其注册到EG(class_table)或EG(function_table)中。命名空间中定义的函数、类的名字加前缀的操作是在生成ZEND_DECLARE_FUNCTION、ZEND_DECALRE_CLASS时完成的。准确地说,名字加前缀是通过zend_prefix_with_ns()获取的,其中如果发现FC(current_namespace)非空,则将其名字前加上FC(current_namespace)作为前缀,接下来注册到EG(class_table)、EG(function_table)中的名字就是修改后的名字了。
2.常量的编译
同上,常量名通过zend_resolve_const_name函数获取,其中最终调用的也是zend_prefix_with_ns函数。
8.3 命名空间的使用
命名空间中的类、函数、常量使用时,可以直接在前面加上命名空间名称前缀来使用,如要使用前例ns_define.php文件中的MY_CONST:
php
include 'ns_define.php'
echo \com\aa\MY_CONST;
上例中以\
开头的名称为完全限定名称,此外还有其他形式的名称:
1.非限定名称:如my_func()。使用时如果当前脚本中声明了命名空间,则被解析为该命名空间中的名字;否则按原始名称解析。
2.部分限定名称:包含命名空间前缀,但不以\
开头,如aa\func()
。使用时如果当前脚本没有使用use导入namespace,那么解析规则同非限定名称;如果使用了use,下面介绍。
8.3.1 use导入
使用命名空间中的名字时,需要每一处都加上namespace前缀,使用use关键字可将一个命名空间中的名字导入,从而使用这些名字时不需再加namespace前缀,此外,use还可以使用as关键字给namespace起一个别名。
php
// ns_define.php
namespace aa\bb\cc\dd;
const MY_CONST = 1234;
我们可在脚本中使用以下方式访问MY_CONST:
php
// 方式一
include 'ns_define.php'
use aa\bb\cc\dd
echo dd\MY_CONST;
php
// 方式二
include 'ns_define.php'
use aa\bb\cc
echo cc\dd\MY_CONST;
php
// 方式三
include 'ns_define.php'
use aa\bb\cc\dd as DD
echo DD\MY_CONST;
php
// 方式四
include 'ns_define.php'
use aa\bb\cc as CC
echo CC\dd\MY_CONST;
实现原理:编译器如果发现use语句,就将use关键字后面的命名空间名插入哈希表FC(imports)。哈希表的key是别名,如果没有别名,则key为use关键字后面的命名空间名中最后一个\
后面的内容,如方式二使用cc作为key;哈希表的值就是use关键字后面的命名空间名。以方式二为例,访问cc\dd\MY_CONST
时,会以要访问的名字的第一个\
前的内容cc
为key查找FC(imports),从而得到aa\bb\cc
,然后拼成完整名字:
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();
这种直接导入类名字的原理同上。从PHP 5.6起,use可以导入函数、常量,语法为use function xxx
、use const xxx
,原理大致同上,区别在于使用的哈希表不是FC(imports),而是FC(imports_function)、FC(imports_const):
另外,如果使用了namespace关键字作为名字的前缀,则表示使用当前脚本的命名空间,而非use导入的:
php
// a.php
namespace aa;
const MY_CONST = 1000;
// b.php
namespace bb;
include 'a.php';
use aa;
const MY_CONST = 2000;
echo namespace\MY_CONST; // 输出2000,用的是当前脚本的命名空间bb
编译use语句时,会把use导入的名称以别名或最后分节为key存到对应哈希表中。
使用命名空间中的名字时,会经历名字补全的步骤,各个类型补全名字的过程:
1.类名补全
类名在编译时通过zend_resolve_class_name()进行类名补全。以下是具体补全规则:
(1)使用namespace关键字
如namespace\xxx\类名
,表示使用当前命名空间,此时zend_resolve_class_name函数中的zend_prefix_with_ns函数的处理如下:如果当前脚本定义了namespace(有FC(current_namespace)
),则将当前脚本的命名空间名替换namespace关键字。
(2)完全限定名称
此时不需补全。在zend_resolve_class_name函数中,以下两种情况被判定为是完全限定名称:一是类名以\
开头;二是类名的类型为ZEND_NAME_FQ。
php
// 在语法分析时将类名标识为ZEND_NAME_FQ
$obj = new \aa\my_class();
// 在抽象语法树编译时根据字符判断
// 但此方法我测试时无法通过编译,我使用的版本是PHP 8.0.7 (cli),可能是版本原因
$obj = new "\aa\my_class()";
(3)部分限定名称
如果当前脚本使用use导入了命名空间,且使用的是部分限定名称,则会以类名的第一节为key检索FC(imports),如果有这个key,将value取出,然后拼上类名,如图8-2。
(4)非限定名称
以类名为key查找FC(imports),如果有这个key,则拼上命名空间前缀。
2.函数名补全
函数名在编译时由zend_resolve_function_name()进行补全。补全时优先去FC(imports_function)中查找,如果没找到且是部分限定名称时,再去FC(imports)中查找。
以下是具体补全规则:
(1)使用namespace关键字
与类名处理一致。
(2)完全限定名称
不需要补全。
(3)非限定名称
即只有函数名,此时会以小写函数名为key查找FC(imports_function),如果找到了key,直接返回value,此value保存的是完整的"命名空间+函数名"。
(4)部分限定名称
如果FC(imports_function)中未找到对应的key,且为部分限定名称,则会去FC(imports)中查找,过程与类的相同。
3.常量名补全
与函数名补全类似,但非限定名称时,查找的是FC(imports_const)哈希表,且大小写敏感。
8.3.2 动态用法
类似以下的动态用法只能使用完全限定名称,无法自动补全名字:
php
$class_name = "\aa\bb\my_class";
$obj = new $class_name;