命名空间

在进入命名空间的学习之前,我们先了解一下 PHP 的文件包含

文件包含

在之前的章节里,我们的示例代码大多都很短,也没有复杂的功能需求,故而集中放在一个文件中也可以接受。但如果创建一个项目呢?拥有许多类和运行实例,如果都放在一个文件将会造成管理困难,合理地分散在不同的文件并进行组合使用才是良好的编程实践。

PHP有文件包含的概念,用来组织不同文件之间的代码。有以下四个语句(语言结构):

  • include
    • 用来引入并运行指定文件(相当于将指定文件内容复制过来),失败时返回false并给出警告。
  • require
    • 同样用来引入并运行指定文件,但失败时将造成致命错误从而结束脚本
  • include_once
    • include一样,但所有文件只包含一次,可以避免重复包含的错误
  • require_once
    • require一样,但所有文件只包含一次,可以避免重复包含的错误

比如以下示例:

<?php

// 文件:val.php

$name = 'PHP教程';
$web = '极速教程';
<?php

// 文件:test1.php

include './val.php';

echo $name;// 输出:PHP教程

可以看到即使文件test1.php中没有定义变量$name也可以使用。

另外,我们来了解一下 PHP 中一些关于文件目录的常量和方法。

文件目录

在上述示例中,我们可以看到include语句在包含文件时使用了'./val.php,其中./表示本文件所在目录,例如test1.php文件路径为e:/tutorial/code/test1.php,那么它所在的目录就是e:/tutorial/code/,我们可以使用./定位同目录下的文件。

类似的还有../,它代表本文件所在目录的上级目录,还是以test1.php文件为例,它所在目录的上级目录就是e:/tutorial/code/。这两个组合符表示的是相对路径,意为“相对于本文件...”。但是没有形如.../这样的符号!

在PHP中有一些关于文件目录的常量和方法,具体看下表:

常量/方法 描述
basename() 返回一个路径的全路径字符串(绝对路径)
dirname() 返回一个路径的父目录字符串(绝对路径)
__FILE__ 返回文件的完整路径和文件名
__DIR__ 返回文件所在目录,相当于dirname(__FILE__)

为什么使用命名空间

了解完PHP的文件包含,我们可以把代码组织在不同的文件中了,但是还有一个问题:当我们引入其他人写的文件时,可能与我们的代码发生命名冲突,以至于我们不得不去一个个修改它们。这很常见,因为许多变量/常量/函数/类名都可能有相似的属性,比如说,小明定义了一个库函数,其中有一个名为add的函数,小红引入了小明的这个库,但是小红自己也定义了一个名为add但作用不同的函数,这时候就会产生重复定义的错误。

为了解决这个问题,我们可能会想到将标识符名称写的很长,用于阐述功能与区分代码。但这样也会导致代码臃肿,也并没有有效解决命名冲突的问题,反而不利于代码阅读。

命名空间应运而生。它将相关的类、常量、变量、函数等组合在一起,给这些具有相关性的代码一个特定的标识,即使它们可能并不在同一个文件。

如何声明命名空间

定义一个命名空间使用关键字namespace声明,除了declare语句(设定一段代码的执行指令),命名空间必须放在代码的最前面(注释不受影响)。如下示例:

<?php

class Test {}

namespace lib;// 输出:Fatal error: Namespace declaration statement has to be the very first statement or after any declare call in the script

正确方式:

<?php
// 文件:code/10/namespace.php

namespace lib;

class Test {}

同时,其他文件也可以声明为同一个命名空间,说个不大恰当的例子:如果把各个代码文件或者代码片段当做分布在各种地方的垃圾,那么命名空间就相当于把这些垃圾收集在一起的垃圾篓。

子命名空间

由名称可以看出,子命名空间算是命名空间的下层级,定义方式与文件目录系统相似,使用反斜线\表示其结构。例如我们经常会用到数据库,那么关于数据库的操作代码可以定义顶级命名空间db,数据库连接放在命名空间db下,而关于数据库增删查改等操作可以放在命名空间db\handle下。

也就是说,我们可以使用命名空间对代码组织进行分层。示例如下:

<?php
// 文件:code/10/Connection.php

namespace db;

class Connection 
{
    // 数据库连接
}
?>

<?php
// 文件:code/10/Search.php

namespace db\handle;

class Search 
{
    // 搜索
}
?>

<?php
// 文件:code/10/CRUD.php

namespace db\handle;

class CRUD
{
    // 增删查改
}
?>

可以在同一文件中声明多个命名空间,但不建议这样做。

如何使用命名空间

使用命名空间的元素(类、Trait、接口、函数、常量)有三种方式:

  • 非限定名称:不包含命名空间前缀(相当于文件系统的“当前文件”)
  • 限定名称:包含命名空间前缀(相当于文件系统的“相对路径”)
  • 完全限定名称:包含全局前缀操作符的命名空间(相当于文件系统的“绝对路径”)

我们定义了一个命名空间为tutorial\test的文件,代码如下:

<?php
// 文件:code/10/namespace.php
namespace tutorial\test;

class Test {
    public function __construct()
    {
        echo '你创建了一个 ',__CLASS__,' 类',"\n";
    }
}

// 函数:冒泡排序
function bubbleSort($arr)
{
    for($i = 0; $i < count($arr); $i++) {
        for($j = 0; $j < count($arr) - 1; $j++) {
            if($arr[$j] > $arr[$j + 1]) {
                $tmp = $arr[$j];
                $arr[$j] = $arr[$j + 1];
                $arr[$j + 1] = $tmp;
            }
        }
    }
    return $arr;
}

