利用 PHP 名称空间编写可读且可维护的代码

“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 类分别放在称为 barbarianobrien 的目录中,然后从其他 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 名称空间,但是自动装载器技术只适用于类。自动装载器非常方便,所以可以不编写函数,而是在适当命名的实用程序类中创建方法并把常量放在不可变的类中。

通过 MVC 应用程序了解实际用法

把 O'Brien 和野蛮人柯南这个示例放在一边,我们来看一个简单的 MVC 示例应用程序。为了有效地使用名称空间,应该在编写代码之前设计自己的命名约定。常用的最佳实践是使用名称空间树。名称空间分为高层名称空间和子名称空间。如果您的公司有多个应用程序,采用公司名作为高层名称空间可能很方便。然后,使用子名称空间表示应用程序。接下来,用一个级别表示目录,进而用名称指定其中包含的 PHP 类的应用程序功能。例如,假设高层名称空间是公司名 denoncourt,第一个子级别是 retail,第二个子级别是功能名称,见清单 4。

清单 4. 名称空间的设计可以包含嵌套的子名称空间
/denoncourt
	/retail
		/common
		/controller
		/model
		/utility
		/view

controllermodelview 子名称空间显然代表 MVC 架构,而 utilitycommon 子名称空间用于表示不属于其他子名称空间的一般性的类。

现在看看这个简单的 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 类(比如 ArrayObjectFindFileKeyFilter)时要记住这一点。

对于函数和常量,如果当前的名称空间不包含这个函数或常量,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__;

原文发布于微信公众号 - nginx(nginx-study)

原文发表时间:2016-02-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏非著名程序员

你真的会用Android中Strings资源吗

Android为了帮助开发者把应用更方便发布给全球不同语言的人们使用,建议开发者在进行开发时不要把UI呈现相关的文本内容硬编码,而是把内容写入到strings....

2539
来自专栏决胜机器学习

RabbitMQ(五) ——话题模式

RabbitMQ(五)——话题模式 (原创内容,转载请注明来源,谢谢) 一、概述 话题模式(topic)可以让队列绑定某一类型的消息,而不仅仅是direct模式...

3415
来自专栏码云1024

net框架运行原理

2853
来自专栏JAVA高级架构

《深入理解java虚拟机-高效并发》读书笔记

Java内存模型与线程 概述   多任务处理在现代计算机操作系统中几乎已是一项必备的功能,多任务运行是压榨手段,就如windows一样,我们使劲的压榨它运行多个...

3277
来自专栏技术专栏

慕课网Flask构建可扩展的RESTful API-3. 自定义异常对象

因为注册的形式就非常多,所以我们不可能用万能的方式来解决。如果我们不能很好的处理多种多样的形式,我们的代码就会非常的杂乱

1432
来自专栏Vamei实验室

Linux并发与同步

典型的UNIX系统都支持一个进程创建多个线程(thread)。在Linux进程基础中提到,Linux以进程为单位组织操作,Linux中的线程也都基于进程。尽管实...

2739
来自专栏栗霖积跬步之旅

java并发编程的艺术——第四章总结

第四章并发编程基础   java语言是内置对多线程支持的。   为什么使用多线程:     首先线程是操作系统最小的调度单元,多核心、多个线程可以同时执行,能够...

2777
来自专栏ml

C/C++ 关于生成静态库(lib)/动态库(dll)文件如何使用(基于windows基础篇)

1. 首先,如何制作一个静态库(lib)?            额, 对于静态库,我们知道,里头是不应该有Main函数,它只是一个配合文件。之所以称之为lib...

9965
来自专栏Java技术分享

Redis特性和应用场景

Redis特性 速度快 Redis使用标准C编写实现,而且将所有数据加载到内存中,所以速度非常快。官方提供的数据表明,在一个普通的Linux机器上,Redis读...

1.1K7
来自专栏性能与架构

ES6 新特性示例

JS的新版本 ES6/ECMAScript2015 在去年出来了,我们现在普遍使用的ES5是在2009年出来的,相隔这么多年,变化比较大,添加了一些很好用的特性...

2916

扫码关注云+社区

领取腾讯云代金券