PHP-编程高级教程-五-

69 阅读50分钟

PHP 编程高级教程(五)

原文:Pro PHP Programming

协议:CC BY-NC-SA 4.0

十二、将 Zend Studio、Eclipse、Bugzilla、Mylyn 和 Subversion 用于敏捷开发

近年来,敏捷开发变得越来越流行。这是一种计算机编程方法,它认为两个人组成的编程团队实际上比程序员单独工作更有效率。两人一组工作的概念也有反对者,有些人可能认为两个程序员同时在同一台机器上工作实际上是浪费开发时间。首先,让我们看看敏捷开发的一些基本原则。然后,我们将看看本章标题中提到的产品和工具的使用,以及如何使用它们来实现这些原则。

敏捷开发的原则

敏捷开发有许多方面,在现有的程序员团队中实现它们需要一些时间。可能会有来自各方的阻力,从程序员到管理层,所以在开始使用这些原则之前,一定要计划好你的方法和交付时间表。

敏捷编程的定义和概念需要范式的转变,以及对概念的坚实基础和理解。一旦你理解了敏捷开发应该如何工作的概念,你当然可以自由地发明你自己的术语,因为这在术语上不是一门精确的科学。在我将一个开发团队转移到敏捷方法的经历中,我喜欢使用汽车拉力赛的概念来表达团队成员“简单”角色的想法。在一场拉力赛中,每辆车都有驾驶员和导航员。集会通常会持续一段时间;如果是短途拉力赛,单日就可以完成(一个小的编程任务),如果是多阶段拉力赛(一个较长的编程任务),那么沿途可能会有进站或者休息点。最终反弹结束,结果确定。这与敏捷开发中采用的方法相同。

images 注意这种敏捷开发的方法和交付是对我成功之处的个人适应和提炼。有不同的类比和方法可能不适合你,也有可能适合你。例如,以快速状态会议开始编程日的每日站立会议的想法在我当前的情况下不工作。这对你的团队来说可能是最好的事情。调味是这里的基本概念。

继续汽车拉力赛的概念,参与者通常会得到一份描述或一张要走的路线的地图。拉力赛组织者准备路线,并向每辆车的导航员提供详细信息。然后导航员计划他们的车在拉力赛开始时将要走的具体路线。有些拉力赛不允许练习跑球场路线,只提供地图;一些组织者提供大量的路线信息——简而言之,路线的知识在不同的集会上可能有很大的不同。

然后,导航员根据手头的信息尽可能准确地规划路线。当事件到达时,信息可能已经改变。

敏捷开发中的计划和完成编程任务没有什么不同。必须利用提供的信息尽可能多地研究这个问题;这是导航员的工作。计划的执行将由司机来完成。驾驶程序员的工作和拉力车手是一样的。专注地听着导航员,以最佳速度操作设备,以便在最佳和最准确的时间框架内到达目的地。

这些是敏捷开发中的基本角色。现在让我们看看这些角色在解决编程任务或修复 bug 的活动中是如何工作的。下面的叙述将有助于展示一次集会从开始到结束是如何进行的。

敏捷开发集会

项目负责人(集会组织者)需要适当的时间来计划在即将到来的集会的时间框架内需要完成的事情。我发现反弹持续时间不应该超过两周,稍后我会解释原因;当然,这取决于要完成的工作。拉力赛课程(要完成的任务)将被记录下来并分配给导航员,导航员将依次花费适当的时间来研究问题或 bug,并计划解决项目所需的步骤。然后,导航员选择一个合适的驱动程序(程序员)开始工作。理想情况下,领航员有权选择他们自己的驾驶员,因为他们最了解要完成什么,因此应该选择最适合这项工作的人。当拉力赛开始时,领航员会亲自坐在车手旁边,和他们讨论任务。

这似乎是浪费时间的地方,但当你真正想一想,有人告诉司机去哪里和注意什么实际上可以更准确和更有效地完成工作。当司机不那么分心时(查看电子邮件、浏览脸书或 YouTube ),专注度和成就感会呈指数级上升。导航员给驾驶员的详细指示水平将取决于他们各自的经验和技能水平,所以这种动态将不得不自行解决。我只想说,导航员不应该一键一键地告诉司机做什么,就像一个真正的导航员不会告诉司机什么时候换挡一样。

images 注意导航员在这里起着更大的作用,应该认真对待。他们必须计划路线,并标记沿途的里程碑或检查点,以便团队不会在途中迷路。当我将敏捷开发引入我的工作环境时,我实际上发现了一些很好的在线视频,是真实的汽车拉力赛,并让我的团队观看了这些视频。它实际上以一种更实际的方式给他们带来了概念。分享一些好的集会和坏的集会(崩溃)的视频,向您的开发团队展示敏捷开发的道路上可能也有障碍。

有一些思想流派认为,导航员和驾驶员坐在一起的实际时间应该限制在几个小时,然后他们应该改变一些事情:改变角色,改变任务,或者两者都改变。这也需要一个团队一个团队地解决,但是不时地休息一下是个好主意。

因此,一旦集会组织者设定了集会的日期范围,就应该计划沿途停车。如果反弹时间很长,那么止损点应该放在时间线上的战略点,如果反弹时间相对较短,那么止损点可以放在最后。无论哪种方式,整个开发团队都应该走到一起,总结已经完成了什么,失败了什么,以及还需要做什么。在敏捷开发的其他术语中,停站会议也被称为 scrums,但是我认为交叉类比和混合这样的术语会令人困惑。

images 注意在使用这种敏捷开发方法进行编程时,应该仔细考虑开发环境。我建议至少有三个独立的环境:一个用于集会时间的本地开发环境,一个用于执行工作质量保证的测试环境,当然还有生产环境。

现在,自然地,试图遵循这种敏捷开发方法进行编程将需要一些时间来适应,并且需要获得适当的工具以便在该环境中工作。例如,你不会在土路拉力赛中驾驶加长豪华轿车,尽管有一天这可能被证明是一种取悦大众的方式……你先看看这里吧!

本章的剩余部分将集中在工具的组合上,我发现这些工具对敏捷开发方法从计划到实现的工作非常有帮助。首先,我将分别讨论这些工具,并向您展示它们各自的好处和优势,然后讨论如何实现集成。

images 如果你确实想在某个时候在你的开发车间实施敏捷开发,本章的结尾将为进一步的学习和准备提供一些参考。

Bugzilla 简介

Bugzilla 是一个开源的基于网络的工具,允许用户跟踪他们的许多项目中的问题和错误。老实说,在*nix 环境下设置有点棘手,我建议找一个有经验的 Linux 管理员来确保它设置正确,但是一旦它稳定下来,就会像广告中说的那样运行。我们已经学会在我们所有的项目任务中使用它,包括状态报告和产品增强工作;任何与项目相关的东西都可以而且应该记录在 Bugzilla 中(不应该严格地局限于 bug)。Bugzilla 的基本主屏幕如图 12-1 所示。这是取自 Bugzilla 的免费在线演示网站。

images

图 12-1。【Bugzilla 的主页

一旦你建立了错误报告网站,你将不得不建立至少一个项目,在其中你将跟踪错误/任务。设置这些很容易,但是应该考虑清楚以便以后使用。例如,将您的开发项目分解成单独的模块,并将它们视为“产品”或发布版本可能会更好。从长远来看,这种粒度级别将对您有所帮助,因为它允许更精确地跟踪任务和 bug。

在 Bugzilla 中键入要跟踪的产品后,您可以开始添加要跟踪的单个任务或 bug。在图 12-2 中,您可以看到在 Bugzilla 的这个实例中被跟踪的一些任务的部分列表;在图 12-3 中,您可以看到单个任务上可记录的部分细节。

images

***图 12-2。*Bugzilla bug/任务列表

Bugzilla 本身就是一个管理项目细节和跟踪任务(有些是真正的 bug)的好工具。此外,在 Bugzilla 中,您可以定制对 bug 的搜索,调整任务类别和严重性级别,甚至使用内置的报告部分来查看任务完成持续时间。因此,就这一点而言,我建议将其纳入您的项目管理工具包。然而,故事远不止如此。

images

***图 12-3。*Bugzilla bug/任务的部分细节

Mylyn 为月食

Mylyn 是一个为 Eclipse 构建的任务记录模块。它是 Eclipse IDE 的一个独立附件,因此可以用于任何 Eclipse 编码风格或语言(Java、C++、PHP 等等)。它最好的特性之一是它如何记录当前正在执行的工作的上下文,稍后会有更多的内容。在 Zend Studio for Eclipse 的世界中,Mylyn 被直接绑定到任务列表视图中,可以立即用于单独的任务。如果你是一个单独的程序员,这很好,因为你不需要和任何人分享你的代码。图 12-4 显示了 Studio 中带有未分类部分的任务列表视图,以及来自示例 Bugzilla 服务器的小部件项目部分。未分类部分是用于独奏的部分。再往下钻一点,图 12-5 显示了一个单独任务的细节。在这里,我们可以看到任务的当前状态、您可能想要添加到任务中的任何日程安排,以及关于任务的任何注释或备注。这本身就很棒;尤其是单人编程。然而,一旦您看到 Mylyn 连接到 Bugzilla 服务器,您就会意识到这个 Zend Studio for Eclipse view 的应用范围要广得多。

images

图 12-4 。Zend Studio 中 Buzilla 服务器内容的示例显示

连接到 Bugzilla 服务器非常简单;您所要做的就是在 Task Repositories 视图中创建一个新条目。这在图 12-6 中显示。一旦用正确的凭证建立了这个连接,您就需要为它创建一个查询。这个查询可以充当 Bugzilla 中所有产品和任务的过滤器,这样,如果您正在处理一个特定的项目或系统发布,您就可以通过识别和过滤所涉及的部分来更好地集中精力。

images

**图 12-5。**Zend Studio中出现的 Bugzilla 项目细节

images

***图 12-6。*样例 Bugzilla 存储库连接设置

Bugzilla 和 Mylyn 在 Eclipse 内结合

更多的时候,程序员在团队中合作和工作。这是 Mylyn 和 Bugzilla 可以以另一种奇妙的方式合作的地方。一旦您为团队开发准备好了任务,您就可以在 Bugzilla 中或者在 Zend Studio for Eclipse 的任务列表视图中将它们分配给团队成员;这是集会组织者的任务。在 Zend Studio for Eclipse 中,您甚至可以对 Bugzilla 任务做更多的事情。可以选择添加附件(文档、图像等)、更改任务状态、更新其属性(任务可能影响什么操作系统、严重性级别、优先级级别等),以及更改团队中工作分配给谁。所有这些都可以在 IDE 中完成,这样您就不必为了完成某些工作而反复切换屏幕。

一旦在 Zend Studio for Eclipse 中建立了到 Bugzilla 服务器的连接,我们鼓励您创建至少一个到该服务器的查询,这样您就可以专注于当前需要关注的任务。您可以进行多个查询,如果愿意,每个项目一个查询,并根据需要在它们之间切换。正如你在图 12-7 中所看到的,查询设计页面可能非常复杂,如果你愿意,可以直接过滤到硬件和操作系统级别。在本例中,我对查询进行了微调,只显示与 Sam 的小部件项目相关的任务。如果我有一个潜在的非常大的查询结果集,如本例所示,我可以在视图工具栏中的 Task List 视图的关键字搜索特性中进一步细化我要查找的内容。

images

***图 12-7。*任务库查询/过滤界面

既然 bug 和任务已经连接到 Zend Studio for Eclipse,那么是时候开始使用这些项目来跟踪您的进度,直到它们得到解决。如果你再次查看图 12-4 ,你会在任务列表视图中从左数第二个位置看到一个模糊的圆圈列。几段前我提到过,跟踪任务或 bug 的上下文是 Mylyn 更好、最强大的特性之一。所以现在我们来看看这个特性背后的细节。一旦你选择了你想要完成的任务,在你执行任何相关的工作之前,一定要点击这个圆圈。这样做是为了“激活”任务,Mylyn 将开始记录正在进行的工作的上下文。Mylyn 开始跟踪您在任务处于“活动”状态时打开的所有文件,然后如果您几天后回到同一任务并重新激活它,则您上次处理该任务时在该任务上下文中打开的所有文件将在编码区域中自动为您打开。图 12-8 显示了一个特定 bug 的上下文以及与之相关的文件;DebugDemo.php在这种情况下及其所属的项目。Mylyn 不仅跟踪 bug 上下文中打开的文件,还跟踪您处理过并随后关闭的文件。

images

***图 12-8。*单个 bug /任务的详细上下文跟踪

这个上下文页面的另一个优点是,如果文件是关闭的(但仍然是 bug 上下文的一部分),您可以双击文件名,它将被打开进行编辑。这可以节省在大型项目文件列表中查找文件的时间。所有这些都是在打开或激活任务时完成的,所以这里唯一的问题是,在处理特定的任务或 bug 时,要确保记得打开和关闭任务的上下文记录功能。

除了任务列表视图中任务或 bug 的上下文显示之外,还有 PHP Explorer 视图的延续。一旦任务被激活,浏览器中列出的文件将被过滤到 bug 上下文中的文件。这是有利的,因为它再次将许多文件的大型项目的混乱减少到只有那些与手头的 bug 或任务相关的文件。这个特性可以通过 PHP Explorer 工具栏图标来切换打开或关闭,这个图标看起来像三个炮弹,带有“关注活动任务”的气泡帮助文本

Bugzilla 的另一个很棒的特性也可以在 Zend Studio for Eclipse 中维护,那就是管理与任务或 bug 相关的附件。如果您有附带的文档,如完整的 bug 报告、设计文档、测试计划或已发现 bug 的屏幕截图,这将非常方便。图 12-9 显示了一个 jpg 图像到一个任务的附件。

images

***图 12-9。*工作室显示的 Bug,附带一个文件

另一件要考虑的事情是,如果你的开发团队中的一些成员不使用 Zend Studio for Eclipse,那会怎样,这可能令人震惊。这将有效地结束在该 IDE 中使用 Mylyn 的巨大优势。值得庆幸的是,一旦通过存储库连接与 Bugzilla 服务器建立了连接(参见图 12-6 ),任务中记录的大部分数据将被转发到 bug 服务器,如果在 Bugzilla 中直接进行了任何更新,那么这些信息将定期下载到 Zend Studio for Eclipse。有一个持续的双向反馈,有助于保持一切同步。这种双向更新的时间可以在任务列表视图的首选项屏幕中控制,或者通过单击鼠标来触发(任务列表工具栏项目,看起来像一个上面有两个箭头的蓝色圆柱体)。要设置自动同步时间,只需通过单击视图任务栏上的视图菜单图标打开首选项控制窗口,您将看到首选项选项。如图图 12-10 所示。可以看到,这里有许多附加选项来控制任务列表视图。我不会在这里详细讨论它们,但是请注意,您可以跟踪每项任务所花费的时间,并且能够计算非活动时间段。

images

***图 12-10。*任务库更新的首选项窗口

在任务列表视图中,还可以按类别(按项目)顺序(这是默认设置)或按即将到来的截止日期查看 bug。这可以让你看到哪些任务需要立即关注,哪些可以等待一段时间。任务栏上有一个开关,允许你用“分类”和“预定”的标题来控制这个特性它是右边第二个工具栏项目。沿着这个任务栏你会看到更多的选项,允许你控制它的信息。从右边数第三个,有一条线穿过勾号图标,是另一个开关;这个工具可以让你过滤掉所有已经完成的任务,这样就可以让你的视野更加清晰。接下来是折叠/展开按钮,允许您管理所有任务的列表显示。下一个(看起来像三个炮弹)是另一个开关,但这个允许你过滤本周的所有任务,再次帮助你管理被太多工作淹没的可能性。最后,在这个工具栏上是“查看”菜单(已经提到用于查看首选项),您可能已经注意到在这个弹出菜单上有许多其他选项。这里值得一提的最后一个是标题为“显示 UI 图例”的。点击此菜单项,您将看到如图图 12-11 所示的项目列表。简而言之,这是您将在任务列表视图中看到的所有图标的备忘单,简要解释了每个图标的含义。

images

图 12-11。【Zend Studio for Eclipse 中的 Mylyn 图标图例

外推利益

既然您已经看到了 Mylyn 和 Bugzilla 集成特性的许多好处,我想向您展示一些可以在 Zend Studio for Eclipse 的其他领域获得的推断性好处。这些是与其他集成有一些重叠的特性,因此对您和您的敏捷开发团队是有益的。