又创建了另一个文件来使用命名空间中的元素:

<?php
// 文件:code/10/test2.php

include __DIR__.'/namespace.php';

$arr = [1, 3, 43, 21, 78];

// 当前文件未处于命名空间中

// 3.使用完全限定名称(包含全局操作符的名称)访问命名空间
$test = new \tutorial\test\Test();// 输出:你创建了一个 tutorial\test\Test 类
$res = \tutorial\test\bubbleSort($arr);
var_dump($res);
/* 输出:
array(5) {
  [0]=>
  int(1)
  [1]=>
  int(3)
  [2]=>
  int(21)
  [3]=>
  int(43)
  [4]=>
  int(78)
}
*/

// 2.使用限定名称(包含前缀的名称)访问命名空间
$test = new tutorial\test\Test();// 输出:你创建了一个 tutorial\test\Test 类
$res = tutorial\test\bubbleSort($arr);
var_dump($res);
/* 输出:
array(5) {
  [0]=>
  int(1)
  [1]=>
  int(3)
  [2]=>
  int(21)
  [3]=>
  int(43)
  [4]=>
  int(78)
}
*/

// 1.使用非限定名称访问命名空间
$test = new Test();// 输出:Fatal error:  Uncaught Error: Class 'Test' not found

分析上面的例子,在未处于命名空间的文件中(相当于处于全局范围):

  • 使用完全限定名称的方式:\完整的namespace\元素
  • 使用限定名称的方式:完整的namespace\元素
  • 使用非限定名称的方式:直接访问命名空间元素,通常会出错

再来看看处于命名空间内的文件访问命名空间的元素会是什么情况:

<?php
// 文件:code/10/test3.php

// 当前文件处于命名空间`tutorial\test`中
namespace tutorial\test;

include __DIR__.'/namespace.php';

$arr = [1, 3, 43, 21, 78];

// 1.使用完全限定名称(包含全局操作符的名称)访问命名空间
$test = new \tutorial\test\Test();// 输出:你创建了一个 tutorial\test\Test 类
$res = \tutorial\test\bubbleSort($arr);
var_dump($res);
/* 输出:
array(5) {
  [0]=>
  int(1)
  [1]=>
  int(3)
  [2]=>
  int(21)
  [3]=>
  int(43)
  [4]=>
  int(78)
}
*/

// 2.使用非限定名称访问命名空间
$test = new Test();// 输出:你创建了一个 tutorial\test\Test 类
$res = bubbleSort($arr);
var_dump($res);
/* 输出:
array(5) {
  [0]=>
  int(1)
  [1]=>
  int(3)
  [2]=>
  int(21)
  [3]=>
  int(43)
  [4]=>
  int(78)
}
*/

// 3.使用限定名称(包含前缀的名称)访问命名空间
$test = new tutorial\test\Test();// 输出:Fatal error:  Uncaught Error: Class 'tutorial\test\tutorial\test\Test' not found

可以看到,使用完全限定名称访问命名空间的元素是正确的。

观察错误输出提示,使用限定名称非限定名称会在当前文件的命名空间(如果未定义,则为全局空间)中查找要访问的命名空间元素

另一种使用命名空间的方式:别名

PHP可以通过关键字use来导入命名空间元素,并使用as关键字定义其别名。

使用方式如下:

<?php

// 文件:code/10/use2.php

include __DIR__.'/namespace.php';

// 使用类:Test
use tutorial\test\Test as Test1;// 定义该命名空间的别名为 Test1
// 使用函数:bubbleSort()
use function tutorial\test\bubbleSort;
// 使用常量:CONSTANT
use const tutorial\test\CONSTANT;

$test = new Test1();// 等同于使用 \tutorial\test\Test()

// 测试:
$arr = [1, 3, 43, 21, 78];
var_dump(bubbleSort($arr));
/* 输出:
array(5) {
  [0]=>
  int(1)
  [1]=>
  int(3)
  [2]=>
  int(21)
  [3]=>
  int(43)
  [4]=>
  int(78)
}
*/
echo CONSTANT;

使用命名空间中的类时只需使用use关键字后跟命名空间的完全限定名称后跟元素名;使用函数时需要在use后跟关键字function;使用常量则需要在use后跟关键字const。如果还需要为它们创建别名,则在元素名后使用as关键字,后跟别名即可。

一行内导入多个命名空间元素

有时候我们可能会用到许多命名空间元素,每个元素都要写一行use语句会显得过于臃肿,所以可以在一行内导入多个命名空间元素,使用如下:

<?php

// 文件:code/10/use3.php

include __DIR__.'/namespace.php';

use tutorial\test\Test as Test1, db\Connection;

use function tutorial\test\bubbleSort, db\handle\search;

use const tutorial\test\CONSTANT as val;

// 测试:
$test = new Test1();
$arr = [1, 3, 43, 21, 78];
var_dump(bubbleSort($arr));
/* 输出:
array(5) {
  [0]=>
  int(1)
  [1]=>
  int(3)
  [2]=>
  int(21)
  [3]=>
  int(43)
  [4]=>
  int(78)
}
*/

echo val;

需要注意的是,导入多个命名空间元素时每次只能导入同一类型(类、函数、常量),使用逗号隔开。


学完本章节,我们可以更好地组织我们的代码,并进行项目开发了。

下一章我们做一个简单的项目实战(需要了解 HTML 表单知识)。