“Conan 是我榜样。” 如果我在餐桌上说这句话,我儿子会以为我说的是游戏 “野蛮人柯南”,而我妻子会以为我说的是脱口秀主持人 Conan O'Brien。这种上下文混淆在 IT 中称为名称冲突。许多语言都有防止名称冲突的战略,PHP V5.3 也是这样。PHP 使用新的名称空间特性解决名称冲突问题。当然,PHP 要解决的冲突的名称并不是人名,而是类、函数和常量的名称。
本文解释为什么应该考虑在项目中使用名称空间。本文概述名称空间的语义,介绍最佳实践,并提供一个使用名称空间的简单的 Model-View-Controller 应用程序。还讨论 Eclipse、NetBeans 和 Zend Studio 中的名称空间支持,特别是在 Eclipse 中使用名称空间的方法。
PHP 语言的优点之一是简单。如果您是 PHP 新手,名称空间只是您需要了解的一个概念。但是如果出现以下任何一种情况,就应该考虑使用名称空间:
如果您独自开发一个相当小的应用程序,可能不需要名称空间。但是对于其他情况,名称空间提供了组织类结构和防止名称冲突的简便方法。这就是许多框架开发人员使用名称空间的原因。例如,强大的 PHP 框架 Zend Framework V2.0 就使用了名称空间。
名称空间为名称提供上下文。清单 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';
}
要想指定名称空间,只需作为源代码的第一个语句添加名称空间声明。
<?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 的文件中。
__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。
/denoncourt
/retail
/common
/controller
/model
/utility
/view
controller
、model
和 view
子名称空间显然代表 MVC 架构,而 utility
和 common
子名称空间用于表示不属于其他子名称空间的一般性的类。
现在看看这个简单的 MVC 应用程序的代码。清单 5 给出 index.php 的代码,这个文件放在根文件夹中。
<?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。
<?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 给出代码。
<?php
namespace denoncourt\retail\model;
class Item {
public $itemNo = '123';
public $price = 2.45;
public $qtyOnHand = 87;
}
?>
我在 denoncourt/retail/utils 中创建了 format.php,见清单 8。
<?php
namespace denoncourt\retail;
function dollar($dollar) {
return "\$$dollar";
}
?>
注意,正如前面提到的,我喜欢把格式化函数放在实用程序类中(这样自动装载器就会处理代码的导入,我不需要为 format.php 编写 require 语句)。
最后,在 denoncourt/retail/views 中创建视图页面 item.php。清单 9 给出代码。
<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。
<?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__;