第一次外推可以在任务窗口的上下文区域中看到。在这里,如果您与 Subversion (SVN)或 CVS 之类的代码存储库进行了集成(现在谁不这样做呢),您可以在有问题的 bug 的过滤视图(上下文)中看到并管理整个存储库的交互。市场上还有其他类似的软件库工具,如 Git 和 Mercurial,但我们关注 SVN 是因为它在 Zend Studio 中的集成点。如图 12-12 中的所示,我打开了一个上下文窗口,显示了一个已经被修改并保存在本地的文件,在文件名DemoDebug.php旁边有一个>标记。正如您所看到的,这是一种更清晰的方式来查看您的任务和项目中的哪些文件需要提交到存储库中。请记住,只有当您在 Mylyn 中“激活”了 bug 时,才能管理这种文件跟踪。

images

图 12-12。【Zend Studio 中一个 bug 的上下文窗口

当您在代码评审期间查看代码变更时,可以看到下一个有益的推断。在这里,您可以从 bug 的上下文中访问短列表;右键单击项目(或单个文件)将允许您将您的工作与代码库中最新版本(修订号)的工作进行比较。如果愿意,您还可以在存储库中追溯到以前的承诺(修订号)。图 12-13 显示了在整个项目中比较承诺历史的可能性。如果您愿意,这种类型的代码更改审查也可以在您保存文件的本地历史上执行。当然,选择使用哪一个(存储库或本地历史)取决于您,但也可能取决于您将工作提交到存储库的频率。

images

***图 12-13。*本地文件与 SVN 存储库文件的历史比较。

在图 12-13 中,你可以在顶部看到 SVN 版本号列表,后面是这些版本提交的详细时间。屏幕下方的下一部分显示了在所选修订号的活动期间发生了更改的文件,这是最后一次执行提交(这不同于我们当前在本地进行的工作,还没有提交到存储库中)。屏幕下方的下一个部分实际上显示了我们正在为这个特定任务工作并且尚未提交的已更改文件,下面的部分显示了已经进行的实际逐行代码更改,以及它们与存储库中文件的当前状态相比如何。

总结

现在,我已经向您介绍了这一奇妙的工具集合,以及如何从 Zend Studio for Eclipse 中管理和控制它(主要是),您应该有足够的信息来开始使用这些工具,从而为您的开发工作带来巨大的好处。如果我没有最终发现这种编程方法,我无法想象今天我管理任务和项目的能力会达到什么程度。当然,我花了几年时间来实现这种工具组合,首先将 Zend Studio for Eclipse 和 SVN 结合使用,然后将 Bugzilla 添加到组合中,最后发现 Zend Studio for Eclipse 和 Mylyn 之间的集成。但是,您可以立即开始使用这种完全集成的产品,节省了我花在探索上的时间。

如果您现在才发现极限编程(XP)、代码集会、早期/经常发布等敏捷开发的巨大优势,那么您也应该看到这种工具的聚合如何帮助您变得更加敏捷。

