“Conan 是我榜样。” 如果我在餐桌上说这句话,我儿子会以为我说的是游戏 “野蛮人柯南”,而我妻子会以为我说的是脱口秀主持人 Conan O'Brien。这种上下文混淆在 IT 中称为名称冲突。许多语言都有防止名称冲突的战略,PHP V5.3 也是这样。PHP 使用新的名称空间特性解决名称冲突问题。当然,PHP 要解决的冲突的名称并不是人名,而是类、函数和常量的名称。
本文解释为什么应该考虑在项目中使用名称空间。本文概述名称空间的语义,介绍最佳实践,并提供一个使用名称空间的简单的 Model-View-Controller 应用程序。还讨论 Eclipse、NetBeans 和 Zend Studio 中的名称空间支持,特别是在 Eclipse 中使用名称空间的方法。
PHP 语言的优点之一是简单。如果您是 PHP 新手,名称空间只是您需要了解的一个概念。但是如果出现以下任何一种情况,就应该考虑使用名称空间:
- 您正在开发一个包含数百个 PHP 文件的大型应用程序。
- 您的应用程序由程序员团队编写。
- 您打算使用的框架使用 V5.3 和名称空间。
- 您在其他语言中使用过名称空间(或包等相似的功能),比如 Java™、Ruby 或 Python 语言。
如果您独自开发一个相当小的应用程序,可能不需要名称空间。但是对于其他情况,名称空间提供了组织类结构和防止名称冲突的简便方法。这就是许多框架开发人员使用名称空间的原因。例如,强大的 PHP 框架 Zend Framework V2.0 就使用了名称空间。
名称空间为名称提供上下文。清单 1 中的两个类有名称冲突。
清单 1. 在没有名称空间的情况下,同名的两个类会导致冲突
class Conan { var $bodyBuild = "extremely muscular"; var $birthDate = 'before history'; var $skill = 'fighting'; } class Conan { var $bodyBuild = "very skinny"; var $birthDate = '1963'; var $skill = 'comedy'; } |
要想指定名称空间,只需作为源代码的第一个语句添加名称空间声明。
清单 2. 两个类同名,但是名称空间解决了冲突
<?php namespace barbarian; class Conan { var $bodyBuild = "extremely muscular"; var $birthDate = 'before history'; var $skill = 'fighting'; } namespace obrien; class Conan { var $bodyBuild = "very skinny"; var $birthDate = '1963'; var $skill = 'comedy'; } $conan = new \barbarian\Conan(); assert('extremely muscular born: before history' == "$conan->bodyBuild born: $conan->birthDate"); $conan = new \obrien\Conan(); assert('very skinny born: 1963' == "$conan->bodyBuild born: $conan->birthDate"); ?> |
上面的代码可以顺利运行。在解释两个都名为 Conan 的类为什么可以同时存在之前,先要指出两点。首先,我使用断言证实代码符合预期。第二,我做了您绝对不应该做的事情:在一个源代码文件中声明多个名称空间。
名称空间为两个 Conan 类提供惟一的限定符。代码能够明确地区分要引用的是野蛮人柯南,还是脱口秀主持人。注意,实例化语法使用反斜杠 (\
),后面跟着名称空间名称:
$conan = new \barbarian\Conan(); |
和:
$conan = new \obrien\Conan(); |
这些限定符看起来像 Windows® 的目录限定符,这样看待它们是有意义的,因为名称空间支持相对和绝对引用(就像目录一样),而且最好把类文件的源代码放在与名称空间匹配的目录中。
更现实的做法是把两个 Conan 类分别放在称为 barbarian 和 obrien 的目录中,然后从其他 PHP 文件引用这些类。有三种引用 PHP 名称空间的方法:
- 在类名前面加上名称空间
- 导入名称空间
- 给名称空间指定别名
要想使用第一种方法,只需在类名前面加上名称空间(当然是在包含源代码文件之后):
include "barbarian/Conan.php"; $conan = new \barbarian\Conan(); |
这非常简单,但是对于大型应用程序,这种方法的问题是必须反复输入名称空间。除了输入量大之外,还会不必要地弄乱代码。对于第二种方法,使用 PHP V5.3 保留字 use 导入名称空间:
include "barbarian/Conan.php"; use barbarian\Conan; $conan = new Conan(); |
第三种方法允许为名称空间指定别名:
include "barbarian/Conan.php"; use \barbarian\Conan as Cimmerian; $conan = new Cimmerian(); |
(顺便说一句,Cimmerian 是野蛮人柯南的绰号。)
以上三个示例都有的一个问题是要使用 include
语句。可以通过使用 __autoload
函数避免使用 include
。每当引用源代码文件中还不包含的类时,调用 __autoload
函数。把清单 3 中的代码放在名为 autoload.php 的文件中。
清单 3. __autoload
函数动态地包含源代码文件
<?php function __autoload($classname) { $classname = ltrim($classname, '\\'); $filename = ''; $namespace = ''; if ($lastnspos = strripos($classname, '\\')) { $namespace = substr($classname, 0, $lastnspos); $classname = substr($classname, $lastnspos + 1); $filename = str_replace('\\', '/', $namespace) . '/'; } $filename .= str_replace('_', '/', $classname) . '.php'; require $filename; } ?> |
然后把 autoload.php 导入源代码:
require_once "autoload.php"; use \barbarian\Conan as Cimmerian; |
自动装载器的主要好处是不必为每个类创建 include
语句。注意,尽管可以对函数、常量和类使用 PHP 名称空间,但是自动装载器技术只适用于类。自动装载器非常方便,所以可以不编写函数,而是在适当命名的实用程序类中创建方法并把常量放在不可变的类中。
把 O'Brien 和野蛮人柯南这个示例放在一边,我们来看一个简单的 MVC 示例应用程序。为了有效地使用名称空间,应该在编写代码之前设计自己的命名约定。常用的最佳实践是使用名称空间树。名称空间分为高层名称空间和子名称空间。如果您的公司有多个应用程序,采用公司名作为高层名称空间可能很方便。然后,使用子名称空间表示应用程序。接下来,用一个级别表示目录,进而用名称指定其中包含的 PHP 类的应用程序功能。例如,假设高层名称空间是公司名 denoncourt,第一个子级别是 retail,第二个子级别是功能名称,见清单 4。
清单 4. 名称空间的设计可以包含嵌套的子名称空间
/denoncourt /retail /common /controller /model /utility /view |
controller
、model
和 view
子名称空间显然代表 MVC 架构,而 utility
和 common
子名称空间用于表示不属于其他子名称空间的一般性的类。
现在看看这个简单的 MVC 应用程序的代码。清单 5 给出 index.php 的代码,这个文件放在根文件夹中。
清单 5. MVC 应用程序的 index PHP 使用 controller 类
<?php require "autoload.php"; use denoncourt\retail\controller as Control; $controller = new Control\Controller(); $controller->execute(); ?> |
注意,名称空间比较长,所以使用别名 Control
。由于两个原因,我喜欢对名称空间使用别名:首先,如果以后要改变名称空间,在每个源代码文件中只有一行需要修改。第二,由于在实例化类时最好完全限定名称空间,使用 Control\Controller()
实际上就等于 \denoncourt\retail\controller\Controller()
。注意,也可以只为高层名称空间创建别名,然后使用子名称空间的名称进行类实例化:
use denoncourt\retail as Retail; $controller = new retail\controller\Controller(); |
当在同一源代码文件中引用名称空间的多个级别时,这个特性很方便。我在 denoncourt/retail/controller 目录中创建了 Controller.php,见清单 6。
清单 6. MVC Controller 类根据用户输入决定操作
<?php namespace denoncourt\retail\controller; use denoncourt\retail as retail; class Controller { public function execute() { switch ($_GET['action']) { case 'showItem' : $item = new retail\model\Item(); require "denoncourt/retail/utils/format.php"; require "denoncourt/retail/view/item.php"; break; } } } ?> |
我在 denoncourt/retail/model 中创建了 Item.php。清单 7 给出代码。
清单 7. MVC Item 类在 model 子名称空间中
<?php namespace denoncourt\retail\model; class Item { public $itemNo = '123'; public $price = 2.45; public $qtyOnHand = 87; } ?> |
我在 denoncourt/retail/utils 中创建了 format.php,见清单 8。
清单 8. dollar PHP 函数说明如何对函数使用名称空间
<?php namespace denoncourt\retail; function dollar($dollar) { return "$$dollar"; } ?> |
注意,正如前面提到的,我喜欢把格式化函数放在实用程序类中(这样自动装载器就会处理代码的导入,我不需要为 format.php 编写 require 语句)。
最后,在 denoncourt/retail/views 中创建视图页面 item.php。清单 9 给出代码。
清单 9. item 页面显示在控制器中实例化的模型
<html> <head> <style> dt { float:left; clear:left; font-weight:bold; margin-right:10px; width:15%; text-align: right; } dd { text-align:left; } </style> </head> <body> <dl> <dt>Item No:</dt><dd><?php echo "$item->itemNo"; ?></dd> <dt>Price:</dt><dd> <?php echo \denoncourt\retail\dollar($item->price); ?> </dd> <dt>Quantity On Hand:</dt><dd><?php echo "$item->qtyOnHand"; ?></dd> </dl> </body> </html> |
注意 item 页面如何用 \denoncourt\retail\ 名称空间限定 dollar
函数。
如果源代码文件中有名称空间声明,那么对类、函数和常量的所有引用都使用名称空间语义。当 PHP 遇到未限定的类、函数或常量时,它会执行后退 (fallback)。用户类上的后退会让编译器假设使用当前的名称空间。要想引用没有名称空间的类,需要加上一个反斜杠。例如,要想引用 PHP Exception
类,应该使用 $error = new \Exception();
。在使用任何 Standard PHP Library 类(比如 ArrayObject
、FindFile
和 KeyFilter
)时要记住这一点。
对于函数和常量,如果当前的名称空间不包含这个函数或常量,PHP 的后退机制会后退到标准的 PHP 函数。例如,如果您编写了自己的 strlen
函数,PHP 会解析出您的函数。但是,如果也希望使用标准的 PHP strlen
函数(比如在自己的 strlen
实现内部),就需要在函数调用前面加上反斜杠,见清单 10。
清单 10. 可以用反斜杠限定 PHP 标准函数以表示全局名称空间
<?php namespace denoncourt\retail; function strlen($str) { return \strlen(); } ?> |
如果您喜欢编写动态的方法,可能想把名称空间放在带双引号的字符串中:"denoncourt\retail\controller"
。但是要记住,需要对反斜杠进行转义:"denoncourt\\retail\\controller"
。一种解决方法是使用单引号:'denoncourt\retail\controller'
。
在进行动态编程时,要记住 PHP V5.3 有一个新的全局变量 __NAMESPACE__
。可以考虑使用这个全局变量而不是输入名称空间:
$echo 'I am using this namespace:'.__NAMESPACE__; |
大多数主流的 IDE 已经支持 PHP V5.3。NetBeans V6.8 对名称空间的支持很不错。它不但有代码补全,还会对通过最佳实践改进代码提出建议。例如,PHP 名称空间的最佳实践之一是,在代码中使用绝对引用完全限定名称空间,而不是相对引用。如果输入使用相对名称空间限定符的代码,NetBeans 会在最左边的代码空白边中显示一个灯泡图标。如果把鼠标停在这个图标上,NetBeans 会显示工具提示,它描述建议的修改。如果单击这个图标,NetBeans 会替您修改代码。
Zend Studio 提供相似的功能。如果您还不太愿意开始使用名称空间,可以考虑升级 IDE,在 IDE 的帮助下尝试使用名称空间。注意,可能会发现甚至不需要升级 IDE,因为许多 IDE 已经提供 PHP V5.3 特性一年多了。
PHP Development Tools (PDT) V2.1 也对名称空间提供很好的支持。PDT 是 Eclipse 的插件。参考资料 中提供 PDT 安装说明的链接。
为了启用名称空间支持,首先必须让 Eclipse/PDT 使用 PHP V5.3。在应用程序主菜单中,单击 Window > Preferences,见 图 1。在树面板中展开 PHP,然后选择 PHP Interpreter。然后,把 PHP 版本改为 PHP 5.3 并单击 OK。
图 1. Eclipse PDT 插件要求把解释器设置为 PHP V5.3
单击 File > New Project,展开 PHP 节点,然后单击 PHP Project,就可以创建 PHP 项目。要想创建 PHP 文件,只需在 PHP Explorer 中右键单击项目,然后单击 PHP file。PDT 对名称空间关键字 namespace
和 use
使用适当的语法突出显示(见图 2)。
图 2. PDT 对名称空间关键字使用语法突出显示并在 PHP Explorer 和 Outline 视图中显示名称空间
让 PDT 在 PHP Explorer 和 Outline 视图中显示名称空间会很方便,这有助于了解如何把名称空间分配给各个类。PDT 还提供我们希望 IDE 具备的功能:代码补全(见 图 3)。当输入 use
语句时,PDT 会调用代码补全。
图 3. PDT 为名称空间提供代码补全
当输入类名时,PDT 也会弹出代码补全窗口。例如,如果输入 new Item
,PDT 会显示一个列出 Item � denoncourt\retail\item
的窗口。
当选择 denoncourt\retail\item 时,PDT 在实例化行上插入所需的 use 语句和限定符:
use denoncourt\retail\model; new model\Item(); |
更酷的是,当输入 new Conan
时,PDT 会显示列出以下内容的窗口:
Conan � obrien Conan � barbarian |
这让我能够选择合适的 Conan 类。既然我们又绕回到了两个 Conan 类的示例,本文应该结束了。
如果您仍然对使用名称空间有点儿犹豫,在放弃学习它之前,我建议打开具有 PHP V5.3 支持的 IDE 并尝试使用名称空间。对于命名约定,一定要设计得比较简单,而不要追求完美的战略。由于我有长期 Java 开发背景,所以喜欢采用 Java 命名约定。我对于 PHP 名称空间使用驼峰大小写形式并去掉下划线。通过在 PHP 项目中使用名称空间,代码会更清晰,更有条理。您会掌握这种在大多数先进语言中很常见的特性。您还会为使用已经使用 PHP V5.3(尤其是名称空间)的许多框架做好准备。