很多语言都提供了名称空间特性,包括 C++ 和 Java 编程语言。引入名称空间是为了帮助组织大型的代码库,因为在大型代码库中,应用程序经常会出现函数名或类名重叠问题,这会引起其他问题。使用名称空间可以帮助识别代码提供的函数或实用程序,甚至可以帮助指定其来源。一个例子就是 C# 中的 System 名称空间,它包含有 .NET 框架提供的所有函数和类。
在其他未提供正式名称空间的语言中(比如 PHP V5.2 以及更早版本),人们常常通过在类或函数名中使用特定的命名约定来发挥名称空间的作用。比如 Zend Framework,其中每个类名以 Zend
开头,并且每个子名称空间使用下划线分隔开。比如,类定义Zend_Db_Table
表示 Zend Framework 中的一个类并且提供数据库功能。这种方法的一个缺点就是产生的代码非常繁琐,尤其是那些包含好几层的类或函数(Zend Framework 中的 Zend_Cache_Backend_Apc
就是一个例子)。另一个问题就是所有代码必须遵循这种风格,因此如果在应用程序中集成了不遵循这种命名约定的第三方代码后,问题就复杂了。
PHP 名称空间的发展也并非一帆风顺。它们最初计划引入到 PHP V5 中,但是由于无法获得恰当的实现,因此在开发阶段被放弃。最后决定将它们并入到 PHP V6 中,在 2007 年决定将所有 nonunicode 增强移到另一个 PHP V5.x 发行版后,名称空间随后被移入到 PHP V5.3 中。尽管自最初的设计之后绝大部分名称空间行为没有发生变化,但是使用哪一种操作符却成了最大的问题,并且社区成员对这个问题有不同的看法。2008 年 10 月最终决定使用反斜杠作为操作符,从而解决了所有在语言设计和适用性方面使用各种其他操作符的问题。
PHP 名称空间
PHP 从其他语言中借鉴了很多名称空间的语法和设计 — 最突出的是 C++。然而,PHP 名称空间在某些方面具有自己的独特性,这对于希望像在其他语言中那样使用名称空间的用户来说是一个挑战。在本节中,我们将研究 PHP 名称空间的工作方式。
定义一个名称空间
定义一个新的名称空间非常简单。要定义新名称空间,在一个文件中添加清单 1 中的代码作为第一个命令或输出。
清单 1. 定义名称空间
<?php namespace Foo; class Example {} ?> |
注意,以上 namespace
的声明必须是文件中的第一个命令或输出。在它的前面添加任何内容都会导致一个致命的错误。清单 2 展示了有关这方面的一些例子。
清单 2. 定义名称空间的错误方法
/* File1.php */ <?php echo "hello world!"; namespace Bad; class NotGood {} ?> /* File2.php */ <?php namespace Bad; class NotGood {} ?> |
在清单 2 的第 1 部分中,我们尝试在名称空间定义之前回传到控制台,这导致产生一个致命错误。在清单的第 2 部分中,我们在 <?php
打开标记的前面多加了一个空格,这样也导致一个致命错误。在编写自己的代码时一定要注意这种情况,因为这是 PHP 名称空间中很常见的一种错误。
但是,上面的两个例子都可以重新编写,将名称空间定义和将在名称空间声明中放入的代码放到独立的文件中,然后再将此文件包含到原始文件中。清单 3 演示了这一点。
清单 3. 修正定义名称空间的错误方法
/* Good.php */ <?php namespace Good; class IsGood() {} ?> /* File1.php */ <?php echo "hello world!"; include './good.php'; ?> /* File2.php */ <?php include './good.php'; ?> |
现在我们已经了解了如何在一个文件中定义代码的名称空间,接下来让我们看看如何在应用程序中利用这个使用名称空间的代码。
使用带有名称空间的代码
定义了名称空间并在其中放入代码后,我们就可以在应用程序中方便地使用它。可以使用很多种方法调用带有名称空间的函数、类或常量。一种方式是显式地将名称空间引用为调用的前缀。另一种方法是为名称空间定义一个别名并使用该别名作为调用的前缀,这样做的目的是简化名称空间前缀。最后,我们可以只在代码中使用名称空间,这就使它成为默认名称空间,并且在默认情况下,使所有代码都引用默认名称空间。清单 4 演示了调用之间的不同之处。
清单 4. 在名称空间内调用函数
/* Foo.php */ <?php namespace Foo; function bar() { echo "calling bar...."; } ?> /* File1.php */ <?php include './Foo.php'; Foo/bar(); // outputs "calling bar...."; ?> /* File2.php */ <?php include './Foo.php'; use Foo as ns; ns/bar(); // outputs "calling bar...."; ?> /* File3.php */ <?php include './Foo.php'; use Foo; bar(); // outputs "calling bar...."; ?> |
清单 4 演示了在名称空间 Foo 内调用函数 bar()
的不同方法。在 File1.php 内,我们看到了如何进行显式调用,使用名称空间的名称作为调用前缀。File2.php 使用名称空间名称的别名,因此我们使用别名代替名称空间的名称。最后,File3.php 仅使用名称空间,这允许我们不需要使用任何前缀来调用 bar()
。
我们还可以在一个文件内定义多个名称空间,只需要在文件中添加更多 namespace
调用。清单 5 演示了这一点。
清单 5. 文件中的多个名称空间
<?php namespace Foo; class Test {} namespace Bar; class Test {} $a = new Foo\Test; $b = new Bar\Test; var_dump($a, $b); Output: object(Foo\Test)#1 (0) { } object(Bar\Test)#2 (0) { } |
现在我们已经基本了解了如何在名称空间内进行调用,让我们了解一些更复杂的名称空间调用以及它们如何工作。
名称空间解析
要熟悉名称空间的使用,其中一个难点就是了解如何进行范围解析。尽管清单 4 所示的简单例子是合理的,但是当我们开始对名称空间进行彼此嵌套时,或者在一个名称空间中试图针对全局空间发出调用是,就会出现问题。PHP V5.3 提供了可以以合理的方式自动解决这些问题的规则。
让我们创建一些包含(include)文件,每个文件都定义了函数 hello()
。
清单 6. 在不同名称空间中定义的 hello()
函数
/* global.php */ <?php function hello() { echo 'hello from the global scope!'; } ?> /* Foo.php */ <?php namespace Foo; function hello() { echo 'hello from the Foo namespace!'; } ?> /* Foo_Bar.php */ <?php namespace Foo/Bar; function hello() { echo 'hello from the Foo/Bar namespace!'; } ?> |
清单 6 在三个不同范围内对 hello()
函数定义了三次:在全局范围内,在 Foo
名称空间中,在 Foo/Bar
名称空间中。根据发出hello()
函数调用的范围,决定对哪个 hello()
函数执行调用。下面展示了这些调用的例子。在这里,我们将使用 Foo
名称空间查看如何在另一个名称空间中调用 hello()
函数。
清单 7. 从 Foo
名称空间调用所有 hello()
函数
<?php include './global.php'; include './Foo.php'; include './Foo_Bar.php'; use Foo; hello(); // outputs 'hello from the Foo namespace!' Bar\hello(); // outputs 'hello from the Foo/Bar namespace!' \hello(); // outputs 'hello from the global scope!' ?> |
可以看到,在当前名称空间内引用子名称空间时,可以缩短名称空间前缀(Foo/Bar/hello()
调用可被缩短为 Bar/hello()
)。并且我们看到如何指定以在全局空间内调用方法:只需使用名称空间操作符作为调用的前缀。
现在,我们已经了解了名称空间的工作机制,下面我们将查看如何在自己的代码中使用它们。
PHP 名称空间用例
名称空间的总体目标就是帮助我们更好地组织代码,减少全局空间内的定义数量。在本节中,我们将查看一些例子,看看名称空间如何帮助我们轻松地实现这些目标。
使用名称空间的第三方代码
许多 PHP 应用程序使用来自不同来源的代码,包括像 PEAR 库那样经过精心设计的代码,或者来自 CakePHP 或 Zend Framework 等各种框架的代码,或是来自 Internet 上不同位置的代码。在集成这些代码时,最主要的问题之一就是这些代码可能无法恰当地融合到已有代码中;函数或类名可能与应用程序中已经在使用的内容冲?。
其中一个例子就是 PEAR Date 包。它使用类名 Date
,这是一个非常通用的类名,并且可以很好地切入到代码中的其他位置。因此,一个良好的解决方法就是在包内部的 Date.php 文件的顶部添加一个简单的名称空间命令。现在,当希望使用 PEAR Date 类而不是我们自己的 PEAR Date 类时,就不会感到迷惑。
清单 8. 按照名称空间的定义使用 PEAR Date 类
<?php require_once('PEAR/Date.php'); use PEAR\Date; // the name of the namespace we've specified in PEAR/Date.php // since the current namespace is PEAR\Date, we don't need to prefix the namespace name $now = new Date(); echo $now->getDate(); // outputs the ISO formatted date // this example shows the full namespace specified as part of the class name $now = new PEAR\Date\Date(); echo $now->getDate(); // outputs the ISO formatted date ?> |
我们已经在 PEAR/Date.php 文件的 PEAR/Date
名称空间内定义了 PEAR Date 类,因此现在只需在我们的文件中包含代码并使用名称空间,或使用名称空间的名称作为类或函数名的前缀。通过这种方法,我们就可以安全地在应用程序中包含第三方代码。
名称冲突不仅仅是第三方代码才有的问题。如果大型代码库的各个部分永远不会互相靠近,那么也会出现此问题。在下一小节中,我们将了解名称空间如何应对这个情况。
避免实用函数名冲突
几乎所有 PHP 应用程序都具有大量实用方法。虽然并非应用程序的任意对象都包含实用方法,并且也不一定存在于应用程序的所有部分,但是总的来说它在应用程序中确实发挥着作用。但是,随着应用程序不断壮大,实用方法会引起维护问题。
其中一个产生问题的位置就是单元测试,我们编写代码来测试运行应用程序的代码。大多数单元测试套件被设计为运行整个测试套件中的所有测试。比如,我们有两个永远不会包含在一起的实用方法文件,但是在测试套件中,它们就会包含在一起,因为我们会一次性测试整个应用程序。尽管使用这种方式设计应用程序不利于长期维护,但是它确实存在于大型遗留代码库中。
清单 9 展示了如何避免这一问题。我们有两个文件 utils_left.php 和 utils_right.php,这是面向主要使用右手的用户和主要使用左手的用户的实用函数集合。对于每个文件,我们在其各自的名称空间内分别定义。
清单 9. utils_left.php 和 utils_right.php
/* utils_left.php */ <?php namespace Utils\Left; function whichHand() { echo "I'm using my left hand!"; } ?> /* utils_right.php */ <?php namespace Utils\Right; function whichHand() { echo "I'm using my right hand!"; } ?> |
我们定义了一个 whichHand()
函数,函数的输出表示我们使用哪一只手。在清单 10 中,我们看到可以方便地包含两个文件并在希望调用的名称空间之间进行切换。
清单 10. 同时使用 utils_left.php 和 utils_right.php 的示例
<?php include('./utils_left.php'); include('./utils_right.php'); Utils\Left\whichHand(); // outputs "I'm using my left hand!" Utils\Right\whichHand(); // outputs "I'm using my right hand!" use Utils\Left; whichHand(); // outputs "I'm using my left hand!" use Utils\Right; whichHand(); // outputs "I'm using my right hand!" |
现在,两个文件可以安全地包含在一起,并且我们指定了处理函数调用所需使用的名称空间。而且,对现有代码的影响很小,因为重构功能只需要我们在文件的顶部添加 use
语句,表示要使用的名称空间。
可以对我们定义的 PHP 代码进一步扩展。在下一小节,我们将了解如何在名称空间内覆盖内部函数。
覆盖内部函数名称
虽然 PHP 的内部函数经常可以提供非常棒的实用方法,但有时它们不能按照我们期望的那样执行。我们需要增强它们的行为,以使函数符合我们的期望,但是我们也需要使用另一个名字重新定义函数,从而避免进一步混淆范围。
文件系统函数就需要我们执行这些操作。假设我们需要确保 file_put_contents()
创建的任何文件具有某些权限集。比如,假设我们希望这些文件的权限为只读;我们可以使用一个新的名称空间重新定义函数,如下所示。
清单 11. 在名称空间内定义 file_put_contents()
<?php namespace Foo; function file_put_contents( $filename, $data, $flags = 0, $context = null ) { $return = \file_put_contents( $filename, $data, $flags, $context ); chmod($filename, 0444); return $return; } ?> |
我们在函数内调用内部 file_put_contents()
函数并使用一个反斜杠作为函数名的前缀,表示该函数应当在全局范围内处理,这表示将调用内部函数。调用了内部函数后,我们随后对文件执行 chmod()
命令来设置相应的权限。
还有许多例子可以演示如何使用名称空间增强代码。在任何情况下,我们应避免执行不恰当的修改,比如将函数名或类名作为前缀以生成独特的名称。我们现在还了解了如何使用名称空间在大型应用程序中更加安全地包含第三方代码,同时不需要担心名称冲突。
结束语
PHP V5.3 的名称空间是该语言中一个非常受欢迎的新增特性,可以帮助开发人员合理地组织应用程序的代码。该特性使您能够避免使用标准来处理名称空间,允许您编写更高效的代码。尽管名称空间的出现经历了很长时间,但对于受名称冲突困扰的大型 PHP 应用程序来说,它是一个非常受欢迎的特性。