您可能已经注意到,我并没有涵盖这些工具的每个方面以及它们各自的工具栏按钮,因为在本章的范围内涵盖会变得非常乏味,而且通过自己发现某些细微差别并获得经验也可以获得相当高的成就。获得一点“发现了!”当你协同使用这些工具的时候,你自己的时刻确实非常令人满意。然而,我不想让你没有一些资源,你可以用这些工具进一步学习和扩展你的知识。它们是:

  • Zend Studio for Eclipse 主页:[www.zend.com/en/products/studio/](http://www.zend.com/en/products/studio/)
  • Bugzilla 主页:[www.bugzilla.org/](http://www.bugzilla.org/)
  • Mylyn 月食页面:[www.eclipse.org/mylyn/](http://www.eclipse.org/mylyn/)
  • Mylyn 主页:[tasktop.com/mylyn/](http://tasktop.com/mylyn/)
  • 视频:Mik Kersten 博士的 W-JAX 2008 主题演讲:重新定义 IDE 的“我”:[tasktop.com/videos/w-jax/kersten-keynote.html](http://tasktop.com/videos/w-jax/kersten-keynote.html)

十三、重构、单元测试和持续集成

作为开发人员,我们的目标是实现稳定、高质量的代码库。这减少了花费在调试上的时间,并且方便了频繁的发布。在这一章中,我们将研究三种技术来提高我们代码的可靠性,并使其更容易和更安全地修改。这些技术是重构、单元测试和持续集成。

重构是一种修改代码结构的方式,目的是提高其质量。当我们重构时,我们并不试图添加或修改功能。重构是编程中必要且自然的一部分。尽管我们最大的意图是在重构时不修改功能,但重要的是要意识到无意中这样做是很容易的。以这种方式引入的 bug 很难被发现,它们会潜伏很长一段时间,直到有人注意到它们。

单元测试有助于确保由重构引入的非预期效果立即被注意到。当一个单元测试失败时,我们检查失败的原因。如果我们预料到失败是因为我们有意地调整了功能,那么我们简单地调整测试直到它通过。然而,如果失败是意外的,那么我们需要修复代码中新引入的 bug。

持续集成(CI)在整个项目中执行质量保证(QA)。团队成员每天(甚至更频繁地)将代码变更集成到项目的源代码控制库中。CI 服务器轮询存储库中的更改,在检测到代码更改后,以固定的时间间隔或按需执行自动“构建”。构建执行诸如运行单元测试和静态分析之类的任务。持续集成的频繁、自动的本质会迅速提醒我们代码中的变化已经“破坏了构建”中断构建指的是在团队成员提交了他们的代码后,自动化步骤不再正确运行。在 PHP 这样的非编译语言中,这通常是指一个或多个单元测试失败。CI 的使用让我们对我们的代码库更有信心,这反过来允许更频繁、更稳定的发布。CI 还允许我们运行构建脚本,它可以为我们执行一系列的命令和任务,否则可能会重复、枯燥、耗时和/或容易出错。

重构

以下是重构的示例:

  • 通过创建一个我们可以调用的新函数来消除重复代码。
  • 用简化的语句或描述性函数名替换复杂的逻辑表达式,以提高代码可读性。
  • 从一个大类中提取方法,并将它们移动到一个新的或更合适的类中。
  • 减少多层控制结构(if/elseforforeachwhileswitch)嵌套。
  • 面向对象的设计变更,如扩展基类或使用设计模式,如 builder 或 singleton。

有很多不同的重构方法可以实现。Martin Fowler 是这一编程领域的先驱,他对几种“代码气味”进行了分类,并给出了解决方法。关于重构的好书包括以下内容:

  • 《重构:改进现有代码的设计》作者:马丁·福勒、肯特·贝克、约翰·布兰特、威廉·奥普戴克和约翰·罗伯茨(艾迪森-韦斯利,1999 年)
  • Francesco Trucchia 和 Jacopo Romei(2010 年出版)的《PHP 重构》

代码中的重复是我们应该重构它的必然信号。将代码的逻辑部分封装到函数中是一个基本的编程原则。如果我们将代码剪切/复制到多个地方,那么我们就绕过了这个原则,大大增加了以后引入 bug 的可能性。

例如,假设我们将一段代码复制到五个不同的地方。随着时间的推移,当我们试图修改块的功能时,我们不太可能记得更新所有五个地方的代码。如果我们将代码块提取为一个函数,我们只需要在一个地方修改它。如果我们在代码中添加测试,我们也只需要测试一个功能单元,而不是五个。

重构的隐患是在我们的代码中引入了意想不到的行为变化。这些行为变化经常发生在我们目前还没有使用的代码区域,并且很难被发现。这就是重构和单元测试携手并进的原因。

重构时,代码的总长度可能会增加,但这没关系。重构的目的不是减少我们代码的大小。许多添加的行都是以空白的形式,这使得我们的代码更容易阅读。

小的重构

清单 13-1 中显示了一个可以使用重构的代码示例。

***清单 13-1。*代码决定我们是否应该去散步

<?php define('OWN_A_DOG', true); define('TIRED', false); define('HAVE_NOT_WALKED_FOR_DAYS', false); define('NICE_OUTSIDE', false); `define('BORED', true);

if ( (OWN_A_DOG && (!TIRED || HAVE_NOT_WALKED_FOR_DAYS)) || (NICE_OUTSIDE && !TIRED) || BORED ) {     goForAWalk(); }

function goForAWalk() {     echo "Going for a walk"; } ?>`

我们的第一次重构(清单 13-2 )将配置选项提取到一个外部文件中(清单 13-3 )。

***清单 13-2。*一个小的重构来包含我们的配置文件(清单 13-3 )

`<?php require_once('walkConfig.php');

if ( (OWN_A_DOG && (!TIRED || HAVE_NOT_WALKED_FOR_DAYS)) || (NICE_OUTSIDE && !TIRED) || BORED ) {     goForAWalk(); }

function goForAWalk() {   echo "Going for a walk"; }

?>`

***清单 13-3。*我们的配置文件,walkConfig.php

<?php define('OWN_A_DOG', true); define('TIRED', false); define('HAVE_NOT_WALKED_FOR_DAYS', false); define('NICE_OUTSIDE', false); define('BORED', true); ?>

长逻辑表达式,如清单 13-1 中的一个,可以提取到一个函数中以增加可读性,如清单 13-4 中的所示。

***清单 13-4。*通过将逻辑表达式放入单独的函数中来提高可读性

<?php require_once('walkConfig.php'); `if (shouldWalk()) {     goForAWalk(); }

function shouldWalk() {     return ( (OWN_A_DOG && (!TIRED || HAVE_NOT_WALKED_FOR_DAYS)) ||              (NICE_OUTSIDE && !TIRED) ||              BORED); }

function goForAWalk() {   echo "Going for a walk"; }

?>`

乍一看,我们似乎只是在逻辑的位置上进行洗牌。这是真的。然而,由于这种变化,主程序流程更容易理解。此外,如果逻辑在我们的程序中重复,我们现在可以重用该函数。此外,我们可以通过继续拆分逻辑来使我们的新函数更具可读性,如清单 13-5 所示。

***清单 13-5。*将一个逻辑函数分成两个附加的更小的函数

`<?php require_once('walkConfig.php');

if (shouldWalk()) {     goForAWalk(); }

function shouldWalk() {     return ( timeToWalkTheDog() || feelLikeWalking() ); }

function timeToWalkTheDog() {    return (OWN_A_DOG && (!TIRED || HAVE_NOT_WALKED_FOR_DAYS)); }

function feelLikeWalking() {    return ((NICE_OUTSIDE && !TIRED) || BORED); }

function goForAWalk() {   echo "Going for a walk"; }

?>`

清单 13-1 和清单 13-5 具有相同的功能。然而,清单 13-5 更容易阅读、重用和测试。

我们的下一个例子(清单 13-6 )重构起来稍微复杂一点,并且向我们提取的函数引入了参数,如清单 13-7 所示。

***列表 13-6。*带重复的 PHP 脚本

`<?php

total=0;total = 0; value = rand(1, 10); if (value > 5) {     multiple = 2;     total  =total  = value;     total=total *= multiple;     total+=(10total += (10 - value);     print "goodbye
";     print "initial value is value<br/>";    print"thetotalisvalue<br/>";     print "the total is total
"; } else {     multiple=7;    multiple = 7;     total  = value;    value;     total *= multiple;    multiple;     total += (10 - value);    print"hello!<br/>";    print"initialvalueisvalue);     print "hello!<br/>";     print "initial value is value
";     print "the total is $total
"; } ?>`

***清单 13-7。*清单 13-6 中的 PHP 脚本被重构以消除重复

`<?php

total=0;total = 0; value = rand(1, 10); if (value > 5) {     total = changeTotalValue(value,2);    displayMessage("goodbye",value, 2);     displayMessage("goodbye", value, total); } else {     total = changeTotalValue(value,7);    displayMessage("goodbye",value, 7);     displayMessage("goodbye", value, $total); }

function changeTotalValue(value,value, multiple){     total=total = value * multiple;    multiple;     total += (10 - value);    returnvalue);     return total; }

function displayMessage(greeting,greeting, value,total){     print "greeting
";     print "initial value is value<br/>";    print"thetotalisvalue<br/>";     print "the total is total
"; } ?>`

当从清单 13-6 中的代码转到清单 13-7 中的代码时,您可能会问自己,“我们怎么知道我们没有引入任何不希望的副作用?”简而言之,没有测试,我们无法确定。

一个更大的遗留代码示例

考虑清单 13-8 中的大脚本,我们将重构它,使它更容易理解。在脚本中,给定一个源和目的地位置,计算出我们的最佳旅行模式,并显示旅行的总时间。例如,我们假设了简单的条件。这包括始终能够以直线路径到达目的地,并且汽车永远不会耗尽汽油。

***清单 13-8。*我们最初遗留的代码脚本,travel_original.php

`<?php

error_reporting ( E_ALL ); //constants define ( 'WALK_STEP', 0.25 ); //quarter meter steps define ( 'BIKE_STEP', 3.00 ); //three meter steps define ( 'BUS_STEP', 30.00 ); //bus steps define ( 'BUS_DELAY', 300 ); //five minutes to wait for bus define ( 'CAR_STEP', 50.00 ); //car steps define ( 'CAR_DELAY', 20 ); //twenty seconds to get car up to speed define ( 'HAS_CAR', true ); define ( 'HAS_MONEY', true ); define ( 'IN_A_RUSH', true ); define ( 'ON_BUS_ROUTE', true ); define ( 'HAS_BIKE', false ); define ( 'STORMY_WEATHER', false ); define ( 'WALKING_MAX_DISTANCE', 2500 );

class Location {

        public x=0;        publicx = 0;         public y = 0;

        public function __construct(x,x, y) {                 this>x=this->x = x;                 this>y=this->y = y;         }

        public function toString() {                 return "(" . round ( this>x,2).",  ".round(this->x, 2 ) . ",  " . round ( this->y, 2 ) . ")";         }

} function travel(Location src,Locationsrc, Location dest) {         //calculate the direction vector         distancey=distance_y = dest->y - src>y;        src->y;         distance_x = dest>xdest->x - src->x;         $angle = null;

        if (distance_x) {             if (distance_y) {                 angle=atan(angle = atan(distance_y / distance_x);             } else {                 if (distance_x > 0) {                     angle = 0.0; //right                 } else {                     angle = 180.0; //left                 }             }         } else {             if (distance_y) {                 if (distance_y < 0) {                     angle = - 90.0;    //down                 } else {                     angle = 90.0;      //up                 }             }         }           angleinradians=deg2rad(angle_in_radians = deg2rad ( angle );

        distance=0.0;        //calculatethestraightlinedistance        if(distance = 0.0;         //calculate the straight line distance         if (dest->y == src->y) {                 distance = dest>xdest->x - src->x;         } else if (dest>x==dest->x == src->x) {                 distance=distance = dest->y - src->y;         } else {                 distance = sqrt ( (distancexdistance_x * distance_x) +    (distanceydistance_y * distance_y) );         }

        print "Trying to go from " . src>toString().              "to".src->toString () .               " to " . dest->toString () . "
\n";         if (IN_A_RUSH) {                 print "In a rush
\n";         }         print "Distance is " . distance."inthedirectionof".               distance . " in the direction of " .                angle . " degrees
";

        $time = 0.0;

        has_options = false;         if (HAS_CAR || (HAS_MONEY && ON_BUS_ROUTE) || HAS_BIKE) {                 has_options = true;         } if (has_options) {                 if (STORMY_WEATHER) {                         if (HAS_CAR) {` `//drive                                 while ( abs ( src->x - dest>x)>CARSTEP                                        abs(dest->x ) > CAR_STEP ||                                         abs ( src->y - dest->y ) > CAR_STEP ) {                                         src->x += (CAR_STEP * cos ( angleinradians));                                        angle_in_radians ));                                         src->y += (CAR_STEP * sin ( angleinradians));                                        ++angle_in_radians ));                                         ++ time;                                         print "driving a car... currently at (" .                                               round ( src>x,2).",  ".                                              round(src->x, 2 ) . ",  " .                                               round ( src->y, 2 ) . ")
\n";                                 }                                 print "Got to destination by driving a car
";                         } else if (HAS_MONEY && ON_BUS_ROUTE) {                                 //take the bus                                 while ( abs ( src>xsrc->x - dest->x ) > BUS_STEP ||                                         abs ( src>ysrc->y - dest->y ) > BUS_STEP ) {                                         src>x+=(BUSSTEPcos(src->x += (BUS_STEP * cos ( angle_in_radians ));                                         src>y+=(BUSSTEPsin(src->y += (BUS_STEP * sin ( angle_in_radians ));                                         ++ time;                                        print"onthebus...currentlyat(".                                               round(time;                                         print "on the bus... currently at (" .                                                round ( src->x, 2 ) . ",  " .                                                round ( src->y, 2 ) . ")<br/>\n";                                 }                                 print "Got to destination by riding the bus<br/>";                         } else {` `//ride bike                                 while ( abs ( src->x - dest>x)>BIKESTEP                                         abs(dest->x ) > BIKE_STEP ||                                          abs ( src->y - dest->y ) > BIKE_STEP ) {                                         src->x += (BIKE_STEP * cos ( angleinradians));                                        angle_in_radians ));                                         src->y += (BIKE_STEP * sin ( angleinradians));                                        ++angle_in_radians ));                                         ++ time;                                         print "biking... currently at (" .                                                round ( src>x,2).",  ".                                               round(src->x, 2 ) . ",  " .                                                round ( src->y, 2 ) . ")
\n";                                 }                                 print "Got to destination by biking
";                         }                 } else {                         if (distance < WALKING_MAX_DISTANCE && ! IN_A_RUSH) { //walk                                 while ( abs ( src->x - dest>x)>WALKSTEP                                        abs(dest->x ) > WALK_STEP ||                                         abs ( src->y - dest->y ) > WALK_STEP ) {                                         src->x += (WALK_STEP * cos ( angleinradians));                                        angle_in_radians ));` `                                        src->y += (WALK_STEP * sin ( angleinradians));                                        ++angle_in_radians ));                                         ++ time;                                         print "walking... currently at (" .                                                round ( src>x,2).",  ".                                               round(src->x, 2 ) . ",  " .                                                round ( src->y, 2 ) . ")
\n";                                 }                                 print "Got to destination by walking
";                         } else {                                 if (HAS_CAR) {                                         //drive                                         time+=CARDELAY;                                        while(abs(time += CAR_DELAY;                                         while ( abs ( src->x - dest>x)>CARSTEP                                                abs(dest->x ) > CAR_STEP ||                                                 abs ( src->y - dest->y ) > CAR_STEP ) {                                                 src->x += (CAR_STEP *                                                             cos ( angleinradians));                                                angle_in_radians ));                                                 src->y += (CAR_STEP *                                                             sin ( angleinradians));                                                ++angle_in_radians ));                                                 ++ time;                                                 print "driving a car... currently at (" .                                                            round ( src>x,2).",  ".                                                       round(src->x, 2 ) . ",  " .                                                        round ( src->y, 2 ) . ")
\n";                                         }                                         print "Got to destination by driving a car
";                                 } else if (HAS_MONEY && ON_BUS_ROUTE) {                                         //take the bus                                         time+=BUSDELAY;                                        while(abs(time += BUS_DELAY;                                         while ( abs ( src->x - dest>x)>BUSSTEP                                                abs(dest->x ) > BUS_STEP ||                                                 abs ( src->y - dest->y ) > BUS_STEP ) {                                                 src->x += (BUS_STEP *                                                             cos ( angleinradians));                                                angle_in_radians ));                                                 src->y += (BUS_STEP *     sin ( angleinradians));                                                ++angle_in_radians ));                                                 ++ time;                                                 print "on the bus... currently at (" .                                                 round ( src>x,2).",  ".                                        round(src->x, 2 ) . ",  " .                                         round ( src->y, 2 ) . ")
\n";                                         }                                         print "Got to destination by riding the bus
";                                 } else {                                         //ride bike                                         while ( abs ( src>xsrc->x - dest->x ) > BIKE_STEP ||                                                     abs ( src>ysrc->y - dest->y ) > BIKE_STEP ) {                                                 src>x+=(BIKESTEP                                                            cos(src->x += (BIKE_STEP *                                                             cos ( angle_in_radians ));                                                 src>y+=(BIKESTEP                                                            sin(src->y += (BIKE_STEP *                                                             sin ( angle_in_radians ));                                                 ++ time;                                                print"biking...currentlyat(".                                                round(time;                                                 print "biking... currently at (" .                                                 round ( src->x, 2 ) . ",  " .                                                 round ( src->y, 2 ) . ")<br/>\n";                                         }                                         print "Got to destination by biking<br/>";                                 }                         }                 }         } else {                 if (STORMY_WEATHER) {                         print "ERROR: Storming<br/>";                 } else if (distance < WALKING_MAX_DISTANCE) {                         //walk                         while ( abs ( src>xsrc->x - dest->x ) > WALK_STEP ||                                 abs ( src>ysrc->y - dest->y ) > WALK_STEP ) {                                 src>x+=(WALKSTEPcos(src->x += (WALK_STEP * cos ( angle_in_radians ));                                 src>y+=(WALKSTEPsin(src->y += (WALK_STEP * sin ( angle_in_radians ));                                 ++ time;                                print"walking...currentlyat(".                                      round(time;                                 print "walking... currently at (" .                                       round ( src->x, 2 ) . ",  " .                                       round ( src->y, 2 ) . ")<br/>\n";                         }                         print "Got to destination by walking<br/>";                 } else {                         print "ERROR: Too far to walk<br/>";                 }         }         print "Total time was: " . date ( "i:s", time ); } //sample usage //travel ( new Location ( 1, 3 ), new Location ( 4, 10 ) ); ?>`

在初始状态,清单 13-8 中的代码是一个单一的、非常大的函数,做了太多的工作。我们完全没有测试。如果我们的代码依赖于这个脚本的准确性,我们就不能确定它是否可信。此外,如果我们需要添加功能,我们将需要谨慎行事,以限制引入副作用的可能性。这段代码并不是遗留代码的典型,但它可能更糟——在这个例子中,我们不需要处理任何全局变量。

travel函数嵌套层次过多。试图像现在这样增加测试是非常困难的。它也承担了太多的责任。它会计算到某条路线的距离,确定旅行模式,并为您指引方向。这个函数充满了行内注释,如果我们把它分解成名字更有意义的小函数,就没有必要了。我们还可以看到有很多重复的代码。

我们可以运行一些对travel函数的示例调用,以直观地感受该函数是否工作,但这一点也不严谨。我们需要的是重构我们的代码并实现单元测试。

当我们重构代码时,我们需要问自己一些基本的问题,例如:

  • 有什么可以很容易修改的?(无依赖性)
  • 有重复吗?(是否应该创建一个函数?)
  • 我们能简化代码或者让它更容易理解吗?(可读性)
  • 我们是否让代码足够简单,以便我们可以添加测试?

重构并不只有一种方式。对于不同的程序员(甚至在不同的时间对于同一个程序员)来说,next 的改进可能会有所不同。通过练习,下一步该怎么做变得更加明显和容易察觉。重构时何时添加测试也是一门艺术。理想情况下,我们希望在做出改变的任何时候添加新的测试。实际上,这并不总是可行的。

相反,我们将一次展示一些重构的结果。

要查看一步一步的过程,请从本书的页面[www.apress.com](http://www.apress.com)下载源代码。

作为第一组重构,我们将删除脚本顶部的define语句,并将它们放入一个新文件config.php ( 清单 13-9 )。这有促进代码重用的额外好处。如果我们需要不同脚本中的定义,我们可以包含配置文件。

我们还将把Location类移动到它自己的文件location.php ( 清单 13-10 )。我们将把travel函数的名字改为execute,并将其封装在一个名为Travel的类中(清单 13-12 )。接下来,我们将提取位于execute函数顶部的代码块,该代码块显示了我们要去的地方,并将它放入一个新的助手类 TravelView 中(清单 13-11 )。最后,我们将提取 travel 函数顶部附近的代码块,该代码块决定我们是否有车辆选项。我们还没有测试过,但是稍微更有组织的主类现在看起来像清单 13-12 中的。

***清单 13-9。*我们的设置文件,config.php

<?php define ( 'WALK_STEP', 0.25 ); //quarter meter steps define ( 'BIKE_STEP', 3.00 ); //three meter steps define ( 'BUS_STEP', 30.00 ); //bus steps define ( 'BUS_DELAY', 300 ); //five minutes to wait for bus define ( 'CAR_STEP', 50.00 ); //car steps define ( 'CAR_DELAY', 20 ); //twenty seconds to get car up to speed define ( 'HAS_CAR', true ); define ( 'HAS_MONEY', true ); define ( 'IN_A_RUSH', true ); define ( 'ON_BUS_ROUTE', true ); define ( 'HAS_BIKE', false ); define ( 'STORMY_WEATHER', false ); define ( 'WALKING_MAX_DISTANCE', 2500 ); ?>

***清单 13-10。*Location类,location.php类。

`<?php

class Location {

    public x=0;    publicx = 0;     public y = 0;

    public function __construct(x,x, y) {         this>x=this->x = x;         this>y=this->y = y;     }     public function toString() {         return "(" . round(this>x,2).",  ".round(this->x, 2) . ",  " . round(this->y, 2) . ")";     }

}

?>`

清单 13-11*。*我们的View班,travelView.php

`<?php

error_reporting(E_ALL); require_once ('config.php'); require_once ('location.php');

class TravelView {

    public static function displayOurIntendedPath( angle,angle, distance,                                                    Location src,Locationsrc, Location dest) {         print "Trying to go from " . src>toString()."to".              src->toString() . " to " .               dest->toString() . "
\n";         if (IN_A_RUSH) {             print "In a rush
\n";         }         print "Distance is " . distance."inthedirectionof".              distance . " in the direction of " .               angle . " degrees
";     }

} ?>`

***清单 13-12。*第一轮重构后的travel_original.php文件

`<?php.

error_reporting(E_ALL); require_once ('config.php'); require_once ('location.php'); require_once ('travelView.php');

class Travel{

public function execute(Location src,Locationsrc, Location dest) {         //calculate the direction vector         distancey=distance_y = dest->y - src>y;        src->y;         distance_x = dest>xdest->x - src->x;         angle=null;        angle = null;         time = 0.0; if (distance_x) {             if (distance_y) {                 angle=atan(angle = atan(distance_y / distance_x);             } else {                 if (distance_x > 0) {                     angle = 0.0; //right                 } else {                     angle = 180.0; //left                 }             }         } else {             if (distance_y) {                 if (distance_y < 0) {                     angle = - 90.0;    //down                 } else {                     angle = 90.0;      //up                 }             }         }         return angle;          angle;           angle_in_radians = deg2rad ( $angle );

        distance=0.0;        //calculatethestraightlinedistance        if(distance = 0.0;         //calculate the straight line distance         if (dest->y == src->y) {                 distance = dest>xdest->x - src->x;         } else if (dest>x==dest->x == src->x) {                 distance=distance = dest->y - src->y;         } else {                 distance = sqrt ( (distancexdistance_x * distance_x) +                            (distanceydistance_y * distance_y) );         }

**        TravelView::displayOurIntendedPath(angle,angle, distance, src,src, dest);**

**        hasoptions=has_options = this->doWeHaveOptions();        **

        if (has_options) {                 if (STORMY_WEATHER) {                         if (HAS_CAR) {                                 //drive                                 while ( abs ( src->x - dest>x)>CARSTEP                                        abs(dest->x ) > CAR_STEP ||                                         abs ( src->y - dest->y ) > CAR_STEP ) {                                         src->x += (CAR_STEP * cos ( angleinradians));                                        angle_in_radians ));                                         src->y += (CAR_STEP * sin ( angleinradians));                                        ++angle_in_radians ));                                         ++ time;                                         print "driving a car... currently at (" .                                               round ( src>x,2).",  ".                                              round(src->x, 2 ) . ",  " .                                               round ( src->y, 2 ) . ")
\n";                                 }                                 print "Got to destination by driving a car
";                         } else if (HAS_MONEY && ON_BUS_ROUTE) { //take the bus                                 while ( abs ( src>xsrc->x - dest->x ) > BUS_STEP ||                                         abs ( src>ysrc->y - dest->y ) > BUS_STEP ) {                                         src>x+=(BUSSTEPcos(src->x += (BUS_STEP * cos ( angle_in_radians ));                                         src>y+=(BUSSTEPsin(src->y += (BUS_STEP * sin ( angle_in_radians ));                                         ++ time;                                        print"onthebus...currentlyat(".                                               round(time;                                         print "on the bus... currently at (" .                                                round ( src->x, 2 ) . ",  " .                                                round ( src->y, 2 ) . ")<br/>\n";                                 }                                 print "Got to destination by riding the bus<br/>";                         } else {                                 //ride bike                                 while ( abs ( src->x - dest>x)>BIKESTEP                                        abs(dest->x ) > BIKE_STEP ||                                         abs ( src->y - dest->y ) > BIKE_STEP ) {                                         src->x += (BIKE_STEP * cos ( angleinradians));                                        angle_in_radians ));                                         src->y += (BIKE_STEP * sin ( angleinradians));                                        ++angle_in_radians ));                                         ++ time;                                         print "biking... currently at (" .                                                round ( src>x,2).",  ".                                               round(src->x, 2 ) . ",  " .                                                round ( src->y, 2 ) . ")
\n";                                 }                                 print "Got to destination by biking
";                         }                 } else {                         if (distance < WALKING_MAX_DISTANCE && ! IN_A_RUSH) { //walk                                 while ( abs ( src->x - dest>x)>WALKSTEP                                        abs(dest->x ) > WALK_STEP ||                                         abs ( src->y - dest->y ) > WALK_STEP ) {                                         src->x += (WALK_STEP * cos ( angleinradians));                                        angle_in_radians ));                                         src->y += (WALK_STEP * sin ( angleinradians));                                        ++angle_in_radians ));                                         ++ time;                                         print "walking... currently at (" .                                                round ( src>x,2).",  ".                                               round(src->x, 2 ) . ",  " .                                                round ( src->y, 2 ) . ")
\n";                                 }                                 print "Got to destination by walking
";                         } else {                                 if (HAS_CAR) {                                         //drive                                         time+=CARDELAY;                                        while(abs(time += CAR_DELAY;                                         while ( abs ( src->x - dest>x)>CARSTEP                                                abs(dest->x ) > CAR_STEP ||                                                 abs ( src->y - dest->y ) > CAR_STEP ) {                                                 src->x += (CAR_STEP *                                                             cos ( angleinradians));                                                angle_in_radians ));                                                 src->y += (CAR_STEP *                                                             sin ( angleinradians));                                                ++angle_in_radians ));                                                 ++ time;                                                 print "driving a car... currently at (" .                                                            round ( src>x,2).",  ".                                                       round(src->x, 2 ) . ",  " .                                                        round ( src->y, 2 ) . ")
\n";                                         }                                         print "Got to destination by driving a car
"; } else if (HAS_MONEY && ON_BUS_ROUTE) {                                         //take the bus                                         time+=BUSDELAY;                                        while(abs(time += BUS_DELAY;                                         while ( abs ( src->x - dest>x)>BUSSTEP                                                abs(dest->x ) > BUS_STEP ||                                                 abs ( src->y - dest->y ) > BUS_STEP ) { src->x += (BUS_STEP *                                                                                                                    cos ( angleinradians));                                                angle_in_radians ));                                                 src->y += (BUS_STEP *     sin ( angleinradians));                                                ++angle_in_radians ));                                                 ++ time;                                                 print "on the bus... currently at (" .                                                       round ( src>x,2).",  ".                                                      round(src->x, 2 ) . ",  " .                                                       round ( src->y, 2 ) . ")
\n";                                         }                                         print "Got to destination by riding the bus
";                                 } else {                                         //ride bike                                         while ( abs ( src>xsrc->x - dest->x ) > BIKE_STEP ||                                                     abs ( src>ysrc->y - dest->y ) > BIKE_STEP ) {                                                 src>x+=(BIKESTEP                                                            cos(src->x += (BIKE_STEP *                                                             cos ( angle_in_radians ));                                                 src>y+=(BIKESTEP                                                            sin(src->y += (BIKE_STEP *                                                             sin ( angle_in_radians ));                                                 ++ time;                                                print"biking...currentlyat(".                                                       round(time;                                                 print "biking... currently at (" .                                                        round ( src->x, 2 ) . ",  " .                                                        round ( src->y, 2 ) . ")<br/>\n";                                         }                                         print "Got to destination by biking<br/>";                                 }                         }                 }         } else {                 if (STORMY_WEATHER) {                         print "ERROR: Storming<br/>";                 } else if (distance < WALKING_MAX_DISTANCE) {                         //walk while ( abs ( src>xsrc->x - dest->x ) > WALK_STEP ||                                 abs ( src>ysrc->y - dest->y ) > WALK_STEP ) {                                 src>x+=(WALKSTEPcos(src->x += (WALK_STEP * cos ( angle_in_radians ));                                 src>y+=(WALKSTEPsin(src->y += (WALK_STEP * sin ( angle_in_radians ));                                 ++ time;                                print"walking...currentlyat(".                                       round(time;                                 print "walking... currently at (" .                                        round ( src->x, 2 ) . ",  " .                                        round ( src->y, 2 ) . ")<br/>\n";                         }                         print "Got to destination by walking<br/>";                 } else {                         print "ERROR: Too far to walk<br/>";                 }         }` `        print "Total time was: " . date ( "i:s", time );   }

  private function doWeHaveOptions(){         has_options = false;         if (HAS_CAR || (HAS_MONEY && ON_BUS_ROUTE) || HAS_BIKE) {                 has_options = true;         }         return $has_options;   } } ?>`

我们将再做一组重构,然后添加一些测试。我们将把乘坐公共汽车、驾驶汽车、步行或骑自行车的代码提取到逻辑函数中。我们还将一些数学计算提取到一个单独的类中,travelMath.php ( 清单 13-13 )。在我们重构之前,注意乘坐公共汽车的实现与上面的不一致(清单 13-12 )。一个实例设置了一个BUS_DELAY时间,而另一个没有。作为一个试图处理这些遗留代码的程序员,我们不得不问,“我们应该总是包括延迟吗,我们应该永远不包括延迟吗,我们应该有时包括延迟吗?这是有意的差异还是一个错误?”这很可能是一个意外的疏忽。如果我们不剪切/复制重复的代码,而是使用函数,这些类型的普遍情况是可以避免的。

清单 13-13*。**Calculation班,travelMath.php*

`<?php

error_reporting ( E_ALL ); require_once ('location.php');

class TravelMath {

        public static function calculateDistance() {                 distance=0.0;                //calculatethestraightlinedistance                if(distance = 0.0;                 //calculate the straight line distance                 if (dest->y == src->y) {                         distance = dest>xdest->x - src->x;                 } else if (dest>x==dest->x == src->x) {                         distance=distance = dest->y - src->y;                 } else {                         distance_y = dest>ydest->y - src->y;                         distancex=distance_x = dest->x - src>x;                        src->x;                         distance = sqrt ( (distancexdistance_x * distance_x) +    (distanceydistance_y * distance_y) );                 }                 return $distance;         }

        public static function calculateAngleInDegrees() {                                 //calculate the direction vector                 distancey=distance_y = dest->y - src>y;        src->y;         distance_x = dest>xdest->x - src->x;                 angle=null;            if(angle = null;` `            if (distance_x) {                        if (distance_y) {                            angle = atan(distancey/distance_y / distance_x);                        } else {                     if (distance_x > 0) {                         angle = 0.0; //right             } else {                         angle = 180.0; //left             }         }            } else {                if (distance_y) {                         if (distance_y < 0) {                     angle = - 90.0;    //down                         } else {                                 angle = 90.0;      //up                         }             }                 }     return angle;           }

        public static function isCloseToDest(src,src, dest, step){                 return (abs ( src->x - dest>x)<dest->x ) < step ||                         abs ( src>ysrc->y - dest->y ) < $step );         } } ?>`

***清单 13-14。*第二轮重构后的Travel

`<?php

error_reporting(E_ALL); require_once('config.php'); require_once('location.php'); require_once('travelMath.php'); require_once('travelView.php');

class Travel {

    private src=null;    privatesrc = null;     private dest = null;     private time=0.0;publicfunctionexecute(Locationtime = 0.0;` `public function execute(Location src, Location dest)     {         this->src = src;        src;         this->dest = dest;        dest;         this->time = 0.0;         angle=TravelMath::calculateAngleInDegrees(angle = TravelMath::calculateAngleInDegrees(src, dest);        dest);         angle_in_radians = deg2rad(angle);        angle);         distance = TravelMath::calculateDistance(src,src, dest);

        TravelView::displayOurIntendedPath(angle,angle, distance, src,src, dest);         hasoptions=has_options = this->doWeHaveOptions();

        if (has_options)         {             if (STORMY_WEATHER)             {                 if (HAS_CAR)                 {                     this->driveCar();                 } else if (HAS_MONEY && ON_BUS_ROUTE)                 {                     this->rideBus();                 } else                 {                     this->rideBike();                 }             } else             {                 if (distance < WALKING_MAX_DISTANCE && !IN_A_RUSH)                 {                     this->walk();                 } else                 {                     if (HAS_CAR)                     {                         this->driveCar();                     } else if (HAS_MONEY && ON_BUS_ROUTE)                     {                         this->rideBus();                     } else                     {                         this->rideBike();                     }                 }             }         } else         {             if (STORMY_WEATHER)             {                 print "ERROR: Storming<br/>";             } else if (distance < WALKING_MAX_DISTANCE)             {                 this->walk();             } else             {                 print "ERROR: Too far to walk<br/>";             }` `}         print "Total time was: " . date("i:s", this->time);     }

    private function doWeHaveOptions()     {         has_options = false;         if (HAS_CAR || (HAS_MONEY && ON_BUS_ROUTE) || HAS_BIKE)         {             has_options = true;         }         return $has_options;     }

    private function driveCar()     {         this>time+=CARDELAY;        //drive        while(abs(this->time += CAR_DELAY;         //drive         while (abs(this->src->x - this>dest>x)>CARSTEP        abs(this->dest->x) > CAR_STEP ||         abs(this->src->y - this->dest->y) > CAR_STEP)         {             this->src->x += ( CAR_STEP * cos(this>angleinradians));            this->angle_in_radians));             this->src->y += ( CAR_STEP * sin(this>angleinradians));            ++this->angle_in_radians));             ++this->time;             print "driving a car... currently at (" . round(this>src>x,2).                    ",".round(this->src->x, 2) .                     ", " . round(this->src->y, 2) . ")
\n";         }

        print "Got to destination by driving a car
";     }

    private function rideBus()     {         //take the bus         this>time+=BUSDELAY;        while(abs(this->time += BUS_DELAY;         while (abs(this->src->x - dthis>est>x)>BUSSTEP        abs(dthis->est->x) > BUS_STEP ||         abs(this->src->y - this->dest->y) > BUS_STEP)         {             this->src->x += ( BUS_STEP * cos(this>angleinradians));            this->angle_in_radians));             this->src->y += ( BUS_STEP * sin(this>angleinradians));            ++this->angle_in_radians));             ++this->time;             print "on the bus... currently at (" . round(this>src>x,2).                    ",".round(this->src->x, 2) .                     ", " . round(this->src->y, 2) . ")
\n";         }         print "Got to destination by riding the bus
";     }

    private function rideBike()     {         //ride bike         while (abs(this>src>xthis->src->x - this->dest->x) > BIKE_STEP ||         abs(this>src>ythis->src->y - this->dest->y) > BIKE_STEP)         {             this>src>x+=(BIKESTEPcos(this->src->x += ( BIKE_STEP * cos(this->angle_in_radians));             this>src>y+=(BIKESTEPsin(this->src->y += ( BIKE_STEP * sin(this->angle_in_radians));             ++this>time;            print"biking...currentlyat(".round(this->time;             print "biking... currently at (" . round(this->src->x, 2) .                     ", " . round($this->src->y, 2) . ")
\n";         }         print "Got to destination by biking
";     }

    private function walk()     {         //walk         while (abs(this>src>xthis->src->x - this->dest->x) > WALK_STEP ||         abs(this>src>ythis->src->y - this->dest->y) > WALK_STEP)         {             this>src>x+=(WALKSTEPcos(this->src->x += ( WALK_STEP * cos(this->angle_in_radians));             this>src>y+=(WALKSTEPsin(this->src->y += ( WALK_STEP * sin(this->angle_in_radians));             ++this>time;            print"walking...currentlyat(".round(this->time;             print "walking... currently at (" . round(this->src->x, 2) .                     ", " . round($this->src->y, 2) . ")
\n";         }         print "Got to destination by walking
";     }

}`

经过几次重构,我们的代码(清单 13-14 )更容易阅读、理解、修改和添加测试。我们消除了许多重复,我们还可以做更多的改进。在下一节中,我们将向我们的代码添加一些测试,从TravelMath类开始,它的函数已经完全没有依赖性了。

单元测试

为了确保我们的代码正常工作,我们需要测试它。我们希望我们的代码是具有很少依赖性的短块,这样我们就可以隔离和测试单个的功能单元。为了促进这一点,我们应该有松散耦合的代码,并在必要时使用依赖注入。我们还应该努力保持函数简短,参数数量少。

我们应该把函数重构到多短?就像一个类应该代表一个对象一样,一个函数应该做一件事情。如果一个功能要做多件事情,那么它应该被分解成更小的功能。这样做时,大多数函数的长度往往是 5 到 15 行。随着函数变得越来越小,它们就越来越容易理解。也有更少的空间给虫子。一本关于最佳函数、类长度和代码可读性的好书是罗伯特·马丁(Prentice Hall,2008)写的干净的代码:敏捷软件工艺手册

两个广泛使用的 PHP 单元测试框架是 PHPUnitSimpletest 。我们将在本章中使用 PHPUnit。PHPUnit 是塞巴斯蒂安·博格曼写的一个 xUnit 端口。由于它是 xUnit 家族的一部分,程序员熟悉 Java 的 JUnit 或 Java 的 NUnit 。NET 会发现入门相当容易。

images 注意这些框架可从以下网址在线获得:

https://github.com/sebastianbergmann/phpunit/

[www.simpletest.org/](http://www.simpletest.org/)

PHPUnit 手册在[www.phpunit.de/manual/current/en/index.html.](http://www.phpunit.de/manual/current/en/index.html.)

要用 PEAR 安装 PHPUnit,请使用以下命令:

pear channel-discover pear.phpunit.de pear channel-discover components.ez.no pear channel-discover pear.symfony-project.com pear install --alldeps phpunit/PHPUnit

当编写单元测试时,我们应该努力编写快速运行的、可重复的测试,这些测试隔离一小块代码的功能。这可能需要使用高级技术,如依赖注入和模拟对象。

PHPUnit 和 Simpletest 都支持模拟对象。模拟对象对于隔离我们想要测试的代码部分非常有用。它们还可以通过返回模拟结果来帮助我们快速测试,而不需要访问一个缓慢的(用单元测试术语来说)资源,比如一个数据库、一个文件或者一个 web 位置。

我们将返回到我们的Travel类,并在本章稍后添加一些测试。首先,我们将回到清单 13-5 中的小例子,它决定了我们是否应该出去走走并给它添加测试。有了测试框架,我们可以添加测试,断言带有给定输入参数的函数的预期结果是我们获得的实际结果。

在清单 13-15 中,我们将创建清单 13-5 中代码的面向对象版本。

***清单 13-15。*面向对象Walk类,walk.php

`<?php

class Walk {

    private optionkeys=array(             ownADog,tired,haveNotWalkedForDays,niceOutside,bored);    privateoption_keys = array(              'ownADog', 'tired', 'haveNotWalkedForDays', 'niceOutside', 'bored');     private options = array();     public function __construct()     {         foreach (this>optionkeysasthis->option_keys as key) {             this>options[this->options[key] = true;         }     }

    public function move()     {         if (this->shouldWalk()) {             this->goForAWalk();         }     }

    public function shouldWalk()     {         return (this>timeToWalkTheDog()this->timeToWalkTheDog() || this->feelLikeWalking());     }

    public function timeToWalkTheDog()     {         return (this->options['ownADog'] &&                (!this->options['tired'] || $this->options['haveNotWalkedForDays']));     }

    public function feelLikeWalking()     {         return ((this->options['niceOutside'] && !this->options['tired']) ||                  $this->options['bored']);     }

    public function __set(name,name, value)     {         if (in_array(name,name, this->option_keys)) {             this>options[this->options[name] = $value;         }     }

    private function goForAWalk()     {         echo "Going for a walk";     } }

//walk=newWalk();//walk = new Walk(); //walk->move(); ?>`

大多数 PHP 集成开发环境(ide),如 Netbeans 和 Eclipse,可以生成框架测试文件来帮助我们。ide 通常将我们的测试结果显示为彩色的红/绿条,以表示成功或失败。但是,您也可以直接从命令行运行 PHPUnit 或 Simpletest。我们还可以生成代码覆盖报告,显示已经测试的代码的百分比,以及哪些行没有被测试覆盖。

我们将创建我们的第一个 PHPUnit 类(清单 13-16 ),它还不包含任何测试。

***清单 13-16。*一个单元的测试骨架为Walk类,walkTest.php

`<?php

require_once dirname(FILE) . '/../walk.php';

/**  * Test class for Walk.  * Generated by PHPUnit on 2011-05-31 at 19:57:43.  */ class WalkTest extends PHPUnit_Framework_TestCase {

    /**      * @var Walk      */     protected $object;

    /**      * Sets up the fixture, for example, opens a network connection.      * This method is called before a test is executed.      */     protected function setUp()     {         $this->object = new Walk;     }

    /**      * Tears down the fixture, for example, closes a network connection.      * This method is called after a test is executed.      */     protected function tearDown()     {

    } } ?>`

在清单 13-16 中,我们扩展了PHPUnit_Framework_TestCasesetUp函数创建一个被测试类Walk的实例,并将其存储在$object中。teardown函数是我们在测试完成后关闭资源或销毁对象的地方。在没有添加测试的情况下,在 Netbeans IDE 中运行测试文件会产生如图图 13-1 所示的输出。

images

***图 13-1。*Netbeans IDE 中没有执行任何测试

我们将演示添加一个在清单 13-17 中失败的单元测试。结果如图图 13-2 所示。

***清单 13-17。*添加第一个失败的单元测试

`<?php

require_once dirname(FILE) . '/../walk.php';

class WalkTest extends PHPUnit_Framework_TestCase {

    protected $object;

    protected function setUp()     {         $this->object = new Walk;     }

    protected function tearDown()     {

    }

    public function testTimeToWalkTheDog_default()     {         print "testTimeToWalkTheDog_default";         this>assertTrue(!this->assertTrue(!this->object->timeToWalkTheDog());     } }

?>` images

图 13-2。 Netbeans IDE 显示单元测试失败

我们的默认选项将ownADoghaveNotWalkedForDays都设置为true。所以我们调用$this->object->timeToWalkTheDog()的结果应该是真的。

在清单 13-18 中,我们调整了之前的测试,使其通过,并增加了第二个测试。

***清单 13-18。*修正了第一个测试并增加了第二个

`<?php

require_once dirname(FILE) . '/../walk.php';

class WalkTest extends PHPUnit_Framework_TestCase {

    protected $object;

    protected function setUp()     {         $this->object = new Walk;     }

    protected function tearDown()     {

    }

    public function testTimeToWalkTheDog_default_shouldReturnTrue()     {         print "testTimeToWalkTheDog_default";         this>assertTrue(this->assertTrue(this->object->timeToWalkTheDog());     }

    public function testTimeToWalkTheDog_haveNoDog_shouldReturnFalse()     {         print "testTimeToWalkTheDog_default";         this>object>ownADog=false;        this->object->ownADog = false;         this->assertTrue(!$this->object->timeToWalkTheDog());     } }

?>`

在我们添加的第二个测试中,我们将ownADog选项设置为false。当然,。

$this->object->timeToWalkTheDog()现在也返回false。我们两次测试的成功结果如图图 13-3 所示。

images

***图 13-3。*两次成功测试

代码覆盖率是已经测试过的代码的百分比。在图 13-4 中,经过我们的两次测试,Walk 类有 61%的代码覆盖率。大多数 ide 都有内置功能或插件,可以突出显示被覆盖的代码行。

images

***图 13-4。*Netbeans IDE 中逐行代码覆盖率

重要的是要知道单元测试的代码覆盖率可以是 100%,而程序仍然会失败。这是因为程序的各个部分可能都可以工作,但作为一个整体却不行。你可以把每个单元想象成汽车的一部分,把整个程序想象成你的汽车。尽管所有的零件都是新的,而且工作正常,但它们可能没有正确连接,所以汽车无法运行。为了测试整个程序,我们需要功能测试。

单元测试有助于提醒我们程序中的变化——一个已知的变化或一些重构的副作用。后一种情况会产生潜伏很长一段时间的病毒。当我们试图找出为什么我们会在几周或几个月后得到一个意想不到的结果时,追踪这些类型的错误可能会非常困难和耗时。五分钟或两年前编写的单元测试所提供的对变化的反应是无价的。

单元测试和功能测试都是回归测试的类型。定期运行回归测试,以确保在功能增强、错误修复或配置更改后不会引入新的错误或回归。

当我们在 IDE、命令行或浏览器中使用 PHPUnit 时,输出是不同的。比较图 13-3 、图 13-5 和图 13-6 。

images

***图 13-5。*示例 Zend Studio 输出的 PHPUnit 结果

images

***图 13-6。*PHPUnit 结果的命令行输出示例

代码覆盖率统计可以让我们看到每个文件被单元测试覆盖的百分比。图 13-7 显示了为我们的旅行计划编写的测试的文件覆盖。

images

**图 13-7。**Netbeans IDE中显示的我们的 Travel 程序中每个测试文件的百分比

在我们实现了覆盖整个TravelMath类的单元测试之后(清单 13-20 ,错误向我们揭示,如图图 13-8 所示。

***清单 13-20。**针对TravelMath类、TravelMathTest.php类、*类的全单元测试

`<?php

require_once dirname(FILE) . '/../TravelMath.php'; require_once 'PHPUnit/Autoload.php';

/**

  • TravelMath test case. */ class TravelMathTest extends PHPUnit_Framework_TestCase {

    /**      * Prepares the environment before running a test.      */     protected function setUp() {         parent::setUp ();     }

    /**      * Cleans up the environment after running a test.      */     protected function tearDown() {         parent::tearDown ();     }

    /**      * Constructs the test case.      */     public function __construct() {         // TODO Auto-generated constructor     }

    public function testCalculateDistance_no_difference() {         $src = new Location(3, 7);

        expected=0;        expected = 0;         actual = TravelMath::calculateDistance(src,src, src);         this>assertEquals(this->assertEquals(expected, $actual);     }

    public function testCalculateDistance_no_y_change() {         src=newLocation(5,7);        src = new Location(5, 7);         dest = new Location(3, 7); expected=2;        expected = 2;         actual = TravelMath::calculateDistance(src,src, dest);         this>assertEquals(this->assertEquals(expected, $actual);     }

    public function testCalculateDistance_no_x_change() {         src=newLocation(3,10);        src = new Location(3, 10);         dest = new Location(3, 7);

        expected=3;        expected = 3;         actual = TravelMath::calculateDistance(src,src, dest);         this>assertEquals(this->assertEquals(expected, $actual);     }

    public function testCalculateDistance_x_and_y_change() {         src=newLocation(6,7);        src = new Location(6, 7);         dest = new Location(3, 11);

        expected=5;        expected = 5;         actual = TravelMath::calculateDistance(src,src, dest);         this>assertEquals(this->assertEquals(expected, $actual, '', 0.01);     }

    public function testCalculateAngleInDegrees_moving_nowhere() {         $src = new Location(3, 7);

        expected=null;        expected = null;         actual = TravelMath::calculateAngleInDegrees(src,src, src);         this>assertEquals(this->assertEquals(expected, $actual);     }

    public function testCalculateAngleInDegrees_moving_straight_up() {         src=newLocation(3,7);        src = new Location(3, 7);         dest = new Location(3, 12);

        expected=90.0;        expected = 90.0;         actual = TravelMath::calculateAngleInDegrees(src,src, dest);         this>assertEquals(this->assertEquals(expected, $actual);     }

    public function testCalculateAngleInDegrees_moving_straight_down() {         src=newLocation(3,12);        src = new Location(3, 12);         dest = new Location(3, 7);

        expected=90.0;        expected = -90.0;         actual = TravelMath::calculateAngleInDegrees(src,src, dest);         this>assertEquals(this->assertEquals(expected, actual);     }` `public function testCalculateAngleInDegrees_moving_straight_left() {         src = new Location(6, 7);         $dest = new Location(3, 7);

        expected=180.0;        expected = 180.0;         actual = TravelMath::calculateAngleInDegrees(src,src, dest);         this>assertEquals(this->assertEquals(expected, actual);     }     public function testCalculateAngleInDegrees_moving_straight_right() {         src = new Location(3, 7);         $dest = new Location(6, 7);

        expected=0.0;        expected = 0.0;         actual = TravelMath::calculateAngleInDegrees(src,src, dest);         this>assertEquals(this->assertEquals(expected, $actual);     }

    public function testCalculateAngleInDegrees_moving_northeast() {         //random values where both x2!=x2 != x1 and y2!=y2 != y1         x1=rand(25,15);        x1 = rand(-25, 15);         y1 = rand(-25, 25);         x2=rand(25,25);        x2 = rand(-25, 25);         y2 = rand(-25, 25);

        while (x2==x2 == x1) {             x2 = rand(-25, 25);         }         while (y2 == y1) {             y2 = rand(-25, 25);         }

        src=newLocation(src = new Location(x1, y1);        y1);         dest = new Location(x2,x2, y2);

        expected=rad2deg(atan((expected = rad2deg(atan((y2 - y1)/(y1) / (x2 - x1)));        x1)));         actual = TravelMath::calculateAngleInDegrees(src,src, dest);         this>assertEquals(this->assertEquals(expected, $actual,  '', 0.01);     }

    public function testIsCloseToDest_x_too_far_should_fail() {         src=newLocation(3,9);        src = new Location(3, 9);         dest = new Location(3.5, 7);         $step = 1.0;

        expected=false;        expected = false;         actual = TravelMath::isCloseToDest(src,src, dest, step);        step);         this->assertEquals(expected,expected, actual);     }     public function testIsCloseToDest_y_too_far_should_fail() {         src=newLocation(4.5,7.5);        src = new Location(4.5, 7.5);         dest = new Location(3.5, 7);         $step = 1.0;

        expected=false;        expected = false;         actual = TravelMath::isCloseToDest(src,src, dest, step);        step);         this->assertEquals(expected,expected, actual);     }

    public function testIsCloseToDest_should_pass() {         src=newLocation(3,7.5);        src = new Location(3, 7.5);         dest = new Location(3.5, 7);         $step = 1.0;

        expected=true;        expected = true;         actual = TravelMath::isCloseToDest(src,src, dest, step);        step);         this->assertEquals(expected,expected, actual);     } } ?>` images

***图 13-8。*我们的测试运行后出现了一些意想不到的错误

通过检查失败的方法,我们可以看到前两个错误是由于没有返回一维距离的绝对值造成的。我们可以通过改变来解决这个问题

        if ($dest->y == $src->y) {                 $distance = $dest->x - $src->x;         } else if ($dest->x == $src->x) {                 $distance = $dest->y - $src->y;

        if ($dest->y == $src->y) {                 $distance = abs($dest->x - $src->x);         } else if ($dest->x == $src->x) {                 $distance = abs($dest->y - $src->y);

第三个错误是因为atan函数以弧度返回结果。我们期待着学位。所以我们可以通过使用rad2deg函数来解决这个问题,改变

      $angle = atan($distance_y / $distance_x);

      $angle = rad2deg(atan($distance_y / $distance_x));          

进行我们的更改并重新运行我们的测试,我们可以验证这确实解决了问题。参见图 13-9 。

images

***图 13-9。*我们的代码现在完全通过了所有测试

我们的单元测试已经通过检测遗留程序中的错误显示了它们的价值。我们继续重构,让读者自己添加更多的单元测试。我们的最终代码将更多的显示语句移到了TravelView类中,并使用了 TravelMath::isCloseToDest 函数。

***清单 13-21。*决赛TravelView.php

`<?php

error_reporting(E_ALL); require_once ('config.php'); require_once ('location.php');

class TravelView {

    public static function displayOurIntendedPath( angle,angle, distance,                                                    Location src,Locationsrc, Location dest) {         print "Trying to go from " . src>toString()."to".              src->toString() . " to " .               dest->toString() . "
\n";         if (IN_A_RUSH) {             print "In a rush
\n";         }         print "Distance is " . distance."inthedirectionof".              distance . " in the direction of " .               angle . " degrees
";     }

    public static function displaySummary(time) {         print "Total time was: " . date("i:s", time);     }     public static function displayError(error){             print "ERROR: ".error. "
";     }

    public static function displayLocationStatusMessage(method,method, x, y){             print method . “… currently at (" .                   round(x,2)."  ".                  round(x, 2). "  " .                   round(y, 2). ")
\n";     }      public static function displayArrived(message){          print "Got to destination by " . strtolower(message). "
";      }

} ?>`

*清单 13-22。travel_original.php*的一个可能的最终重构

`<?php

error_reporting(E_ALL); require_once('config.php'); require_once('location.php'); require_once('travelView.php'); require_once('travelMath.php');

class Travel {

    private distance=null;    privatedistance = null;     private angle = 0.0;     private angleinradians=0.0;    privateangle_in_radians = 0.0;     private time = 0.0;     private src=0.0;    privatesrc = 0.0;     private dest = 0.0;

    public function __construct() {         $this->distance = new Location(0, 0);     }

    public function execute(Location src,Locationsrc, Location dest) {         this>src=this->src = src;         this>dest=this->dest = dest;

        this>calculateAngleAndDistance();        TravelView::displayOurIntendedPath(this->calculateAngleAndDistance();         TravelView::displayOurIntendedPath( this->angle, this>distance,                                            this->distance,                                             this->src, this>dest);if(this->dest);` `if (this->doWeHaveOptions ()) {             this->pickBestOption ();         } else {             this->tryToWalkThere ();         }         TravelView::displaySummary($this->time);     }

    public function calculateAngleAndDistance() {         this>angle=TravelMath::calculateAngleInDegrees(this->angle = TravelMath::calculateAngleInDegrees(this->src, this>dest);        this->dest);         this->angle_in_radians = deg2rad(this>angle);        this->angle);         this->distance = TravelMath::calculateDistance(this>src,this->src, this->dest);     }

    public function tryToWalkThere() {         if (STORMY_WEATHER) {             TravelView::displayError("Storming");         } else if (this->distance < WALKING_MAX_DISTANCE) {             this->walk ();         } else {             TravelView::displayError("Too far to walk");         }     }

    public function pickBestOption() {         if (STORMY_WEATHER) {             this->takeFastestVehicle ();         } else {             if (this->this->distance < WALKING_MAX_DISTANCE && !IN_A_RUSH) {                 this->walk()             } else {                 $this->takeFastestVehicle ();             }         }     }

    private function takeFastestVehicle() {         if (HAS_CAR) {             this->driveCar ();         } else if (HAS_MONEY && ON_BUS_ROUTE) {             this->rideBus ();         } else {             $this->rideBike ();         }     }     private function doWehaveOptions() {

        has_options = false;         if (HAS_CAR || (HAS_MONEY && ON_BUS_ROUTE) || HAS_BIKE) {             has_options = true;         }         return $has_options;     }

    private function move(step,step, message) {         while (!TravelMath::isCloseToDest(this>src,this->src, this->dest, step)) {             this->moveCloserToDestination(step,step, message);         } TravelView::displayArrived($message);     }

    private function driveCar() {         this>time=CARDELAY;        this->time = CAR_DELAY;         this->move(CAR_STEP, "Driving a Car");     }

    private function rideBus() {         this>time=BUSDELAY;        this->time = BUS_DELAY;         this->move(BUS_STEP, "On the Bus");     }

    private function rideBike() {         $this->move(BIKE_STEP, "Biking");     }

    private function walk() {         $this->move(WALK_STEP, "Walking");     }

    private function moveCloserToDestination(step,step, method) {         this>src>x+=(this->src->x += ( step * cos(this>angleinradians));        this->angle_in_radians));         this->src->y += ( stepsin(step * sin(this->angle_in_radians));         ++this>time;        TravelView::displayLocationStatusMessage(this->time;         TravelView::displayLocationStatusMessage(method, this>src>x,this->src->x, this->src->y);     }

} ?>`

将我们主类的重构版本(清单 13-22 )与初始代码(清单 13-8 )进行比较。

如果我们要为Travel类添加测试,我们可以通过将它们添加到测试套件中来一次运行所有的测试(清单 13-23 )。

***清单 13-23。*我们的测试套件,AllTests.php

`<?php

error_reporting(E_ALL ^ ~E_NOTICE); require_once 'PHPUnit/Autoload.php'; require_once 'travelMathTest.php'; require_once 'travelTest.php';

class AllTests {     public static function suite())     {         suite=newPHPUnitFrameworkTestSuite(TravelTestSuite);        suite = new PHPUnit_Framework_TestSuite('Travel Test Suite');         suite->addTestSuite('TravelTest');         suite>addTestSuite(TravelMathTest);        returnsuite->addTestSuite('TravelMathTest');         return suite;     } }

?>`

我们现在可以更有把握地展示我们修改后的代码(清单 13-24 )的使用示例,它会按预期工作。

***清单 13-24。*调用我们的脚本

`<?php

error_reporting(E_ALL); require_once ('travel.php');

travel=newTravel();travel = new Travel(); travel->execute(new Location(1, 3), new Location(4,7));

?>`

运行清单 13-24 (将IN_A_RUSH配置标志设置为false)的输出如下所示。

Trying to go from (1, 3) to (4, 7) Distance is 5 in the direction of 53.130102354156 degrees Walking... currently at (1.15, 3.2) Walking... currently at (1.3, 3.4) Walking... currently at (1.45, 3.6) Walking... currently at (1.6, 3.8) Walking... currently at (1.75, 4) Walking... currently at (1.9, 4.2) Walking... currently at (2.05, 4.4) Walking... currently at (2.2, 4.6) Walking... currently at (2.35, 4.8) Walking... currently at (2.5, 5) Walking... currently at (2.65, 5.2) Walking... currently at (2.8, 5.4) Walking... currently at (2.95, 5.6) Walking... currently at (3.1, 5.8)> Walking... currently at (3.25, 6) Walking... currently at (3.4, 6.2) Walking... currently at (3.55, 6.4) Walking... currently at (3.7, 6.6) Walking... currently at (3.85, 6.8) Got to destination by walking Total time was: 00:19

单元测试和重构配合得很好。事实上,测试驱动开发(TDD)的原则更进了一步,它规定在没有为新代码编写单元测试之前,不要编写任何新代码..

TDD 的基本原则是:

  1. 写一个测试。
  2. 测试失败是因为还没有编写出满足它的代码。
  3. 实现能使测试通过的最少功能。
  4. 重复一遍。

如果可以选择的话,用全新的代码库进行 TDD,或者用现有的单元测试安全网进行重构是非常好的。然而,更多的时候我们是在处理遗留代码。这不是让修改变得可怕的借口。打破依赖关系和重构代码可能会导致意想不到的行为。然而,你等待重构的时间越长,风险就越大。最好是经常重构,小改小改。

类似地,即使少量的单元测试也比没有好。测试覆盖率为 10%的代码库确实比零覆盖率的代码库更稳定。这种对代码稳定性的信任有助于加速进一步的重构和测试创建。当我们打破紧密耦合的依赖关系,以便我们可以实现测试时,代码库的设计也得到改进。这反过来让我们在以前有多个依赖关系的领域中打破依赖关系。

通常,当试图添加测试时,有两种不同的项目状态。这些措施如下:

  1. 开始一个新的项目或者添加到一个已经有 100%测试覆盖率的项目中。然后我们可以安全地使用 TDD(如果我们希望的话),并继续测试和重构。
  2. 从遗留代码库开始。这可能是一个未经测试的开源项目,或者是您继承的公司代码,或者是您自己的未经测试的代码。事实上,在 Michael Feathers (Prentice Hall,2004)的优秀著作《有效地使用遗留代码工作》中,遗留代码被定义为“任何没有测试的代码”。

作为一名 PHP 程序员,你会遇到这两种类型的项目。第二种情况更常见。然而,PHP 开发人员开始采用更严格的“企业”级代码。这包括更强的测试和开发标准。

images 注意大部分引用的重构和单元测试的参考书都不是为 PHP 编写的。Java、C++或 C#等强类型语言的知识并不是必需的,但对于完全理解所介绍的技术是有用的。

持续集成

我们希望我们的代码测试经常运行并且自动化。这有助于实现快速、稳定的发布周期。持续集成服务器执行一组预定义的构建任务,例如代码部署、测试代码或生成分析报告。因为代码已经被提交到存储库中,所以每次存储库发生变化时都要执行这些操作,或者以一定的时间间隔(例如每小时)执行,或者按需执行。

持续集成(CI)允许您设置计算机可以自动执行的重复任务。这些任务可能单调、乏味、涉及多个步骤、复杂和/或容易出错。

您可以设置构建系统来执行的多步骤任务的一个示例是:

  1. 从源代码控制中签出我们代码的当前版本。
  2. 从网站获取第三方库的最新版本。
  3. 对我们的程序进行静态分析。
  4. 对我们程序中的 PHP 代码进行单元测试。

假设现在我们想发布一个新版本的程序。使用 CI,在我们的单元测试成功后,我们可以设置额外的构建步骤来:

  1. 混淆 PHP。
  2. 创建一个WAR文件工件。
  3. 向版本系统查询修订号。
  4. 从数据库或文件中读取活动发布版本。
  5. 在此修订版和以前发布的版本之间创建一个补丁。
  6. 将内部版本标记为发布版本。
  7. 在发布版本数据库中插入新记录或更新活动发布版本文件。
  8. WAR文件部署到可公开访问的服务器上。

现在想象一下,您手工执行上面的每一个步骤,然后意识到代码中有一个小错误或者丢失了一个文件,或者类似的事情。发布正确的版本需要再次执行所有步骤。手动重复执行所有这些步骤很快就会消耗比我们希望花费的更多的时间,容易出错,而且通常不好玩。

有了 CI,如果我们愿意,我们可以在每次提交后自动执行所有这些步骤。我们还可以为额外的八个部署步骤只标记某些构建。

持续集成服务器

PHP 可用的两个最好的免费 CI 服务器是 Jenkins 和 phpUnderControl。Jenkins 是 Hudson 项目的一个分支,是世界上最常用的 CI 系统之一。phpUnderControl 与 CruiseControl CI 框架集成。Jenkins 和 CruiseControl 都是用 Java 编写的,支持多种构建系统和语言,并提供许多附加插件。我们将在本章中使用詹金斯。

images 詹金斯、phpUnderControl 和 CruiseControl 可从以下网站获得:

[jenkins-ci.org/](http://jenkins-ci.org/)

[phpundercontrol.org/](http://phpundercontrol.org/)

[sourceforge.net/projects/cruisecontrol/files/](http://sourceforge.net/projects/cruisecontrol/files/)

CI 服务器使用以下工具:

  1. 版本控制
  2. 单元测试和代码覆盖率
  3. 静态分析
  4. 构建自动化
版本控制

重申上一章的讨论,版本控制也被称为源代码控制或修订控制。对于任何程序员来说,这都是一个必不可少的工具,不管他是否敏捷。版本控制就像一个数字录音机和混音器。我们可以播放现有的内容。我们可以添加新的内容。我们可以倒回到某个地方。我们可以分支到不同的轨道。我们可以混合我们最喜欢的东西。有时片段会发生冲突,我们需要进行编辑,让不同的部分再次和谐工作。但总而言之,它是我们使用的一个强有力的工具。

最流行的版本控制系统之一是 Subversion (SVN),这将在第十二章中介绍。像 GitMercurial 这样的新一波分布式版本控制系统也在开发生态中开发出高级别的支持。

images 注意关于这些版本控制系统的在线文档可从以下网址获得:

[svnbook.red-bean.com/](http://svnbook.red-bean.com/)

[gitref.org/](http://gitref.org/)

[hgbook.red-bean.com/](http://hgbook.red-bean.com/)

静态分析

静态分析工具使用度量来检查我们的代码,并且可以揭示有用的信息,例如:

  • 计算复杂性水平(越高越差)
  • 依赖性(越少越好)
  • 最佳实践建议
  • 遵守代码风格惯例
  • 检测有问题的代码和可能的错误
  • 重复代码的显示
  • 制作文档

大多数 PHP 静态分析工具都以 IDE 插件或 PEAR 包的形式提供。按类别划分的一些现有工具有:

遵守一套代码约定:

PhpCheckstyle

[code.google.com/p/phpcheckstyle/](http://code.google.com/p/phpcheckstyle/)

PHP 代码嗅探器

[pear.php.net/package/PHP_CodeSniffer/](http://pear.php.net/package/PHP_CodeSniffer/)

API 生成:

PHP 文档管理器

[www.phpdoc.org/](http://www.phpdoc.org/)

代码质量指标:

PHP 代码行

关于函数、类等代码行的度量 [github.com/sebastianbergmann/phploc](https://github.com/sebastianbergmann/phploc)

p 结束

类和函数依赖关系

[pdepend.org/](http://pdepend.org/)

代码质量建议:

PHP 复制/粘贴检测器

[github.com/sebastianbergmann/phpcpd](https://github.com/sebastianbergmann/phpcpd)

phpcpd - (php 复制/粘贴检测器)重复代码

PHP mess 检测器

[phpmd.org/](http://phpmd.org/)

类型不匹配的 phantm - PHp 分析器

PHP 是松散类型的。Phantm 有助于发现由类型不匹配引起的潜在错误

[github.com/colder/phantm](https://github.com/colder/phantm)

学徒

代码反模式和“气味”

[github.com/mayflowergmbh/padawan](https://github.com/mayflowergmbh/padawan)

突出显示:

phpcb

PHP 代码浏览器——与代码嗅探器 PHPUnit 一起使用

[github.com/mayflowergmbh/PHP_CodeBrowser](https://github.com/mayflowergmbh/PHP_CodeBrowser)

安全

PHP 安全审计工具

[sourceforge.net/projects/phpsecaudit/](http://sourceforge.net/projects/phpsecaudit/)

构建自动化

为了自动化重复的任务,我们需要知道如何使用一个构建系统,比如 Apache Ant、Maven 或 Phing。这些构建系统基于 XML 文件。XML 包含在第十四章中。典型的构建文件包含一个或多个目标,每个目标都有子任务。这些任务可用于添加或删除文件,从存储库中签出文件,运行单元测试,执行静态分析,生成文档,等等。

images 注意关于这些构建系统的更多信息可以在它们各自的网站上找到:

[ant.apache.org/](http://ant.apache.org/)

[www.phing.info/trac](http://www.phing.info/trac)

[maven.apache.org/](http://maven.apache.org/)

一个非常基本的构建文件可能是这样的:

`

                            `

在这个示例构建文件中,我们有一个构建目标,"testAutomate"。目标回显一条消息并创建一个目录。构建文件可以有几个目标,本质上变得非常复杂。

从未使用过 CI 服务器的程序员很容易看不到设置 CI 系统所涉及的额外工作的好处。随着时间、构建和测试的增加,我们将真正体验到全部的效用和好处。

詹金斯服务器设置

本节将概述 PHP 与 Jenkins CI 服务器的设置(见图 13-10 )。詹金斯的布局相当直观。这是一个非常受欢迎的配置项,拥有大量的支持社区。詹金斯是复杂和强大的,但也相当容易使用。Jenkins 有一个写得很好的 GUI,也为那些喜欢它的人提供了命令行脚本。

images

***图 13-10。*詹金斯网站

用 PHP 设置 Jenkins 的权威参考资料位于[jenkins-php.org/](http://jenkins-php.org/),作者是塞巴斯蒂安·博格曼(PHPUnit 的作者)。

基本步骤是:

  1. 下载并安装詹金斯。
  2. 配置 Jenkins 插件。
  3. 升级 PHP pear 包。
  4. 创建构建文件。
  5. 创造一个新的詹金斯工作。

[jenkins-ci.org/.](http://jenkins-ci.org/.)下载 Jenkins 确切的安装过程将因操作系统和发布版本而异。wiki 上有扩展帮助[wiki.jenkins-ci.org/display/JENKINS/Home.](https://wiki.jenkins-ci.org/display/JENKINS/Home.)

默认情况下,使用端口 8080,仪表板可在[localhost:8080/dashboard.](http://localhost:8080/dashboard.)访问

要配置 Jenkins 插件,我们可以使用 web 界面或命令行实用程序(见清单 13-25 )。参见图 13-11 。

***清单 13-25。*通过 Jenkins 命令行实用程序安装插件

wget http://localhost:8080/jnlpJars/jenkins-cli.jar java –jar jenkins-cli.jar –s http://localhost:8080 install-plugin checkstyle java –jar jenkins-cli.jar –s http://localhost:8080 install-plugin clover java –jar jenkins-cli.jar –s http://localhost:8080 install-plugin jdepend java –jar jenkins-cli.jar –s http://localhost:8080 install-plugin pmd java –jar jenkins-cli.jar –s http://localhost:8080 install-plugin phing java –jar jenkins-cli.jar –s http://localhost:8080 install-plugin xunit java –jar jenkins-cli.jar –s http://localhost:8080 safe-restart images

***图 13-11。*通过 GUI 管理 Jenkins】

您可能需要升级现有的 pear 模块或通道,或者安装额外的模块。为了让构建自动化正常工作,修复 pear 安装尝试中报告的任何错误都是非常重要的。

`pear channel-discover pear.pdepend.org pear channel-discover pear.phpmd.org pear channel-discover pear.phpunit.de pear channel-discover components.ez.no pear channel-discover pear.symfony-project.com

pear install pdepend/PHP_Depend pear install phpmd/PHP_PMD pear install PHPDocumentor pear install PHP_CodeSniffer pear install phpunit/phpcpd //copy paste detector pear install –alldeps phpunit/PHP_CodeBrowser pear install –alldeps phpunit/PHPUnit`

images 注意如果您收到错误消息,您可能需要更改您的 pear 配置。例如,您的data_dir可能设置不正确。

[pear.php.net/manual/en/guide.users.commandline.config.php](http://pear.php.net/manual/en/guide.users.commandline.config.php)

错误:无法创建目录 C:\ PHP \ pear \ data \ PHP _ PMD \ resources \ rulesets

pear config-get data_dir

“C:\php5”    #incorrect

pear config-set data_dir “C:\xampp\php\pear\data”

pear config-set doc_dir “C:\xampp\php\pear\docs”

pear config-set test_dir “C:\xampp\php\pear\tests”

博格曼还编写了几个实用程序脚本,可以作为 CI 设置的良好起点和模板。您很可能需要调整路径和/或调整构建目标。这些脚本可以通过 pear 包管理器使用以下命令获得:

pear install phpunit/ppw

或在线访问

https://github.com/sebastianbergmann/php-project-wizard

图 13-12 显示了 Jenkins 主菜单屏幕,我们可以在此创建一个新任务。

images

***图 13-12。*詹金斯主菜单

总结

在这一章中,我们介绍了三种开发实践,它们可以帮助我们创建更高质量的 PHP 代码。这些是重构、单元测试和持续集成。所有这三个领域都很广阔,有许多关于每个主题的书籍。

世界上大多数代码都是遗留代码,因此作为开发人员,您需要知道如何从最初笨拙的代码中创建稳定性。重构是一项至关重要的技能,通过实践会变得更加容易。然而,即使是经验丰富的重构者和代码专家也会在重构时不知不觉地引入微妙的行为修改。这些可能会被忽视,并且以后很难确定。单元测试可以帮助跟踪函数的预期结果,从而检测出代码结构改变时引入的行为变化。

为了定期运行我们的测试,我们可以使用持续集成。持续集成系统帮助您消除枯燥、重复(但容易出错且耗时)的任务。计算机非常擅长重复的任务,它们从不抱怨它们正在做的工作很枯燥。

作为开发人员,我们应该努力在我们的代码中有高水平的质量保证,并且总是在寻找可用的新技术和工具。

十四、XML

可扩展标记语言(XML)是一种非常强大的数据存储和传输工具。当文档用 XML 编写时,它可以被普遍理解和交换。不应该低估 XML 作为全球标准的效用。XML 用于现代文字处理器文档、SOAP 和 REST web 服务、RSS 提要和 XHTML 文档。

在这一章中,我们将主要介绍 PHP SimpleXML 扩展,它使得操作 XML 文档变得非常容易。我们还将涉及文档对象模型(DOM)和 XMLReader 扩展。DOM 保证了无论使用哪种计算机语言,文档都能被同样地查看。存在多个用于解析和编写 XML 的库的主要原因是易用性、功能深度以及 XML 的操作方式。

XML 第一

XML 允许我们定义使用任何我们想要的标记元素或属性的文档。在文本编辑器中查看 XML 文档时,您可能会注意到它类似于 HTML。这是因为,像 HTML(超文本标记语言)一样,XML 也是一种标记语言,包含一个层次结构的标记内容的集合。层次结构是树状的,有一个根元素(标签)作为主干,子元素从根中分支出来,子元素从它们的父元素中分支出来。您也可以将 XML 文档按顺序看作一系列离散的事件。请注意,按顺序查看元素不需要了解整个文档代表什么,但这也增加了搜索元素的难度。

XML“应用”的一个具体例子是 XHTML。XHTML 和 HTML 的相似之处在于使用了相同的标签。然而,XHTML 也遵循 XML 标准,因此更加严格。XHTML 有以下附加要求:

  • 标签区分大小写。在 XHTML 中,元素名需要总是小写。
  • 单个元素,如<br>,需要关闭。在这种情况下,我们将使用<br />.
  • 实体&, <, >, ', "需要分别作为&amp;, &lt;, &gt;, &apos;&quot;进行转义
  • 属性需要用引号括起来。例如,<img src=dog.jpg />是非法的,而<img src="dog.jpg" />是合法的。

为了解析 XML,我们可以使用基于树或事件驱动的模型。SimpleXML 和 DOM 中使用的基于树的模型将 HTML 和 XML 文档表示为元素树,并将整个文档加载到内存中。除了根元素,每个元素都有一个父元素。元素可以包含属性和值。基于事件的模型,如 Simple API for XML (SAX ),一次只能读取 XML 文档的一部分。对于大文档,SAX 更快;对于非常大的文档,这可能是唯一可行的选择。但是,基于树的模型通常更容易使用,也更直观,一些 XML 文档要求一次性加载文档。

一个基本的 XML 文档可能如下所示:

<animal>     <type id="9">dog</type>     <name>snoopy</name> </animal>

根元素是<animal>,有两个子元素,<type><name><type>元素的值是“狗”,而<name>元素的值是“史努比”<type>元素有一个属性id,其值为“9”。此外,每个开始标记都有一个匹配的结束标记,属性值用引号括起来。

图式

XML 模式为 XML 文档提供了额外的约束。约束的例子是可选的或必须包含的特定元素、元素的可接受值和属性以及元素可以放置的位置。

如果没有模式,就没有什么可以阻止我们得到无意义的数据,就像你在清单 14-1 中看到的那样。

***清单 14-1。*显示需要更严格模式的例子

<animals>   black     <dog>     <name>snoopy</name>     <breed>       <cat>         brown         <breed>tabby</breed>       </cat>       beagle cross     </breed>  </dog>  <name>teddy</name>   </animals>

这个文档对人类来说没有太大意义。一个cat不能是一个dog的一部分。colorname不是动物,应该用dogcat元素括起来。然而,从机器的角度来看,这是一个完全有效的文档。我们必须告诉机器这个文件不被接受的原因。模式允许我们通知机器如何实施数据的布局。这种增加的刚性确保了文档中数据的完整性。有了模式,我们可以明确地说<cat>标签不能进入<dog>标签。我们也可以说<name>和``标签只能直接进入<cat><dog>标签内部。

三种最流行的模式生成语言是文档类型定义(DTD)、XML 模式和 RELAX NG(下一代 XML 的常规语言)。因为这本书关注的是 PHP,所以我们不会详细介绍如何创建模式,而只是简单地提到您在文档的开头声明了模式。参见清单 14-2 。

***清单 14-2。*显示使用 xhtml1-transitional 模式声明的代码片段

`

`

简单 XML

SimpleXML 使得将 XML 存储为 PHP 对象变得容易,反之亦然。SimpleXML 简化了 XML 结构的遍历和特定元素的查找。SimpleXML 扩展需要 PHP 5 或更高版本,默认情况下是启用的。

从字符串解析 XML

让我们直接看第一个例子。我们将把字符串中的 XML 加载到一个SimpleXMLElement对象中,并遍历该结构。见清单 14-3 。

清单 14-3。第一个例子:animal.php

`<?php

error_reporting(E_ALL ^ E_NOTICE);

$xml = <<<THE_XML        dog     snoopy         THE_XML;

//to load the XML string into a SimpleXMLElement object takes one line xmlobject=simplexmlloadstring(xml_object = simplexml_load_string(xml);

foreach (xmlobjectasxml_object as element => value) {     print element . ": " . $value . "
"; }

?>`

在 XML 字符串被加载到清单 14-3 中之后,$xml_object位于根元素处,<animal>.文档被表示为一个SimpleXMLElement对象,因此我们可以使用一个foreach循环遍历子元素。清单 14-3 的输出如下:

  type: dog   name: snoopy

清单 14-4。【更复杂的例子:animals.php

`<?php error_reporting(E_ALL ^ E_NOTICE);

$xml = <<<THE_XML        snoopy     brown     beagle cross           teddy     brown     tabby           jade     black     lab cross    THE_XML;

xmlobject=simplexmlloadstring(xml_object = simplexml_load_string(xml);

//output all of the dog names foreach(xmlobject>dogasxml_object->dog as dog){    print $dog->name."
"; } ?>`

清单 14-4 的输出如下:

snoopy jade

清单 14-4 中的大部分内容使用 PHP heredoc语法以可读的方式加载字符串。寻找元素值的实际代码只有几行。很简单。SimpleXML 足够智能,可以迭代所有的<dog>标签,即使在<dog>标签之间有一个<cat>标签。

从文件中解析 XML

当你加载 XML 的时候,如果文档无效,PHP 会给出一个有用的警告信息。该消息可以通知您需要关闭一个标记或对一个实体进行转义,并将指示错误的行号。参见清单 14-5 。

***清单 14-5。*无效 XML 的 PHP 警告消息示例

**Warning**: simplexml_load_string() [function.simplexml-load-string]: Entity: line 1: parser error : attributes construct error in **E:\xampp\htdocs\xml\animals.php** on line **29**

我们接下来的两个例子将从清单 14-6 中的文件加载 XML。一些 XML 元素有属性。我们将在清单 14-7 中展示如何通过重复使用 SimpleXML 函数调用来天真地找到属性值。然后在清单 14-10 中,我们将展示如何使用 XPath 来查找属性值,这是为了简化搜索。

***清单 14-6。*我们的示例 XHTML 文件:template.xhtml

`

             
            header would be here         
        
            menu would be here         
        
            
                left sidebar             
            
                main story             
            
                right sidebar             
        
              `

清单 14-6 中的前两行定义了使用的 XML 版本和DOCTYPE,它们不是加载到SimpleXMLElement中的树的一部分。所以根源是<html>元素。

清单 14-7 展示了如何使用面向对象的 SimpleXML 方法找到带有id="main_center"<div>的内容。

***清单 14-7。*根据属性查找特定值

`<?php error_reporting(E_ALL ^ E_NOTICE);

xml=simplexmlloadfile("template.xhtml");findDivContentsByID(xml = simplexml_load_file("template.xhtml"); findDivContentsByID(xml, "main_center"); function findDivContentsByID(xml,xml, id) {   foreach (xml>body>divasxml->body->div as divs) {       if (!empty(divs->div)) {           foreach (divs->div as inner_divs) {               if (isElementWithID(inner_divs, id)) {                   break 2;               }           }       } else {           if (isElementWithID(divs, $id)) {               break;           }       }   } }

function isElementWithID(element,element, id) {     actualid=(String)actual_id = (String) element->attributes()->id;     if (actualid==actual_id == id) {         value=trim((String)value = trim((String) element);         print "value of #idis:id is: value";         return true;     }     return false; } ?>`

清单 14-7 将找到<body>元素的所有<div>元素,以及这些<div>元素的直接子<div>元素。然后每个匹配的<div>元素都有它的id属性与我们的 id 搜索值进行比较,"main_center."如果它们相等,那么我们打印出这个值并从循环中脱离。该脚本的输出如下:

value of #main_center is: main story

我们不能简单地在我们的isElementWithID函数中输出$element,因为我们将输出整个SimpleXMLElement对象。

object(SimpleXMLElement)[9]   public '@attributes' =>     array       'id' => string 'main_center' (length=11)       'class' => string 'foobar' (length=6)   string '                 main story             ' (length=40)

所以我们需要将一个Object的返回值转换成一个String。(回想一下,强制转换将变量从一种数据类型显式转换为另一种数据类型)。还要注意,空白在元素值中被捕获,所以我们可能需要在字符串上使用 PHP trim()函数。

为了获得元素的属性,SimpleXML 有一个attributes()函数,它返回一个属性对象。

var_dump($element->attributes()); object(SimpleXMLElement)[9]   public '@attributes' =>     array       'id' => string 'main_center' (length=11)       'class' => string 'foobar' (length=6)

我们还需要转换$element->attributes()->id;的返回值,否则我们将再次得到整个SimpleXMLElement对象。

清单 14-7 不健壮。如果文档的结构发生变化或者比两层更深,它将无法找到 id。

您可能认识到 XHTML 文档遵循我们熟悉的 HTML 文档对象模型(DOM)。现有的解析器和遍历工具(如 XPath 和 XQuery)使得查找嵌套元素变得相对容易。XPath 是 SimpleXML 库和 PHP DOM 库的一部分。使用 SimpleXML,您可以通过函数调用$simple_xml_object->xpath()调用 XPath。在 DOM 库中,通过创建一个对象DOMXPath来使用 XPath,然后调用该对象的查询方法。

我们将在清单 14-10 中展示如何用 XPath 找到特定的 id 属性。首先,我们将展示如何使用 XPath 找到我们在清单 14-3 和 14-4 中检索到的元素。参见清单 14-8 。

***清单 14-8。*使用 XPath 查找元素

`<?php

error_reporting(E_ALL);

$xml = <<<THE_XML        dog     snoopy         THE_XML;

xmlobject=simplexmlloadstring(xml_object = simplexml_load_string(xml);

type=type = xml_object->xpath("type"); foreach(typeastype as t) {     echo $t."

"; }

xmlobject=simplexmlloadstring(xml_object = simplexml_load_string(xml); children=children = xml_object->xpath("/animal/*"); foreach(childrenaschildren as element) {     echo element>getName().":".element->getName().": ".element."
"; } ?>`

清单 14-8 的输出是:

`dog

type: dog name: snoopy`

在清单 14-8 的第一部分,我们使用 XPath 选择器“type选择<animal>的内部元素<type>。这将返回匹配 XPath 查询的一组SimpleXMLElement对象。清单的第二部分使用 XPath 选择器“/animal/*, where选择<animal>的所有子元素。星号是通配符。当SimpleXMLElement对象从xpath()调用中返回时,我们也可以通过使用getName()方法输出元素名称。

www.w3.org/TR/xpath/.可… XPath 选择器的完整规范

清单 14-9 展示了如何匹配一个特定的子元素,而不管其父元素的类型。它还演示了如何找到一个SimpleXMLElement的父元素。

***清单 14-9。*使用 XPath 匹配孩子和父母

`<?php

error_reporting(E_ALL ^ E_NOTICE);

$xml = <<<THE_XML        snoopy     brown     beagle cross           teddy     brown     tabby           jade     black     lab cross    THE_XML;

xmlobject=simplexmlloadstring(xml_object = simplexml_load_string(xml);

names=names = xml_object->xpath("*/name"); foreach (namesasnames as element) {     parent=parent = element->xpath("..");     type=type = parent[0]->getName();     echo "element(element (type)
"; } ?>`

清单 14-9 的输出将是这样的:

snoopy (dog) teddy (cat) jade (dog)

我们用 XPath 查询“*/name”匹配了<name>元素,不管它是包含在<dog>还是<cat>元素中。为了获得当前SimpleXMLElement的父节点,我们使用了查询".."。我们可以使用查询“parent::*”。

***清单 14-10。*使用 XPath 匹配属性值

`<?php

error_reporting(E_ALL);

xml=simplexmlloadfile("template.xhtml");xml = simplexml_load_file("template.xhtml"); content = xml>xpath("//[@id=maincenter]");print(String)xml->xpath("//*[@id='main_center']"); print (String)content[0];

?>`

在清单 14-10 中,我们使用查询“//*[@id='main_center']”来查找属性id等于'main_center'的元素。为了用 XPath 匹配一个属性,我们使用了@符号。比较使用 XPath 的清单 14-10 和清单 14-7 的简单性。

名称空间

XML 名称空间定义一个元素属于哪个集合,从而防止数据模糊。这一点很重要,如果不同的节点类型包含相同名称的元素,就会出现这种情况。例如,您可以为catdog定义不同的名称空间,以确保它们的内部元素有唯一的名称,如清单 14-11 和清单 14-12 所示。

关于 PHP 名称空间的信息,请参考第五章 -前沿 PHP。

拥有 XML 名称空间的第一部分是用xmlns:*your_namespace*声明一个名称空间:

<animals xmlns:dog='http://foobar.com:dog' xmlns:cat='http://foobar.com:cat'>

然后将名称空间作为元素的前缀。当你想检索狗的名字时,你可以搜索dog:name,这样只会过滤掉dog的名字。

清单 14-11 显示了如何在 XML 文档中使用名称空间。

***清单 14-11。*使用 XPath 无法在带有未注册名称空间的文档中找到内容

`<?php

error_reporting(E_ALL ^ E_NOTICE);

$xml = <<<THE_XML   dog:namesnoopy</dog:name>   dog:colorbrown</dog:color>   dog:breedbeagle cross</dog:breed>   cat:nameteddy</cat:name>   cat:colorbrown</cat:color>   cat:breedtabby</cat:breed>   dog:namejade</dog:name>   dog:colorblack</dog:color>   dog:breedlab cross</dog:breed> THE_XML;

xmlobject=simplexmlloadstring(xml_object = simplexml_load_string(xml); names=names = xml_object->xpath("name");

foreach (namesasnames as name) {     print $name . "
"; } ?>`

运行包含名称空间的清单 10-11 不会输出任何内容。运行 XPath 时,我们需要注册名称空间。参见清单 14-12 。

***清单 14-12。*使用 XPath 在注册了名称空间的文档中查找内容

`<?php

error_reporting(E_ALL ^ E_NOTICE);

$xml = <<<THE_XML   dog:namesnoopy</dog:name>   dog:colorbrown</dog:color>   dog:breedbeagle cross</dog:breed>   cat:nameteddy</cat:name>   cat:colorbrown</cat:color>   cat:breedtabby</cat:breed>   dog:namejade</dog:name>   dog:colorblack</dog:color>   dog:breedlab cross</dog:breed> THE_XML;

xmlobject=simplexmlloadstring(xml_object = simplexml_load_string(xml);

xmlobject>registerXPathNamespace(cat,http://foobar.com/cat);xml_object->registerXPathNamespace('cat', 'http://foobar.com/cat');** **xml_object->registerXPathNamespace('dog', 'foobar.com/dog'); names=names = xml_object->xpath("dog:name");

foreach (namesasnames as name) {     print $name . "
"; } ?>`

输出如下:

snoopy jade

在清单 14-12 中,在用 XPath 注册了名称空间之后,我们需要将它作为查询元素的前缀。

在清单 14-13 中,我们将使用 XPath 通过值匹配一个元素。然后我们将读取这个元素的属性值..

***清单 14-13。*使用 XPath 找到具有特定值的元素的属性值

`?<?php

error_reporting(E_ALL);

$xml = <<<THE_XML        snoopy     brown     beagle cross           teddy     brown     tabby           jade     black     lab cross    THE_XML;

xmlobject=simplexmlloadstring(xml_object = simplexml_load_string(xml);

result=result = xml_object->xpath("dog/name[contains(., 'jade')]"); print (String)$result[0]->attributes()->id;

?>`

在清单 14-13 中,我们使用了 XPath 函数contains,它有两个参数,第一个是在哪里搜索—'.'代表当前节点,第二个是搜索字符串。这个函数有一个*(草堆,针)*参数格式。然后我们接收一个匹配的SimpleXMLObject,并输出它的id属性。

XPath 非常强大,任何熟悉高级 JavaScript 语言(如 jQuery)的人都已经知道很多语法。学习 XPath 和 DOM 将为您节省大量时间,并使您的脚本更加可靠

RSS

真正简单的联合(RSS)提供了一种发布和订阅内容的简单方法。

任何 RSS 提要都可以,但以《连线 》杂志的提要为例。提要在http://feeds.wired.com/wired/index?format=xml.可用。提要的来源如下所示:

`

<rss xmlns:dc="purl.org/dc/elements…" xmlns:feedburner= "rssnamespace.org/feedburner/…" version="2.0">        Wired Top Stories     www.wired.com/rss/index.x…     Top Stories<img src="www.wired.com/rss_views /index.gif">     en-us     Copyright 2007 CondeNet Inc. All rights reserved.     Sun, 27 Feb 2011 16:07:00 GMT          dc:creatorWired.com</dc:creator>     <dc:subject />     dc:date2011-02-27T16:07:00Z</dc:date>     dc:languageen-us</dc:language>     dc:rightsCopyright 2007 CondeNet Inc. All rights reserved.</dc:rights>     <atom10:link xmlns:atom10="www.w3.org/2005/Atom" rel="self"  type="application/rss+xml" href="feeds.wired.com/wired/index" /><feedburner:info  uri="wired/index" /><atom10:link xmlns:atom10="www.w3.org/2005/Atom" rel="hub"  href="pubsubhubbub.appspot.com/" />)

      Peers Or Not? Comcast And Level 3 Slug It Out At FCC's Doorstep       http://feeds.wired.com/~r/wired/index/~3/QJQ4vgGV4qM/       *the first description*       Sun, 27 Feb 2011 16:07:00 GMT       http://www.wired.com/epicenter/2011/02 /comcast-level-fcc/       Matthew Lasar       2011-02-27T16:07:00Z     http://www.wired.com/epicenter/2011/02 /comcast-level-fcc/

           360 Cams, AutoCAD and Miles of Fiber: Building an Oscars Broadcast       feeds.wired.com/~r/wired/in…       the second description       Sun, 27 Feb 2011 00:19:00 GMT       www.wired.com/underwire/2… /oscars-broadcast/       dc:creatorTerrence Russell</dc:creator>       dc:date2011-02-27T00:19:00Z</dc:date>     feedburner:origLinkwww.wired.com/underwire/2… /oscars-broadcast/</feedburner:origLink> … … … `

为简洁起见,描述已被替换。您可以看到 RSS 文档就是 XML。有许多库可以解析来自 XML 提要的内容,我们在第十章 -库中展示了如何使用 SimplePie 解析这个提要。但是,凭借您的 XML 知识,您可以轻松地自己解析内容。

清单 14-14 是一个用提要中的要点构建表格的例子。它有链接到整篇文章的文章标题、文档的创建者和发布日期。注意,在 XML 中,creator元素位于名称空间之下,所以我们用 XPath 检索它。输出如图 14-1 中所示。

**清单 14-14。**解析有线 RSS 提要:wired_rss.php

`

    

channel->item as $item){     print "";     print "";     $creator_by_xpath = $item->xpath("dc:creator");     print "";     //equivalent creator, using children function instead of xpath function     //$creator_by_namespace = $item->children('http://purl.org/dc/elements/1.1/')->creator;     //print ""; } ?>
StoryDateCreator
".$item->title."".$item->pubDate."".(String)$creator_by_xpath[0]."
".(String)$creator_by_namespace[0]."
` ![images](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/978657c897784a56bf517f2975005435~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1773458173&x-signature=aIBY%2BGRyUS81p9PmPsC08AzNbmw%3D)

***图 14-1。*清单 14-14 中 RSS 提要解析器的输出

在清单 14-14 中,我们使用 XPath 来获取属于名称空间dccreator元素。

我们也可以用特定的名称空间检索我们的$item元素的子元素。这是一个两步过程。首先我们要找到dc代表什么。

<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" version="2.0">

第二步是将这个名称空间地址作为参数传递给children函数

  //$creator_by_namespace = $item->children('http://purl.org/dc/elements/1.1/')->creator;

用 SimpleXML 生成 XML

我们专门使用 SimpleXML 来解析现有的 XML。但是,我们也可以用它从现有数据生成 XML 文档。这些数据可以是数组、对象或数据库的形式。

为了以编程方式创建一个 XML 文档,我们需要创建一个新的SimpleXMLElement,它将指向我们的文档根。然后我们可以向根添加子元素,以及这些子元素的子元素。见清单 14-15 。

***清单 14-15。*用 SimpleXML 生成一个基本的 XML 文档

`<?php

error_reporting(E_ALL ^ E_NOTICE);

//generate the xml, starting with the root animals=newSimpleXMLElement(<animals/>);animals = new SimpleXMLElement('<animals/>'); animals->{0} = 'Hello World';

$animals->asXML('animals.xml');

//verify no errors with our newly created output file var_dump(simplexml_load_file('animals.xml'));

?>`

产出:

object(SimpleXMLElement)[2]   string 'Hello World' (length=11)

并生成包含以下内容的文件animals.xml:

<?xml version="1.0"?> <animals>Hello World</animals>=

清单 14-15 创建一个根元素<animal>,给它赋值,并调用方法asXML保存到一个文件。为了测试这是否可行,我们加载保存的文件并输出内容。请确保您对该文件位置具有写权限。

在清单 14-16 中,它是对清单 14-4 的补充,我们将动物数据存储为数组,并希望根据这些信息创建一个 XML 文档。

***清单 14-16。*用 SimpleXML 生成一个基本的 XML 文档

`<?php

error_reporting(E_ALL ^ E_NOTICE);

//our data, stored in arrays $dogs_array = array(     array("name" => "snoopy",         "color" => "brown",         "breed" => "beagle cross"     ),     array("name" => "jade",         "color" => "black",         "breed" => "lab cross"     ), );

$cats_array = array(     array("name" => "teddy",         "color" => "brown",         "breed" => "tabby"     ), );

//generate the xml, starting with the root $animals = new SimpleXMLElement('');

catsxml=cats_xml = animals->addChild('cats'); dogsxml=dogs_xml = animals->addChild('dogs');

foreach (catsarrayascats_array as c) {     cat=cat = cats_xml->addChild('cat');     foreach (casc as key => value) {         tmp = cat>addChild(cat->addChild(key);         tmp>0=tmp->{0} = value;     } }

foreach (dogsarrayasdogs_array as d) {     dog=dog = dogs_xml->addChild('dog');     foreach (dasd as key => value) {         tmp = dog>addChild(dog->addChild(key);         tmp>0=tmp->{0} = value;     } }

var_dump(animals);animals); animals->asXML('animals.xml'); print '

'; //verify no errors with our newly created output file var_dump(simplexml_load_file('animals.xml'));

?>`

在清单 14-16 的中,我们用调用new SimpleXMLElement('<animals/>')创建了一个新的SimpleXMLElement根。为了从顶层元素向下填充我们的文档,我们通过调用addChild来创建子元素,并存储对新创建元素的引用。使用元素引用,我们可以添加子元素。通过重复这个过程,我们可以生成一个完整的节点树。

不幸的是,输出函数asXML()根本没有很好地格式化我们的输出。所有内容都显示为一行。为了解决这个问题,我们可以使用DOMDocument类,我们将在本章后面讨论这个类来很好地输出 XML。

$animals_dom = new DOMDocument('1.0'); $animals_dom->preserveWhiteSpace = false; $animals_dom->formatOutput = true; //returns a DOMElement $animals_dom_xml = dom_import_simplexml($animals); $animals_dom_xml = $animals_dom->importNode($animals_dom_xml, true); $animals_dom_xml = $animals_dom->appendChild($animals_dom_xml); $animals_dom->save('animals_formatted.xml');

这段代码创建了一个新的DOMDocument对象,并将其设置为格式化输出。然后我们将SimpleXMLElement对象导入到一个新的DOMElement对象中。我们递归地将节点导入到文档中,然后将格式化的输出保存到文件中。替换清单 14-16 中asXML调用的上述代码会产生干净的嵌套输出:

<?xml version="1.0"?> <animals>   <cats>     <cat>       <name>teddy</name>       brown       <breed>tabby</breed>     </cat>   </cats>   <dogs>     <dog>       <name>snoopy</name>       brown       <breed>beagle cross</breed>     </dog>     <dog>       <name>jade</name>       black       <breed>lab cross</breed>     </dog>   </dogs> </animals>

images 注意 SimpleXML 也可以通过函数simplexml_import_dom导入 DOM 对象。

`<?php

error_reporting(E_ALL ^ ~E_STRICT); domxml=DOMDocument::loadXML("<root><name>Brian</name></root>");dom_xml = DOMDocument::loadXML("<root><name>Brian</name></root>"); simple_xml = simplexml_import_dom(domxml);printdom_xml); print simple_xml->name; // brian

?>`

在清单 14-17 中,我们将生成一个带有名称空间和属性的 RSS 样本。我们的目标是输出具有以下结构的 XML 文档:

<?xml version="1.0" ?> <rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"> <channel>         <title>Brian’s RSS Feed</title>         <description>Brian’s Latest Blog Entries</description>         <link>http://www.briandanchilla.com/node/feed </link>         <lastBuildDate>Fri, 04 Feb 2011 00:11:08 +0000 </lastBuildDate>         <pubDate>Fri, 04 Feb 2011 08:25:00 +0000 </ pubDate>         <item>                 <title>Pretend Topic </title>                 <description>Pretend description</description>                 <link>http://www.briandanchilla.com/pretend-link/</link>                 <guid>unique generated string</guid>                 <dc:pubDate>Fri, 04 Feb 2011 08:25:00 +0000 </dc:pubDate>         </item> </channel> </rss>

***清单 14-17。*用 SimpleXML 生成 RSS 文档

`<?php

error_reporting(E_ALL);

$items = array(     array(         "title" => "a",         "description" => "b",         "link" => "c",         "guid" => "d",         "lastBuildDate" => "",         "pubDate" => "e"),     array(         "title" => "a2",         "description" => "b2",         "link" => "c2",         "guid" => "d2",         "lastBuildDate" => "",         "pubDate" => "e2"), );

rssxml=newSimpleXMLElement(<rssxmlns:dc="http://purl.org/dc/elements/1.1/"/>);rss_xml = new SimpleXMLElement('<rss xmlns:dc="http://purl.org/dc/elements/1.1/"/>'); rss_xml->addAttribute('version', '2.0'); channel=channel = rss_xml->addChild('channel');

foreach (itemsasitems as item) {     itemtmp=item_tmp = channel->addChild('item');

    foreach (itemasitem as key => value) {         if (key == "pubDate") {             tmp=tmp = item_tmp->addChild(key,key, value, "purl.org/dc/elements…");         } else if(key == "lastBuildDate") {             //Format will be: Fri, 04 Feb 2011 00:11:08 +0000             tmp = itemtmp>addChild(item_tmp->addChild(key, date('r', time()));         } else {             tmp=tmp = item_tmp->addChild(key,key, value);         }     } }

//for nicer formatting rssdom=newDOMDocument(1.0);rss_dom = new DOMDocument('1.0'); rss_dom->preserveWhiteSpace = false; rssdom>formatOutput=true;//returnsaDOMElementrss_dom->formatOutput = true; //returns a DOMElement rss_dom_xml = dom_import_simplexml(rssxml);rss_xml); rss_dom_xml = rssdom>importNode(rss_dom->importNode(rss_dom_xml, true); rssdomxml=rss_dom_xml = rss_dom->appendChild(rssdomxml);rss_dom_xml); rss_dom->save('rss_formatted.xml'); ?>`

清单 14-17 中的主要代码行是在根元素$rss_xml = new SimpleXMLElement('<rss xmlns:dc="http://purl.org/dc/elements/1.1/"/>')中设置名称空间,如果条目关键字是pubDate则获取名称空间,如果关键字是lastBuildDate则生成 RFC 2822 格式的日期。

运行清单 14-17 中的后,文件的内容将与此类似:

<?xml version="1.0"?> <rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">   <channel>     <item>       <title>a</title>       <description>b</description>       <link>c</link>       <guid>d</guid>       <lastBuildDate>Fri, 27 May 2011 01:20:04 +0200</lastBuildDate>       <dc:pubDate>e</dc:pubDate>     </item>     <item>       <title>a2</title>       <description>b2</description>       <link>c2</link>       <guid>d2</guid>       <lastBuildDate>Fri, 27 May 2011 01:20:04 +0200</lastBuildDate>       <dc:pubDate>e2</dc:pubDate>     </item>   </channel> </rss>

images 注意关于 SimpleXML 的更多信息可以在[php.net/manual/en/book.simplexml.php.](http://php.net/manual/en/book.simplexml.php.)找到

images要对没有验证的 XML 文档进行故障诊断,您可以在[validator.w3.org/check](http://validator.w3.org/check)使用在线验证器。

DOMDocument

正如本章开始时提到的,SimpleXML 绝不是 PHP 中 XML 操作的唯一选择。另一个流行的 XML 扩展是 DOM。我们已经看到,DOMDocument 在输出格式方面有一些比 SimpleXML 更强大的特性。DOMDocument 比 SimpleXML 更强大,但是正如您所料,它并不容易使用。

大多数情况下,您可能会选择使用 SimpleXML 而不是 DOM。然而,DOM 扩展具有以下附加特性:

  • 遵循 W3C DOM API,所以如果您熟悉 JavaScript DOM,这将很容易适应。
  • 支持 HTML 解析。
  • 不同的节点类型提供了更多的控制。
  • 可以将原始 XML 追加到现有的 XML 文档中。
  • 通过更新或删除节点,可以更容易地修改现有文档。
  • 为 CDATA 和注释提供更好的支持。

使用 SimpleXML,所有节点都是一样的。所以一个元素使用相同的底层对象作为属性。DOM 有不同的节点类型。这些是XML_ELEMENT_NODEXML_ATTRIBUTE_NODE,XML_TEXT_NODE。根据类型的不同,相应的对象属性是元素的tagName、属性的namevalue以及文本的nodeNamenodeValue

//creating a DOMDocument object $dom_xml = new DOMDocument();

DOMDocument可以从字符串、文件中加载 XML,或者从SimpleXML对象中导入。

`//from a string $dom_xml->loadXML('the full xml string');

// from a file $dom_xml->load('animals.xml');

// imported from a SimpleXML object domelement=domimportsimplexml(dom_element = dom_import_simplexml(simplexml); domelement=dom_element = dom_xml->importNode(domelement,true);dom_element, true); dom_element = domxml>appendChild(dom_xml->appendChild(dom_element);`

为了操纵一个 DOM 对象,您可以通过对如下函数的面向对象调用来实现:

$dom_xml->item(0)->firstChild->nodeValue $dom_xml->childNodes $dom_xml->parentNode $dom_xml->getElementsByTagname('div');

有几个保存功能可用- save saveHTML saveHTMLFile,saveXML

DOMDocument 有一个validate函数来检查一个文档是否合法。要在 DOM 中使用 XPath,您需要构造一个新的DOMXPath对象。

$xpath = new DOMXPath($dom_xml);

为了更好地说明 SimpleXML 和 DOM 扩展之间的区别,下面两个使用 DOM 的例子相当于本章前面使用 SimpleXML 的例子。

清单 14-18 输出括号中的所有动物名称和动物类型。它相当于使用 SimpleXML 的清单 14-9 。

***清单 14-18。*用 DOM 查找元素

`<?php

error_reporting(E_ALL ^ E_NOTICE);

$xml = <<<THE_XML        snoopy     brown     beagle cross           teddy     brown     tabby           jade     black     lab cross    THE_XML;

xmlobject=newDOMDocument();xml_object = new DOMDocument(); xml_object->loadXML(xml);xml); xpath = new DOMXPath($xml_object);

names=names = xpath->query("*/name"); foreach (namesasnames as element) {     parenttype=parent_type = element->parentNode->nodeName;     echo "element>nodeValue(element->nodeValue (parent_type)
"; } ?>`

注意在清单 14-18 中,我们需要构造一个DOMXPath对象,然后调用它的query方法。与清单 14-9 中的不同,我们可以直接访问父节点。最后,注意我们在前面的清单中访问节点值和名称作为属性,并通过清单 14-9 中的方法调用。

清单 14-19 展示了如何搜索一个元素值,然后找到该元素的一个属性值。它是清单 14-13 中的 DOM 等价物..

***清单 14-19。*用 DOM 搜索元素和属性值

`<?php

error_reporting(E_ALL);

$xml = <<<THE_XML        snoopy     brown     beagle cross           teddy     brown     tabby           jade     black     lab cross    THE_XML;

xmlobject=newDOMDocument();xml_object = new DOMDocument(); xml_object->loadXML(xml);xml); xpath = new DOMXPath(xmlobject);xml_object);` `results = xpath>query("dog/name[contains(.,jade)]");foreach(xpath->query("dog/name[contains(., 'jade')]"); foreach (results as element) {     print element->attributes->getNamedItem("id")->nodeValue; } ?>`

在清单 14-19 的中需要注意的主要事情是,对于 DOM,我们使用attributes->getNamedItem("id")->nodeValue来查找id属性元素。对于 SimpleXML,在清单 14-13 中,我们使用了attributes()->id

XMLReader 和 XMLWriter

XMLReader 和 XMLWriter 扩展一起使用。它们比 SimpleXML 或 DOM 扩展更难使用。但是,对于非常大的文档,使用 XMLReader 和 XMLWriter 是一个很好的选择(通常是唯一的选择),因为读取器和编写器是基于事件的,不需要将整个文档加载到内存中。但是,由于 XML 不是一次性加载的,所以使用 XMLReader 或 XMLWriter 的先决条件之一是应该事先知道 XML 的确切模式。

通过反复调用read(),查找nodeType,获得value,可以用 XMLReader 获得大部分值。

清单 14-20 是清单 14-4 的的 XMLReader 等价物,它使用 SimpleXML。

***清单 14-20。*用 XMLReader 查找元素

`<?php

error_reporting(E_ALL ^ E_NOTICE);

xml= <<<THEXML<animals>  <dog>    <name>snoopy</name>    brown    <breed>beaglecross</breed>  </dog>  <cat>    <name>teddy</name>    brown    <breed>tabby</breed>  </cat>  <dog>    <name>jade</name>    black    <breed>labcross</breed>  </dog></animals>THEXML;xml = <<<THE_XML <animals>   <dog>     <name>snoopy</name>     brown     <breed>beagle cross</breed>   </dog>   <cat>     <name>teddy</name>     brown     <breed>tabby</breed>   </cat>   <dog>     <name>jade</name>     black     <breed>lab cross</breed>   </dog> </animals> THE_XML;` `xml_object = new XMLReader(); xmlobject>XML(xml_object->XML(xml); dogparent=false;while(dog_parent = false; while (xml_object->read()) {     if (xml_object->nodeType == XMLREADER::ELEMENT) {         if (xml_object->name == "cat") {             dog_parent = false;         } else if (xml_object->name == "dog") {             dog_parent = true;         } else         if (xml_object->name == "name" && dog_parent) {             xml_object->read();             if (xml_object->nodeType == XMLReader::TEXT) {                 print xml_object->value . "
";                 $dog_parent = false;             }         }     } } ?>`

注意,清单 14-20 不包含名称空间元素或 XPath 的用法,仍然很复杂。

一个有用的 XMLReader 函数是expand()。它将返回当前节点的副本作为一个DOMNode。这意味着您现在可以通过标记名搜索子树了。

                $subtree = $xml_reader->expand();                 $breeds = $subtree->getElementsByTagName('breed');

当然,您只希望在本身不是很大的子树上这样做。XMLReader 和 XMLWriter 比基于树的扩展复杂得多,仅节点类型就有 20 种左右。与 SimpleXML 和 DOM 相比,XMLReader 和 XMLWriter 的难度使得它只在必要时使用。

总结

XML 是一种非常有用的交流和存储数据的工具。XML 的跨语言、独立于平台的特性使它成为许多应用的理想选择。XML 文档可以是简单明了的,也可以是非常复杂的,具有复杂的模式和多个名称空间。

在这一章中,我们给出了 XML 的概述。然后我们讨论了用 SimpleXML 扩展解析和生成 XML 文档。SimpleXML 使使用 XML 变得非常容易,同时仍然很强大。我们展示了如何找到元素和属性值,以及如何处理名称空间。

虽然 SimpleXML 是大多数文档的最佳解决方案,但是在适当的时候也应该使用 DOM 和 XMLReader 等替代方案。对于 XHTML 文档,DOM 是有意义的,对于非常大的文档,XMLReader 和 XMLWriter 可能是唯一可行的选择。在任何情况下,了解多种 XML 解析器都不是坏事。