简介
为了避免代码中自定义的类、函数、常量等标识符,与PHP内置或第三方库中的已有标识符重名导致冲突,PHP从5.3版本开始支持命名空间,旨在给原本同名的标识符加上命名空间前缀,从而在代码中区分它们。
命名空间实际上是解决上述问题的一种思想和方案,许多高级语言都提供了不同形式的支持。接下来,本文从简入手,介绍PHP命名空间的基础。
定义命名空间
定义命名空间的最简单形式,是在PHP代码开头,通过namespace关键字声明,指定该文件中自定义的类、函数、常量标识符均属于声明的命名空间之下。例如:
<?php
namespace habon;
// todo: code
注意:
1、如上文所述,PHP是从5.3开始支持命名空间的,按规范来说,此后的所有代码都应该声明所属命名空间。那么,5.3之前的历史代码咋办?而5.3之后还是没按照规范声明命名空间的代码咋办?为此,PHP提供了默认的命名空间来处理这种情况。这个缺省的命名空间就是全局命名空间。也就是说,如果PHP代码中没有声明命名空间,则该代码文件内自定义的类、函数、常量等标识符均属于全局命名空间。
2、上述所说的函数是指不属于任何对象、类的独立函数。
3、上述形式下定义命名空间,namespace声明必须写在PHP代码最开头,前面不能有别的代码(除了用于指定PHP解释模式的declare声明)。
当然,我们可以在同一个PHP代码文件中定义多个命名空间。例如:
<?php
namespace TestNS1;
const TEST_CONST = 1;
function testFun(){/* todo: code */}
class TestClass{/* todo: code */}
namespace TestNS2;
const TEST_CONST = 2;
function testFun(){/* todo: code */}
class TestClass{/* todo: code */}
为了更清晰界定各个命名空间的范围,采用如下形式声明比较合适:
<?php
namespace TestNS1{
const TEST_CONST = 1;
function testFun(){/* todo: code */}
class TestClass{/* todo: code */}
}
namespace TestNS2{
const TEST_CONST = 2;
function testFun(){/* todo: code */}
class TestClass{/* todo: code */}
}
如果同一个PHP代码文件中,同时存在属于全局与自定义命名空间的标识符,则必须使大括号的形式声明各命名空间,且全局者采用不带命名空间名称的namespace关键字声明。例如:
<?php
namespace TestNS{
const TEST_CONST = 1;
function testFun(){/* todo: code */}
class TestClass{/* todo: code */}
}
namespace{
const TEST_CONST = 1;
function testFun(){/* todo: code */}
class TestClass{/* todo: code */}
}
上文例子中的命名空间名称均为简单的标识符,实际上,命名空间的名称也支持分层次路径的形式,例如:
<?php
namespace TestNS\GodOfJiong\habon;
// todo: code
注意:实际上,所有自定义的命名空间均可视为全局命名空间下的子命名空间。因此,上文中声明过的TestNS与\TestNS等价,TestNS\GodOfJiong\habon与\TestNS\GodOfJiong\habon等价。由此也看出,全局命名空间“\”与自定义命名空间的“TestNS\GodOfJiong\habon”,和操作系统文件路径的形式是类似的。
使用命名空间
定义了命名空间之后,我们就可以使用了。使用方法有两大路线,首先介绍最直接简单粗暴的前缀方式,其进一步分为部分限定前缀与完全限定前缀两种。鄙人直接通过代码示例解释:
代码test_ns1.php:
<?php
namespace TestNS\GodOfJiong\habon;
const TEST_CONST = 1;
function testFun(): void
{
echo "testFun called\n";
}
class TestClass
{
public static function f(): void
{
echo "f called\n";
}
}
代码test_ns2.php:
<?php
namespace TestNS\GodOfJiong;
include 'test_ns1.php';
const TEST_CONST = 2;
function testFun(): void
{
echo "hello world\n";
}
class TestClass
{
public static function f(): void
{
echo "PHP is the best!\n";
}
}
echo TEST_CONST . "\n"; // 使用了空的部分限定前缀,标识符TEST_CONST前面拼接本代码文件所属命名空间TestNS\GodOfJiong,解析为:TestNS\GodOfJiong\TEST_CONST。
testFun(); // 使用了空的部分限定前缀,标识符testFun前面拼接本代码文件所属命名空间TestNS\GodOfJiong,解析为:TestNS\GodOfJiong\testFun。
TestClass::f(); // 使用了空的部分限定前缀,标识符TestClass前面拼接本代码文件所属命名空间TestNS\GodOfJiong,解析为:TestNS\GodOfJiong\TestClass。
echo "\n";
echo habon\TEST_CONST . "\n"; // 使用了非空的部分限定前缀,则habon\TEST_CONST前面拼接本代码文件所属命名空间TestNS\GodOfJiong,解析为:TestNS\GodOfJiong\habon\TEST_CONST。
habon\testFun(); // 使用了非空的部分限定前缀,则habon\testFun前面拼接本代码文件所属命名空间TestNS\GodOfJiong,解析为:TestNS\GodOfJiong\habon\testFun。
habon\TestClass::f(); // 使用了非空的部分限定前缀,则habon\TestClass前面拼接本代码文件所属命名空间TestNS\GodOfJiong,解析为:TestNS\GodOfJiong\habon\TestClass。
echo "\n";
echo \TestNS\GodOfJiong\habon\TEST_CONST . "\n"; // 使用了完全限定前缀,直接解析成\TestNS\GodOfJiong\habon\TEST_CONST。
\TestNS\GodOfJiong\habon\testFun(); // 使用了完全限定前缀,直接解析成\TestNS\GodOfJiong\habon\testFun。
\TestNS\GodOfJiong\habon\TestClass::f(); // 使用了完全限定前缀,直接解析成\TestNS\GodOfJiong\habon\TestClass。
运行结果:
通过上述例子能清晰看出,部分限定前缀与完全限定前缀两种命名空间使用方式的效果与区别。也能明显看出,解析结果中的TestNS\GodOfJiong\TEST_CONST、TestNS\GodOfJiong\habon\TEST_CONST分别与\TestNS\GodOfJiong\TEST_CONST、\TestNS\GodOfJiong\habon\TEST_CONST是等价的,其它几个解析结果同理。
注意:1、如果PHP代码中声明了命名空间,但要使用全局命名空间中的标识符,则需在其前方拼上“\”,这点和完全限定前缀是一样的道理:
<?php
namespace habon;
const INI_ALL = 3;
function strlen(string $s): int
{
return 0;
}
class Exception
{
public function __construct()
{
echo "Let's use Java~\n";
}
}
echo INI_ALL . "\n";
echo strlen('hello') . "\n";
$e = new Exception();
echo "\n";
echo \INI_ALL . "\n";
echo \strlen('hello') . "\n";
$ex = new \Exception('error');
echo $ex->getMessage() . "\n";
2、如果PHP代码中没有声明命名空间,则效果如下:
<?php
// test_ns1.php
namespace habon;
const TEST_CONST = 1;
function testFun(): void
{
echo "testFun called\n";
}
class TestClass
{
public static function f(): void
{
echo "f called\n";
}
}
<?php
include 'test_ns1.php';
const TEST_CONST = 2;
function testFun(): void
{
echo "hello world\n";
}
class TestClass
{
public static function f(): void
{
echo "PHP is the best!\n";
}
}
echo TEST_CONST . "\n"; // 使用了空的部分限定前缀,本代码文件未声明命名空间,解析为TEST_CONST,与\TEST_CONST等价。
testFun(); // 使用了空的部分限定前缀,本代码文件未声明命名空间,解析为testFun,与\testFun等价。
TestClass::f(); // 使用了空的部分限定前缀,本代码文件未声明命名空间,解析为TestClass,与\TestClass等价。
echo "\n";
echo habon\TEST_CONST . "\n"; // 使用了非空的部分限定前缀,本代码文件未声明命名空间,解析为habon\TEST_CONST,与\habon\TEST_CONST等价。
habon\testFun(); // 使用了非空的部分限定前缀,本代码文件未声明命名空间,解析为habon\testFun,与\habon\testFun等价。
habon\TestClass::f(); // 使用了非空的部分限定前缀,本代码文件未声明命名空间,解析为habon\TestClass,与\habon\TestClass等价。
3、在使用部分前缀限定时,对函数、常量标识符而言,拼上当前PHP代码文件声明的命名空间后,若未能找到对应的函数、常量定义,则会退而求其次拼上“\”,在全局命名空间下寻找;但对类而言,拼上当前PHP代码文件声明的命名空间后,若未能找到对应的类定义,直接报错。
<?php
// test_ns1.php
const TEST_CONST = 1;
function testFun(): void
{
echo "testFun called\n";
}
class TestClass
{
public static function f(): void
{
echo "f called\n";
}
}
<?php
namespace habon;
include 'test_ns1.php';
echo TEST_CONST . "\n"; // 使用了空的部分限定前缀,标识符TEST_CONST拼接本代码文件所属命名空间habon,解析为habon\TEST_CONST。但找不到对应定义,则解析为\TEST_CONST,在test_ns1.php中找到了对应定义。
testFun(); // 使用了空的部分限定前缀,标识符testFun拼接本代码文件所属命名空间habon,解析为habon\testFun。但找不到对应定义,则解析为\testFun,在test_ns1.php中找到了对应定义。
TestClass::f(); // 使用了空的部分限定前缀,标识符TestClass拼接本代码文件所属命名空间habon,解析为habon\TestClass。但找不到对应定义,直接报错。
由于PHP具有动态语言特性,如果命名空间与该特性结合使用,需要注意以下规则及其效果:
<?php
// test_ns1.php
const TEST_CONST = 1;
function testFun(): void
{
echo "testFun called\n";
}
class TestClass
{
public static function f(): void
{
echo "f called\n";
}
}
<?php
namespace habon;
include 'test_ns1.php';
const TEST_CONST = 2;
function testFun(): void
{
echo "hello world\n";
}
class TestClass
{
public static function f(): void
{
echo "PHP is the best!\n";
}
}
$c = 'TEST_CONST';
echo constant($c) . "\n"; // 命名空间与PHP动态语言特性结合时,会按照$c的值原样解析为TEST_CONST,也即\TEST_CONST。
$f = 'testFun';
$f(); // 命名空间与PHP动态语言特性结合时,会按照$f的值原样解析为testFun,也即\testFun。
$o = 'TestClass';
$o::f(); // 命名空间与PHP动态语言特性结合时,会按照$o的值原样解析为TestClass,也即\TestClass。
echo "\n";
$c = '\habon\TEST_CONST';
echo constant($c) . "\n"; // 由上述可知,若要使用本代码文件所属命名空间habon下的TEST_CONST,$c的值必须是完整的\habon\TEST_CONST。
$c = 'habon\TEST_CONST';
echo constant($c) . "\n"; // 由于此时$c的值会按原样解析,均被视为使用完全限定前缀,则最前面的"\"可省略,下述的$f、$o同理。
$f = 'habon\testFun';
$f(); // 由上述可知,若要使用本代码文件所属命名空间habon下的testFun,$f的值必须是完整的habon\testFun。
$o = 'habon\TestClass';
$o::f(); // 由上述可知,若要使用本代码文件所属命名空间habon下的TestClass,$o的值必须是完整的habon\TestClass。
__NAMESPACE__与namespace
可使用__NAMESPACE__与namespace获取当前代码所属的命名空间,具体如下:
<?php
namespace TestNS\GodOfJiong\habon;
const TEST_CONST = 1;
function testFun(): void
{
echo "hello world\n";
}
class TestClass
{
public static function f(): void
{
echo "Let's use Java~\n";
}
}
$c = __NAMESPACE__ . '\TEST_CONST'; // __NAMESPACE__为魔术常量,其值为字符串形式的本代码文件所属命名空间"TestNS\GodOfJiong\habon"。
echo constant($c) . "\n";
$f = __NAMESPACE__ . '\testFun'; // __NAMESPACE__为魔术常量,其值为字符串形式的本代码文件所属命名空间"TestNS\GodOfJiong\habon"。
$f();
$o = __NAMESPACE__ . '\TestClass'; // __NAMESPACE__为魔术常量,其值为字符串形式的本代码文件所属命名空间"TestNS\GodOfJiong\habon"。
$o::f();
echo "\n";
echo namespace\TEST_CONST . "\n"; // 关键字namespace在此处相当于操作符,返回本代码文件所属的命名空间"TestNS\GodOfJiong\habon"。
namespace\testFun(); // 关键字namespace在此处相当于操作符,返回本代码文件所属的命名空间"TestNS\GodOfJiong\habon"。
namespace\TestClass::f(); // 关键字namespace在此处相当于操作符,返回本代码文件所属的命名空间"TestNS\GodOfJiong\habon"。
若当前代码没有声明命名空间,则:
<?php
const TEST_CONST = 1;
function testFun(): void
{
echo "hello world\n";
}
class TestClass
{
public static function f(): void
{
echo "Let's use Java~\n";
}
}
$c = __NAMESPACE__ . '\TEST_CONST'; // __NAMESPACE__为魔术常量,其值为字符串形式的本代码文件所属命名空间,由于未声明命名空间,则值为为空字符串。
echo constant($c) . "\n";
$f = __NAMESPACE__ . '\testFun'; // __NAMESPACE__为魔术常量,其值为字符串形式的本代码文件所属命名空间,由于未声明命名空间,则值为为空字符串。
$f();
$o = __NAMESPACE__ . '\TestClass'; // __NAMESPACE__为魔术常量,其值为字符串形式的本代码文件所属命名空间,由于未声明命名空间,则值为为空字符串。
$o::f();
echo "\n";
echo namespace\TEST_CONST . "\n"; // 关键字namespace在此处相当于操作符,返回本代码文件所属的命名空间,由于未声明命名空间,则返回空。
namespace\testFun(); // 关键字namespace在此处相当于操作符,返回本代码文件所属的命名空间,由于未声明命名空间,则返回空。
namespace\TestClass::f(); // 关键字namespace在此处相当于操作符,返回本代码文件所属的命名空间,由于未声明命名空间,则返回空。
指定命名空间的别名
由上文可见,采用直接简单粗暴的形式使用命名空间,则每次遇到标识符都要拼上前缀,无论是部分限定还是完全限定,都是非常麻烦的。有没有更优雅的方式使用命名空间呢?当然有了!
通过use关键字,在代码开头指定命名空间甚至标识符的别名,代码中使用标识符时就简洁很多!但要及其注意:
1、use只是用于定义别名,并没有执行导入或加载操作,导入或加载是通过include、require或者__autoload()、spl_autoload_register()等进行的。只是Laravel、Yii、TP等等这类PHP框架已经帮我们实现了自动加载而已,使得我们基于这些框架开发时,貌似只需要通过use就好像完成了导入或加载那样。
2、命名空间只是个名字,或者叫前缀,尽管其也有分层次路径的形式,但这也只是让开发者觉得更好看而已,只是显得更有层次感更友好而已,并没有实质的分层作用。这意味着,A\B与A\B\C实际上是两个不同的相互独立的命名空间,为前者指定的别名并非也是后者的别名。这点其实与第1点在原理上也是相辅相成的。
3、use指定别名的对象一定是完全限定前缀形式的命名空间或标识符,道理类似于命名空间与PHP动态语言特性结合时的规则。因此,最前面的“\”可以省略。
接下来,首先看最简单最常见的使用use的形式,给类的标识符定义别名:
<?php
// test_ns1.php
namespace TestNS\GodOfJiong;
const TEST_CONST = 1;
function testFun(): void
{
echo "testFun called\n";
}
class TestClass
{
public static function f(): void
{
echo "f called\n";
}
}
<?php
namespace TestNS\GodOfJiong\habon;
include 'test_ns1.php';
use TestNS\GodOfJiong\TestClass as TC;
TC::f();
当然,我们更常见的是用更懒的形式给类的标识符定义别名:
<?php
namespace TestNS\GodOfJiong\habon;
include 'test_ns1.php';
use TestNS\GodOfJiong\TestClass;
TestClass::f(); // 没有as时,直接以TestClass作为TestNS\GodOfJiong\TestClass的别名。
虽然可以给类的标识符定义别名,但不能给函数、常量的标识符定义别名:
<?php
namespace TestNS\GodOfJiong\habon;
include 'test_ns1.php';
use TestNS\GodOfJiong\TEST_CONST;
use TestNS\GodOfJiong\testFun;
echo TEST_CONST . "\n";
testFun();
虽然可以通过use定义别名,但别名要保证不与当前代码中的标识符等命名冲突。当然了,该别名也只在当前代码文件内有效。
<?php
namespace TestNS\GodOfJiong\habon;
include 'test_ns1.php';
use TestNS\GodOfJiong\TestClass;
class TestClass
{
public static function f(): void
{
echo "Let's use Java~\n";
}
}
TestClass::f();
不过,这样子是没毛病的(该例子先临时修改下test_ns1.php,后面的例子恢复上面使用的test_ns1.php):
<?php
// test_ns1.php
namespace TestNS\GodOfJiong;
class Exception
{
public function __construct()
{
echo "PHP is the best!\n";
}
}
<?php
namespace TestNS\GodOfJiong\habon;
include 'test_ns1.php';
use TestNS\GodOfJiong\Exception;
$e = new Exception();
$ex = new \Exception("Let's use Java~");
echo $ex->getMessage() . "\n";
然后,我们还可以通过use对命名空间取别名:
<?php
namespace TestNS\GodOfJiong\habon;
include 'test_ns1.php';
use TestNS\GodOfJiong;
class TestClass
{
public static function f(): void
{
echo "Let's use Java~\n";
}
}
TestClass::f();
GodOfJiong\TestClass::f();
我们当然也可以对全局类标识符定义别名:
<?php
namespace TestNS\GodOfJiong\habon;
use Exception;
$e = new Exception('error');
echo $e->getMessage() . "\n";
再然后,我们通过use一次定义多个别名:
<?php
namespace TestNS\GodOfJiong\habon;
include 'test_ns1.php';
use Exception, TestNS\GodOfJiong as GOJ;
$e = new Exception('error');
echo $e->getMessage() . "\n";
GOJ\testFun();
最后,需要强调一下,通过use定义了别名后,该别名若与PHP动态语言特性结合,还是会不起作用:
<?php
namespace TestNS\GodOfJiong\habon;
include 'test_ns1.php';
use TestNS\GodOfJiong\TestClass;
TestClass::f();
$o = 'TestClass';
$o::f(); // 此处依旧会按$o的值原样解析成TestClass,也即\TestClass,由于未找到其定义,导致报错。
后记
如果代码中类、函数、常量等标识符使用了完全限定前缀,根据上文“使用命名空间”部分的描述,代码中的namespace、use等声明定义内容均会被忽略,就按照完全限定前缀的字面原样解析。
需要特别强调,根据上文“指定命名空间的别名”那部分的描述,use一定是为完全限定前缀形式的命名空间或标识符指定别名的。如果代码中类、函数、常量等标识符使用了部分限定前缀,则PHP会优先检查其是否匹配通过use指定的别名,一旦匹配上,道理就很明显了,解析过程将会走完全限定前缀的路子。看下面直观的例子:
<?php
// test_ns1.php
namespace TestNS\PHP;
class TestPHP
{
public static function f(): void
{
echo "PHP is the best!\n";
}
}
<?php
// test_ns2.php
namespace TestNS\Java;
const TEST_JAVA = 'Java';
function testJava(): void
{
echo "hello Java\n";
}
class TestJava
{
public static function f(): void
{
echo "Let's use Java~\n";
}
}
<?php
include 'test_ns1.php';
include 'test_ns2.php';
use TestNS\PHP\TestPHP, TestNS\Java;
TestPHP::f(); // 使用了空的部分限定前缀,TestPHP匹配到了use指定的别名,相当于解析为:\TestNS\PHP\TestPHP,解析成功。
echo Java\TEST_JAVA . "\n"; // 使用了非空的部分限定前缀,Java匹配到了use指定的别名,相当于解析为:\TestNS\Java\TEST_JAVA,解析成功。
Java\testJava(); // 使用了非空的部分限定前缀,Java匹配到了use指定的别名,相当于解析为:\TestNS\Java\testJava,解析成功。
Java\TestJava::f(); // 使用了非空的部分限定前缀,Java匹配到了use指定的别名,相当于解析为:\TestNS\Java\TestJava,解析成功。
echo Java\TEST_CONST . "\n"; // 使用了非空的部分限定前缀,Java匹配到了use指定的别名,相当于解析为:\TestNS\Java\TEST_CONST,未能找到其定义,报错。
Java\testFun(); // 使用了非空的部分限定前缀,Java匹配到了use指定的别名,相当于解析为:\TestNS\Java\testFun,未能找到其定义,报错。
Java\TestClass::f(); // 使用了非空的部分限定前缀,Java匹配到了use指定的别名,相当于解析为:\TestNS\Java\TestClass,未能找到其定义,报错。
如果代码中类、函数、常量等标识符使用了部分限定前缀,且匹配不上use指定的任何别名,或者代码中压根没有任何use,则解析流程就会变为上文的“使用命名空间”那部分规则,不再赘述。