PHP5-面向对象编程-全-
PHP5 面向对象编程(全)
原文:
zh.annas-archive.org/md5/380cfa72fbf5dd58636d4c4c33d2ee67译者:飞龙
第一章:引言
面向对象编程主要关于隐藏对用户不重要的内容并突出显示重要的内容。PHP 5 提供了标准化的方法来指定通常由功能齐全的面向对象语言提供的各种属性作用域。
本书涵盖的内容
第一章 介绍了面向对象编程以及它如何适用于 PHP。突出了函数式编程相对于过程式编程的一些好处。
在 第二章 中,你将学习如何创建对象并定义它们的属性和方法。接着是类、属性和方法的具体细节,以及方法的范围。本章展示了在 PHP 中使用接口和其他一些基本面向对象特性来启动面向对象编程旅程的好处。
现在你已经完成了 PHP 中面向对象编程的基础,第三章 将帮助你巩固基础。它帮助你处理更多细节和一些高级特性。例如,你将了解类信息函数,这允许你调查任何类的细节。本章将带你了解一些实用的面向对象信息函数、异常处理、迭代器和使用序列化存储对象。
在 第四章 中,你将学习一些设计模式以及如何在 PHP 中实现它们。这些是面向对象编程的必要组成部分,使你的代码更有效、更高效,并且更容易维护。有时我们在代码中实现这些设计模式,却不知道这些解决方案是由设计模式定义的。正确使用正确的模式可以使你的代码表现更佳;同样,不正确地使用它们可能会使你的代码运行得更慢,效率更低。
第五章 专注于 PHP 中面向对象编程的两个非常重要的特性:反射和单元测试。PHP5 用更智能的新 API 替换了许多旧的 API。其中之一是反射 API,它允许你反转或工程化任何类或对象,以了解其属性和方法。你可以动态地调用这些方法等。单元测试是良好、稳定和可管理应用程序设计的必要部分。我们关注一个非常流行的包,PHPUnit,它是 JUnit 的 PHP 版本。如果你遵循本章提供的指南,你将能够成功设计自己的单元测试。
PHP 中的一些内置对象和接口使 PHP 开发者的生活变得更加容易。在 第六章 中,你将了解一个名为标准 PHP 库或 SPL 的大型对象存储库。
第七章: 在本章中,我们讨论了改进的 MySQL API,即 MySQLi,并对 PHP 数据对象(PDO)、adoDB 和 PEAR::MDB2 进行了基本介绍。我们探讨了使用 adoDB 的 Active Record 库在 PHP 中实现 Active Record 模式,以及使用 Propel 实现对象关系映射(ORM)模式。我们关注了一些对使用面向对象方式访问数据库的 PHP 开发者来说有趣的具体主题。
在第八章中,您将学习如何使用 PHP 处理 XML。您将了解不同的 API,如 SimpleXML API 用于读取 XML,以及 DOMDocument 对象用于解析和创建 XML 文档。
第九章: 在第四章中,您学习了设计模式如何通过提供解决问题的共同方法来简化编程中的日常生活。应用架构中最常用的设计模式之一是模型-视图-控制器(MVC)。在本章中,我们讨论了 MVC 框架的基本结构,然后向您介绍一些这些流行的框架。框架在 PHP 应用的快速开发中扮演着非常重要的角色。您将在本章学习如何构建框架,这将也有助于您理解对象加载、数据抽象层、分离的重要性,并最终更深入地了解应用是如何实现的。
这本书适合谁
从 PHP5 的初学者到中级用户
惯例
在这本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
代码有三种样式。文本中的代码词汇如下所示:“在某些情况下,您可能需要调查当前作用域中的哪些类。您可以使用get_declared_classes()函数轻松完成此操作。”
代码块将按照以下方式设置:
<?
class ParentClass
{
}
class ChildClass extends ParentClass
{
}
$cc = new ChildClass();
if (is_a($cc,"ChildClass")) echo "It’s a ChildClass Type Object";
echo "\n";
if (is_a($cc,"ParentClass")) echo "It’s also a ParentClass Type
Object";
?>
新术语和重要词汇将以粗体字形式介绍。屏幕上显示的词汇,例如在菜单或对话框中,将以我们的文本如下所示:“如果您将服务器放置在您的网络服务器(此处为localhost)文档中,根目录在一个名为proxy的文件夹中,然后访问客户端,您将得到以下输出:”
2007 年 3 月 28 日 16:13:20。
注意
重要的注意事项将以这样的框呈现。
小贴士
小贴士和技巧将以这样的形式出现。
读者反馈
我们始终欢迎读者的反馈。让我们知道您对这本书的看法,您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。
要向我们发送一般反馈,只需将电子邮件发送到 <feedback@packtpub.com>,确保在邮件的主题中提及书名。
如果您需要一本书并且希望我们出版,请通过www.packtpub.com上的建议标题表单或发送电子邮件至<suggest@packtpub.com>给我们留言。
如果您在某个主题上具有专业知识,并且对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载本书的示例代码
访问www.packtpub.com/files/code/2561_Code.zip,从标题列表中选择本书以下载任何示例代码或额外资源。然后,将显示可下载的文件。
可下载的文件包含如何使用它们的说明。
错误清单
尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并有助于改进本书的后续版本。如果您发现任何错误,请通过访问www.packtpub.com/support,选择您的书籍,点击提交错误链接,并输入您的错误详情来报告它们。一旦您的错误得到验证,您的提交将被接受,并将错误添加到现有错误清单中。现有的错误可以通过从www.packtpub.com/support选择您的标题来查看。
问题
如果您在本书的某个方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。
本书献给我的儿子阿菲夫——小爱因斯坦
第一章:面向对象编程(OOP)与过程式编程的比较
PHP 是过去几年中最受欢迎的脚本语言之一。几乎有 60% 的 Web 服务器运行在 Apache 上,并使用 PHP。它的受欢迎程度如此之高,以至于每个月都有数百万个网站和 Web 应用程序使用 PHP 开发。PHP 的旅程始于作为 Perl 的简单替代品,几年后它变得极其流行和强大。该语言本身与 ANSI C 非常相似。
PHP 变得如此受欢迎的一个原因是它的学习曲线很短。学习 PHP 不是一个很大的任务,尤其是如果你熟悉 Java 或 C 的语法。由于编写 PHP 脚本很容易,任何人都可以编写 PHP 代码,而不必遵循约定,将表示层与业务逻辑混合(这是大量不可管理项目存在的主要原因之一)。由于在 PHP 中没有遵循严格的编码约定,随着时间的推移,随着项目规模的扩大,它可能会变成一个难以管理的恶魔。
OOP 或 面向对象编程 是一种良好的编程实践,可以更容易地创建可管理的项目。过程式编程意味着编写没有对象的代码。过程式编程由带有或没有例程的代码组成。OOP 为任何语言提供了更好的编码、最佳性能以及编写非常大的项目而无需过多担心管理的功能。OOP 为你提供了创建可重用对象的便利,你可以或其他开发者可以在他们的项目中使用这些对象,而无需反复重新发明它们。OOP 消除了编写和管理大型应用程序的麻烦和困难。
在这本书中,我们将讨论如何使用 PHP 的面向对象编程(OOP)获得最大效益,使用逐步指导、真实生活示例来展示 OOP 如何帮助你编写有效的代码,如何改进你的编码风格,以及如何随着时间的推移重用它们。这本书不会作为 PHP 语言的参考书;我们只会涵盖 PHP 的 OOP 特性,而不是一般 PHP 的基础知识。如果你正在寻找一本好的参考书,首先查阅 PHP 手册,然后你可以学习由 Leon Atkinson 编写的《Core PHP Programming》,这是一本非常好的书。
PHP 简介
如果你已经是一名 PHP 开发者,本节不是为你准备的,而是为那些刚开始学习 PHP 并使用这本书的人准备的。虽然我在一开始就说,我假设你在阅读这本书时将在 PHP 领域有一些预开发经验,但如果你是一个完全的新手,并想通过这本书学习 OOP,那么这一节可能值得回顾一下基本的 PHP 语言特性。如果你已经足够熟悉,请不要跳过这一节,因为我们在这里还有其他主题要讨论。
所以你可能想知道 PHP 的介绍在哪里,我没有在这里看到任何代码!好吧,你不需要。互联网上最好的资源是免费的。请访问www.php.net,下载手册并阅读基本章节。为了详细学习 PHP,你可以研究 David Sklar 所著的《Learning PHP5》这本书。
准备,设置,出发
在这本书中,我们使用 PHP5.1.2 作为示例,但几乎在 99%的情况下,它都可以运行在 PHP 版本 5x 上。我们在机器上安装了 MySQL 5,并使用 Apache 2 作为我们的 Web 服务器。如果你不熟悉在你的机器上配置所有这些,你可以下载预先配置好的 WAMP 或 LAMP 发行版,如 XAMPP(apachefriends.org)或 Apache2Triad(www.apache2triad.net)。你可以在每个产品的网站上找到相应的安装和自定义文档。
PHP 中面向对象编程的历史简述
当 PHP 被开发出来时,它自身并没有实现面向对象的功能。在 PHP/FI 之后,当 Zeev、Rasmus 和 Andy 重写了核心并发布了 PHP3 时,引入了非常基础的面向对象功能。当 PHP4 发布时,面向对象功能得到了成熟,并且性能有了巨大的提升。但 PHP 团队再次重写了核心引擎,引入了全新的对象模型,并发布了 PHP5。现在有两个版本的 PHP 正在开发中。不要因为将 PHP 版本与其他语言进行比较而感到困惑。PHP5 并不意味着它是最新的 PHP 版本。正如我之前所说的,PHP4 和 PHP5 仍在积极发布(尽管 PHP4 将在 2007 年 12 月之后不再发布)。在这两个版本之间,PHP5 实现了几乎完整的面向对象功能,而 PHP4 则没有。在撰写这本书的时候,这两个版本的最新版本是 PHP5.2 和 PHP4.4。
过程式编程与面向对象编程风格
PHP 允许你以两种风格编写代码,一种是通过过程式编程,另一种是通过面向对象编程。你甚至可以在 PHP5 中编写过程式代码,而不会出现任何问题。如果你对过程式编程和面向对象编程不甚了解,那么我们将探讨这两种不同的编程风格。以下两个示例不是完整的运行示例,而是伪代码:
<?
$user_input = $_POST[‘field‘];
$filtered_content = filter($user_input); //user input filtering
mysql_connect("dbhost","dbuser","dbpassword"); //database
mysql_select_db("dbname");
$sql = "some query";
$result = mysql_query($sql);
while ($data = mysql_fetch_assoc())
{
process ($data);
}
process_user_input($filtered_content);
?>
你会注意到使用了很多内联处理,无论是直接使用还是通过使用函数。这可以作为一个典型过程式操作的例子。让我们看看转换成面向对象编程后它看起来会怎样:
<?
$input_filter = new filter();
$input_filter->filter_user_input(); //filter the user inputs
$db = new dal("mysql"); //data access layer
$db->connect($dbconfig);//we wre using mysql
$result = $db->execute($sql);
ReportGenerator::makereport($result); //process data
$model = new Postmodel($filter->get_filtered_content());
$model->insert();
?>
现在如果你看看这两个代码片段,你会发现后者要容易阅读得多。嗯,你可以通过在第一个片段中引入更多的函数来使其更易读,但当你使用它们时,你准备好搜索多少个函数呢?后者的片段组织得更好,因为你知道哪个对象正在处理哪个过程。如果你用过程式风格编写大型应用程序,那么在几个版本之后几乎不可能管理。当然,你可以实施严格的编码规范,但数百万开发者都认为,除非以面向对象的方式实现,否则过程式编程不会给你带来终极的可管理性和可用性。几乎所有的大型应用程序都是使用面向对象的方法编写的。
面向对象编程(OOP)的好处
面向对象编程(OOP)是为了让开发者的生活更轻松而发明的。使用面向对象编程,你可以将问题分解成相对容易理解的小问题。面向对象编程的主要目标是:你想做的任何事情,都通过对象来完成。对象基本上是小的离散代码块,可以将数据和行为结合在一起。在应用程序中,所有这些对象都相互连接,它们之间共享数据,解决问题。
从许多方面来看,面向对象编程(OOP)可以被认为更好,尤其是当你考虑到开发时间和维护开销时。面向对象编程的主要好处可以概括如下:
-
可重用性:一个对象是一个具有大量属性和方法并可以与其他对象交互的实体。一个对象可能是充分的,或者它可能依赖于其他对象。但通常开发对象是为了解决一组特定的问题。因此,当其他开发者遇到相同的问题时,他们可以直接将你的类集成到他们的项目中,并使用它,而不会影响他们的现有工作流程。这防止了 DRY(不要重复自己),在函数式或模块化编程中,重用是可能的,但比较复杂。
-
重构:当你需要重构你的项目时,面向对象编程(OOP)能给你带来最大的好处,因为所有对象都是小的实体,并且将它们的属性和方法作为自身的一部分。因此,重构相对容易。
-
可扩展性:如果你需要向你的项目添加功能,你可以通过面向对象编程(OOP)实现最佳结果。面向对象编程的核心特性之一就是可扩展性。你可以重构你的对象以添加功能。在这个过程中,你仍然可以保持这个对象的向后兼容性,使其与旧代码库兼容。或者,你可以扩展这个对象,创建一个完全新的对象,它保留了从其派生出来的父对象的所有必要属性和方法,然后暴露新的功能。这被称为“继承”,是面向对象编程的一个非常重要的特性。
-
维护:面向对象的代码更容易维护,因为它遵循某种严格的编码规范,并且以自解释的格式编写。例如,当开发者扩展它、重构它或调试它时,他们可以轻松地找出内部编码结构,并一次又一次地维护代码。此外,只要你的项目中存在团队开发环境,面向对象编程(OOP)就可以是最佳解决方案,因为你可以将代码分成小块后进行分发。这些小块可以作为一个单独的对象来开发,因此开发者可以几乎独立地开发它们。最后,合并代码将变得非常容易。
-
效率:面向对象编程的概念实际上是开发出来以提高效率和简化开发过程的。已经开发出几种设计模式来创建更好、更高效的代码。此外,在 OOP 中,你可以用比过程式编程更好的方法来思考你的解决方案。因为你首先将问题分解成一小部分问题,然后为每个问题找到解决方案,大问题就自动解决了。
对象剖析
那么,什么是对象呢?嗯,它不过是一段包含许多属性和方法代码。那么它和数组相似吗,因为数组可以存储通过属性(嗯,它们被称为键)识别的数据?对象比数组多得多,因为它们内部包含一些方法。它们可以隐藏它们或公开它们,这在数组中是不可能的。对象在某种程度上可以与数据结构、数据结构相提并论,并且可以将其自身包含许多其他对象,要么在它们之间创建紧密耦合,要么创建松散耦合。对象可以将其自身包含许多其他对象,要么在它们之间创建紧密耦合,要么创建松散耦合。我们将在本书的后面部分学习更多关于松散耦合和紧密耦合的内容,并了解它们将如何对我们有用。
让我们看看 PHP 中一个对象的代码。以下对象是一个非常简单的对象,可以向一组用户发送电子邮件。在 PHP5 中,对象与 PHP4 中的对象有很大的不同。我们不会讨论它的细节,这只是一个介绍性的对象,以展示如何在 PHP 中编写对象。
<?
//class.emailer.php
class emailer
{
private $sender;
private $recipients;
private $subject;
private $body;
function __construct($sender)
{
$this->sender = $sender;
$this->recipients = array();
}
public function addRecipients($recipient)
{
array_push($this->recipients, $recipient);
}
public function setSubject($subject)
{
$this->subject = $subject;
}
public function setBody($body)
{
$this->body = $body;
}
public function sendEmail()
{
foreach ($this->recipients as $recipient)
{
$result = mail($recipient, $this->subject, $this->body,
"From: {$this->sender}\r\n");
if ($result) echo "Mail successfully sent to
{$recipient}<br/>";
}
}
}
?>
上述对象包含四个私有属性、三个访问器方法和一个将电子邮件发送给收件人的方法。那么我们如何在 PHP 代码中使用它呢?让我们看看下面:
<?
$emailer = new emailer("hasin@pageflakes.com"); //construcion
$emailer->addRecipients("hasin@somewherein.net"); //accessing methods
// and passing some data
$emailer->setSubject("Just a Test");
$emailer->setBody("Hi Hasin, How are you?");
$emailer->sendEmail();
?>
我相信上面的代码片段要更加自解释和易于阅读。如果你遵循适当的规范,你可以使你的代码易于管理和维护。WordPress 开发者在其网站上使用了一个口号,即www.wordpress.org,它是“编码即诗歌”。编码确实就像一首诗;如果你知道如何写它。
PHP4 和 PHP5 中面向对象的差异
PHP5 中的对象与 PHP4 中的对象有很大不同。从 PHP5 开始,面向对象编程(OOP)在真正意义上成熟起来。OOP 自 PHP3 引入以来,但那只是真正面向对象编程的错觉。在 PHP4 中,你可以创建对象,但无法真正感受到对象的真正风味。在 PHP4 中,它几乎是一个较差的对象模型。
PHP4 中 OOP 的主要区别之一是一切都是开放的;没有关于方法或属性使用的限制。你不能为你的方法使用 public、private 和 protected 修饰符。在 PHP4 中,开发者通常使用双下划线声明私有方法。但这并不意味着以这种格式声明方法实际上阻止你从类外部访问该方法。这只是一种遵循的纪律。
在 PHP4 中,你可以找到接口,但没有抽象或 final 关键字。接口是一段代码,任何对象都可以实现,这意味着对象必须实现接口中声明的所有方法。它严格检查你必须实现其中的所有函数。在接口中,你只能声明任何方法的名称和访问类型。抽象类是某些方法可能也有一些内容的地方。然后任何对象都可以扩展那个抽象类,并扩展那个抽象类中定义的所有这些方法。一个 final 类是一个不允许扩展的对象。在 PHP5 中,你可以使用所有这些。
在 PHP4 中,接口没有多重继承。这意味着一个接口只能扩展一个接口。但在 PHP5 中,通过实现多个接口,支持多重继承。
在 PHP4 中,几乎一切都是静态的。这意味着如果你在类中声明任何方法,你可以直接调用它,而无需创建其实例。例如,以下代码在 PHP4 中是有效的:
<?
class Abc
{
var $ab;
function abc()
{
$this->ab = 7;
}
function echosomething()
{
echo $this->ab;
}
}
echo abc::echosomething();
?>
然而,在 PHP5 中这是无效的,因为echosomething()方法使用了$this关键字,而在静态调用中不可用。
PHP4 中没有基于类的常量。PHP4 中的对象没有静态属性,也没有析构函数。
每当复制一个对象时,它都是该对象的浅拷贝。但在 PHP5 中,只有使用 clone 关键字才能进行浅拷贝。
PHP4 中没有异常对象。但在 PHP5 中,异常管理是一个很大的新增功能。
在 PHP4 中,有一些函数用于调查类的方法和属性,但在 PHP5 中,除了这些函数之外,还引入了一个强大的 API 集合(反射 API),用于此目的。
通过像__get()和__set()这样的魔术方法进行方法重载在 PHP5 中可用。还有许多内置对象可以使你的生活更轻松。
但最重要的是,PHP5 在 OOP 方面有巨大的性能提升。
一些基本的 OO 术语
一些基本的面向对象术语如下:
类:类是对象的模板。类包含定义对象如何行为和交互的代码,无论是相互之间还是与它。每次你在 PHP 中创建一个对象时,实际上你就是在开发类。因此,有时在这本书中,我们将对象命名为“类”,因为它们是同义的。
属性:属性是类中的一个容器,可以保留一些信息。与其它语言不同,PHP 不检查属性变量的类型。属性可能只能在本类中访问,由其子类访问,或者由任何人访问。本质上,属性是声明在类内部但不在该类任何函数内部的变量。
方法:方法是类内的函数。像属性一样,方法也可以被这三种类型的用户访问。
封装:封装是将代码及其操作的数据绑定在一起,并使它们免受外部干扰和误用的机制。将数据和方法的包装成一个单一单元(称为类)的过程称为封装。封装的好处是它执行任务而不让你担心。
多态:对象可以是任何类型。离散对象可以具有离散的属性和方法,它们与其他对象独立工作。然而,一组对象可以从父对象派生出来并保留父类的某些属性。这个过程称为多态。一个对象可以变形为其他几个对象,同时保留其部分行为。
继承:通过扩展另一个对象来派生新对象的关键过程称为继承。当你从一个对象继承对象时,子类(继承者)继承了超类(被继承)的所有属性和方法。子类可以处理超类中的每个方法(这被称为重写)。
耦合:耦合是类之间相互依赖的行为。松耦合架构比紧耦合对象更易于重用。在下一章中,我们将学习耦合的细节。耦合是设计更好的对象时非常重要的一个考虑因素。
设计模式:最初由“四人帮”发明,设计模式是面向对象编程中解决相似问题集的一种更智能的方法的技巧。使用设计模式(DP)可以在开发者编写的代码最少的情况下提高整个应用程序的性能。有时不使用 DP 可能无法设计优化的解决方案。但过度和不计划使用 DP 也可能降低应用程序的性能。本书中有一章专门介绍设计模式。
子类:在面向对象编程中一个非常常见的术语,本书中我们一直使用这个术语。当一个对象从另一个对象派生出来时,派生出来的对象被称为其派生自的子类。
超类:如果一个对象是从它派生出来的,那么这个类就是该对象的超类。为了简化,当你扩展一个对象时,你正在扩展的对象是新扩展对象的超类。
实例:每次通过调用其构造函数创建一个对象时,它将被称作一个实例。为了简化,每次你写一些像这样 $var = new Object(); 的东西时,你实际上创建了一个对象类的实例。
通用编码约定
在本书中,我们将遵循一些编码约定。这些约定不会过于严格,但它们将帮助你在大范围内维护你的应用程序。此外,它还将提高代码的可维护性。它还将通过避免重复和冗余对象来帮助你编写更高效的代码。最后但同样重要的是,它将使你的代码更加易于阅读。
-
在一个单独的
php文件中,我们一次不会写超过一个类。超出该类的范围,我们不会写任何过程式代码。 -
我们将使用适当的命名约定来保存任何类。例如,我们将保存放置在本章前面引入的
Emailer类的文件为class.emailer.php。使用这种命名约定你能获得哪些好处?嗯,不进入那个文件,你现在至少可以确认这个文件包含一个名为 "Emailer" 的类。 -
永远不要在文件名中混合大小写。这会创建丑陋的应用程序结构。继续使用全部小写字母。
-
就像类一样,我们将任何接口保存为
interface.name.php,抽象类保存为abstract.name.php,最终类保存为final.name.php。 -
我们将始终使用驼峰式命名我们的类。这意味着主要部分的第一个字母总是大写,其余部分是小写。例如,一个名为 "arrayobject" 的类,如果我们写成
ArrayObject,将更容易阅读。 -
在编写属性或类变量名称时,我们将遵循相同的约定。
-
在编写方法名称时,我们将以小写字母开头,然后其余部分使用驼峰式。例如,一个用于发送电子邮件的方法可以命名为
sendEmail。 -
好吧,这本书中不再使用更多的约定。
摘要
在本章中,我们学习了面向对象编程以及它是如何与 PHP 结合的。我们还了解了一些相对于过程式和函数式编程的优势。然而,我们还没有详细学习 PHP 中的面向对象语言。在下一章中,我们将更多地了解对象及其方法和属性,特别是创建对象、扩展其功能和它们之间的交互。所以,让我们开始这段旅程吧,快乐地用 PHP 进行面向对象编程。
第二章:启动 OOP
在本章中,我们将学习如何创建对象,定义它们的属性(或属性)和方法。PHP 中的对象总是使用“class”关键字创建的。在本章中,我们将学习类、属性和方法的具体细节。我们还将学习方法的范围以及修饰符和使用接口的好处。本章还将介绍 PHP 中其他基本面向对象编程(OOP)特性。总的来说,本章是你在 PHP 中启动 OOP 的较好资源之一。
让我们制作一些对象
如我之前所说,你可以在 PHP 中使用class关键字创建一个对象。一个类由一些属性和方法组成,可以是公开的或私有的。让我们以我们在第一章中看到的Emailer类为例。在这里,我们将讨论它实际上做了什么:
<?
//class.emailer.php
class Emailer
{
private $sender;
private $recipients;
private $subject;
private $body;
function __construct($sender)
{
$this->sender = $sender;
$this->recipients = array();
}
public function addRecipients($recipient)
{
array_push($this->recipients, $recipient);
}
public function setSubject($subject)
{
$this->subject = $subject;
}
public function setBody($body)
{
$this->body = $body;
}
public function sendEmail()
{
foreach ($this->recipients as $recipient)
{
$result = mail($recipient, $this->subject, $this->body,
"From: {$this->sender}\r\n");
if ($result) echo "Mail successfully sent to {$recipient}<br/>";
}
}
}
?>
在这段代码中,我们以class Emailer开始,这意味着我们类的名字是Emailer。在命名类时,遵循与变量相同的命名约定,即你不能以数字字母开头等。
之后,我们声明了这个类的属性。这里有四个属性,分别是$sender、$recipient、$subject和$body。请注意,我们用关键字private声明了它们。私有属性意味着这个属性只能从这个类内部访问。属性不过是类内部的变量。
如果你记得方法是什么,它只是类内部的一个函数。在这个类中,有五个函数,__construct()、addRecipient()、setSubject()、setBody()和sendEmail()。请注意,最后四个方法被声明为公开的。这意味着当有人实例化这个对象时,他们可以访问这些方法。
__construct()是一个类内的特殊方法,称为构造方法。每当从这个类创建一个新的对象时,这个方法将自动执行。所以如果我们需要在初始化对象时执行一些初步任务,我们将从构造方法中执行。例如,在这个Emailer类的构造方法中,我们只设置了$recipients为一个空数组,我们还设置了发件人姓名。
在类内部访问属性和方法
你想知道函数是如何从其内容内部访问类属性的吗?让我们用以下代码看看:
public function setBody($body)
{
$this->body = $body;
}
在我们的类中有一个名为$body的私有属性,如果我们想从函数内部访问它,我们必须用$this来引用它。$this意味着对当前对象实例的引用。因此,我们可以通过$this->body访问body属性。请注意,我们必须使用一个跟在实例后面的"->"来访问类的属性(即类变量)。
同样,就像属性一样,我们可以在这种格式中从另一个成员方法内部访问任何成员方法。例如,我们可以调用setSubject方法为$this->setSubject()。
注意
请注意,$this关键字仅在方法的作用域内有效,只要它没有被声明为静态。你不能从类外部使用$this关键字。我们将在本章后面的“修饰符”部分更详细地学习“静态”、“私有”、“公共”这些关键字。
使用对象
让我们在 PHP 代码内部使用新创建的Emailer对象。在使用对象之前,我们必须注意一些事项。在使用对象之前,你必须初始化它。初始化之后,你可以使用"->"符号来访问其实例的所有公共属性和方法。让我们通过以下代码来看一下:
<?
$emailerobject = new Emailer("hasin@pageflakes.com");
$emailerobject->addRecipients("hasin@somewherein.net");
$emailerobject->setSubject("Just a Test");
$emailerobject->setBody("Hi Hasin, How are you?");
$emailerobject->sendEmail();
?>
在上述代码片段中,我们首先在第一行创建了一个Emailer类的实例,并将其赋值给变量$emailerobject。在这里,有一点很重要:我们在实例化这个对象时提供了一个发送地址:
$emailerobject = new Emailer("hasin@pageflakes.com");
记得我们类中有一个构造函数方法,名为__construct($sender)。当我们初始化对象时,我们说构造函数会自动调用。所以,当我们初始化这个Emailer类时,我们必须提供构造函数方法中声明的正确参数。例如,以下代码将创建一个警告:
<?
$emailer = new emailer();
?>
当你执行上述代码时,会显示如下警告:
Warning: Missing argument 1 for emailer::__construct(),
called in C:\OOP with PHP5\Codes\ch1\class.emailer.php on line 42
and defined in <b>C:\OOP with PHP5\Codes\ch1\class.emailer.php</b>
on line <b>9</b><br />
看到区别了吗?如果你的类没有构造函数方法,或者有一个不带参数的构造函数,你可以使用上述代码来实例化它。
修饰符
你已经看到我们在类中使用了private或public等关键字。那么这些是什么,为什么我们需要使用它们呢?好吧,这些关键字被称为修饰符,并在 PHP5 中引入。它们在 PHP4 中是不可用的。这些关键字帮助你定义这些变量和属性将如何被类的用户访问。让我们看看这些修饰符实际上做了什么。
私有(Private):声明为私有的属性或方法不允许从类外部调用。然而,同一类内的任何方法都可以无问题地访问它们。在我们的Emailer类中,我们把这些属性都声明为私有,所以如果我们执行以下代码,我们会发现一个错误。
<?
include_once("class.emailer.php");
$emobject = new Emailer("hasin@somewherein.net");
$emobject->subject = "Hello world";
?>
执行上述代码后,会显示如下所示的致命错误:
<b>Fatal error</b>: Cannot access private property emailer::$subject
in <b>C:\OOP with PHP5\Codes\ch1\class.emailer.php</b> on line
<b>43</><br />
这意味着你不能从类外部访问任何私有属性或方法。
公共(Public):任何未明确声明为私有或受保护的属性或方法都是公共方法。你可以从类内部或外部访问公共方法。
受保护(Protected):这是另一个在面向对象编程中有特殊意义的修饰符。如果任何属性或方法被声明为受保护,你只能从其子类中访问该方法。我们将在本章后面的部分详细学习子类。但为了看到受保护的属性或方法实际上是如何工作的,我们将使用以下示例:
首先,让我们打开class.emailer.php文件(Emailer类)并更改$sender变量的声明。让它如下所示:
protected $sender
现在创建另一个名为class.extendedemailer.php的文件,并包含以下代码:
<?
class ExtendedEmailer extends emailer
{
function __construct(){}
public function setSender($sender)
{
$this->sender = $sender;
}
}
?>
现在用这个对象这样使用:
<?
include_once("class.emailer.php");
include_once("class.extendedemailer.php");
$xemailer = new ExtendedEmailer();
$xemailer->setSender("hasin@pageflakes.com");
$xemailer->addRecipients("hasin@somewherein.net");
$xemailer->setSubject("Just a Test");
$xemailer->setBody("Hi Hasin, How are you?");
$xemailer->sendEmail();
?>
现在如果你仔细查看ExtendedEmailer类的代码,你会发现我们访问了其父类(实际上是Emailer类)的$sender属性。我们之所以能够访问这个属性,仅仅是因为它被声明为受保护的。在这里我们得到的另一个好处是,属性$sender仍然不能直接从这两个类的作用域之外直接访问。这意味着如果我们执行以下代码,它将生成一个致命错误。
<?
include_once("class.emailer.php");
include_once("class.extendedemailer.php");
$xemailer = new ExtendedEmailer();
$xemailer->sender = "hasin@pageflakes.com";
?>
在执行时,它给出以下错误:
<b>Fatal error</b>: Cannot access protected property
extendedEmailer::$sender in <b>C:\OOP with
PHP5\Codes\ch1\test.php</b> on line <b>5</b><br />
构造函数和析构函数
我们在本章前面讨论了构造方法。构造方法是在创建类的实例时自动执行的方法。在 PHP5 中,你可以在类内部以两种方式编写构造方法。第一种是在类内部创建一个名为__construct()的方法。第二种是创建一个与类名完全相同名称的方法。例如,如果你的类名是Emailer,构造方法的名称将是Emailer()。让我们看看以下这个计算任何数字阶乘的类:
<?
//class.factorial.php
class factorial
{
private $result = 1;// you can initialize directly outside
private $number;
function __construct($number)
{
$this->number = $number;
for($i=2; $i<=$number; $i++)
{
$this->result *= $i;
}
}
public function showResult()
{
echo "Factorial of {$this->number} is {$this->result}. ";
}
}
?>
在上面的代码中,我们使用了__construct()作为我们的构造函数。如果你将__construct()函数重命名为factorial(),行为将是相同的。
现在,你可能想知道一个类是否可以同时以两种风格拥有构造方法?这意味着一个名为__construct()的函数和一个与类名相同的函数。那么哪个构造方法会执行,或者它们都会执行吗?这是一个好问题。实际上,没有机会同时执行。如果有两种风格的构造方法,PHP5 将优先选择__construct()函数,另一个将被忽略。让我们通过以下示例来看看:
<?
//class.factorial.php
class Factorial
{
private $result = 1;
private $number;
function __construct($number)
{
$this->number = $number;
for($i=2; $i<=$number; $i++)
{
$this->result*=$i;
}
echo "__construct() executed. ";
}
function factorial($number)
{
$this->number = $number;
for($i=2; $i<=$number; $i++)
{
$this->result*=$i;
}
echo "factorial() executed. ";
}
public function showResult()
{
echo "Factorial of {$this->number} is {$this->result}. ";
}
}
?>
现在如果你像下面这样使用这个类:
<?
include_once("class.factorial.php");
$fact = new Factorial(5);
$fact->showResult();
?>
你会发现输出是:
__construct() executed. Factorial of 5 is 120
与构造方法类似,还有一个析构方法,它实际上是在销毁对象时工作的。你可以通过将其命名为__destruct()来显式创建析构方法。这个方法将在你的脚本执行结束时由 PHP 自动调用。为了测试这一点,让我们在我们的阶乘类中添加以下代码:
function __destruct()
{
echo " Object Destroyed.";
}
现在再次执行使用脚本,这次你会看到以下输出:
__construct() executed. Factorial of 5 is 120\. Object Destroyed.
类常量
希望你已经知道,你可以使用define关键字在你的 PHP 脚本中创建常量。但要在类中创建常量,你必须使用const关键字。这些常量实际上就像静态变量一样工作,唯一的区别是它们是只读的。让我们看看我们如何创建常量并使用它们:
<?
class WordCounter
{
const ASC=1; //you need not use $ sign before Constants
const DESC=2;
private $words;
function __construct($filename)
{
$file_content = file_get_contents($filename);
$this->words =
(array_count_values(str_word_count(strtolower
($file_content),1)));
}
public function count($order)
{
if ($order==self::ASC)
asort($this->words);
else if($order==self::DESC)
arsort($this->words);
foreach ($this->words as $key=>$val)
echo $key ." = ". $val."<br/>";
}
}
?>
这个 WordCounter 类可以计算任何给定文件中单词的频率。这里我们定义了两个常量名称 ASC 和 DESC,它们的值分别是 1 和 2。要访问类内的这些常量,我们使用 self 关键字来引用它们。请注意,我们使用 :: 操作符来访问它们,而不是 -> 操作符,因为这些常量就像静态成员一样。
最后,为了使用这个类,让我们创建一个如下所示的片段。在这个片段中,我们也在访问那些常量:
<?
include_once("class.wordcounter.php");
$wc = new WordCounter("words.txt");
$wc->count(WordCounter::DESC);
?>
请注意,我们通过在类名后直接跟随 :: 操作符来从类外部访问类常量,而不是在类的实例之后。现在让我们测试脚本,请创建一个名为 words.txt 的文件,其内容如下,并将其放在放置上述脚本的同一目录中:
words.txt
Wordpress is an open source blogging engine. If you are not familiar
with blogging, it is something like keeping a diary on the web.
A blog stands for web log. Wordpress is totally free and
released under the GPL.
现在,如果你执行使用脚本,这次,你会看到以下输出。
is = 3
a = 2
blogging = 2
web = 2
wordpress = 2
stands = 1
blog = 1
in = 1
diary = 1
for = 1
free = 1
under = 1
gpl = 1
released = 1
and = 1
totally = 1
log = 1
something = 1
if = 1
you = 1
engine = 1
source = 1
an= 1
open = 1
are = 1
not = 1
ï = 1
like = 1
it = 1
with = 1
familiar = 1
keeping = 1
很好的实用工具,您觉得呢?
扩展类 [继承]
面向对象编程中最伟大的特性之一是你可以扩展一个类并创建一个全新的对象。新的对象可以保留从其扩展的父对象的所有功能,或者可以覆盖。新的对象也可以引入一些特性。让我们扩展我们的 Emailer 类并覆盖 sendEmail 函数,以便它可以发送 HTML 邮件。
<?
class HtmlEmailer extends emailer
{
public function sendHTMLEmail()
{
foreach ($this->recipients as $recipient)
{
$headers = 'MIME-Version: 1.0' . "\r\n";
$headers .= 'Content-type: text/html; charset=iso-8859-1' .
"\r\n";
$headers .= 'From: {$this->sender}' . "\r\n";
$result = mail($recipient, $this->subject, $this->body,
$headers);
if ($result) echo "HTML Mail successfully sent to
{$recipient}<br/>";
}
}
}
?>
由于这个类扩展了 Emailer 类并引入了一个新函数 sendHTMLEmail(),你仍然可以拥有其父类中的所有方法。这意味着以下代码是完全有效的:
<?
include_once("class.htmlemailer.php");
$hm = new HtmlEmailer();
//.... do other things
$hm->sendEmail();
$hm->sendHTMLEmail();
?>
如果你想要访问从父类(或者你可能称之为超类)继承的任何方法,你可以使用 parent 关键字来调用。例如,如果你想访问一个名为 sayHello 的方法,你应该写 parent::sayHello();
请注意,我们没有在 HtmlEmailer 类中编写任何名为 sendEmail() 的函数,但该方法是从其父类 Emailer 中工作的。
注意
在上面的例子中,HtmlEmailer 是 Emailer 类的子类,而 Emailer 类是 HtmlEmailer 的超类。你必须记住,如果子类中没有构造函数,将调用超类的构造函数。在撰写本书时,类级别上没有多重继承的支持。这意味着你一次不能扩展多个类。然而,接口支持多重继承。一个接口可以一次扩展任意数量的其他接口。
覆盖方法
在扩展对象中,您可以覆盖任何方法(无论是声明为 protected 还是 public),并按您的意愿执行任何操作。那么您如何覆盖任何方法呢?只需创建一个与您想要覆盖的名称相同的函数即可。例如,如果您在 HtmlEmailer 类中创建一个名为 sendEmail 的函数,它将覆盖其父类 Emailer 中的 sendEmail() 方法。如果您在子类中声明任何在超类中也存在的变量,那么当您访问该变量时,将访问子类中的变量。
防止覆盖
如果您将任何方法声明为 final 方法,它就不能在其任何子类中被覆盖。所以如果您不希望有人覆盖您的类方法,请将其声明为 final。让我们看看以下示例:
<?
class SuperClass
{
public final function someMethod()
{
//..something here
}
}
class SubClass extends SuperClass
{
public function someMethod()
{
//..something here again, but it wont run
}
}
?>
如果您执行上述代码,它将生成一个致命错误,因为类 SubClass 尝试覆盖 SuperClass 中声明为 final 的方法。
防止扩展
与最终方法类似,您可以将一个类声明为 final,这将阻止任何人扩展它。所以如果您声明任何类,如以下示例所示,它就不再可扩展了。
<?
final class aclass
{
}
class bclass extends aclass
{
}
?>
如果您执行上面的代码,它将触发以下错误:
<b>Fatal error</b>: Class bclass may not inherit from final class
(aclass) in <b>C:\OOP with PHP5\Codes\ch1\class.aclass.php</b> on
line <b>8</b><br />
多态
如我们之前所解释的,多态是从特定的基类创建多个对象的过程。例如,看看以下案例。我们需要本章中创建的三个类,Emailer、ExtendedEmailer 和 HtmlEmailer。让我们看看以下代码。
<?
include("class.emailer.php");
include("class.extendedemailer.php");
include("class.htmlemailer.php");
$emailer = new Emailer("hasin@somewherein.net");
$extendedemailer = new ExtendedEmailer();
$htmlemailer = new HtmlEmailer("hasin@somewherein.net");
if ($extendedemailer instanceof emailer )
echo "Extended Emailer is Derived from Emailer.<br/>";
if ($htmlemailer instanceof emailer )
echo "HTML Emailer is also Derived from Emailer.<br/>";
if ($emailer instanceof htmlEmailer )
echo "Emailer is Derived from HTMLEmailer.<br/>";
if ($htmlemailer instanceof extendedEmailer )
echo "HTML Emailer is Derived from Emailer.<br/>";
?>
如果您执行上面的脚本,您将找到以下输出:
Extended Emailer is Derived from Emailer.
HTML Emailer is also Derived from Emailer.
这是一个多态的例子。
注意
您可以使用 instanceof 操作符始终检查一个类是否从另一个类派生。
接口
接口是一个只包含方法声明的空类。所以任何实现这个接口的类都必须包含这些声明函数。所以,接口不过是一种严格的规则,它有助于扩展任何类并严格实现接口中定义的所有方法。一个类可以通过使用 implements 关键字使用任何接口。请注意,在接口中您只能声明方法,但不能编写它们的主体。这意味着所有方法的主体都必须保持空白。
那么,为什么接口是必要的呢?其中一个原因是它在创建类时隐含了严格的规则。例如,我们知道我们需要在我们的应用程序中创建一些处理数据库操作的驱动类。对于 MySQL,将有一个类,对于 PostgreSQL 将会有另一个,对于 SQLite,又会有另一个,以此类推。现在您的开发团队有三个开发者,他们将分别创建这三个类。
现在如果每个驱动程序都在自己的类中实现自己的风格会怎样呢?将要使用这些驱动程序类的开发者将不得不检查它们如何定义它们的方法,然后根据这些方法编写它们的代码,这既无聊又难以维护。所以如果你定义了,所有驱动程序类都必须有两个名为 connect() 和 execute() 的方法。现在开发者不需要担心在更改驱动程序时,因为他们知道所有这些类都有相同的方法定义。接口有助于这种情况。让我们在这里创建接口:
<?
//interface.dbdriver.php
interface DBDriver
{
public function connect();
public function execute($sql);
}
?>
你注意到接口中的函数是空的吗?现在让我们创建我们的 MySQLDriver 类,它实现了这个接口:
<?
//class.mysqldriver.php
include("interface.dbdriver.php");
class MySQLDriver implements DBDriver
{
}
?>
现在如果你执行上面的代码,它将给出以下错误,因为 MySQLDriver 类没有接口中定义的 connect() 和 execute() 函数。让我们运行代码并读取错误:
<b>Fatal error</b>: Class MySQLDriver contains 2 abstract methods
and must therefore be declared abstract or implement the remaining
methods (DBDriver::connect, DBDriver::execute) in <b>C:\OOP with
PHP5\Codes\ch1\class.mysqldriver.php</b> on line <b>5</b><br />
好吧,现在我们得在我们的 MySQLDriver 类中添加这两种方法。让我们看看下面的代码:
<?
include("interface.dbdriver.php");
class MySQLDriver implements DBDriver
{
public function connect()
{
//connect to database
}
public function execute()
{
//execute the query and output result
}
}
?>
如果我们现在运行代码,我们再次得到以下错误信息:
<b>Fatal error</b>: Declaration of MySQLDriver::execute() must be
compatible with that of DBDriver::execute() in <b>C:\OOP with
PHP5\Codes\ch1\class.mysqldriver.php</b> on line <b>3</b><br />
错误信息表明我们的 execute() 方法与接口中定义的 execute() 方法结构不兼容。如果你现在查看接口,你会发现 execute() 方法应该有一个参数。这意味着每次我们在类中实现接口时,每个方法结构都必须与接口中定义的完全相同。让我们按照以下方式重写我们的 MySQLDriver 类:
<?
include("interface.dbdriver.php");
class MySQLDriver implements DBDriver
{
public function connect()
{
//connect to database
}
public function execute($query)
{
//execute the query and output result
}
}
?>
抽象类
抽象类几乎与接口相同,但现在方法可以包含主体。抽象类也必须被“扩展”,而不是“实现”。所以如果扩展的类有一些具有共同功能的方法,那么你可以在抽象类中定义这些函数。让我们看看下面的示例:
<?
//abstract.reportgenerator.php
abstract class ReportGenerator
{
public function generateReport($resultArray)
{
//write code to process the multidimensional result array and
//generate HTML Report
}
}
?>
在我们的抽象类中有一个名为 generateReport 的方法,它接受一个多维数组作为参数,然后使用它生成一个 HTML 报告。现在,为什么我们要在这个抽象类中放置这个方法呢?因为生成报告将是所有数据库驱动程序的一个通用功能,它不会影响代码,因为它只接受一个数组作为参数,而不是与数据库本身相关的任何内容。现在我们可以在下面的 MySQLDriver 类中使用这个抽象类。请注意,生成报告的所有代码已经编写好了,所以我们不需要在我们的驱动程序类中再次为该方法编写代码,就像我们为接口所做的那样。
<?
include("interface.dbdriver.php");
include("abstract.reportgenerator.php");
class MySQLDriver extends ReportGenerator implements DBDriver
{
public function connect()
{
//connect to database
}
public function execute($query)
{
//execute the query and output result
}
// You need not declare or write the generateReport method here
//again as it is extended from the abstract class directly."
}
?>
请注意,我们可以在上面的示例中同时使用抽象类和实现接口。
注意
你不能声明一个抽象类为 final,因为抽象类意味着它必须被扩展,而 final 类意味着它不能被扩展。所以这两个关键字一起使用是完全没有意义的。PHP 不会允许你这样使用它们。
与声明抽象类类似,你也可以声明任何方法为抽象方法。当一个方法被声明为抽象方法时,这意味着子类必须重写该方法。抽象方法在其定义的地方不应该包含任何方法体。抽象方法可以像下面这样声明:
abstract public function connectDB();
静态方法和属性
在面向对象编程中,static 关键字非常重要。静态方法和属性在应用程序设计和设计模式中起着至关重要的作用。那么静态方法和属性是什么呢?
你已经看到,要访问类中的任何方法或属性,你必须创建一个实例(即使用 new 关键字,如 $object = new emailer()),否则你不能访问它们。但是静态方法和属性有所不同。你可以直接访问静态方法或属性,而无需创建该类的任何实例。静态成员就像该类的全局成员,以及该类的所有实例。此外,静态属性会持久化其被分配的最后状态,这在某些情况下非常有用。
你可能会问为什么有人使用静态方法。嗯,大多数静态方法类似于实用方法。它们执行一个非常特定的任务,或者返回一个特定的对象(静态属性和方法在设计中使用得非常广泛,我们将在以后学习)。所以为这些工作每次都声明一个新的对象可能被认为是资源密集型的。让我们看看静态方法的例子。
考虑到在我们的应用程序中我们支持所有三种数据库,MySQL、PostgreSQL 和 SQLite。现在我们需要一次使用一个特定的驱动程序。为此,我们正在设计一个 DBManager 类,它可以根据需要实例化任何驱动程序并将其返回给我们。
<?
//class.dbmanager.php
class DBManager
{
public static function getMySQLDriver()
{
//instantiate a new MySQL Driver object and return
}
public static function getPostgreSQLDriver()
{
//instantiate a new PostgreSQL Driver object and return
}
public static function getSQLiteDriver()
{
//instantiate a new MySQL Driver object and return
}
}
?>
我们如何使用这个类?你可以使用 :: 操作符而不是 -> 操作符来访问任何静态属性。让我们看看下面的例子:
<?
//test.dbmanager.php
include_once("class.dbmanager.php");
$dbdriver = DBManager::getMySQLDriver();
//now process db operation with this $dbdriver object
?>
注意我们没有创建任何 DBManager 对象的实例,比如 $dbmanager = new DBManager()。相反,我们直接使用 :: 操作符访问其方法之一。
这将给我们带来什么好处呢?嗯,我们只需要一个驱动程序对象,所以不需要创建一个新的 DBManager 对象并将其提交到内存中,只要我们的脚本在执行。静态方法通常执行一个特定的任务并完成它。
这里有一些需要注意的重要事项。在静态方法内部不能使用 $this 伪对象。因为类没有被实例化,所以 $this 在静态方法内部不存在。你最好使用 self 关键字。
让我们看看下面的例子。它展示了静态属性实际上是如何工作的:
<?
//class.statictester.php
class StaticTester
{
private static $id=0;
function __construct()
{
self::$id +=1;
}
public static function checkIdFromStaticMehod()
{
echo "Current Id From Static Method is ".self::$id."\n";
}
public function checkIdFromNonStaticMethod()
{
echo "Current Id From Non Static Method is ".self::$id."\n";
}
}
$st1 = new StaticTester();
StaticTester::checkIdFromStaticMehod();
$st2 = new StaticTester();
$st1->checkIdFromNonStaticMethod(); //returns the val of $id as 2
$st1->checkIdFromStaticMehod();
$st2->checkIdFromNonStaticMethod();
$st3 = new StaticTester();
StaticTester::checkIdFromStaticMehod();
?>
你将看到输出如下:
Current Id From Static Method is 1
Current Id From Non Static Method is 2
Current Id From Static Method is 2
Current Id From Non Static Method is 2
Current Id From Static Method is 3
每次我们创建一个新的实例时,它都会影响所有实例,因为变量被声明为静态。使用这个特殊功能,一个特殊的设计模式“单例”在 PHP 中可以完美工作。
小贴士
注意:使用静态成员
静态成员使面向对象编程与旧的过程式编程非常相似;不创建实例,你可以直接调用任何函数,就像过去一样。这就是为什么我们谨慎地使用静态方法。过多的静态方法毫无用处。除非你有任何特定的目的,否则不要使用静态成员。
访问器方法
访问器方法只是专门用于获取和设置任何类属性值的简单方法。使用访问器方法而不是直接设置或获取它们的值来访问类属性是一种良好的实践。尽管访问器方法与其他方法相同,但在编写它们时有一些约定。
访问器方法有两种类型。一种被称为getter,其目的是返回任何类的属性值。另一种是setter,它将值设置到类的属性中。让我们看看如何为类属性编写getter和setter方法:
<?
class Student
{
private $name;
private $roll;
public function setName($name)
{
$this->name= $name;
}
public function setRoll($roll)
{
$this->roll =$roll;
}
public function getName()
{
return $this->name;
}
public function getRoll()
{
return $this->roll;
}
}
?>
在上面的例子中,有两个getter方法和两个setter方法。在编写accessor方法时有一个约定。setter方法应该以set开头,属性名以第一个字母大写。getter方法应该以get开头,然后是变量名,第一个字母大写。这意味着如果我们有一个名为email的属性,则getter方法应该命名为getEmail,而setter方法应该命名为setEmail。就是这样。
你可能会问,为什么有人要做这些额外的工作,当他们可以轻松地将这些变量设置为公共的,并且让其他一切都保持原样。难道这些不都是一样的吗?嗯,不是的。使用访问器方法,你将获得一些额外的好处。在设置或检索任何属性的值时,你将拥有完全的控制权。“那又怎样?”你可能会问。让我们用一个场景来说明,你需要过滤用户的输入并将其设置到属性中。在这种情况下,一个setter可以帮助你在将它们设置到工作状态之前过滤输入。
这是否意味着如果我们类中有 100 个属性,我们就必须编写 100 个getter和setter方法?你提出了一个好问题。PHP 足够仁慈,可以让你摆脱这种无聊。如何?让我们看看下一节,我们将讨论使用魔法方法动态设置和获取属性值。这些方法可以将压力减少到 90%。你不相信我吗?让我们看看。
使用魔法方法设置/获取类属性
在上一节中,我们讨论了为许多属性编写访问器方法将是一场真正的噩梦。为了避免这种无聊,你可以使用魔法方法。这个过程被称为属性重载。
PHP5 在类中引入了一些魔法方法,以减少在某些情况下面向对象编程的痛苦。其中两种魔法方法被引入以在类中设置和获取动态属性值。这两个魔法方法分别命名为__get()和__set()。让我们看看如何使用它们:
<?
//class.student.php
class Student
{
private $properties = array();
function __get($property)
{
return $this->properties[$property];
}
function __set($property, $value)
{
$this->properties[$property]="AutoSet {$property} as: ".$value;
}
}
?>
现在,让我们看看代码的实际应用。使用上面的类,使用以下脚本:
<?
$st = new Student();
$st->name = "Afif";
$st->roll=16;
echo $st->name."\n";
echo $st->roll;
?>
当你执行前面的代码时,PHP 会立即识别出在类中不存在名为name或roll的属性。由于没有命名属性存在,__set()方法被调用,然后它将值分配给类的新创建的属性,这样你就能看到以下输出:
AutoSet name as: Afif
AutoSet roll as: 16
看起来很有趣,不是吗?使用魔法方法,你仍然可以完全控制类中设置和检索属性值。然而,如果你使用魔法方法,你有一个限制。当使用反射 API 时,你无法调查类属性(我们将在后面的章节中讨论反射 API)。此外,你的类在“可读性”和“可维护性”方面损失了很多。为什么?请看之前Student类和新Student类的代码,你就会自己理解。
用于重载类方法的魔法方法
与重载和访问器方法一样,还有魔法方法可以重载类中的任何方法调用。如果你还不熟悉方法重载,那么这是一个访问任何甚至不存在于类中的方法的进程。听起来很有趣,对吧?让我们更仔细地看看。
有一个魔法方法,它有助于在 PHP5 类上下文中重载任何方法调用。这个魔法方法的名称是__call()。这允许你在对象上调用未定义的方法时提供操作或返回值。它可以用来模拟方法重载,甚至在对象上调用未定义方法时提供平滑的错误处理。__call接受两个参数:方法的名称和传递给未定义方法的参数数组。
例如,请看下面的代码:
<?
class Overloader
{
function __call($method, $arguments)
{
echo "You called a method named {$method} with the following
arguments <br/>";
print_r($arguments);
echo "<br/>";
}
}
$ol = new Overloader();
$ol->access(2,3,4);
$ol->notAnyMethod("boo");
?>
如果你看到上面的代码,那么你会看到没有名为access和notAnyMethod的方法。所以因此,它应该引发错误,对吧?然而,方法重载器仍然可以帮助你调用任何不存在的方法。如果你执行上面的代码,你会得到以下输出。
You called a method named access with the following arguments
Array
(
[0] => 2
[1] => 3
[2] => 4
)
You called a method named notAnyMethod with the following arguments
Array
(
[0] => boo
)
这意味着你将作为一个数组获取所有参数。这本书中你将逐步学习到更多魔法方法。
以视觉方式表示类
在面向对象编程中,有时你必须以视觉方式表示你的类。让我们学习如何以视觉方式表示一个类。为此,我们这次将使用我们的Emailer类。

在这个图形表示中,有三个部分。在最上面的部分应该写上类名。在第二部分写上所有带参数或不带参数的方法。在第三部分写上所有属性。就是这样!
摘要
在本章中,我们学习了如何创建对象以及它们之间的交互。与 PHP4 相比,PHP5 在对象模型方面带来了惊人的改进。作为 PHP5 核心的 Zend Engine 2,在处理这些特性时也非常高效,具有出色的性能优化。
在下一章中,我们将深入了解 PHP 中面向对象编程(OOP)的更多细节和核心特性。但在开始下一章之前,请务必练习这里讨论的所有内容,否则你可能会在某些主题上感到困惑。尽可能多地练习,尝试重构你之前所有的面向对象代码。你练习得越多,效率就越高。
第三章。更多面向对象编程
前一章为我们创建了一个基础,以便我们可以用 PHP 启动面向对象编程。这一章将更详细地处理一些高级功能。例如,我们将学习关于类信息函数,通过这些函数我们可以调查任何类的详细信息。然后我们将学习一些实用的面向对象信息函数,以及 PHP5 中的一个伟大新特性,即异常处理。
本章还将介绍迭代器以简化数组访问。为了存储任何对象以供以后使用,我们需要使用面向对象编程中的特殊功能,即序列化,我们也将在这里学习这一点。总的来说,本章将加强你在面向对象编程方面的基础。
类信息函数
如果你想要调查和收集有关任何类的更多信息,这些函数将是你的光明。这些函数可以检索有关类的几乎所有信息。但是,这些函数有一个改进版本,并在 PHP5 中作为全新的 API 集引入。这个 API 被称为反射。我们将在第五章学习关于反射 API 的内容。
检查类是否已存在
当你需要检查当前作用域中是否存在任何类时,你可以使用一个名为class_exists()的函数。看看以下例子:
<?
include_once("../ch2/class.emailer.php");
echo class_exists("Emailer");
//returns true otherwise false if doesn't exist
?>
使用class_exists()函数的最佳方式是首先检查类是否已经存在。如果类存在,你可以创建该类的实例,这将使你的代码更加稳定。
<?
include_once("../ch2/class.emailer.php");
if( class_exists("Emailer"))
{
$emailer = new Emailer("hasin@pageflakes.com");
}
else
{
die("A necessary class is not found");
}
?>
查找当前加载的类
在某些情况下,你可能需要调查当前作用域中加载了哪些类。你可以使用get_declared_classes()函数来完成这项工作。这个函数将返回一个包含当前可用类的数组。
<?
include_once("../ch2/class.emailer.php");
print_r(get_declared_classes());
?>
你将在屏幕上看到当前可用的类列表。
查找方法和属性是否存在
要找出类内部是否有属性和/或方法可用,你可以使用method_exists()和property_exists()函数。请注意,这些函数只有在属性和方法在公共作用域中定义时才会返回 true。
检查类的类型
有一个名为is_a()的函数,你可以用它来检查类的类型。看看以下例子:
<?
class ParentClass
{
}
class ChildClass extends ParentClass
{
}
$cc = new ChildClass();
if (is_a($cc,"ChildClass")) echo "It's a ChildClass Type Object";
echo "\n";
if (is_a($cc,"ParentClass")) echo "It's also a ParentClass Type
Object";
?>
你将看到以下输出:
Its a ChildClass Type Object
Its also a ParentClass Type Object
查找类名
在前面的例子中,我们检查了类是否是已知类型。如果我们需要获取类的原始名称呢?不用担心,我们有get_class()函数来帮助我们。
<?
class ParentClass
{
}
class ChildClass extends ParentClass
{
}
$cc = new ChildClass();
echo get_class($cc)
?>
作为输出,你应该得到ChildClass。现在看看以下例子,这是“brjann”在 PHP 手册用户备注部分中提到的意外行为。
<?
class ParentClass
{
public function getClass()
{
echo get_class(); //using "no $this"
}
}
class Child extends ParentClass
{
}
$obj = new Child();
$obj->getClass(); //outputs "ParentClass"
?>
如果你运行这段代码,你会看到输出为 ParentClass。但是为什么?你是在调用 Child 类的方法。这是意外的吗?好吧,不是的。认真看看代码。虽然 Child 扩展了 ParentClass 对象,但它没有重写 getClass() 方法。所以方法仍然在 ParentClass 的作用域下运行。这就是为什么它返回 ParentClass 的结果。
所以实际上发生了什么?为什么它返回 Child?
<?
class ParentClass {
public function getClass(){
echo get_class($this); //using "$this"
}
}
class Child extends ParentClass {
}
$obj = new Child();
$obj->getClass(); //outputs "child"
?>
在 ParentClass 对象中,get_class() 函数返回 $this 对象,它明显持有 Child 类的引用。这就是为什么你得到 Child 作为输出。
异常处理
PHP5 中最改进的特性之一是现在你可以使用异常,就像其他现有的面向对象编程语言一样。PHP5 引入了这些异常对象来简化你的错误管理。
让我们看看这些异常是如何发生的以及如何处理它们。看看以下类,它简单地连接到 PostgreSQL 服务器。在无法连接到服务器的情况下,让我们看看它通常会返回什么:
<?
//class.db.php
class db
{
function connect()
{
pg_connect("somehost","username","password");
}
}
$db = new db();
$db->connect();
?>
输出如下。
<b>Warning</b>: pg_connect() [<a href='function.pg-connect'>
function.pg-connect</a>]: Unable to connect to PostgreSQL
server: could not translate host name "somehost" to address:
Unknown host in <b>C:\OOP with PHP5\Codes\ch3\exception1.php</b>
on line <b>6</b><br />
你如何在 PHP4 中处理它?通常,可以通过以下类似的方式:
<?
//class.db.php
error_reporting(E_ALL - E_WARNING);
class db
{
function connect()
{
if (!pg_connect("somehost","username","password")) return false;
}
}
$db = new db();
if (!$db->connect()) echo "Falied to connect to PostgreSQL Server";
?>
现在让我们看看如何使用异常来解决它。
<?
//class.db.php
error_reporting(E_ALL - E_WARNING);
class db
{
function connect()
{
if (!pg_connect("host=localhost password=pass user=username
dbname=db")) throw new Exception("Cannot connect
to the database");
}
}
$db = new db();
try {
$db->connect();
}
catch (Exception $e)
{
print_r($e);
}
?>
输出将类似于以下内容:
Exception Object
(
[message:protected] => Cannot connect to the database
[string:private] =>
[code:protected] => 0
[file:protected] => C:\OOP with PHP5\Codes\ch3\exception1.php
[line:protected] => 8
[trace:private] => Array
(
[0] => Array
(
[file] => C:\OOP with PHP5\Codes\ch3\exception1.php
[line] => 14
[function] => connect
[class] => db
[type] => ->
[args] => Array
(
)
)
[1] => Array
(
[file] => C:\Program Files\Zend\ZendStudio-
5.2.0\bin\php5\dummy.php
[line] => 1
[args] => Array
(
[0] => C:\OOP with PHP5\Codes\ch3\exception1.php
)
[function] => include
)
)
)
所以你在这个异常类中得到了很多内容。你可以使用这个 try-catch 块来捕获所有错误。你可以在另一个 try-catch 块中使用 try-catch。看看以下示例。这里我们开发了两个自己的异常对象,使错误处理更加结构化。
<?
include_once("PGSQLConnectionException.class.php");
include_once("PGSQLQueryException.class.php");
error_reporting(0);
class DAL
{
public $connection;
public $result;
public function connect($ConnectionString)
{
$this->connection = pg_connect($ConnectionString);
if ($this->connection==false)
{
throw new PGSQLConnectionException($this->connection);
}
}
public function execute($query)
{
$this->result = pg_query($this->connection,$query);
if (!is_resource($this->result))
{
throw new PGSQLQueryException($this->connection);
}
//else do the necessary works
}
}
$db = new DAL();
try{
$db->connect("dbname=golpo user=postgres2");
try{
$db->execute("select * from abc");
}
catch (Exception $queryexception)
{
echo $queryexception->getMessage();
}
}
catch(Exception $connectionexception)
{
echo $connectionexception->getMessage();
}
?>
现在,如果代码无法连接到数据库,它会捕获错误并显示“抱歉,无法连接到 PostgreSQL 服务器”的消息。如果连接成功但问题出在查询上,它将显示适当的信息。如果你检查代码,你会发现对于连接失败,我们抛出一个 PGSQLConnectionException 对象,而对于查询失败,我们只抛出一个 PGSQLQueryException 对象。我们可以通过扩展 PHP5 的核心 Exception 类来自定义开发这些对象。让我们看看代码。第一个是 PGSQLConnectionException 类。
<?
Class PGSQLConnectionException extends Exception
{
public function __construct()
{ $message = "Sorry, couldn't connect to postgresql server:";
parent::__construct($message, 0000);
}
}
?>
现在出现了 PGSQLQueryException 类
<?
Class PGSQLQueryException extends Exception
{
public function __construct($connection)
{
parent::__construct(pg_last_error($connection),0);
}
}
?>
就这样!
收集所有 PHP 错误作为异常
如果你想要收集所有 PHP 错误(除了 FATAL 错误)作为异常,你可以使用以下代码:
<?php
function exceptions_error_handler($severity, $message,
$filename, $lineno) {
throw new ErrorException($message, 0, $severity,
$filename, $lineno);
}
set_error_handler('exceptions_error_handler');
?>
上述代码片段的功劳归功于 <fjoggen@gmail.com>,这是我从 PHP 手册用户笔记中收集的。
迭代器
迭代器是 PHP5 中引入的一个新命令,用于帮助遍历任何对象。查看以下示例,了解迭代器实际上用于什么。在 PHP4 中,你可以像以下示例那样使用 foreach 语句遍历数组:
<?
foreach($anyarray as $key=>$val)
{
//do something
}
?>
你也可以对对象执行 foreach 操作,让我们看看以下示例。
<?
class EmailValidator
{
public $emails;
public $validemails;
}
$ev = new EmailValidator();
foreach($ev as $key=>$val)
{
echo $key."<br/>";
}
?>
这段代码将输出以下内容:
emails
validemails
请注意,它只能遍历公共属性。但是,如果我们只想得到有效的电子邮件地址作为输出呢?在 PHP5 中,通过实现Iterator和IteratorAggregator接口是可能的。让我们通过以下示例来看看。在这个例子中,我们创建了一个QueryIterator,它可以遍历有效的 PostgreSQL 查询结果,并在每次迭代中返回一行。
<?
class QueryIterator implements Iterator
{
private $result;
private $connection;
private $data;
private $key=0;
private $valid;
function __construct($dbname, $user, $password)
{
$this->connection = pg_connect("dbname={$dbname} user={$user}");
}
public function exceute($query)
{
$this->result = pg_query($this->connection,$query);
if (pg_num_rows($this->result)>0)
$this->next();
}
public function rewind() {}
public function current() {
return $this->data;
}
public function key() {
return $this->key;
}
public function next() {
if ($this->data = pg_fetch_assoc($this->result))
{
$this->valid = true;
$this->key+=1;
}
else
$this->valid = false;
}
public function valid() {
return $this->valid;
}
}
?>
让我们看看代码的实际应用。
<?
$qi= new QueryIterator("golpo","postgres2","");
$qi->exceute("select name, email from users");
while ($qi->valid())
{
print_r($qi->current());
$qi->next();
}
?>
例如,如果我们的users表中有两个记录,您将得到以下输出:
Array
(
[name] => Afif
[email] => mayflower@phpxperts.net
)
Array
(
[name] => Ayesha
[email] => florence@phpxperts.net
)
非常方便,不是吗?
ArrayObject
在 PHP5 中引入的另一个有用的对象是ArrayObject,它包装了常规的 PHP 数组并给它添加了面向对象的特性。您可以通过简单地将数组传递给ArrayObject构造函数来创建一个ArrayObject对象。ArrayObject有以下有用的方法:
append()
此方法可以在集合的末尾添加任何值。
getIterator()
此方法简单地创建一个Iterator对象并返回,这样您就可以使用迭代器风格进行迭代。这是一个非常有用的方法,可以从任何数组中获取Iterator对象。
offsetExists()
此方法可以确定指定的偏移量是否存在于集合中。
offsetGet()
此方法返回指定偏移量的值。
offsetSet()
与offsetGet()类似,此方法可以将任何值设置到指定的index()。
offsetUnset()
此方法可以取消指定索引处的元素。
让我们看看ArrayObject的一些示例:
<?
$users = new ArrayObject(array("hasin"=>"hasin@pageflakes.com",
"afif"=>"mayflower@phpxperts.net",
"ayesha"=>"florence@pageflakes.net"));
$iterator = $users->getIterator();
while ($iterator->valid())
{
echo "{$iterator->key()}'s Email address is
{$iterator->current()}\n";
$iterator->next();
}
?>
数组到对象
我们可以通过其键来访问任何数组元素,例如$array[$key]。然而,如果我们想以$array->key的方式访问它呢?这非常简单,我们可以通过扩展ArrayObject来实现。让我们通过以下示例来看看。
<?
class ArrayToObject extends ArrayObject
{
public function __get($key)
{
return $this[$key];
}
public function __set($key,$val)
{
$this[$key] = $val;
}
}
?>
现在我们来看看它的实际应用:
<?
$users = new ArrayToObject(array("hasin"=>"hasin@pageflakes.com",
"afif"=>"mayflower@phpxperts.net",
"ayesha"=>"florence@pageflakes.net"));
echo $users->afif;
?>
它将输出与键afif关联的电子邮件地址,如下所示:
mayflower@phpxperts.net
如果您想将任何已知格式的数组转换为对象,这个示例可能会很有用。
以数组风格访问对象
在前面的章节中,我们学习了如何以面向对象的方式访问任何数组。如果我们想以数组风格访问任何对象怎么办?PHP 提供了这样的功能。您只需要在您的类中实现ArrayAccess接口。
ArrayAccess接口有四个方法,您必须在类中实现这些方法。这些方法是offsetExists()、offsetGet()、offsetSet()、offsetUnset()。让我们创建一个实现ArrayAccess接口的示例类。
<?php
class users implements ArrayAccess
{
private $users;
public function __construct()
{
$this->users = array();
}
public function offsetExists($key)
{
return isset($this->users[$key]);
}
public function offsetGet($key)
{
return $this->users[$key];
}
public function offsetSet($key, $value)
{
$this->users[$key] = $value;
}
public function offsetUnset($key)
{
unset($this->users[$key]);
}
}
$users = new users();
$users['afif']="mayflower@phpxperts.net";
$users['hasin']="hasin@pageflakes.com";
$users['ayesha']="florence@phpxperts.net";
echo $users['afif']
?>
输出将是mayflower@phpxperts.net。
序列化
到目前为止,我们已经学习了如何创建对象并操作它们。现在,如果您需要保存对象的状态并在稍后以完全相同的形式检索它,会发生什么?在 PHP 中,您可以通过序列化来实现此功能。
序列化是将对象的状态持久化在任何位置的过程,无论是物理文件还是变量中。为了检索该对象的状态,另一个过程被使用,这被称为“反序列化”。您可以使用 serialize() 函数序列化任何对象。让我们看看我们如何序列化一个对象:
<?
class SampleObject
{
public $var1;
private $var2;
protected $var3;
static $var4;
public function __construct()
{
$this->var1 = "Value One";
$this->var2 = "Value Two";
$this->var3 = "Value Three";
SampleObject::$var4 = "Value Four";
}
}
$so = new SampleObject();
$serializedso =serialize($so);
file_put_contents("text.txt",$serializedso);
echo $serializedso;
?>
脚本将输出一个字符串,PHP 知道如何反序列化它。
现在是时候检索我们的序列化对象并将其转换为可用的 PHP 对象了。请记住,您正在反序列化的类文件必须首先加载。
<?
include_once("class.sampleobject.php");
$serializedcontent = file_get_contents("text.txt");
$unserializedcontent = unserialize($serializedcontent);
print_r($unserializedcontent);
?>
你认为输出会是什么?看看:
SampleObject Object
(
[var1] => Value One
[var2:private] => Value Two
[var3:protected] => Value Three
)
现在它是一个常规的 PHP 对象;与序列化之前完全相同。请注意,所有变量都保留了它们在序列化之前设置的值,除了静态变量。您不能通过序列化来保存静态变量的状态。
如果我们在反序列化之前没有使用 include_once 包含类文件,会怎样?让我们只注释掉第一行,包含类文件的行,然后运行示例代码。你会得到以下输出:
__PHP_Incomplete_Class Object
(
[__PHP_Incomplete_Class_Name] => SampleObject
[var1] => Value One
[var2:private] => Value Two
[var3:protected] => Value Three
)
到这一点,您不能再将其用作对象了。
序列化中的魔法方法
你还记得我们使用一些魔法方法如 __get、__set 和 __call 重载属性和方法吗?对于序列化,您可以使用一些魔法方法来挂钩序列化过程。PHP5 提供了两个名为 __sleep 和 __awake 的魔法方法来达到这个目的。这些方法在整个过程中提供了一些控制。
让我们使用这些魔法方法来开发一个进程的所有静态变量,通常我们无法不通过黑客手段做到这一点。通常情况下,无法序列化任何静态变量的值,并返回具有该静态变量的对象处于相同状态。然而,我们可以让它发生,让我们看看下面的代码。
<?
class SampleObject
{
public $var1;
private $var2;
protected $var3;
public static $var4;
private $staticvars = array();
public function __construct()
{
$this->var1 = "Value One";
$this->var2 = "Value Two";
$this->var3 = "Value Three";
SampleObject::$var4 = "Value Four";
}
public function __sleep()
{
$vars = get_class_vars(get_class($this));
foreach($vars as $key=>$val)
{
if (!empty($val))
$this->staticvars[$key]=$val;
}
return array_keys( get_object_vars( $this ) );
}
public function __wakeup()
{
foreach ($this->staticvars as $key=>$val){
$prop = new ReflectionProperty(get_class($this), $key);
$prop->setValue(get_class($this), $val);
}
$this->staticvars=array();
}
}
?>
如果我们序列化对象,将其写入文件,然后稍后检索状态会发生什么?你会发现静态值仍然保持最后分配给它的值。
让我们讨论一下代码。__sleep 函数执行所有必要的操作。它搜索具有值的公共属性,并在找到时将变量的名称存储到一个私有变量 staticvars 中。当有人尝试反序列化对象时,它会从 staticvars 中检索每个值并将其写入属性本身。非常方便,不是吗?
您会注意到,除了 __sleep() 和 __wakeup() 函数的理论能力之外,我们没有使用任何技巧。那么这两个函数有什么用?我们可以在实践中在哪里使用它们?这实际上相当简单。例如,如果您的类与其相关联任何资源对象(一个活动的数据库连接,一个打开文件的引用)在 sleep 函数中,您可以适当地关闭它们,因为当有人反序列化时它们将不再可用。请记住,在反序列化状态下,有人仍然可能使用那些资源指针。所以,在 __wakeup() 函数中,您可以打开那些数据库连接或文件指针,使其恢复到之前的确切形状。让我们通过以下示例来看看:
<?
class ResourceObject
{
private $resource;
private $dsn;
public function __construct($dsn)
{
$this->dsn = $dsn;
$this->resource = pg_connect($this->dsn);
}
public function __sleep()
{
pg_close($this->resource);
return array_keys( get_object_vars( $this ) );
}
public function __wakeup()
{
$this->resource = pg_connect($this->dsn);
}
}
?>
当这个对象被序列化时,它将释放 $resource 消耗的内存。稍后,当它将被反序列化时,它将使用 DSN 字符串再次打开连接。所以现在,在反序列化之后,一切如故。这就是关键!
对象克隆
PHP5 在从一个对象复制到另一个对象时引入了一种新的方法,这与 PHP4 完全不同。在 PHP4 中,当您将一个对象复制到另一个对象时,它执行深度复制。这意味着它只是创建了一个全新的对象,该对象保留了被复制对象的属性。然而,对新的对象所做的任何更改都不会影响主对象。
PHP5 在复制对象时创建浅复制的方式与这个不同。为了清楚地理解这种情况,您需要理解以下代码。
<?
$sample1 = new StdClass();
$sample1->name = "Hasin";
$sample2 = $sample1;
$sample2->name = "Afif";
echo $sample1->name;
?>
如果您在 PHP5 中运行上述代码,您能猜到您会得到什么结果?Hasin 还是 Afif?令人惊讶的是,输出是 Afif。正如我之前提到的,PHP5 在复制对象时执行浅复制;$sample2 只是 $sample1 的引用。所以,无论何时您对 $sample1 对象或 $sample2 对象进行任何更改,它都会影响两者。
在 PHP4 中,它的工作方式不同;它将输出 Hasin,因为它们彼此不同。
如果您想在 PHP5 中执行相同的操作,您必须使用 clone 关键字。让我们看看以下示例
<?
$sample1 = new stdClass();
$sample1->name = "Hasin";
$sample2 =clone $sample1;
$sample2->name = "Afif";
echo $sample1->name;
?>
现在的输出将是 Hasin。
按需自动加载类或类
在处理大型项目时,另一个非常好的实践是在需要时才加载类。这意味着您不应该通过不断加载不必要的类来过度消耗内存。
在我们的示例中,您已经看到我们在将它们在我们的脚本中可用之前,包括原始类文件。除非您包括类文件,否则您无法创建其实例。PHP5 引入了一个自动加载类文件的功能,这样您就不必麻烦地手动包含它们。通常,这个特性在大型应用程序中非常有用,在这些应用程序中,您必须处理大量的类,并且不想每次都调用 include。
<?
function __autoload($class)
{
include_once("class.{$class}.php");
}
$s = new Emailer("hasin@somewherein.net");
?>
当你执行上面的脚本时,请注意我们没有包含Emailer类的任何类文件。正因为有这个__autoload()函数,PHP5 会自动加载当前目录下名为class.emailer.php的文件。所以你不需要担心自己包含类。
方法链式调用
方法链式调用是 PHP5 中引入的另一种过程,通过它可以直接访问由任何函数返回的对象的方法和属性。它有点像以下这样:
$SomeObject->getObjectOne()->getObjectTwo()->callMethodOfObjectTwo();
上述代码表示$someObject类有一个名为getObjectOne()的方法,它返回一个名为$objectOne的对象。这个$objectOne还有一个名为getObjectTwo()的方法,它返回一个对象,其方法是通过最后的调用调用的。
那么,谁会使用这样的东西呢?让我们看看下面的代码;它完美地展示了方法链在现实生活中的使用:
$dbManager->select("id","email")->from("user")->where("id=1")
->limit(1)->result();
你觉得上面的代码有意义且易于阅读吗?该代码从user表中返回一行,包含 ID 和电子邮件,其中 ID 的值为 1。你有没有想过如何设计这样的 DB 管理器对象?让我们看看下面的这个优秀的例子:
<?
class DBManager
{
private $selectables = array();
private $table;
private $whereClause;
private $limit;
public function select()
{
$this->selectables=func_get_args();
return $this;
}
public function from($table)
{
$this->table = $table;
return $this;
}
public function where($clause)
{
$this->whereClause = $clause;
return $this;
}
public function limit($limit)
{
$this->limit = $limit;
return $this;
}
public function result()
{
$query = "SELECT ".join(",",$this->selectables)." FROM
{$this->table}";
if (!empty($this->whereClause))
$query .= " WHERE {$this->whereClause}";
if (!empty($this->limit))
$query .= " LIMIT {$this->limit}";
echo "The generated Query is : \n".$query;
}
}
$db= new DBManager();
$db->select("id","name")->from("users")->where("id=1")->
limit(1)->result();
?>
输出如下:
The generated Query is :
SELECT id,name FROM users WHERE id=1 LIMIT 1
该类会自动构建查询。那么它是如何工作的呢?嗯,在 PHP5 中,你可以返回对象;所以利用这个特性,我们在每个我们希望成为链式的一部分的方法上返回对象。现在,只需几分钟就可以执行那个查询并返回结果。令人惊讶的是,你还可以执行以下代码,它会产生相同的结果:
$db->from("users")->select("id","name")->limit(1)->where("id=1")
->result();
这就是 PHP5 的美丽之处;它非常强大。
PHP 中对象的生命周期和对象缓存
如果你感兴趣于了解对象的生命周期,那么对象在脚本结束时是活跃的。一旦脚本执行完毕,由该脚本实例化的任何对象也会死亡。与 Java 中的 Web 层不同,PHP 中没有全局或应用级别的范围。因此,你不能正常地持久化对象。如果你想持久化一个对象,你可以将其序列化,并在必要时反序列化它。手动处理这个序列化和反序列化过程有时可能显得无聊。如果能在某处存储对象并在以后检索它(嗯,就像序列化和反序列化过程一样,但更灵活)那就真的很好了。
PHP 中确实有一些对象缓存技术可用,效率非常高。其中最成功的是memcached。PHP 有一个扩展名为 memcached API,可以从 PECL 下载。Memcached 作为一个独立的服务器运行,并将对象直接缓存到内存中。Memcached 服务器监听一个端口。PHP memcached API 理解如何与 memcached 服务器通信,因此借助它保存和检索对象。在本节中,我们将演示如何使用 memcached,但不会过多地深入细节。
你可以从danga.com/memcached下载 memcached 服务器。如果你使用 Linux,你必须自己编译它。在一些发行版中,你会找到 memcached 包。你可以在jehiah.cz/projects/memcached-win32/找到 memcached 1.2.1 服务器的win32二进制版本,该版本由 kronuz 开发(<kronuz@users.sourceforge.net>)。获取可执行文件后,在控制台输入以下命令。这将启动 memcached 服务器。
memcached –d install
这将把 memcached 安装为服务。
memcached –d start
这将启动守护进程/服务。
现在是时候将一些对象存储到 memcached 服务器中并检索它们了。
<?
$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die ("Could not connect");
$tmp_object = new stdClass;
$tmp_object->str_attr = 'test';
$tmp_object->int_attr = 12364;
$memcache->set('obj', $tmp_object, false, 60*5) or die ("Failed to save data at the server");
?>
当你执行上面的代码时,memcache 服务器会将对象$tmp_object与键obj关联存储五分钟。五分钟后,这个对象将不再存在。到那时,如果你需要恢复该对象,你可以执行以下代码:
<?
$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die ("Could not connect");
$newobj = $memcache->get('obj');
?>
就这样。Memcache 非常流行,它有 Perl、Python、Ruby、Java 和 Dot Net 以及 C 语言的端口。
摘要
在本章中,我们学习了如何在 PHP 中使用一些高级面向对象编程(OOP)概念。我们学习了如何从任何对象中检索信息,并了解了 ArrayAccess、ArrayObject、迭代器和一些其他简化开发者生活的原生对象。本章我们从中学到的另一件非常重要的事情是异常处理。
在下一章中,我们将学习设计模式以及如何在 PHP 中使用它们。在此之前,祝大家探索愉快…
第四章:设计模式
面向对象编程最初是为了简化开发过程以及通过减少代码量来缩短开发时间而引入的。如果规划得当,设计合理,面向对象编程(OOP)可以极大地提高程序的性能。其中之一是“设计模式”,这是由埃里克·伽玛和他的三位朋友在 1972 年出版的《设计模式》一书中提出的。由于有四位作者,这本书被介绍为“四人帮”的作品,或者简单地称为“Goff”。在这本传奇性的书中,“四人帮”介绍了几种模式,以最大限度地减少代码量并引入有效的编码实践。在本章中,我们将学习一些这些模式,以便在 PHP 中实现。
你可能以前做过这件事…
在编码过程中,我们中的许多人使用这些模式,却不知道这些技术实际上被称为模式。即使在早期的编码生涯中,我也使用了一些编码技术,后来发现它们与某些模式相似。所以,不要害怕使用模式。它们是日常编码技巧,你可能一直都在使用,但你可能不知道。
在软件开发过程中,一些问题会定期出现。几乎每个软件开发都会遇到这些问题。这些问题被称为“设计模式”,并给出了一些常见的解决方案。因此,了解设计模式可以为软件开发者节省大量时间。让我们更深入地了解设计模式。
策略模式
我们在编程过程中遇到的一个常见问题是,我们必须在不同的策略上做出决策。策略模式是一种常见的模式,它帮助我们更容易地在不同情况下做出决策。为了更好地理解这一点,让我们使用一个场景,即你正在开发一个通知程序。这个通知程序将检查用户的给定选项。用户可能希望以多种方式被通知,如电子邮件、短信或传真。你的程序必须检查可用的联系方式,然后据此做出决策。这种情况可以通过策略模式轻松解决:

在上述模式中,我们使用了三个类,分别是SMSNotifier、EmailNotifier和FaxNotifier。所有这些类都实现了Notifier接口,该接口有一个名为notify的方法。每个类都独立实现了该方法。
让我们先创建接口。
<?
//interface.Notifier.php
interface notifier
{
public function notify();
}
?>
现在我们将创建不同类型的通知器。
class.emailnotifier.php
<?
include_once("interface.notifier.php");
class EmailNotifier implements notifier
{
public function notify()
{
//do something to notify the user by Email
}
}
?>
class.faxnotifier.php
<?
include_once("notifier.php");
class FaxNotifier implements notifier
{
public function notify()
{
//do something to notify the user by Fax
}
}
?>
class.smsnotifier.php
<?
include_once("notifier.php");
class SMSNotifier implements notifier
{
public function notify()
{
//do something to notify the user by SMS
}
}
?>
现在我们将使用以下代码:
<?
include_once("EmailNotifier.php");
include_once("FaxNotifier.php");
include_once("SMSNotifier.php");
/**
* Let's create a mock object User which we assume has a method named
* getNotifier(). This method returns either "sms" or "fax" or "email"
*/
$user = new User();
$notifier = $user->getNotifier();
switch ($notifier)
{
case "email":
$objNotifier = new EmailNotifier();
break;
case "sms":
$objNotifier = new SMSNotifier();
break;
case "fax":
$objNotifier = new FaxNotifier();
break;
}
$objNotifier->notify();
?>
我确信你会同意这很简单。我也确信你已经在你的现有代码中不止一次使用过这样的解决方案。
工厂模式
另一个常见的设计模式是工厂模式。这种模式的主要目标是通过隐藏其背后的所有复杂性来提供对象。这可能听起来有些神秘,所以让我们用一个现实生活中的场景来看看它。
你正在进行一个针对非常复杂系统的项目。在这个例子中,你正在创建一个在线文档存储库,它将文档保存在临时存储中。为此,你需要支持 PostgreSQL、MySQL、Oracle 和 SQLite,因为用户可能会使用这些中的任何一种来部署你的应用程序。所以你创建了一个对象,该对象连接到 MySQL 并执行必要的任务。你的 MySQL 对象是:
<?
class MySQLManager
{
public function setHost($host)
{
//set db host
}
public function setDB($db)
{
//set db name
}
public function setUserName($user)
{
//set user name
}
public function setPassword($pwd)
{
//set password
}
public function connect()
{
//now connect
}
}
s
?>
好吧,现在你可以这样使用这个类:
<?
$MM = new MySQLManager();
$MM->setHost("host");
$MM->setDB("db");
$MM->setUserName("user");
$MM->setPassword("pwd");
$MM->connect();
?>
现在你可以看到,在你开始使用你的类之前,你需要做很多事情。你的 PostgreSQL 类看起来也很相似:
<?
class PostgreSQLManager
{
public function setHost($host)
{
//set db host
}
public function setDB($db)
{
//set db name
}
public function setUserName($user)
{
//set user name
}
public function setPassword($pwd)
{
//set password
}
public function connect()
{
//now connect
}
}
?>
使用方式也是一样的:
<?
$PM = new PostgreSQLManager();
$PM->setHost("host");
$PM->setDB("db");
$PM->setUserName("user");
$PM->setPassword("pwd");
$PM->connect();
?>
但现在当它们合并在一起时,使用可能会有些困难:
<?
If ($dbtype=="mysql")
//use mysql class
Else if ($dbtype=="postgresql")
//use postgresql class
?>
简短地说,当你添加更多的数据库引擎时,核心代码会显著变化,你必须将这些所有东西硬编码到核心类中。然而,编程的一个非常好的实践是松耦合。在这里,你创建一个名为 DBManager 的单独类,它将从中央位置执行所有这些操作。让我们来定义它:
<?
class DBManager
{
public static function setDriver($driver)
{
$this->driver = $driver;
//set the driver
}
public static function connect()
{
if ($this->driver=="mysql")
{
$MM = new MySQLManager();
$MM->setHost("host");
$MM->setDB("db");
$MM->setUserName("user");
$MM->setPassword("pwd");
$this->connection = $MM->connect();
}
else if($this->driver=="pgsql")
{
$PM = new PostgreSQLManager();
$PM->setHost("host");
$PM->setDB("db");
$PM->setUserName("user");
$PM->setPassword("pwd");
$this->connection= $PM->connect();
}
}
}
?>

现在,你可以从单个位置 DBManager 使用它。这使得事情比以前容易得多。
<?
$DM = new DBManager();
$DM->setDriver("mysql");
$DM->connect("host","user","db","pwd");
?>
这是工厂设计模式在现实生活中的真实例子。DBManager 现在作为一个工厂工作,它封装了幕后所有的复杂性,并提供了两种产品。工厂通过封装其内部的困难来简化编程。
抽象工厂
抽象工厂几乎与工厂相似,唯一的区别是所有具体的对象都必须扩展一个共同的抽象类。你可能会问这样做的好处是什么。好吧,只要具体的对象是从一个已知的抽象对象派生出来的,编程就会简化,因为它们都遵循相同的标准。
让我们看看之前的例子。我们首先创建一个抽象类,然后扩展该对象以开发所有具体的驱动类。
<?
abstract class DBDriver
{
public function connect();
public function executeQuery();
public function insert_id();
public function setHost($host)
{
//set db host
}
public function setDB($db)
{
//set db name
}
public function setUserName($user)
{
//set user name
}
public function setPassword($pwd)
{
//set password
}
//.....
}
?>
现在,我们的 MySQL 将从它派生出来:
<?
class MySQLManager extends DBDriver
{
public function connect()
{
//implement own connection procedures
}
public function executeQuery()
{
//execute mysql query and return result
}
public function insertId()
{
//find the latest inserted id
}
}
?>

之后,我们将像往常一样在 DBManager 中使用这个 MySQLManager 类。一个主要的好处是我们可以在一个地方定义所有必要的函数,这些函数在所有派生类中都有相同的标准。我们还可以在抽象类中封装常见的函数/过程。
适配器模式
面向对象编程中另一个有趣的问题是通过一个名为适配器的设计模式来解决的。那么适配器模式是什么,它解决了哪些类型的问题?
适配器实际上是一个在现实生活中充当适配器的对象,它将一种东西转换成另一种东西。使用适配器,你可以将电源从高电压转换为低电压。同样,在面向对象编程中,使用适配器模式,一个对象可以适应另一个对象的相同方法。
让我们更详细地讨论现实生活中的编码模式。假设你开发了一个在线文档存储库,它可以将文档导出至流行的在线文件存储服务。你已经开发了一个包装器,它可以使用 Writely 的本地 API 存储和检索文档。然而,在谷歌收购 Writely 不久后,你发现他们暂时关闭了服务,你必须使用谷歌文档作为该存储库的基础。现在你该怎么办?你找到了一些开源解决方案与谷歌文档一起使用,但不幸的是,你发现那个谷歌文档对象的操作方法与 Writely 对象不同。
这是一个非常常见的场景,它发生在不同开发者开发类的时候。你想要使用这个谷歌文档对象,但你不想更改你的核心代码,因为那样你将不得不大量更改它。而且,在这些核心更改之后,代码可能会出错。
在这种情况下,适配器模式可以拯救你的生命。你开发了一个通用接口,Writely 对象实现了这个接口。现在你只需要开发另一个包装类,它实现了谷歌文档实现的相同接口。那么我们的包装类会做什么呢?它将谷歌文档类的所有方法包装到接口中可用的方法里。在成功包装完所有内容后,你可以在代码中直接使用这个对象。你可能需要更改一两行,但核心代码的其他部分保持不变。
这就是使用适配器模式的好处。即使第三方依赖和外部 API 的代码发生变化,你也能保持核心代码不变。让我们更仔细地看看它:

这是我们的第一个 Writely 对象版本:
<?
class Writely implements DocManager()
{
public function authenticate($user, $pwd)
{
//authenticate using Writely authentication scheme
}
public function getDocuments($folderid)
{
//get documents available in a folder
}
public function getDocumentsByType($folderid, $type)
{
//get documents of specific type from a folder
}
public function getFolders($folderid=null)
{
//get all folders under a specific folder
}
public function saveDocuments($document)
{
//save the document
}
}
?>
这里是 DocManager 接口:
<?
interface DocManager
{
public function authenticate($user, $pwd);
public function getDocuments($folderid);
public function getDocumentsByType($folderid, $type);
public function getFolders($folderid=null);
public function saveDocument($document);
}
?>
现在的 GoogleDoc 对象看起来像下面这样:
<?
class GoogleDocs
{
public function authenticateByClientLogin()
{
//authenticate using Writely authentication scheme
}
public function setUser()
{
//set user
}
public function setPassword()
{
//set password
}
public function getAllDocuments()
{
//get documents available in a folder
}
public function getRecentDocuments()
{
}
public function getDocument()
{
}
}
?>
那么它是如何与我们的现有代码兼容的呢?
为了使其与我们的现有代码兼容,我们需要开发一个包装对象,它实现了相同的 DocManager 接口,但使用 GoogleDoc 对象执行实际工作。
<?php
Class GoogleDocsAdapter implements DocManager
{
private $GD;
public function __construct()
{
$this->GD = new GoogleDocs();
}
public function authenticate($user, $pwd)
{
$this->GD->setUser($user);
$this->GD->setPwd($pwd);
$this->GD->authenticateByClientLogin();
}
public function getDocuments($folderid)
{
return $this->GD->getAllDocuments();
}
public function getDocumentsByType($folderid, $type)
{
//get documents using GoogleDocs object and return only
// which match the type
}
public function getFolders($folderid=null)
{
//for example there is no folder in GoogleDocs, so
//return anything.
}
public function saveDocument($document)
{
//save the document using GoogleDocs object
}
}
?>
现在,我们将实例化一个 GoogleDocsAdapter 的实例,然后在我们的核心代码中使用这个实例。因为它实现了相同的接口,所以不需要更改核心代码。
然而,还有一点需要注意:缺失的功能怎么办?例如,你的 WritelyDocs 对象支持 getFolders() 方法,但在 GoogleDocs 中这个方法没有用。你必须更仔细地实现这些方法。例如,如果你的核心代码需要这个方法返回的一些文件夹 ID,在 GoogleDocsAdapter 中你可以生成一个随机的文件夹 ID 并返回它们(这在 GoogleDocsAdapter 中没有用)。所以你的核心代码根本不会出错。
单例模式
最常用的设计模式之一是单例。这个模式解决了面向对象编程中的一个非常重要的问题,并在实际编程中拯救了数百万程序员的性命。
单例模式的主要目的是无论你实例化多少次,都提供对象的单个实例。也就是说,如果一个对象被实例化一次,使用单例模式,你可以在代码中再次需要时只提供那个实例。这样可以节省内存消耗,防止创建多个对象实例。因此,单例模式用于提高应用程序的性能。

让我们以之前示例中创建的MySQLManager类为例。现在我们正在使用单例模式添加一个单例实例功能。
<?
class MySQLManager
{
private static $instance;
public function __construct()
{
if (!self::$instance)
{
self::$instance = $this;
echo "New Instance\n";
return self::$instance;
}
else
{
echo "Old Instance\n";
return self::$instance;
}
}
//keep other methods same
}
?>
现在,让我们看看它实际上是如何工作的。如果你执行以下脚本,你会对看到的结果感到惊讶。
<?
$a = new MYSQLManager();
$b = new MYSQLManager();
$c = new MYSQLManager();
$d = new MYSQLManager();
$e = new MYSQLManager();
?>
输出结果为:
New Instance
Old Instance
Old Instance
Old Instance
Old Instance
奇怪,不是吗?MySQLManager类在第一次调用时只创建了一个实例,之后它就使用相同的旧对象,而不是每次都创建一个新对象。让我们看看我们是如何实现这一点的。
private static $instance;
我们类中有一个名为$instance的静态变量。在构造函数中,我们检查这个静态变量实际上是否包含任何内容。如果它是空的,我们就实例化对象本身,并将实例设置在这个静态变量中。由于它是静态的,它将在整个脚本执行过程中保持可用。
让我们回到构造函数。在第二次调用时,我们只是检查$instance变量是否包含任何内容。我们发现$instance变量实际上包含了这个对象的实例,并且它仍然被保留,因为它是一个静态变量。所以,在第二次调用中,我们实际上返回了由前一次调用创建的这个对象的实例。
单例是一个非常重要的模式,你应该正确理解它实际上做什么。你可以通过正确使用这个模式来优化你的应用程序并提高其性能。
迭代器模式
迭代器是一种常见的模式,它可以帮助你更轻松地操作集合。几乎每种语言都有内置的迭代器支持。甚至 PHP5 也有内置的迭代器对象。迭代器非常有用,可以提供一个简单的接口来按顺序操作集合。
让我们考虑这样一个场景,当迭代器模式可以在复杂应用程序中拯救开发者时。让我们想象你正在创建一个博客,用户在这里写下他们的日常网络日志。你该如何逐个显示不同的帖子呢?
在下面的示例中,你将所有由作者创建的post_id传递到你的模板中,并且模板设计者编写了以下代码来在模板中正确显示:
<?
$posts = getAllPosts(); //example function return all post ids of this author
for($i = 0; $i<count($posts); $i++)
{
$title = getPostTitle($post[$i]);
echo $title;
$author = getPostAuthor($post[$i]);
$content = parseBBCode(getPostContent($post[$i]));
echo "Content";
$comments = getAllComments($post[$i]);
for ($j=0; $j<count($comments); $j++)
{
$commentAuthor = getCommentAuthor($comments[$j]);
echo $commentAuthor;
$comment = getCommentContent($comments[$j]);
echo $comment;
}
}
?>
在这个例子中,我们在模板中做所有的事情;我们获取所有帖子 ID,然后获取作者、评论、内容并显示。我们还在模板代码中获取评论列表。整个代码太模糊,难以阅读和管理,可能在任何核心更改时连续崩溃。但想想,如果我们把评论转换成针对该帖子的评论对象集合,把所有帖子转换成帖子对象集合以便更容易访问,这将减轻模板设计的负担,同时创建可管理的代码。
让我们为我们的评论和帖子实现迭代器模式,看看它如何有效地将你的代码变成一首可读的诗。毕竟,编码就是诗歌。
在 PHP5 中,为了有效地使用迭代,我们可以使用Iterator接口。该接口如下所示:
<?
interface Iterator
{
function rewind();
function current();
function key();
function next();
function valid();
}
?>
rewind()函数将迭代器的索引设置到集合的开始。Current()返回当前对象。key()函数返回当前键。next()函数返回在当前循环计数器前是否有更多对象。如果返回是肯定的,此函数返回 true,否则返回 false。valid()函数返回当前对象,如果它有任何值。让我们为我们的帖子对象创建一个迭代器。
我们将创建一个名为getAllPosts()的函数,该函数将从数据库返回所有帖子。所有这些帖子都以Post对象的形式返回,该对象具有getAuthor()、getTitle()、getDate()、getComments()等方法。现在我们将创建一个Iterator:
<?php
class Posts implements Iterator
{
private $posts = array();
public function __construct($posts)
{
if (is_array($posts)) {
$this->posts = $posts;
}
}
public function rewind() {
reset($this->posts);
}
public function current() {
return current($this->posts);
}
public function key() {
return key($this->var);
}
public function next() {
return next($this->var);
}
public function valid() {
return ($this->current() !== false);
}
}
?>
现在让我们使用我们刚刚创建的Iterator。
<?
$blogposts = getAllPosts();
$posts = new Posts($posts);
foreach ($posts as $post)
{
echo $post->getTitle();
echo $post->getAuthor();
echo $post->getDate();
echo $post->getContent();
$comments = new Comments($post->getComments());
//another Iterator for comments, code is same as Posts
foreach ($comments as $comment)
{
echo $comment->getAuthor();
echo $comment->getContent();
}
}
?>
代码现在变得更容易阅读和维护。
注意
在 PHP 数组中,对象默认实现这个Iterator接口。但当然,你可以实现它来添加更多用户自定义的功能,以简化你的开发周期。
观察者模式
你可能会想知道这些事件实际上是如何工作的以及它们是如何被触发的。好吧,如果你熟悉观察者模式,你可以比以往任何时候都更容易地创建事件驱动应用程序。
观察者模式解决了面向对象中的常见问题。例如,如果你想当某些对象在发生某些事情(一个事件被触发)时自动通知,你可以用这个模式解决这个问题。让我们更仔细地看看。
观察者模式由两种类型的对象组成;一种是可观察对象,它被observer对象观察。当可观察对象的状态发生变化时,它会通知所有与之注册的观察者。
那么它可以用在哪里呢?实际上,它无处不在。想想一个日志应用,当发生错误时,可以以不同的方式记录错误。想想一个消息应用,当收到最新消息时,会弹出。想想一个网络公告板,每当有新消息发布时,最新消息会自动显示。好吧,还有成千上万的其他应用。让我们实现这个模式。

我们所有的observer对象都实现了如下所示的observer接口:
<?
interface observer
{
public function notify();
}
?>
现在一些observer对象,当可观察对象的状态发生变化时,我们将通知它们:
<?
class YMNotifier implements observer
{
public function notify()
{
//send alerts using YM
echo "Notifying via YM\n";
}
};
?>
另一个通知器:
<?
class EmailNotifier implements observer
{
public function notify()
{
//send alerts using Email
echo "Notifying via Email\n";
}
};
?>
现在我们需要创建我们的observer。
<?
class observable
{
private $observers = array();
public function register($object)
{
if ($object instanceof observer )
$this->observers[] =$object;
else
echo "The object must implement observer interface\n";
}
public function stateChange()
{
foreach ($this->observers as $observer)
{
$observer->notify();
}
}
}
?>
现在让我们使用它:
<?
$postmonitor = new observable();
$ym = new YMNotifier();
$em = new EmailNotifier();
$s= new stdClass();
$postmonitor->register($ym);
$postmonitor->register($em);
$postmonitor->register($s);
$postmonitor->stateChange();
?>
输出如下:
The object must implement observer interface
Notifying via YM
Notifying via Email
代理模式或懒加载
在面向对象编程中,另一个非常重要的编程实践是懒加载和松耦合。主要思想是在编码时减少对象之间的具体依赖。这种编程有什么好处?一个简单的答案——它总是增加了你代码的可移植性。
使用代理模式,你可以创建远程对象的本地版本。它提供了一个通用的 API 来访问远程对象的方法,而不需要了解幕后的事情。代理模式的一个最佳例子是 PHP 的 XML RPC 客户端和服务器。
让我们看看以下代码。这里我们创建了一个类,它可以访问远程创建的任何方法。远程对象的方法通过 XML RPC 服务器公开,然后通过 XML RPC 客户端访问。

如果你想知道它是如何工作的,你会发现几乎每个博客引擎都支持三种流行的博客 API,即 Blogger、MetaWebLog 和 MovableType。使用这些方法,你可以远程管理你的博客。支持哪些方法,将取决于博客引擎。
我们将使用 Incutio PHP XML-RPC 库来创建一个示例服务器和客户端对象。让我们先创建一个服务器。你可以从这里下载 XML-RPC 库:scripts.incutio.com/xmlrpc/IXR_Library.inc.php.txt
我们正在创建一个时间服务器,我们可以从中获取格林威治标准时间(GMT):
<?php
include('IXR_Library.inc.php');
function gmtTime() {
return gmdate("F, d Y H:i:s");
}
$server = new IXR_Server(array(
'time.getGMTTime' => 'gmtTime',
));
?>
好吧,非常简单。我们只是创建了一些方法,然后将它们映射到 XML RPC 服务器。现在让我们看看我们如何为客户编写代码:
<?
include('IXR_Library.inc.php');
$client = new IXR_Client('http://localhost/proxy/server.php');
if (!$client->query('time.getGMTTime'))
{
die('Something went wrong - '.$client->getErrorCode().' :
'.$client->getErrorMessage());
}
echo ($client->getResponse());
?>
如果你将服务器放在你的网络服务器(这里localhost)文档中,根目录在一个名为proxy的文件夹中,然后访问客户端,你将得到以下输出:
2007 年 3 月 28 日 16:13:20
就这样!这就是代理模式的工作原理,并为本地应用程序提供远程对象的接口。
装饰器模式
装饰器模式是 GoF 在他们传奇的设计模式书中引入的一个重要的问题解决方法。使用这个模式,你可以在不扩展对象的情况下向现有对象添加额外的功能。所以你可能会问,在不使用继承的情况下添加额外的功能有什么好处?
好吧,当然有一些好处。要扩展一个对象,有时你需要知道那个类的许多内部事情。有时在不重写现有功能的情况下无法扩展该类。如果你想要将相同的功能添加到许多类型的对象中,使用装饰器模式而不是逐个扩展它们会更好。否则,可能会让你陷入可怕的维护噩梦。

让我们考虑一个常见的场景。例如,想象你正在构建一个博客或论坛,其中所有帖子评论都作为单独的帖子评论对象出现。这两个对象都有一个公共方法 getContents(),它返回该帖子或评论的过滤内容。
现在经理要求添加解析这些帖子评论中的表情符号和 BBCode 的功能。核心代码很复杂,你不想再触碰它了。这就是装饰器模式拯救你的生命的时候。
让我们先看看我们的 Post 和 Comment 对象。
<?
class Post
{
private $title;
private $content;
//additional properties
public function filter()
{
//do necessary processing
$this->content = $filtered_content;
$this->title = $filtered_title;
}
public function getContent()
{
return $this->content;
}
//additional methods
}
?>
<?
class Comment
{
private $date;
private $content;
//additional properties
public function filter()
{
//do necessary processing
$this->content = $filtered_content;
}
public function getContent()
{
return $this->content;
}
//additional methods
}
?>
现在我们创建了两个装饰器对象,分别可以解析 BBCode 和 Emoticon:
<?
class BBCodeParser
{
private $post;
public function __construct($object)
{
$this->post = $object;
}
public function getContent()
{
//parse bbcode
$post->filter();
$content = $this->parseBBCode($post->getContent());
return $content;
}
private function parseBBCode($content)
{
//process BB code in the content and return it
}
}
?>
接下来是表情符号解析器:
<?
class EmoticonParser
{
private $post;
public function __construct($object)
{
$this->post = $object;
}
public function getContent()
{
//parse bbcode
$post->filter();
$content = $this->parseEmoticon($post->getContent());
return $content;
}
private function parseEmoticon($content)
{
//process Emoticon code in the content and return it
}
}
?>
这些装饰器对象只是为现有对象添加了 BBCode 和 EmoticonCode 解析能力,而没有触及它们。
让我们看看我们如何使用它:
<?
$post = new Post();//set the properties of the post object
$comment = new Comment();//set the properties of the comment object
$post->filter();
$comment->filter();
if ($BBCodeEnabled==false && $EmoticonEnabled==false)
{
$PostContent = $post->getContent();
$CommentContent = $comment->getContent();
}
elseif ($BBCodeEnabled==true && $EmoticonEnabled==false)
{
$bb = new BBCodeParser($post);//passing a post object to
//BBCodeParser
$PostContent = $bb->getContent();
$bb = new BBCodeParser($comment);//passing a comment object to
//BBCodeParser
$CommentContent = $bb->getContent();
}
elseif ($BBCodeEnabled==true && $EmoticonEnabled==false)
{
$em = new EmoticonParser($post);
$PostContent = $bb->getContent();
$em = new EmoticonParser($comment);
$CommentContent = $bb->getContent();
}
?>
这就是你可以在不触及现有对象的情况下添加额外功能的方法。然而,你看到 BBCodeParser 和 EmoticonParser 接受任何对象,这意味着如果你提供一个没有任何名为 getContent() 方法的对象,代码将会崩溃。因此,你可以在那些可能想要装饰的对象中实现一个公共接口。同样,在装饰器对象中,你只能接受实现了该接口或那些接口的对象。
活动记录模式
这是另一个非常重要的设计模式,用于简化数据库操作。我们将在第七章中了解更多关于这个模式的内容。
门面模式
到目前为止,我们已经学习了使用面向对象设计模式解决许多常见问题的方法。现在,我们将了解另一个有趣的模式,我们经常在代码中无意中使用它,而不知道它也是一个模式。让我们了解这个名为门面模式的常见模式。
门面(Facade)为许多对象提供了一个公共接口。换句话说,它只是简化了编程,提供了一个必要的接口,实际上在幕后使用了大量其他对象。因此,它最小化了开发者的学习曲线。当一位新开发者加入团队时,他突然会接触到许多带有大量方法和属性的对象,其中他可能只需要几个来完成他的工作。那么为什么还要浪费时间学习它们呢?这就是门面帮助开发者并节省他们大量时间的地方。让我们看看一些例子,以便更清楚地理解它。
假设你正在创建一个公寓租赁系统,在你的存储库中有三个对象。一个对象使用在线地理编码服务进行地理编码。另一个对象使用地图服务定位该地点。最后,另一个服务在该地区搜索所有出售的公寓。
现在你想要创建一个更简单的接口,以便任何未来的开发者都可以使用你的库,而不是一起学习它们。以下图片显示了在门面出现之前的代码结构:

使用外观模式(Facade)后的代码结构如下:

现在我们来看一下代码:
<?
class ApartmentFinder
{
public function locateApartments($place)
{
//use the web service and locate all apartments suitable
//search name
//now return them all in an array
return $apartmentsArray();
}
}
?>
<?
class GeoLocator
{
public function getLocations($place)
{
//use public geo coding service like yahoo and get the
//lattitude and
//longitude of that place
return array("lat"=>$lattitude, "lng"=>$longitude);
}
}
?>
<?
class GoogleMap
{
public function initialize()
{
//do initialize
}
public function drawLocations($locations /* array */)
{
//locate all the points using Google Map Locator
}
public function dispatch($divid)
{
//draw the map with in a div with the div id
}
}
?>
这些是我们的具体类。现在你想要使用它们全部来开发一个外观模式(Facade),并为开发者提供一个更简单的接口。看看它是如何使三个类的组合变得如此简单:
<?
class Facade
{
public function findApartments($place, $divid)
{
$AF = new ApartmentFinder();
$GL =new GeoLocator();
$GM = new GoogleMap();
$apartments = $AF->locateApartments($place);
foreach ($apartments as $apartment)
{
$locations[] = $GL->getLocations($apartment);
}
$GM->initialize();
$GM->drawLocations($locations);
$GM->dispatch($divid);
}
}
?>
现在任何人都可以通过使用单个接口外观模式(Facade)来使用所有三个类的服务:
<?
$F = new Facade();
$F->findApartments("London, Greater London","mapdiv");
?>
如我之前所说,在面向对象编程中,我们在项目中的某个时期已经多次做过这类工作,然而我们可能并不知道这种技术被定义为名为外观模式(Facade)的设计模式。
摘要
设计模式是面向对象编程(OOP)的一个基本组成部分。它使你的代码更有效,性能更好,并且更容易维护。有时我们在代码中实现这些设计模式,却不知道这些解决方案被定义为设计模式。设计模式有很多种,这本书中无法全部涵盖,因为那样的话它就仅仅是一本关于设计模式的书籍了。然而,如果你对学习其他设计模式感兴趣,可以阅读由 O'Reilly 出版的《Head First Design Patterns》和由 Addison-Wesley 出版的《Design Patterns Explained》。
不要认为你必须在代码中实现设计模式。只有在需要时才使用它们。正确使用正确的模式可以使你的代码性能更佳;同样,不正确地使用它们可能会使你的代码变慢且效率降低。
在下一章中,我们将学习 PHP 中面向对象编程(OOP)的另一个重要部分。那就是单元测试和反射。在此之前,继续玩转这些模式并探索它们。
第五章:反射与单元测试
PHP5 相比 PHP4 带来了许多新的特性。它用更智能的新 API 替换了许多旧的 API。其中之一就是反射 API。使用这个酷炫的 API 集合,你可以逆向工程任何类或对象,以了解其属性和方法。你可以动态地调用这些方法并做更多的事情。在本章中,我们将更详细地学习反射以及这些函数的用法。
软件开发的另一个非常重要的部分是为你的作品构建测试套件以进行自动化测试。这是为了确保它能够正确工作,并且在任何更改之后保持向后兼容性。为了简化 PHP 开发者的过程,市场上有很多测试工具。其中一些非常流行的工具有 PHPUnit。在本章中,我们将学习使用 PHP 进行单元测试。
Reflection
反射 API 提供了一些功能,可以在运行时找出对象或类中的内容。除此之外,反射 API 允许你动态地调用任何对象的任何方法或属性。让我们来实际操作一下反射。反射 API 中引入了众多对象。其中,以下对象非常重要:
class Reflection { }
interface Reflector { }
class ReflectionException extends Exception { }
class ReflectionFunction implements Reflector { }
class ReflectionParameter implements Reflector { }
class ReflectionMethod extends ReflectionFunction { }
class ReflectionClass implements Reflector { }
class ReflectionObject extends ReflectionClass { }
class ReflectionProperty implements Reflector { }
class ReflectionExtension implements Reflector { }
让我们先去玩一玩 ReflectionClass 吧。
ReflectionClass
这是反射 API 中的一个主要核心类。这个类帮助你以广义的方式逆向工程任何对象。这个类的结构如下所示:
<?php
class ReflectionClass implements Reflector
{
final private __clone()
public object __construct(string name)
public string __toString()
public static string export(mixed class, bool return)
public string getName()
public bool isInternal()
public bool isUserDefined()
public bool isInstantiable()
public bool hasConstant(string name)
public bool hasMethod(string name)
public bool hasProperty(string name)
public string getFileName()
public int getStartLine()
public int getEndLine()
public string getDocComment()
public ReflectionMethod getConstructor()
public ReflectionMethod getMethod(string name)
public ReflectionMethod[] getMethods()
public ReflectionProperty getProperty(string name)
public ReflectionProperty[] getProperties()
public array getConstants()
public mixed getConstant(string name)
public ReflectionClass[] getInterfaces()
public bool isInterface()
public bool isAbstract()
public bool isFinal()
public int getModifiers()
public bool isInstance(stdclass object)
public stdclass newInstance(mixed args)
public stdclass newInstanceArgs(array args)
public ReflectionClass getParentClass()
public bool isSubclassOf(ReflectionClass class)
public array getStaticProperties()
public mixed getStaticPropertyValue(string name [, mixed default])
public void setStaticPropertyValue(string name, mixed value)
public array getDefaultProperties()
public bool isIterateable()
public bool implementsInterface(string name)
public ReflectionExtension getExtension()
public string getExtensionName()
}
?>
让我们讨论一下这个类是如何实际工作的。首先,我们将找到它们的方法和目的:
-
export()方法将任何对象的内部结构输出,这与var_dump函数类似。 -
getName()函数返回对象的内部名称,即类名。 -
isInternal()方法如果类是 PHP5 内置对象则返回 true。 -
isUserDefined()方法与isInternal()方法的相反。它仅仅返回对象是否是由用户定义的。 -
getFileName()函数返回包含类的 PHP 脚本文件名。 -
getStartLine()返回该类代码在脚本文件中的起始行。 -
getDocComment()是另一个有趣的函数,它返回该对象的类级别文档。我们将在本章后面的示例中演示它。 -
getConstructor()返回对象的构造函数的引用,作为一个ReflectionMethod对象。 -
getMethod()函数返回传递给它的任何方法的地址。返回的对象是一个ReflectionMethod对象。 -
getMethods()返回对象中所有方法的数组。在该数组中,每个方法都返回为一个ReflectionMethod对象。 -
getProperty()函数返回该对象中任何属性的引用,作为一个ReflectionProperty对象。 -
getConstants()返回该对象中常量的数组。 -
getConstant()返回任何特定常量的值。 -
如果您想查看一个类实现(如果有)的接口引用,您可以使用
getInterfaces()函数,该函数返回一个包含ReflectionClass对象的接口数组。 -
getModifiers()方法返回与该类相关的修饰符列表。例如,它可能是公共的、私有的、受保护的、抽象的、静态的或最终的。 -
newInstance()函数返回该类的新实例,并以常规对象的形式返回它(实际上是stdClas;stdClass`是每个 PHP 对象的基础类)。 -
您想获取任何类的父类引用?您可以使用
getParentClass()方法来获取它,作为ReflectionClass对象。 -
ReflectionClass()的另一个酷炫功能是它可以告诉你一个类是从哪个扩展中起源的。例如,ArrayObject类是从 SPL 类起源的。您必须使用getExtensionName()函数来实现这一点。
现在我们来写一些代码。我们将看到这些函数在实际代码中的应用。在这里,我展示了一个来自 PHP 手册的精彩示例。
<?php
interface NSerializable
{
// ...
}
class Object
{
// ...
}
/**
* A counter class
*/
class Counter extends Object implements NSerializable
{
const START = 0;
private static $c = Counter::START;
/**
* Invoke counter
*
* @access public
* @return int
*/
public function count()
{
return self::$c++;
}
}
// Create an instance of the ReflectionClass class
$class = new ReflectionClass('Counter');
// Print out basic information
printf(
"===> The %s%s%s %s '%s' [extends %s]\n" .
" declared in %s\n" .
" lines %d to %d\n" .
" having the modifiers %d [%s]\n",
$class->isInternal() ? 'internal' : 'user-defined',
$class->isAbstract() ? ' abstract' : '',
$class->isFinal() ? ' final' : '',
$class->isInterface() ? 'interface' : 'class',
$class->getName(),
var_export($class->getParentClass(), 1),
$class->getFileName(),
$class->getStartLine(),
$class->getEndline(),
$class->getModifiers(),
implode(' ', Reflection::getModifierNames(
$class->getModifiers()))
);
// Print documentation comment
printf("---> Documentation:\n %s\n",
var_export($class->getDocComment(), 1));
// Print which interfaces are implemented by this class
printf("---> Implements:\n %s\n",
var_export($class->getInterfaces(), 1));
// Print class constants
printf("---> Constants: %s\n",
var_export($class->getConstants(), 1));
// Print class properties
printf("---> Properties: %s\n",
var_export($class->getProperties(), 1));
// Print class methods
printf("---> Methods: %s\n",
var_export($class->getMethods(), 1));
// If this class is instantiable, create an instance
if ($class->isInstantiable())
{
$counter = $class->newInstance();
echo '---> $counter is instance? ';
echo $class->isInstance($counter) ? 'yes' : 'no';
echo "\n---> new Object() is instance? ";
echo $class->isInstance(new Object()) ? 'yes' : 'no';
}
?>
现在将上述代码保存到名为class.counter.php的文件中。当您运行上述代码时,您将得到以下输出:
X-Powered-By: PHP/5.1.1
内容类型: text/html
===> 用户定义的类 'Counter' [扩展 ReflectionClass::__set_state(array(
'name' => 'Object',
))]
在 PHPDocument2 中声明
第 15 行到第 29 行
具有修饰符 0 []
---> 文档:
'/**
*** 一个计数器类**
*/'
---> 实现:
array (
0 =>
ReflectionClass::__set_state(array(
'name' => 'NSerializable',
)),
)
---> 常量: array (
'START' => 0,
)
---> 属性: array (
0 =>
ReflectionProperty::__set_state(array(
'name' => 'c',
'class' => 'Counter',
)),
)
---> 方法: array (
0 =>
ReflectionMethod::__set_state(array(
'name' => 'count',
'class' => 'Counter',
)),
)
---> $counter 是实例?是
---> new Object() 是实例?否
ReflectionMethod
这是一个用于调查类的任何方法并调用它的类。让我们看看这个类的结构:
<?php
class ReflectionMethod extends ReflectionFunction
{
public __construct(mixed class, string name)
public string __toString()
public static string export(mixed class, string name, bool return)
public mixed invoke(stdclass object, mixed args)
public mixed invokeArgs(stdclass object, array args)
public bool isFinal()
public bool isAbstract()
public bool isPublic()
public bool isPrivate()
public bool isProtected()
public bool isStatic()
public bool isConstructor()
public bool isDestructor()
public int getModifiers()
public ReflectionClass getDeclaringClass()
// Inherited from ReflectionFunction
final private __clone()
public string getName()
public bool isInternal()
public bool isUserDefined()
public string getFileName()
public int getStartLine()
public int getEndLine()
public string getDocComment()
public array getStaticVariables()
public bool returnsReference()
public ReflectionParameter[] getParameters()
public int getNumberOfParameters()
public int getNumberOfRequiredParameters()
}
?>
这个类最重要的方法是getNumberOfParameters、getNumberOfRequiredParameters、getParameters和invoke。前三个方法很容易理解;让我们看看第四个方法,即调用。这是一个来自 PHP 手册的精彩示例:
<?php
class Counter
{
private static $c = 0;
/**
* Increment counter
*
* @final
* @static
* @access public
* @return int
*/
final public static function increment()
{
return ++self::$c;
}
}
// Create an instance of the Reflection_Method class
$method = new ReflectionMethod('Counter', 'increment');
// Print out basic information
printf(
"===> The %s%s%s%s%s%s%s method '%s' (which is %s)\n" .
" declared in %s\n" .
" lines %d to %d\n" .
" having the modifiers %d[%s]\n",
$method->isInternal() ? 'internal' : 'user-defined',
$method->isAbstract() ? ' abstract' : '',
$method->isFinal() ? ' final' : '',
$method->isPublic() ? ' public' : '',
$method->isPrivate() ? ' private' : '',
$method->isProtected() ? ' protected' : '',
$method->isStatic() ? ' static' : '',
$method->getName(),
$method->isConstructor() ? 'the constructor' :
'a regular method',
$method->getFileName(),
$method->getStartLine(),
$method->getEndline(),
$method->getModifiers(),
implode(' ', Reflection::getModifierNames(
$method->getModifiers()))
);
// Print documentation comment
printf("---> Documentation:\n %s\n",
var_export($method->getDocComment(), 1));
// Print static variables if existant
if ($statics= $method->getStaticVariables()) {
printf("---> Static variables: %s\n", var_export($statics, 1));
}
// Invoke the method
printf("---> Invokation results in: ");
var_dump($method->invoke(NULL));
?>
当执行此代码时,将给出以下输出:
===> The user-defined final public static method 'increment' (which is a regular method)
declared in PHPDocument1
lines 14 to 17
having the modifiers 261[final public static]
---> Documentation:
'/**
* Increment counter
*
* @final
* @static
* @access public
* @return int
*/'
---> Invokation results in: int(1)
ReflectionParameter
反射家族中另一个非常重要的对象是ReflectionParameter。使用这个类,您可以分析任何方法的参数并相应地采取行动。让我们看看这个对象的结构:
<?php
class ReflectionParameter implements Reflector
{
final private __clone()
public object __construct(string name)
public string __toString()
public static string export(mixed function, mixed parameter,
bool return)
public string getName()
public bool isPassedByReference()
public ReflectionFunction getDeclaringFunction()
public ReflectionClass getDeclaringClass()
public ReflectionClass getClass()
public bool isArray()
public bool allowsNull()
public bool isPassedByReference()
public bool getPosition()
public bool isOptional()
public bool isDefaultValueAvailable()
public mixed getDefaultValue()
}
?>
为了使事情更简单,请查看以下示例以了解这个功能是如何工作的。
<?php
function foo($a, $b, $c) { }
function bar(Exception $a, &$b, $c) { }
function baz(ReflectionFunction $a, $b = 1, $c = null) { }
function abc() { }
// Create an instance of Reflection_Function with the
// parameter given from the command line.
$reflect = new ReflectionFunction("baz");
echo $reflect;
foreach ($reflect->getParameters() as $i => $param)
{
printf(
"-- Parameter #%d: %s {\n".
" Class: %s\n".
" Allows NULL: %s\n".
" Passed to by reference: %s\n".
" Is optional?: %s\n".
"}\n",
$i,
$param->getName(),
var_export($param->getClass(), 1),
var_export($param->allowsNull(), 1),
var_export($param->isPassedByReference(), 1),
$param->isOptional() ? 'yes' : 'no'
);
}
?>
如果您运行上述代码片段,您将得到以下输出:
Function [ <user> <visibility error> function baz ]
{
@@ C:\OOP with PHP5\Codes\ch5\test.php 4 - 4
- Parameters [3]
{
Parameter #0 [ <required> ReflectionFunction &$a ]
Parameter #1 [ <optional> $b = 1 ]
Parameter #2 [ <optional> $c = NULL ]
}
}
-- Parameter #0: a
{
Class: ReflectionClass::__set_state(array(
'name' => 'ReflectionFunction',
))
Allows NULL: false
Passed to by reference: true
Is optional?: no
}
-- Parameter #1: b
{
Class: NULL
Allows NULL: true
Passed to by reference: false
Is optional?: yes
}
-- Parameter #2: c
{
Class: NULL
Allows NULL: true
Passed to by reference: false
Is optional?: yes
}
ReflectionProperty
这是我们要在这里讨论的反射家族中的最后一个。这个类帮助你调查类属性并逆向工程它们。这个类具有以下结构:
<?php
class ReflectionProperty implements Reflector
{
final private __clone()
public __construct(mixed class, string name)
public string __toString()
public static string export(mixed class, string name, bool return)
public string getName()
public bool isPublic()
public bool isPrivate()
public bool isProtected()
public bool isStatic()
public bool isDefault()
public int getModifiers()
public mixed getValue(stdclass object)
public void setValue(stdclass object, mixed value)
public ReflectionClass getDeclaringClass()
public string getDocComment()
}
?>
下面是一个直接从 PHP 手册中摘取的例子,有助于描述它实际上是如何工作的。
<?php
class String
{
public $length = 5;
}
// Create an instance of the ReflectionProperty class
$prop = new ReflectionProperty('String', 'length');
// Print out basic information
printf(
"===> The%s%s%s%s property '%s' (which was %s)\n" .
" having the modifiers %s\n",
$prop->isPublic() ? ' public' : '',
$prop->isPrivate() ? ' private' : '',
$prop->isProtected() ? ' protected' : '',
$prop->isStatic() ? ' static' : '',
$prop->getName(),
$prop->isDefault() ? 'declared at compile-time' :
'created at run-time',
var_export(Reflection::getModifierNames(
$prop->getModifiers()), 1)
);
// Create an instance of String
$obj= new String();
// Get current value
printf("---> Value is: ");
var_dump($prop->getValue($obj));
// Change value
$prop->setValue($obj, 10);
printf("---> Setting value to 10, new value is: ");
var_dump($prop->getValue($obj));
// Dump object
var_dump($obj);
?>
在执行时,代码产生以下输出。此代码使用ReflectionProperty检查一个属性,并显示以下输出:
===> The public property 'length' (which was declared at compile-time)
having the modifiers array (
0 => 'public',
)
---> Value is: int(5)
---> Setting value to 10, new value is: int(10)
object(String)#2 (1) {
["length"]=>
int(10)
}
我们将在后面的章节中看到 Reflection API 的更多用途,当我们学习构建 MVC 框架时。
单元测试
编程的另一个非常重要的部分是单元测试,通过它可以测试代码片段,是否工作得完美。你可以针对代码的任何版本编写测试用例,以检查重构后代码是否工作正常。单元测试确保代码的可工作性,并在问题发生时帮助定位问题。当你编写应用程序时,单元测试就像你的骨架。单元测试是所有语言程序员的编程必经之路。几乎所有主要的编程语言都有单元测试包可用。
与其他任何编程语言一样,有一个 Java 包被认为是其他语言每个单元测试包的标准模型。这个包被称为JUnit,它是为 Java 开发者准备的。JUnit 中维护的标准和测试风格通常被许多其他单元测试包所遵循。因此,JUnit 已经成为单元测试领域的默认选择。为 PHP 开发者提供的 JUnit 版本被称为PHPUnit,由 Sebastian Bergmann 开发。PHPUnit 是一个非常流行的单元测试包。
编写单元测试的主要原因是,如果你只是编写代码并部署应用程序,你无法找出所有的错误。可能会有一些小错误,通过返回一个不相关的值,可能会使你的应用程序突然崩溃。不要忽视这些小场景。可能会有你无法想象到你的代码返回一个极其奇怪结果的情况。单元测试通过编写不同的测试用例来帮助你。单元测试不是一件需要花费很多时间来编写的事情,然而结果却是惊人的。
在下一节中,我们将学习单元测试的基础知识,并亲自动手编写成功的单元测试。
单元测试的好处
单元测试有很多好处,其中一些是它:
-
确保你的应用程序的一致性。
-
确保在重构后你的完整应用程序仍然可用。
-
检查冗余并从你的代码中移除它们。
-
设计良好的 API。
-
可以轻松地找出问题所在。
-
如果出现问题,可以加快调试过程;正如你所知,尤其是你知道错误所在的地方。
-
通过提供 API 的工作示例来最小化文档的工作量。
-
帮助进行回归测试,以确保不再发生回归。
脆弱漏洞的简要介绍
缺陷可能有不同类型。一些缺陷可能会困扰你的用户,一些缺陷会停止功能,而一些缺陷漏洞会损坏你的资源。让我们考虑以下示例。你编写了一个函数,它接受两个参数并相应地更新数据库。第一个参数是字段的名称,第二个参数是那个字段的值,通过这个值它应该定位数据并更新它们。现在让我们设计它:
function selectUser($field, $condition)
{
if (!empty($condition))
{
$query = "{$field}= '{$condition}'";
}
else
$query = "{$field}";
echo "select * from users where {$query}";
$result = mysql_query("select * from users where {$query}");
$results = array();
while ($data = mysql_fetch_array($result))
{
$results[] = $data;
}
return $results;
}
现在当你这样调用它时,它会显示特定的数据:
print_r(selectUser("id","1");
输出如下:
(
[0] => Array
(
[0] => 1
[id] => 1
[1] => afif
[name] => afif
[2] => 47bce5c74f589f4867dbd57e9ca9f808
[pass] => 47bce5c74f589f4867dbd57e9ca9f808
)
)
但是当你这样调用它时:
print_r(selectUser("id",$_SESSION['id']);
它显示了以下内容:
(
[0] => Array
(
[0] => 1
[id] => 1
[1] => afif
[name] => afif
[2] => 47bce5c74f589f4867dbd57e9ca9f808
[pass] => 47bce5c74f589f4867dbd57e9ca9f808
)
1] => Array
(
[0] => 2
[id] => 2
[1] => 4b8ed057e4f0960d8413e37060d4c175
[name] => 4b8ed057e4f0960d8413e37060d4c175
[2] => 74b87337454200d4d33f80c4663dc5e5
[pass] => 74b87337454200d4d33f80c4663dc5e5
)
)
这不是一个正确的输出;并且如果在运行时它是一个update查询而不是select查询,你的整个数据可能会被损坏。那么你如何确保输出始终是有效的呢?嗯,我们将在本章的后面通过单元测试轻松地做到这一点。
准备进行单元测试
要使用 PHPUnit 为 PHP 应用程序编写成功的单元测试,你需要下载该包,配置它,然后在实际执行测试之前做一些小任务。
你可以从命令行或从你的脚本内部运行 PHPUnit 测试。目前,我们将从我们的脚本内部运行测试,但在后面的章节中,我们将学习如何从命令行运行单元测试。
首先,从www.phpunit.de下载该包,并将其提取到你的包含路径中。如果你不确定你的包含路径是什么,你可以从php.ini中的include_path设置中获取它。或者,你可以执行以下 PHP 脚本来显示输出:
<?
echo get_include_path()
?>
现在,提取 PHPUnit 存档,并将 PHPUnit 文件夹放置在包含路径中的一个文件夹中。这个 PHPUnit 文件夹包含两个其他文件夹,分别命名为PHPUnit和PHPUnit2。
当你将文件夹放置在你的包含路径目录中时,你就完成了。现在我们准备出发了。
开始单元测试
单元测试实际上是对你的代码进行的一系列不同测试。使用 PHPUnit 编写单元测试并不是一项大工程。你只需要简单地遵循一套约定。让我们看看以下示例,其中你创建了一个字符串处理类,该类返回字符串中可用的单词数量。
<?
//class.wordcount.php
class wordcount
{
public function countWords($sentence)
{
return count(split(" ",$sentence));
}
}
?>
现在,我们将为这个类编写一个单元测试。我们必须扩展PHPUnit_Framework_TestCase来编写任何单元测试。我们必须使用PHPUnit_Framework_TestSuite来创建测试套件,它实际上包含了一系列测试。然后我们将使用PHPUnit_TextUI_TestRunner从套件中运行测试并打印结果。
<?
//class.testwordcount.php
require_once "PHPUnit/Framework/TestCase.php";
require_once "class.wordcount.php";
class TestWordCount extends PHPUnit_Framework_TestCase
{
public function testCountWords()
{
$Wc = new WordCount();
$TestSentence = "my name is afif";
$WordCount = $Wc->countWords($TestSentence);
$this->assertEquals(4,$WordCount);
}
}
?>
运行测试:
<?
//testsuite.wordcount.php
require_once 'PHPUnit/TextUI/TestRunner.php';
require_once "PHPUnit/Framework/TestSuite.php";
require_once "class.testwordcount.php";
$suite = new PHPUnit_Framework_TestSuite();
$suite->addTestSuite("TestWordCount");
PHPUnit_TextUI_TestRunner::run($suite);
?>
现在如果你在testsuite.wordcount.php中运行代码,你将得到以下输出:
PHPUnit 3.0.5 by Sebastian Bergmann.
Time: 00:00
OK (1 test)
这意味着我们的测试已经通过,我们的单词计数函数工作得非常完美,然而,我们还将为该函数编写更多的测试用例。
让我们在class.testwordcount.php中添加这个新的测试用例:
public function testCountWordsWithSpaces()
{
$wc= new WordCount();
$testSentence = "my name is Anonymous ";
$wordCount = $Wc->countWords($testSentence);
$this->assertEquals(4,$wordCount);
}
现在如果我们运行我们的测试套件,我们将得到以下结果:
PHPUnit 3.0.5 by Sebastian Bergmann.
.F
Time: 00:00
There was 1 failure:
1) testCountWordsWithSpaces(TestWordCount)
Failed asserting that <integer:5> is equal to <integer:4>.
C:\OOP with PHP5\Codes\ch5\UnitTest\FirstTest.php:34
C:\OOP with PHP5\Codes\ch5\UnitTest\FirstTest.php:40
C:\Program Files\Zend\ZendStudio-5.2.0\bin\php5\dummy.php:1
FAILURES!
Tests: 2, Failures: 1.
在这里,我们发现我们的万无一失的单词计数函数失败了。那么我们的测试输入是什么?我们只是在测试参数my name is afif中添加了更多的空格,然后我们的函数失败了。这是因为它用空白字符分割句子,并返回分割的部分数。因为有更多的空白字符,所以我们的函数优雅地失败了。这是一个相当好的测试用例;我们发现,如果我们用这个版本的单词计数器发布我们的代码,我们的函数在现实生活中可能会失败。PHPUnit 已经对我们很有用了。现在我们将解决我们的函数,使其在句子包含更多空格时返回正确的结果。我们将class.wordcount.php更改为这个新版本:
class WordCount
{
public function countWords($sentence)
{
$newsentence = preg_replace("~\s+~"," ",$sentence);
return count(split(" ",$newsentence));
}
}
现在如果我们运行我们的测试套件,它将给出以下输出。
PHPUnit 3.0.5 by Sebastian Bergmann.
..
Time: 00:00
OK (2 tests)
然而,我们希望有更多的证据表明我们的函数在野外将工作得更好。因此,我们正在编写另一个测试用例。让我们在class.testwordcount.php中添加这个新的测试用例:
public function testCountWordsWithNewLine()
{
$Wc = new WordCount();
$TestSentence = "my name is \n\r Anonymous";
$WordCount = $Wc->countWords($TestSentence);
$this->assertEquals(4,$WordCount);
}
让我们再次运行这个套件。现在结果是什么?
PHPUnit 3.0.5 by Sebastian Bergmann.
...
Time: 00:00
OK (3 tests)
这相当令人满意。所有的测试都在正常运行。现在这个函数已经很好了。
这就是单元测试如何在现实生活中帮助我们。
测试电子邮件验证器对象
现在,让我们再次重复这些步骤。这次我们将为我们的全新Emailvalidator类编写单元测试,我们的开发者说这是一个好的类。让我们首先看看我们的验证器函数:
//class.emailvalidator.php
class EmailValidator
{
public function validateEmail($email)
{
$pattern = "/[A-z0-9]{1,64}@[A-z0-9]+\.[A-z0-9]{2,3}/";
preg_match($pattern, $email,$matches);
return (strlen($matches[0])==strlen($email)?true:false);
}
}
?>
接下来是我们的测试用例:
class TestEmailValidator extends PHPUnit_Framework_TestCase
{
private $Ev;
protected function setup()
{
$this->Ev = new EmailValidator();
}
protected function tearDown()
{
unset($this->Ev);
}
public function testSimpleEmail()
{
$result = $this->Ev->validateEmail("has.in@somewherein.net");
$this->assertTrue($result);
}
}
现在你必须编写测试套件并运行:
$suite = new PHPUnit_Framework_TestSuite();
$suite->addTestSuite("TestEmailValidator");
PHPUnit_TextUI_TestRunner::run($suite);
当你运行这个测试套件时,你会得到以下输出:
PHPUnit 3.0.5 by Sebastian Bergmann.
...
Time: 00:00
OK (1 test)
现在更加努力;尝试破坏你的代码。尝试所有可能出现在电子邮件中的情况,尽可能多地尝试。我们将添加更多的测试用例:
class TestEmailValidator extends PHPUnit_Framework_TestCase
{
private $Ev;
protected function setUp()
{
$this->Ev = new EmailValidator();
}
protected function tearDown()
{
unset($this->Ev);
}
public function testSimpleEmail()
{
$result = $this->Ev->validateEmail("hasin@somewherein.net");
$this->assertTrue($result);
}
public function testEmailWithDotInName()
{
$result = $this->Ev->validateEmail("has.in@somewherein.net");
$this->assertTrue($result);
}
public function testEmailWithComma()
{
$result = $this->Ev->validateEmail("has,in@somewherein.net");
$this->assertFalse($result);
}
public function testEmailWithSpace()
{
$result = $this->Ev->validateEmail("has in@somewherein.net");
$this->assertTrue($result);
}
public function testEmailLengthMoreThan64Char()
{
$result =
$this->Ev->validateEmail(str_repeat("h",67)."@somewherein.net");
$this->assertFalse($result);
}
public function testEmailWithInValidCharacters()
{
$result = $this->Ev->validateEmail("has#in@somewherein.net");
$this->assertFalse($result);
}
public function testEmailWithNoDomain()
{
$result = $this->Ev->validateEmail("hasin@");
$this->assertFalse($result);
}
public function testEmailWithInvalidDomain()
{
$result =
$this->Ev->validateEmail("hasin@somewherein.comnetorg");
$this->assertFalse($result);
}
}
当你运行测试套件时,你会得到以下结果:
PHPUnit 3.0.5 by Sebastian Bergmann.
.F.F....
Time: 00:00
There were 1 failures:
1) testEmailWithDotInName(TestEmailValidator)
Failed asserting that <boolean:false> is identical to <boolean:true>.
C:\OOP with PHP5\Codes\ch5\UnitTest\EmailValidatorTest.php:40
C:\OOP with PHP5\Codes\ch5\UnitTest\EmailValidatorTest.php:83
C:\Program Files\Zend\ZendStudio-5.2.0\bin\php5\dummy.php:1
FAILURES!
Tests: 8, Failures: 1.
因此,我们的电子邮件验证器失败了!如果你查看结果,你会看到它因testEmailWithDotInName而失败。因此,我们必须更改我们使用的正则表达式模式,并允许在名称中使用.。
让我们按照以下方式重新设计验证器:
class EmailValidator
{
public function validateEmail($email)
{
$pattern = "/[A-z0-9\.]{1,64}@[A-z0-9]+\.[A-z0-9]{2,3}/";
preg_match($pattern, $email,$matches);
return (strlen($matches[0])==strlen($email)?true:false);
}
}
现在你再次运行你的测试套件,你会看到以下输出:
PHPUnit 3.0.5 by Sebastian Bergmann.
........
Time: 00:00
OK (8 tests)
我们的测试通过了。
那么好处是什么?一次又一次,当你需要向你的正则表达式添加新的验证规则时,这个单元测试将帮助你进行回归测试,以确保同样的错误不再发生。
这就是单元测试的美丽之处。
注意
你会在上面的例子中找到两个名为setUp()和tearDown()的函数。setUp()用于为测试设置一切;你可以用它来连接到数据库,打开一个文件或类似的东西。tearDown()用于清理。它在脚本执行完毕时被调用。
每日脚本单元测试
除了这些函数和小类的单元测试之外,你还需要为不同函数最终得到的结果编写单元测试。然而,你的单元测试越具体,你预期的结果就越好。也要记住,在你写的许多单元测试中,只有少数是有用的。
现在我们将讨论如何测试与数据库一起工作的例程。让我们创建一个小的类,它可以直接与数据库中的users表交互,我们将为它编写单元测试。以下是我们的小型类,它直接与数据库中的users表交互。
<?
class DB
{
private $connection;
public function __construct()
{
$this->connection = mysql_connect("localhost","root","root1234");
mysql_select_db("test",$this->connection);
}
public function insertData($data)
{
$fields = join(array_keys($data),",");
$values = "'".join(array_values($data),",")."'";
$query = "INSERT INTO users({$fields}) values({$values})";
return mysql_query($query, $this->connection);
}
public function deleteData($id)
{
$query = "delete from users where id={$id}";
return mysql_query($query, $this->connection);
}
public function updateData($id, $data)
{
$queryparts = array();
foreach ($data as $key=>$value)
{
$queryparts[] = "{$key} = '{$value}'";
}
$query = "UPDATE users SET ".join($queryparts,",")."
WHERE id='{$id}'";
return mysql_query($query, $this->connection);
}
}
?>
我们需要测试这个类中的所有公共方法,以确保它们正常工作。因此,我们的测试用例如下。
<?
require_once "PHPUnit/Framework/TestCase.php";
class DBTester extends PHPUnit_Framework_TestCase
{
private $connection;
private $Db;
protected function setup()
{
$this->Db = new DB();
$this->connection = mysql_connect("localhost","root","root1234");
mysql_select_db("abcd",$this->connection);
}
protected function tearDown()
{
mysql_close($this->connection);
}
public function testValidInsert()
{
$data = array("name"=>"afif","pass"=>md5("hello world"));
mysql_query("delete from users");
$result = $this->Db->insertData($data);
$this->assertNotNull($result);
$affected_rows = mysql_affected_rows($this->connection);
$this->assertEquals(1, $affected_rows);
}
public function testInvalidInsert()
{
$data = array("names"=>"afif","passwords"=>md5("hello world"));
mysql_query("delete from users");
$result = $this->Db->insertData($data);
$this->assertNotNull($result);
$affected_rows = mysql_affected_rows($this->connection);
$this->assertEquals(-1, $affected_rows);
}
public function testUpdate()
{
$data = array("name"=>"afif","pass"=>md5("hello world"));
mysql_query("truncate table users");
$this->Db->insertData($data);
$data = array("name"=>"afif2","pass"=>md5("hello world"));
$result = $this->Db->updateData(1, $data);
$this->assertNotNull($result);
$affected_rows = mysql_affected_rows($this->connection);
$this->assertEquals(1, $affected_rows);
}
public function testDelete()
{
$data = array("name"=>"afif","pass"=>md5("hello world"));
mysql_query("truncate table users");
$this->Db->insertData($data);
$result = $this->Db->deleteData(1);
$this->assertNotNull($result);
$affected_rows = mysql_affected_rows($this->connection);
$this->assertEquals(1, $affected_rows);
}
}
?>
测试套件如下:
<?
require_once 'PHPUnit/TextUI/TestRunner.php';
require_once "PHPUnit/Framework/TestSuite.php";
$suite = new PHPUnit_Framework_TestSuite();
$suite->addTestSuite("DBTester");
PHPUnit_TextUI_TestRunner::run($suite);
?>
你会得到什么结果呢?
PHPUnit 3.0.5 by Sebastian Bergmann.
....
Time: 00:00
OK (4 tests)
然而,这些都是基本功能测试。我们必须创建更多样化的测试,并找出我们的对象可能失败的方式。让我们添加两个更多的测试,如下所示:
public function testInvalidUpdate()
{
$data = array("name"=>"afif","pass"=>md5("hello world"));
mysql_query("truncate table users");
$this->Db->insertData($data);
$data = array("name"=>"afif2","pass"=>md5("hello world"));
$result = $this->Db->updateData(2, $data);
$affected_rows = mysql_affected_rows($this->connection);
$this->assertEquals(0, $affected_rows);
}
public function testInvalidDelete()
{
$data = array("name"=>"afif","pass"=>md5("hello world"));
mysql_query("truncate table users");
$this->Db->insertData($data);
$result = $this->Db->deleteData("*");
$this->assertNotNull($result);
$affected_rows = mysql_affected_rows($this->connection);
$this->assertEquals(-1, $affected_rows);
}
现在如果你运行测试套件,你会得到以下结果:
PHPUnit 3.0.5 by Sebastian Bergmann.
......
Time: 00:00
OK (6 tests)
我们的数据库代码看起来很难破坏。
在现实生活中的单元测试中,你需要考虑如何破坏你自己的代码。如果你能编写破坏现有代码的单元测试,那就更好了。
测试驱动开发
现在是时候进一步学习单元测试了。你可能想知道在为应用程序编码之前何时需要编写单元测试:是在开发期间,还是编码完成后?嗯,来自不同领域的开发者有不同的看法,然而发现先编写测试然后进行实际应用更为有用。这被称为测试驱动开发或简称TDD。TDD 可以帮助你为应用程序设计更好的 API。
你可能会问在没有实际代码的情况下如何编写测试,以及要测试哪些内容?你不需要真实对象进行 TDD。只需想象一些模拟对象,它们只具有函数。你将使用这些函数与想象的结果。你也可以编写不完整的测试,这意味着一个空体的测试。在你方便的时候,你可以编写测试的内容。让我们看看以下示例,了解在实际代码编写之前的单元测试是如何适合项目开发的。
PHPUnit 为你提供了许多用于测试优先编程的有用 API,例如markTestSkipped()和markTestIncomplete()。我们将使用这两个方法来标记一些尚未实现的自定义测试。让我们设计一个小型反馈管理器,它可以接受用户的反馈并将邮件发送给你。那么反馈管理器有哪些有用的功能呢?我建议以下功能:
-
它可以生成一个反馈表单。
-
它将处理用户的输入并正确过滤它。
-
它将具有防垃圾邮件功能。
-
它将防止任何由机器人或垃圾邮件发送者提交的自动化反馈。
-
在提交反馈后,它会生成一个确认,并将邮件发送给所有者。
让我们为这个创建一些空白单元测试。以下是我们的测试用例,在我们有实际代码之前:
<?
class FeedbackTester extends PHPUnit_Framework_TestCase
{
public function testUsersEmail()
{
$this->markTestIncomplete();
}
public function testInvalidDomain()
{
$this->markTestIncomplete();
}
public function testCaptchaGenerator()
{
$this->markTestIncomplete();
}
public function testCaptchaChecker()
{
$this->markTestIncomplete();
}
public function testFormRenderer()
{
$this->markTestIncomplete();
}
public function testFormHandler()
{
$this->markTestIncomplete();
}
public function testValidUserName()
{
$this->markTestIncomplete();
}
public function testValidSubject()
{
$this->markTestIncomplete();
}
public function testValidContent()
{
$this->markTestIncomplete();
}
public function testFeedbackSender()
{
$this->markTestIncomplete();
}
public function testConfirmer()
{
$this->markTestIncomplete();
}
}
?>
这很好;我们现在已经创建了 11 个空白测试。现在如果你使用测试套件运行这个测试用例,你会得到以下结果:
PHPUnit 3.0.5 by Sebastian Bergmann.
IIIIIIIIIII
Time: 00:00
OK, but incomplete or skipped tests!
Tests: 11, Incomplete: 11.
PHPUnit 成功识别出我们所有的测试都被标记为不完整。现在让我们再思考一下。如果你生成一个 InputValidator 对象,它验证用户输入并过滤掉所有恶意数据,那么我们可能只有一个测试用例,即 testValidInput(),而不是所有的这些 testValidUserName()、testValidSubject()、testValidContent()。因此,我们可以跳过这些测试。现在让我们创建新的测试例程 testValidInput() 并将其标记为不完整:
public function testValidInput()
{
$this->markTestIncomplete();
}
对于我们计划跳过的三个测试,我们将如何处理?我们不会删除它们,但会将它们标记为跳过。将 $this->markTestIncomplete() 行修改为 $this->markTestSkipped()。例如:
public function testValidUserName()
{
$this->markTestSkipped();
}
现在如果你再次运行你的测试套件,你会得到以下结果:
PHPUnit 3.0.5 by Sebastian Bergmann.
IIIIIISSSIII
Time: 00:00
OK, but incomplete or skipped tests!
Tests: 12, Incomplete: 9, Skipped: 3.
PHPUnit 显示它跳过了三个测试。
为了使我们的讨论简短并集中,我们现在将只实现这九个测试中的一个。我们将测试反馈表单渲染器是否真正工作正常。
现在我们来看一下我们测试用例中修改后的测试例程 testFormRenderer()。
public function testFormRenderer(){
$testResult = true;
$message = "";
$Fm= new FeedbackManager();
ob_start();
$Fm->renderFeedbackForm();
$output = ob_get_clean();
if (strpos($output, "name='email'")===false && $testResult==true)
list($testResult, $message) = array(false,
"Email field is not present");
if (strpos($output, "name='username'")===false &&
$testResult==true)
list($testResult, $message) = array(false,
"Username is field not present");
if (strpos($output, "name='subject'")===false && $testResult==true)
list($testResult, $message) = array(false,
"Subject field is not present");
if (strpos($output, "name='message'")===false && $testResult==true)
list($testResult, $message) = array(false,
"Message field is not present");
$this->assertTrue($testResult, $message);
//$this->markTestIncomplete();
}
它清楚地表明,在我们的反馈管理器中必须有一个名为 renderFeedbackForm() 的方法,并且在生成的输出中必须有四个输入字段,即 email、subject、username 和 message。现在让我们创建我们的 FeedBackManager 对象。以下是具有单个渲染反馈表单方法的 FeedBackManager:
class FeedBackManager
<?
{
public function renderFeedbackForm()
{
$form = <<< END
<form method=POST action="">
Name: <br/>
<input type='text' name='username'><br/>
Email: <br/>
<input type='text' name='email'><br/>
Subject: <br/>
<input type='text' name='subject'><br/>
<input type='submit' value='submit>
</form>
END;
echo $form;
}
}
?>
现在如果你运行单元测试套件,你会得到以下结果:
PHPUnit 3.0.5 by Sebastian Bergmann.
IIIIFISSSIII
Time: 00:00
There was 1 failure:
1) testFormRenderer(FeedbackTester)
Message field is not present
Failed asserting that <boolean:false> is identical to <boolean:true>.
C:\OOP with PHP5\Codes\ch5\UnitTest\BlankTest.php:52
C:\OOP with PHP5\Codes\ch5\UnitTest\BlankTest.php:104
C:\Program Files\Zend\ZendStudio-5.2.0\bin\php5\dummy.php:1
FAILURES!
Tests: 12, Failures: 1, Incomplete: 8, Skipped: 3.
我们的表单渲染器失败了。为什么?看看 PHPUnit 输出的结果。它说 Message field is not present。哦!我们忘记放置一个名为 message 的 textarea 对象了。让我们修改我们的 renderFeedbackForm() 方法并纠正它。
class FeedBackManager
{
public function renderFeedbackForm()
{
$form = <<< END
<form method=POST action="">
Name: <br/>
<input type='text' name='username'><br/>
Email: <br/>
<input type='text' name='email'><br/>
Subject: <br/>
<input type='text' name='subject'><br/>
Message: <br/>
<textarea name='message'></textarea><br/>
<input type='submit' value='submit>
</form>
END;
echo $form;
}
}
我们已经添加了消息字段。现在让我们再次运行套件。你会得到以下输出:
PHPUnit 3.0.5 by Sebastian Bergmann.
IIII.ISSSIII
Time: 00:00
OK, but incomplete or skipped tests!
Tests: 12, Incomplete: 8, Skipped: 3.
太好了!我们的测试通过了。这意味着我们的渲染表单可能没有错误。
这是测试驱动开发(TDD)的风格。在实际编写代码之前,你必须预见你的应用程序代码。使用 TDD 可以帮助你设计良好的 API 和良好的代码。
编写多个断言
不要在一个测试下写多个断言。按照上面的示例进行拆分。为了澄清,以下示例是一个单元测试的糟糕例子。
public function testFormRenderer(){
$testResult = true;
$message = "";
$Fm = new FeedBackManager();
ob_start();
$Fm->renderFeedbackForm();
$output = ob_get_clean();
$testResult = strpos($output, "name='email'");
$this->assertEquals(true, $testResult,
"Email field is not present");
$testResult = strpos($output, "name='username'");
$this->assertEquals(true, $testResult,
"Username field is not present");
$testResult = strpos($output, "name='subject'");
$this->assertEquals(true, $testResult,
"Subject field is not present");
$testResult = strpos($output, "name='message'");
$this->assertEquals(true, $testResult,
"Message field is not present");
}
这段代码将会运行,但在单个例程中多个断言是被禁止的,并且违反了良好的应用程序设计。
PHPUnit API
PHPUnit 提供了几种断言 API。在我们的示例中,我们使用了 assertTrue()、assertEquals()、assertFalse() 和 assertNotNull() 等函数。然而,还有更多。函数名是自我解释的。以下表格取自 Sebastian Bergmann 本人撰写的《PHPUnit 口袋指南》一书,由 O'Reilly 出版。这本书在 Creative Commons 许可下免费提供。这本书的最新版本目前可在www.phpunit.de/pocket_guide/3.0/en/index.html找到。
以下表格显示了 PHPUnit 所有可能的断言函数:
| 断言 | 含义 |
|---|---|
void assertTrue(bool $condition) | 如果 $condition 是 FALSE,则报告一个错误。 |
|
void assertTrue(bool $condition, string $message) |
如果 $condition 是 FALSE,则通过 $message 指定的错误信息报告一个错误。 |
void assertFalse(bool $condition) | 如果 $condition 是 TRUE,则报告一个错误。 |
|
void assertFalse(bool $condition, string $message) |
如果 $condition 是 TRUE,则通过 $message 指定的错误信息报告一个错误。 |
void assertNull(mixed $variable) | 如果 $variable 不是 NULL,则报告一个错误。 |
|
void assertNull(mixed $variable, string $message) |
如果 $variable 不是 NULL,则通过 $message 指定的错误信息报告一个错误。 |
void assertNotNull(mixed $variable) | 如果 $variable 是 NULL,则报告一个错误。 |
|
void assertNotNull(mixed $variable, string $message) |
如果 $variable 是 NULL,则通过 $message 指定的错误信息报告一个错误。 |
void assertSame(object $expected, object $actual) |
如果两个变量 $expected 和 $actual 不引用同一个对象,则报告一个错误。 |
void assertSame(object $expected, object $actual, string $message) | 如果两个变量 $expected 和 $actual 不引用同一个对象,则通过 $message 指定的错误信息报告一个错误。 |
|
void assertSame(mixed $expected, mixed $actual) |
如果两个变量 $expected 和 $actual 不具有相同的类型和值,则报告一个错误。 |
void assertSame(mixed $expected, mixed $actual, string $message) | 如果两个变量 $expected 和 $actual 不具有相同的类型和值,则通过 $message 指定的错误信息报告一个错误。 |
|
void assertNotSame(object $expected, object $actual) |
如果两个变量 $expected 和 $actual 引用了同一个对象,则报告一个错误。 |
void assertNotSame(object $expected, object $actual, string $message) | 如果两个变量 $expected 和 $actual 引用了同一个对象,则通过 $message 指定的错误信息报告一个错误。 |
|
void assertNotSame(mixed $expected, mixed $actual) |
如果两个变量 $expected 和 $actual 具有相同的类型和值,则报告一个错误。 |
void assertNotSame(mixed $expected, mixed $actual, string $message) | 报告一个错误,如果两个变量 $expected 和 $actual 具有相同的类型和值,错误信息由 $message 指定。 |
|
void assertAttributeSame(object $expected, string $actualAttributeName, object $actualObject) | 如果 $actualObject->actualAttributeName 和 $actual 不引用同一个对象,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeSame(object $expected, string $actualAttributeName, object $actualObject, string $message) |
如果 $actualObject->actualAttributeName 和 $actual 不引用相同的对象,则报告由 $message 标识的错误。该 $actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeSame(mixed $expected, string $actualAttributeName, object $actualObject) | 如果 $actualObject->actualAttributeName 和 $actual 没有相同的类型和值,则报告一个错误。该 $actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeSame(mixed $expected, string $actualAttributeName, object $actualObject, string $message) |
如果 $actualObject->actualAttributeName 和 $actual 没有相同的类型和值,则报告由 $message 标识的错误。该 $actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeNotSame(object $expected, string $actualAttributeName, object $actualObject) | 如果 $actualObject->actualAttributeName 和 $actual 引用相同的对象,则报告一个错误。该 $actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeNotSame(object $expected, string $actualAttributeName, object $actualObject, string $message) |
如果 $actualObject->actualAttributeName 和 $actual 引用相同的对象,则报告由 $message 标识的错误。该 $actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeNotSame(mixed $expected, string $actualAttributeName, object $actualObject) | 如果 $actualObject->actualAttributeName 和 $actual 具有相同的类型和值,则报告一个错误。该 $actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeNotSame(mixed $expected, string $actualAttributeName, object $actualObject, string $message) |
如果 $actualObject->actualAttributeName 和 $actual 具有相同的类型和值,则报告由 $message 标识的错误。该 $actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertEquals(array $expected, array $actual) |
如果两个数组 $expected 和 $actual 不相等,则报告一个错误。 |
void assertEquals(array $expected, array $actual, string $message) | 报告一个错误,如果两个数组 $expected 和 $actual 不相等。 |
|
void assertNotEquals(array $expected, array $actual) |
如果两个数组 $expected 和 $actual 相等,则报告一个错误。 |
void assertNotEquals(array $expected, array $actual, string $message) | 报告一个错误,如果两个数组 $expected 和 $actual 相等,错误由 $message 标识。 |
|
void assertEquals(float $expected, float $actual, '', float $delta = 0) | 报告一个错误,如果两个浮点数 $expected 和 $actual 不在 $delta 范围内。 |
|
void assertEquals(float $expected, float $actual, string $message, float $delta = 0) |
报告一个错误,如果两个浮点数 $expected 和 $actual 不在 $delta 范围内,错误由 $message 标识。 |
void assertNotEquals(float $expected, float $actual, '', float $delta = 0) | 报告一个错误,如果两个浮点数 $expected 和 $actual 在 $delta 范围内。 |
|
void assertNotEquals(float $expected, float $actual, string $message, float $delta = 0) |
报告一个错误,如果两个浮点数 $expected 和 $actual 在 $delta 范围内,错误由 $message 标识。 |
void assertEquals(string $expected, string $actual) |
报告一个错误,如果两个字符串 $expected 和 $actual 不相等。错误报告为两个字符串之间的差值。 |
void assertEquals(string $expected, string $actual, string $message) | 报告一个错误,如果两个字符串 $expected 和 $actual 不相等,错误由 $message 标识。错误报告为两个字符串之间的差值。 |
|
void assertNotEquals(string $expected, string $actual) |
报告一个错误,如果两个字符串 $expected 和 $actual 相等。 |
void assertNotEquals(string $expected, string $actual, string $message) | 报告一个错误,如果两个字符串 $expected 和 $actual 相等,错误由 $message 标识。 |
|
void assertEquals(mixed $expected, mixed $actual) |
报告一个错误,如果两个变量 $expected 和 $actual 不相等。 |
void assertEquals(mixed $expected, mixed $actual, string $message) | 报告一个错误,如果两个变量 $expected 和 $actual 不相等,错误由 $message 标识。 |
|
void assertNotEquals(mixed $expected, mixed $actual) |
报告一个错误,如果两个变量 $expected 和 $actual 相等。 |
void assertNotEquals(mixed $expected, mixed $actual, string $message) | 报告一个错误,如果两个变量 $expected 和 $actual 相等,错误由 $message 标识。 |
|
void assertAttributeEquals(array $expected, string $actualAttributeName, object $actualObject) | 报告一个错误,如果两个数组 $expected 和 $actualObject->actualAttributeName 不相等。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeEquals(array $expected, string $actualAttributeName, object $actualObject, string $message) |
如果两个数组 $expected 和 $actualObject->actualAttributeName 不相等,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeNotEquals(array $expected, string $actualAttributeName, object $actualObject) | 如果两个数组 $expected 和 $actualObject->actualAttributeName 相等,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeNotEquals(array $expected, string $actualAttributeName, object $actualObject, string $message) |
如果两个数组 $expected 和 $actualObject->actualAttributeName 相等,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeEquals(float $expected, string $actualAttributeName, object $actualObject, '', float $delta = 0) |
如果两个浮点数 $expected 和 $actualObject->actualAttributeName 不在 $delta 范围内,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeEquals(float $expected, string $actualAttributeName, object $actualObject, string $message, float $delta = 0) | 报告一个错误,如果两个浮点数 $expected 和 $actualObject->actualAttributeName 不在 $delta 范围内。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeNotEquals(float $expected, string $actualAttributeName, object $actualObject, '', float $delta = 0) |
如果两个浮点数 $expected 和 $actualObject->actualAttributeName 在 $delta 范围内,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeNotEquals(float $expected, string $actualAttributeName, object $actualObject, string $message, float $delta = 0) | 如果两个浮点数 $expected 和 $actualObject->actualAttributeName 在 $delta 范围内,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeEquals(string $expected, string $actualAttributeName, object $actualObject) | 如果两个字符串 $expected 和 $actualObject->actualAttributeName 不相等,则报告一个错误。错误报告为两个字符串之间的差异。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeEquals(string $expected, string $actualAttributeName, object $actualObject, string $message) |
报告一个错误,如果两个字符串 $expected 和 $actualObject->actualAttributeName 不相等。错误报告为两个字符串之间的差异。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeNotEquals(string $expected, string $actualAttributeName, object $actualObject) | 如果两个字符串 $expected 和 $actualObject->actualAttributeName 相等,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeNotEquals(string $expected, string $actualAttributeName, object $actualObject, string $message) |
报告一个错误,如果两个字符串 $expected 和 $actualObject->actualAttributeName 相等。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeEquals(mixed $expected, string $actualAttributeName, object $actualObject) | 如果两个变量 $expected 和 $actualObject->actualAttributeName 不相等,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeEquals(mixed $expected, string $actualAttributeName, object $actualObject, string $message) |
报告一个错误,如果两个变量 $expected 和 $actualObject->actualAttributeName 不相等。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeNotEquals(mixed $expected, string $actualAttributeName, object $actualObject) | 如果两个变量 $expected 和 $actualObject->actualAttributeName 相等,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeNotEquals(mixed $expected, string $actualAttributeName, object $actualObject, string $message) |
报告一个错误,如果两个变量 $expected 和 $actualObject->actualAttributeName 相等。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertContains(mixed $needle, array $expected) |
如果$needle不是$expected的元素,则报告一个错误。 |
void assertContains(mixed $needle, array $expected, string $message) | 如果$needle不是$expected的元素,则报告一个由$message标识的错误。 |
|
void assertNotContains(mixed $needle, array $expected) |
如果$needle是$expected的元素,则报告一个错误。 |
void assertNotContains(mixed $needle, array $expected, string $message) | 报告一个错误,如果$needle是$expected数组的一个元素。 |
|
void assertContains(mixed $needle, Iterator $expected) |
如果$needle不是$expected的元素,则报告一个错误。 |
void assertContains(mixed $needle, Iterator $expected, string $message) | 如果$needle不是$expected的元素,则报告一个由$message标识的错误。 |
|
void assertNotContains(mixed $needle, Iterator $expected) |
如果$needle是$expected的元素,则报告一个错误。 |
void assertNotContains(mixed $needle, Iterator $expected, string $message) | 如果$needle是$expected的元素,则报告一个由$message标识的错误。 |
|
void assertContains(string $needle, string $expected) |
如果$needle不是$expected的子串,则报告一个错误。 |
void assertContains(string $needle, string $expected, string $message) | 如果$needle不是$expected的子串,则报告一个由$message标识的错误。 |
|
void assertNotContains(string $needle, string $expected) |
如果$needle是$expected的子串,则报告一个错误。 |
void assertNotContains(string $needle, string $expected, string $message) | 如果$needle是$expected的子串,则报告一个由$message标识的错误。 |
|
void assertAttributeContains(mixed $needle, string $actualAttributeName, object $actualObject) | 如果$needle不是$actualObject->actualAttributeName的元素,该元素可以是数组、字符串或实现 Iterator 接口的对象,则报告一个错误。$actualObject->actualAttributeName属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeContains(mixed $needle, string $actualAttributeName, object $actualObject, string $message) |
报告一个由$message标识的错误,如果$needle不是$actualObject->actualAttributeName的元素,该元素可以是数组、字符串或实现 Iterator 接口的对象。$actualObject->actualAttributeName属性的可见性可能是公共的、受保护的或私有的。 |
void assertAttributeNotContains(mixed $needle, string $actualAttributeName, object $actualObject) | 如果 $needle 是 $actualObject->actualAttributeName 的一个元素,该元素可以是数组、字符串或实现 Iterator 接口的对象,则报告一个错误。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
|
void assertAttributeNotContains(mixed $needle, string $actualAttributeName, object $actualObject, string $message) |
如果 $needle 是 $actualObject->actualAttributeName 的一个元素,该元素可以是数组、字符串或实现 Iterator 接口的对象,则报告一个错误,错误信息由 $message 指定。$actualObject->actualAttributeName 属性的可见性可能是公共的、受保护的或私有的。 |
void assertRegExp(string $pattern, string $string) |
如果 $string 不匹配正则表达式 $pattern,则报告一个错误。 |
void assertRegExp(string $pattern, string $string, string $message) | 如果 $string 不匹配正则表达式 $pattern,则报告一个错误,错误信息由 $message 指定。 |
|
void assertNotRegExp(string $pattern, string $string) |
如果 $string 匹配正则表达式 $pattern,则报告一个错误。 |
void assertNotRegExp(string $pattern, string $string, string $message) | 如果 $string 匹配正则表达式 $pattern,则报告一个错误,错误信息由 $message 指定。 |
|
void assertType(string $expected, mixed $actual) |
如果变量 $actual 不是类型 $expected,则报告一个错误。 |
void assertType(string $expected, mixed $actual, string $message) | 如果变量 $actual 不是类型 $expected,则报告一个错误,错误信息由 $message 指定。 |
|
void assertNotType(string $expected, mixed $actual) |
如果变量 $actual 是类型 $expected,则报告一个错误。 |
void assertNotType(string $expected, mixed $actual, string $message) | 如果变量 $actual 是类型 $expected,则报告一个错误,错误信息由 $message 指定。 |
|
void assertFileExists(string $filename) | 如果指定的文件 $filename 不存在,则报告一个错误。 |
|
void assertFileExists(string $filename, string $message) |
报告一个错误,如果指定的文件 $filename 不存在,错误信息由 $message 指定。 |
void assertFileNotExists(string $filename) | 如果指定的文件 $filename 存在,则报告一个错误。 |
|
void assertFileNotExists(string $filename, string $message) |
如果指定的文件 $filename 存在,则报告一个错误,错误信息由 $message 指定。 |
void assertObjectHasAttribute(string $attributeName, object $object) |
如果 $object->attributeName 不存在,则报告一个错误。 |
void assertObjectHasAttribute(string $attributeName, object $object, string $message) | 如果$object->attributeName不存在,则通过$message报告错误。 |
|
void assertObjectNotHasAttribute(string $attributeName, object $object) |
如果$object->attributeName存在,则报告错误。 |
void assertObjectNotHasAttribute(string $attributeName, object $object, string $message) | 如果$object->attributeName存在,则报告错误。 |
摘要
本章重点介绍 PHP 面向对象编程的两个非常重要的特性。一个是反射,它是所有主要编程语言(如 Java、Ruby 和 Python)的一部分。另一个是单元测试,它是良好、稳定和可管理应用程序设计的重要组成部分。我们关注了一个非常流行的包,它是 JUnit 在 PHP 中的移植,名为 PHPUnit。如果你遵循本章提供的指南,你将能够成功设计你的单元测试。
在下一章中,我们将学习 PHP 中的一些内置对象,这将使你的生活比平时更容易。我们将学习一个名为标准 PHP 库或 SPL 的庞大对象存储库。在此之前,通过编写自己的单元测试来享受调试的乐趣。
第六章. 标准 PHP 库
PHP5 通过引入许多内置对象,使开发者的生活比以前容易得多。标准 PHP 库(SPL)是一组 PHP 5 中引入的对象,用于 PHP 开发者。它们附带了许多接口和对象,以简化您的编码。在本章中,我们将介绍其中的一些,并展示它们的用法。
SPL 中的可用对象
您可以通过执行以下代码来找出 SPL 中可用的对象。
<?php
// a simple foreach() to traverse the SPL class names
foreach(spl_classes() as $key=>$value)
{
echo $value."\n";
}
?>
结果将显示您当前 PHP 安装中所有可用的类:
AppendIterator
ArrayIterator
ArrayObject
BadFunctionCallException
BadMethodCallException
CachingIterator
Countable
DirectoryIterator
DomainException
EmptyIterator
FilterIterator
InfiniteIterator
InvalidArgumentException
IteratorIterator
LengthException
LimitIterator
LogicException
NoRewindIterator
OuterIterator
OutOfBoundsException
OutOfRangeException
OverflowException
ParentIterator
RangeException
RecursiveArrayIterator
RecursiveCachingIterator
RecursiveDirectoryIterator
RecursiveFilterIterator
RecursiveIterator
RecursiveIteratorIterator
RuntimeException
SeekableIterator
SimpleXMLIterator
SplFileObject
SplObjectStorage
SplObserver
SplSubject
UnderflowException
UnexpectedValueException
ArrayObject
这是一个 SPL 中引入的非常棒的对象,用于简化数组操作并丰富常规 PHP 数组的功能。您可以将 ArrayObject 作为简单的数组使用,但内部可以逐步增强它并添加新的功能。在本节中,我们将看到这个对象支持的属性和方法。此外,我们还将设计一个增强的 ArrayObject 以便于数组访问。
这里列出了这个类的公共成员:
-
__construct($array,$flags=0,$iterator_class="ArrayIterator") -
append($value) -
asort() -
count() -
exchangeArray($array) -
getArrayCopy() -
getFlags() -
getIterator() -
getIteratorClass() -
ksort() -
natcasesort() -
natsort() -
offsetExists($index) -
offsetGet($index) -
offsetSet($index,$newval) -
offsetUnset($index) -
setFlags($flags) -
setIteratorClass($itertor_class) -
uasort(mixedcmp_function) -
uksort(mixedcmp_function)
许多这些函数也适用于数组操作。以下是一些函数的简要介绍,它们与数组函数不同:
| 函数 | 功能 |
|---|---|
exchangeArray($array) |
此函数用新的数组替换 ArrayObject 的内部数组,并返回旧的数组。 |
getArrayCopy() |
此函数返回 ArrayObject 内部数组的副本。 |
getIteratorClass() |
此函数返回 Iterator 类的名称。如果您没有为该对象显式设置任何其他 Iterator 类,您将始终得到 ArrayIterator 作为结果。 |
setIteratorClass() |
使用此函数,您可以设置任何 Iterator 类作为数组对象的迭代器。然而,有一个限制;这个 Iterator 类必须扩展 arrayIterator 类。 |
setFlags() |
此函数将一些位运算标志设置到 ArrayObject 中。标志是 0 或 1。0 表示当作为列表访问(var_dump、foreach 等)时,对象的属性具有其正常功能,而 1 表示可以以属性的形式读写数组索引。 |
在下面有趣的例子中,我们扩展了ArrayObject,创建了一个更灵活的ExtendedArrayObject以实现类似原型的功能。扩展数组提供了通过集合进行遍历的简便性。让我们看看:
<?
class ExtendedArrayObject extends ArrayObject {
private $_array;
public function __construct()
{
if (is_array(func_get_arg(0)))
$this->_array = func_get_arg(0);
else
$this->_array = func_get_args();
parent::__construct($this->_array);
}
public function each($callback)
{
$iterator = $this->getIterator();
while($iterator->valid())
{
$callback($iterator->current());
$iterator->next();
}
}
public function without()
{
$args = func_get_args();
return array_values(array_diff($this->_array,$args));
}
public function first()
{
return $this->_array[0];
}
public function indexOf($value)
{
return array_search($value,$this->_array);
}
public function inspect()
{
echo "<pre>".print_r($this->_array, true)."</pre>";
}
public function last()
{
return $this->_array[count($this->_array)-1];
}
public function reverse($applyToSelf=false)
{
if (!$applyToSelf)
return array_reverse($this->_array);
else
{
$_array = array_reverse($this->_array);
$this->_array = $_array;
parent::__construct($this->_array);
return $this->_array;
}
}
public function shift()
{
$_element = array_shift($this->_array);
parent::__construct($this->_array);
return $_element;
}
public function pop()
{
$_element = array_pop($this->_array);
parent::__construct($this->_array);
return $_element;
}
}
?>
如果你想看看如何使用它,这里就是:
<?
include_once("ExtendedArrayObject.class.php");
function speak($value)
{
echo $value;
}
$newArray = new ExtendedArrayObject(array(1,2,3,4,5,6));
/* or you can use this */
$newArray = new ExtendedArrayObject(1,2,3,4,5,6);
$newArray->each(speak); //pass callback for loop
print_r($newArray->without(2,3,4)); //subtract
$newArray->inspect(); //display the array in a nice manner
echo $newArray->indexOf(5); //position by value
print_r($newArray->reverse()); //reverse the array
print_r($newArray->reverse(true)); /*for changing array itself*/
echo $newArray->shift();//shifts the first value of the array
//and returns it
echo $newArray->pop();// pops out the last value of array
echo $newArray->last();
echo $newArray->first(); //the first element
?>
结果看起来像这样:
123456
Array
(
[0] => 1
[1] => 5
[2] => 6
)
Array
(
[0] => 1
[1] => 2
[2] => 3
[3] => 4
[4] => 5
[5] => 6
)
4
Array
(
[0] => 6
[1] => 5
[2] => 4
[3] => 3
[4] => 2
[5] => 1
)
Array
(
[0] => 6
[1] => 5
[2] => 4
[3] => 3
[4] => 2
[5] => 1
)
6125
ArrayIterator
ArrayIterator用于遍历数组的元素。在 SPL 中,ArrayObject有一个内置的迭代器,你可以使用getIterator函数访问它。你可以使用这个对象遍历任何集合。让我们看看下面的例子:
<?php
$fruits = array(
"apple" => "yummy",
"orange" => "ah ya, nice",
"grape" => "wow, I love it!",
"plum" => "nah, not me"
);
$obj = new ArrayObject( $fruits );
$it = $obj->getIterator();
// How many items are we iterating over?
echo "Iterating over: " . $obj->count() . " values\n";
// Iterate over the values in the ArrayObject:
while( $it->valid() )
{
echo $it->key() . "=" . $it->current() . "\n";
$it->next();
}
?>
这将输出以下内容:
Iterating over: 4 values
apple=yummy
orange=ah ya, nice
grape=wow, I love it!
plum=nah, not me
然而,迭代器还实现了IteratorAggregator接口,因此你甚至可以在foreach()循环中使用它们。
<?php
$fruits = array(
"apple" => "yummy",
"orange" => "ah ya, nice",
"grape" => "wow, I love it!",
"plum" => "nah, not me"
);
$obj = new ArrayObject( $fruits );
$it = $obj->getIterator();
// How many items are we iterating over?
echo "Iterating over: " . $obj->count() . " values\n";
// Iterate over the values in the ArrayObject:
foreach ($it as $key=>$val)
echo $key.":".$val."\n";
?>
你将得到与上一个相同的输出。
如果你想要在自己的集合中实现迭代器,我建议你查看第三章。如果你想了解如何实现IteratorAggregator,这里有一个例子供你参考:
<?php
class MyArray implements IteratorAggregate
{
private $arr;
public function __construct()
{
$this->arr = array();
}
public function add( $key, $value )
{
if( $this->check( $key, $value ) )
{
$this->arr[$key] = $value;
}
}
private function check( $key, $value )
{
if( $key == $value )
{
return false;
}
return true;
}
public function getIterator()
{
return new ArrayIterator( $this->arr );
}
}
?>
请注意,如果在迭代过程中键和值相同,则不会返回该值。你可以这样使用它:
<?php
$obj = new MyArray();
$obj->add( "redhat","www.redhat.com" );
$obj->add( "php", "php" );
$it = $obj->getIterator();
while( $it->valid() )
{
echo $it->key() . "=" . $it->current() . "\n";
$it->next();
}
?>
输出如下:
redhat=www.redhat.com
DirectoryIterator
PHP5 中引入的另一个非常有趣的类是DirectoryIterator。这个对象可以遍历目录中存在的项目(好吧,那些只是文件),你可以使用这个对象检索该文件的不同属性。
在 PHP 手册中,这个对象没有很好地记录。所以如果你想了解这个对象的结构以及支持的方法和属性,你可以使用ReflectionClass来查看。还记得我们在上一章中使用的ReflectionClass吗?让我们看看以下例子:
<?php
ReflectionClass::export(DirectoryIterator);
?>
结果是:
Class [ <internal:SPL> <iterateable> class DirectoryIterator
implements Iterator, Traversable ]
{
- Constants [0] { }
- Static properties [0] { }
- Static methods [0] { }
- Properties [0] { }
- Methods [27]
{
Method [ <internal> <ctor> public method __construct ]
{
- Parameters [1]
{
Parameter #0 [ <required> $path ]
}
}
Method [ <internal> public method rewind ] { }
Method [ <internal> public method valid ] { }
Method [ <internal> public method key ] { }
Method [ <internal> public method current ] { }
Method [ <internal> public method next ] { }
Method [ <internal> public method getPath ] { }
Method [ <internal> public method getFilename ] { }
Method [ <internal> public method getPathname ] { }
Method [ <internal> public method getPerms ] { }
Method [ <internal> public method getInode ] { }
Method [ <internal> public method getSize ] { }
Method [ <internal> public method getOwner ] { }
Method [ <internal> public method getGroup ] { }
Method [ <internal> public method getATime ] { }
Method [ <internal> public method getMTime ] { }
Method [ <internal> public method getCTime ] { }
Method [ <internal> public method getType ] { }
Method [ <internal> public method isWritable ] { }
Method [ <internal> public method isReadable ] { }
Method [ <internal> public method isExecutable ] { }
Method [ <internal> public method isFile ] { }
Method [ <internal> public method isDir ] { }
Method [ <internal> public method isLink ] { }
Method [ <internal> public method isDot ] { }
Method [ <internal> public method openFile ]
{
- Parameters [3] {
Parameter #0 [ <optional> $open_mode ]
Parameter #1 [ <optional> $use_include_path ]
Parameter #2 [ <optional> $context ]
}
}
Method [ <internal> public method __toString ] { }
}
}
这里有一些有用的方法。让我们充分利用它们。在下面的例子中,我们将创建一个目录爬虫,它会显示特定驱动器中的所有文件和目录。看看我的 C 盘上的一个名为spket的目录:

现在,如果你运行以下代码,你将得到其中文件和目录的列表:
<?
$DI = new DirectoryIterator("c:/spket");
foreach ($DI as $file) {
echo $file."\n";
}
?>
输出如下:
.
..
plugins
features
readme
.eclipseproduct
epl-v10.html
notice.html
startup.jar
configuration
spket.exe
spket.ini
但是输出没有任何意义。你能检测出哪些是目录,哪些是文件吗?这非常困难,所以让我们使结果对我们有用。
<?
$DI = new DirectoryIterator("c:/spket");
$directories = array();
$files = array();
foreach ($DI as $file) {
$filename = $file->getFilename();
if ($file->isDir()){
if(strpos($filename,".")===false)
$directories[] = $filename;
}
else
$files[] = $filename;
}
echo "Directories\n";
print_r($directories);
echo "\nFiles\n";
print_r($files);
?>
输出如下:
Directories
Array
(
[1] => plugins
[2] => features
[3] => readme
[4] => configuration
)
Files
Array
(
[0] => .eclipseproduct
[1] => epl-v10.html
[2] => notice.html
[3] => startup.jar
[4] => spket.exe
[5] => spket.ini
)
你可能会问,如果有一个快捷链接,你怎么能检测它。简单,只需使用$file->isLink()函数来检测该文件是否是快捷方式。
让我们来看看DirectoryIterator对象的其他有用方法:
| 方法 | 功能 |
|---|---|
getPathname() |
返回此文件的绝对路径名(包含文件名)。 |
getSize() |
返回文件的字节数。 |
getOwner() |
返回所有者 ID。 |
getATime() |
返回以时间戳表示的最后访问时间。 |
getMTime() |
返回以时间戳表示的修改时间。 |
getCTime() |
返回以时间戳表示的创建时间。 |
getType() |
返回 "file"、"dir" 或 "link"。 |
其他方法相当直观,所以我们在这里不讨论它们。然而,还有一件事需要记住,那就是在 win32 机器上,getInode()、getOwner() 和 getGroup() 将返回 0。
RecursiveDirectoryIterator
那么,这个对象是什么?还记得我们之前的例子吗?我们只得到了目录和文件列表。然而,如果我们想在不实现递归的情况下获取该目录内所有目录的列表,该怎么办?那么 RecursiveDirectoryIterator 就在这里救你一命。
递归目录迭代器可以与 RecursiveIeratorIterator 结合使用,以实现递归。让我们看看以下示例,它遍历目录下的所有目录(无论嵌套多深):
<?php
// Create the new iterator:
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
'c:/spket' ));
foreach( $it as $key=>$file )
{
echo $key."=>".$file."\n";
}
?>
输出如下所示:
c:/spket/epl-v10.html=>epl-v10.html
c:/spket/notice.html=>notice.html
c:/spket/startup.jar=>startup.jar
c:/spket/configuration/config.ini=>config.ini
c:/spket/configuration/org.eclipse.osgi/.manager/
.fileTableLock=>.fileTableLock
c:/spket/configuration/org.eclipse.osgi/.manager/
.fileTable.4=>.fileTable.4
c:/spket/configuration/org.eclipse.osgi/.manager/
.fileTable.5=>.fileTable.5
c:/spket/configuration/org.eclipse.osgi/bundles/4/1/.cp/
swt-win32-3236.dll=>swt-win32-3236.dll
c:/spket/configuration/org.eclipse.osgi/bundles/4/1/.cp/
swt-gdip-win32-3236.dll=>swt-gdip-win32-3236.dll
c:/spket/configuration/org.eclipse.osgi/bundles/48/1/.cp/os/win32/
x86/localfile_1_0_0.dll=>localfile_1_0_0.dll
c:/spket/configuration/org.eclipse.osgi/bundles/69/1/.cp/os/win32/
x86/monitor.dll=>monitor.dll
c:/spket/spket.exe=>spket.exe
c:/spket/spket.ini=>spket.ini
………
我能听到你在问自己:“为什么这些无用的文件会打印在这里?”只需看看目录结构,看看它是如何以路径作为键检索整个文件名的。
RecursiveIteratorIterator
要递归地遍历一个集合,你可以利用 SPL 中引入的这个对象。让我们看看以下示例,了解它在日常编程中如何有效使用。在前面的章节和即将到来的章节中,我们看到许多使用 RecursiveIteratorIterator 的示例;因此,我们在这个章节中不再给出更多示例。
AppendIterator
如果你想要使用一系列迭代器进行迭代,那么这可能是你的救命稻草。此对象将所有迭代器存储在一个集合中,并一次性遍历它们。
让我们看看以下 append 迭代器的示例,其中我们遍历一系列迭代器,然后最小化代码:
<?
class Post
{
public $id;
public $title;
function __construct($title, $id)
{
$this->title = $title;
$this->id = $id;
}
}
class Comment{
public $content;
public $post_id;
function __construct($content, $post_id)
{
$this->content = $content;
$this->post_id = $post_id;
}
}
$posts = new ArrayObject();
$comments = new ArrayObject();
$posts->append(new post("Post 1",1));
$posts->append(new post("Post 2",2));
$comments->append(new Comment("comment 1",1));
$comments->append(new Comment("comment 2",1));
$comments->append(new Comment("comment 3",2));
$comments->append(new Comment("comment 4",2));
$a = new AppendIterator();
$a->append($posts->getIterator());
$a->append($comments->getIterator());
//print_r($a->getInnerIterator());
foreach ($a as $key=>$val)
{
if ($val instanceof post)
echo "title = {$val->title}\n";
else if ($val instanceof Comment )
echo "content = {$val->content}\n";
}
?>
接下来是输出:
title = Post 1
title = Post 2
content = comment 1
content = comment 2
content = comment 3
content = comment 4
FilterIterator
如其名所示,这个迭代器帮助你通过迭代过滤结果,以便你只得到所需的结果。这个迭代器在带有过滤的迭代中非常有用,
FilterIterator 在常规迭代器之上暴露了两个额外的方法。一个是 accept(),它在内部迭代每次调用,是你的过滤关键点。另一个是 getInnerIterator(),它返回当前 FilterIterator 内部的迭代器。
在这个例子中,我们使用 FilterIterator 在遍历集合时过滤数据。
<?php
class GenderFilter extends FilterIterator
{
private $GenderFilter;
public function __construct( Iterator $it, $gender="F" )
{
parent::__construct( $it );
$this->GenderFilter = $gender;
}
//your key point to implement filter
public function accept()
{
$person = $this->getInnerIterator()->current();
if( $person['sex'] == $this->GenderFilter )
{
return TRUE;
}
return FALSE;
}
}
$arr = array(
array("name"=>"John Abraham", "sex"=>"M", "age"=>27),
array("name"=>"Lily Bernard", "sex"=>"F", "age"=>37),
array("name"=>"Ayesha Siddika", "sex"=>"F", "age"=>26),
array("name"=>"Afif", "sex"=>"M", "age"=>2)
);
$persons = new ArrayObject( $arr );
$iterator = new GenderFilter( $persons->getIterator() );
foreach( $iterator as $person )
{
echo $person['name'] . "\n";
}
echo str_repeat("-",30)."\n";
$persons = new ArrayObject( $arr );
$iterator = new GenderFilter( $persons->getIterator() ,"M");
foreach( $iterator as $person )
{
echo $person['name'] . "\n";
}
?>
如果你运行代码,你将得到以下结果:
Lily Bernard
Ayesha Siddika
------------------------------
John Abraham
Afif
我相信你会同意这相当有趣,然而你抓住了关键吗?这是通过以下入口点过滤的:
public function accept()
{
$person = $this->getInnerIterator()->current();
if( $person['sex'] == $this->GenderFilter )
{
return TRUE;
}
return FALSE;
}
}
LimitIterator
如果你想定义迭代开始的位置以及你想迭代的次数,该怎么办?这可以通过 LimitIterator 实现。
LimitIterator在构造时接受三个参数。第一个是一个常规的 Iterator,第二个是起始偏移量,第三个是它将迭代的次数。看看以下示例:
<?
$arr = array(
array("name"=>"John Abraham", "sex"=>"M", "age"=>27),
array("name"=>"Lily Bernard", "sex"=>"F", "age"=>37),
array("name"=>"Ayesha Siddika", "sex"=>"F", "age"=>26),
array("name"=>"Afif", "sex"=>"M", "age"=>2)
);
$persons = new ArrayObject($arr);
$LI = new LimitIterator($persons->getIterator(),1,2);
foreach ($LI as $person) {
echo $person['name']."\n";
}
?>
输出如下:
Lily Bernard
Ayesha Siddika
NoRewindIterator
这是一个您不能调用rewind方法的另一个 Iterator。这意味着它是一个单向 Iterator,只能读取集合一次。看看结构;如果您执行以下代码,您将得到这个 Iterator 支持的方法:
<?
print_r(get_class_methods(NoRewindIterator));
//you can also use refelection API as before to see the methods.
?>
输出将是以下所示的方法:
Array
(
[0] => __construct
[1] => rewind
[2] => valid
[3] => key
[4] => current
[5] => next
[6] => getInnerIterator
)
令人惊讶的是,它没有 rewind 方法,但您可以看到它,不是吗?嗯,该方法没有实现,它是空的。它之所以存在,是因为它实现了 Iterator 接口,但没有实现该函数,所以您不能回滚。
<?
$arr = array(
array("name"=>"John Abraham", "sex"=>"M", "age"=>27),
array("name"=>"Lily Bernard", "sex"=>"F", "age"=>37),
array("name"=>"Ayesha Siddika", "sex"=>"F", "age"=>26),
array("name"=>"Afif", "sex"=>"M", "age"=>2)
);
$persons = new ArrayObject($arr);
$LI = new NoRewindIterator($persons->getIterator());
foreach ($LI as $person) {
echo $person['name']."\n";
$LI->rewind();
}
?>
如果rewind()方法工作,这段代码将是一个无限循环。但在实际中,它显示的输出如下:
John Abraham
Lily Bernard
Ayesha Siddika
Afif
SeekableIterator
这是一个在 SPL 中引入的接口,许多 Iterator 类实际上在内部实现。如果实现了这个接口,您可以在数组内部执行seek()操作。
让我们看看以下示例,其中我们实现SeekableIterator以在集合上提供搜索功能:
<?
$arr = array(
array("name"=>"John Abraham", "sex"=>"M", "age"=>27),
array("name"=>"Lily Bernard", "sex"=>"F", "age"=>37),
array("name"=>"Ayesha Siddika", "sex"=>"F", "age"=>26),
array("name"=>"Afif", "sex"=>"M", "age"=>2)
);
$persons = new ArrayObject($arr);
$it = $persons->getIterator();
$it->seek(2);
while ($it->valid())
{
print_r($it->current());
$it->next();
}
?>
输出如下:
Array
(
[name] => Ayesha Siddika
[sex] => F
[age] => 26
)
Array
(
[name] => Afif
[sex] => M
[age] => 2
)
RecursiveIterator
这是 SPL 引入的另一个接口,用于轻松地对嵌套集合进行递归。通过实现此接口并使用RecursiveIteratorIterator,您可以轻松地遍历嵌套集合。
如果您实现RecursiveIterator,您必须应用两个方法,一个是hasChildren(),它必须确定当前对象是否为数组(这意味着它是否有子对象)以及第二个是getChildren(),它必须返回集合中相同类的实例。就是这样。为了理解更大的图景,请看以下示例:
<?
$arr = array(
"john"=>array("name"=>"John Abraham", "sex"=>"M", "age"=>27),
"lily"=>array("name"=>"Lily Bernard", "sex"=>"F", "age"=>37),
"ayesha"=>array("name"=>"Ayesha Siddika", "sex"=>"F", "age"=>26),
"afif"=>array("name"=>"Afif", "sex"=>"M", "age"=>2)
);
class MyRecursiveIterator extends ArrayIterator implements
RecursiveIterator
{
public function hasChildren()
{
return is_array($this->current());
}
public function getChildren()
{
return new MyRecursiveIterator($this->current());
}
}
$persons = new ArrayObject($arr);
$MRI = new RecursiveIteratorIterator(new MyRecursiveIterator($persons));
foreach ($MRI as $key=>$person)
echo $key." : ".$person."\n";
?>
输出如下:
name : John Abraham
sex : M
age : 27
name : Lily Bernard
sex : F
age : 37
name : Ayesha Siddika
sex : F
age : 26
name : Afif
sex : M
age : 2
SPLFileObject
这是在 SPL 中引入的另一个用于基本文件操作的出色对象。您可以使用此对象以更优雅的方式遍历文件内容。在SPLFileObject中,支持以下方法:
Array
(
[0] => __construct
[1] => getFilename
[2] => rewind
[3] => eof
[4] => valid
[5] => fgets
[6] => fgetcsv
[7] => flock
[8] => fflush
[9] => ftell
[10] => fseek
[11] => fgetc
[12] => fpassthru
[13] => fgetss
[14] => fscanf
[15] => fwrite
[16] => fstat
[17] => ftruncate
[18] => current
[19] => key
[20] => next
[21] => setFlags
[22] => getFlags
[23] => setMaxLineLen
[24] => getMaxLineLen
[25] => hasChildren
[26] => getChildren
[27] => seek
[28] => getCurrentLine
[29] => __toString
)
如果您仔细观察,您会发现 PHP 中的通用文件函数都是在这个对象中实现的,这为您提供了更多的工作灵活性。
在以下示例中,我们将讨论如何使用SPLFileObject:
<?
$file = new SplFileObject("c:\\lines.txt");
foreach( $file as $line ) {
echo $line;
}
?>
因此,它的工作方式与 Iterator 相同,您可以回滚、定位并执行其他一般任务。还有一些有趣的功能,如getMaxLineLen、fstat、hasChildren、getChildren等。
使用SPLFileObject,您还可以检索远程文件。
SPLFileInfo
这是 SPL 引入的另一个对象,它帮助您检索任何特定文件的文件信息。让我们首先看看它的结构:
Array
(
[0] => __construct
[1] => getPath
[2] => getFilename
[3] => getPathname
[4] => getPerms
[5] => getInode
[6] => getSize
[7] => getOwner
[8] => getGroup
[9] => getATime
[10] => getMTime
[11] => getCTime
[12] => getType
[13] => isWritable
[14] => isReadable
[15] => isExecutable
[16] => isFile
[17] => isDir
[18] => isLink
[19] => getFileInfo
[20] => getPathInfo
[21] => openFile
[22] => setFileClass
[23] => setInfoClass
[24] => __toString
)
你可以使用SPLFileInfo打开任何文件。然而,更有趣的是,它支持文件打开的重载。你可以向它提供一个打开文件管理器类,它将在打开文件时被调用。
让我们看看以下示例。
<?php
class CustomFO extends SplFileObject
{
private $i=1;
public function current()
{
return $this->i++ . ": " .
htmlspecialchars($this->getCurrentLine())."";
}
}
$SFI= new SplFileInfo( "splfileinfo2.php" );
$SFI->setFileClass( "CustomFO" );
$file = $SFI->openFile( );
echo "<pre>";
foreach( $file as $line )
{
echo $line;
}
?>
这个示例将输出以下内容:
1:
2: <?php
3:
4: class CustomFO extends SplFileObject
{
5: private $i=1;
6: public function current()
{
7:
8: return $this->i++ . ": " .
htmlspecialchars($this->getCurrentLine())."";
9: }
10: }
11: $SFI= new SplFileInfo( "splfileinfo2.php" );
12:
13: $SFI->setFileClass( "CustomFO" );
14: $file = $SFI->openFile( );
15: echo "<pre>";
16: foreach( $file as $line )
{
17: echo $line;
18: }
19:
20: ?>
21:
22:
SPLObjectStorage
除了目录、文件对象和迭代器之外,SPL 还引入了另一个非常酷的对象,它可以存储任何对象,并具有特殊功能。这个对象被称为SPLObjectStorage。我们将在本章后面的示例中了解它。
SPLObjectStorage可以存储任何对象。当你更改主对象时,存储在SPLObjectStorage中的对象也会发生变化。如果你尝试添加一个特定的对象多次,实际上它不会添加。你还可以随时从存储中删除对象。
此外,SPLObjectStorage提供了遍历存储对象集合的功能。让我们看看以下示例,它演示了SPLObjectStorage的使用:
<?
$os = new SplObjectStorage();
$person = new stdClass();// a standard object
$person->name = "Its not a name";
$person->age = "100";
$os->attach($person); //attached in the storage
foreach ($os as $object)
{
print_r($object);
echo "\n";
}
$person->name = "New Name"; //change the name
echo str_repeat("-",30)."\n"; //just a format code
foreach ($os as $object)
{
print_r($object); //you see that it changes the original object
echo "\n";
}
$person2 = new stdClass();
$person2->name = "Another Person";
$person2->age = "80";
$os->attach($person2);
echo str_repeat("-",30)."\n";
foreach ($os as $object)
{
print_r($object);
echo "\n";
}
echo "\n".$os->contains($person);//seek
$os->rewind();
echo "\n".$os->current()->name;
$os->detach($person); //remove the object from collection
echo "\n".str_repeat("-",30)."\n";
foreach ($os as $object)
{
print_r($object);
echo "\n";
}
?>
输出如下:
stdClass Object
(
[name] => It's not a name
[age] => 100
)
------------------------------
stdClass Object
(
[name] => New Name
[age] => 100
)
------------------------------
stdClass Object
(
[name] => New Name
[age] => 100
)
stdClass Object
(
[name] => Another Person
[age] => 80
)
1
New Name
------------------------------
stdClass Object
(
[name] => Another Person
[age] => 80
)
摘要
在将 PHP5 介绍给世界之后,PHP 团队向 PHP 开发者引入了强大的面向对象编程。PHP5 附带了许多实用的内置对象,其中 SPL 就是一个非常出色的例子。它简化了许多曾经相当困难的编程任务。因此,SPL 引入了许多我们刚刚讨论并学习如何使用的对象。由于 PHP 手册没有关于所有这些类的更新和详细信息,你可以将这一章视为使用 SPL 对象的良好参考。
第七章:面向对象的数据库
除了在面向对象(OOP)方面的常规改进外,PHP5 还引入了许多新的库,以便以面向对象的方式无缝地与数据库工作。这些库为您提供了改进的性能,有时还提供了改进的安全性功能,当然还有一大堆与数据库服务器提供的新功能交互的方法。
在本章中,我们将讨论 MySQL 改进的 API,即 MySQLi。看看基本的 PDO(好吧,不是详细的,因为 PDO 如此庞大,以至于可以写一本书专门介绍它),ADOdb,以及 PEAR::MDB2。同时,我们还将看看使用 ADOdb 的 active 在 PHP 中实现的 Active Record 模式。在这里要注意的一点是,我们不是关注如何进行一般的数据库操作。我们只会关注一些对在面向对象方式中做数据库编程的 PHP 开发者来说有趣的具体主题。
MySQLi 简介
MySQLi 是 PHP5 中引入的一个改进的扩展,用于处理高级 MySQL 功能,如预定义语句和存储过程。从性能角度来看,MySQLi 比 MySQL 扩展要好得多。此外,此扩展提供了完全面向对象的接口来与 MySQL 数据库交互,这在 PHP5 之前是不存在的。但请记住,如果你的 MySQL 版本至少是 4.1.3 或更高版本,你将能够使其工作。
那么主要的改进有哪些呢?让我们先看看:
-
性能优于 MySQL 扩展
-
灵活的面向对象和非面向对象接口
-
相较于新 MySQL 对象的优点
-
能够创建压缩连接
-
支持通过 SSL 连接
-
支持预定义语句
-
支持存储过程(SP)
-
支持更好的复制和事务
我们将在下面的例子中查看一些这些功能。但当然,我们不是要介绍 MySQL 的基础知识,因为这超出了本书的范围。我们只会展示如何使用 MySQLi 的面向对象接口,以及如何使用这些高级功能中的某些功能。
以面向对象的方式连接到 MySQL
记得那些老日子,那时候你不得不使用过程式函数call来连接 MySQL,即使是来自你的对象。那些日子已经过去了。现在你可以利用 MySQLi 的完整面向对象接口与 MySQL 进行通信(好吧,还有一些过程式方法,但总体上是完全面向对象的)。看看下面的例子:
<?
$mysqli = new mysqli("localhost", "user", "password", "dbname");
if (mysqli_connect_errno()) {
echo("Failed to connect, the error message is : ".
mysqli_connect_error());
exit();
}
?>
如果连接失败,你可能会得到如下错误信息:
Failed to connect, the error message is : Access denied for user
'my_user'@'localhost' (using password: YES)
以面向对象的方式选择数据
让我们看看如何使用 MySQLi API 以面向对象的方式从表中选择数据。
<?php
$mysqli = new mysqli("localhost", "un" "pwd", "db");
if (mysqli_connect_errno()) {
echo("Failed to connect, the error message is : ".
mysqli_connect_error());
exit();
}
/* close connection */
$result = $mysqli ->query("select * from users");
while ($data = $result->fetch_object())
{
echo $data->name." : '".$data->pass."' \n";
}
?>
输出如下:
robin : 'no password'
tipu : 'bolajabena'
注意
请注意,在数据库中不加密地将用户密码以纯文本形式存储并不是一个好的做法。最好的方式是使用某些散列例程(如md5())仅存储他们密码的散列。
以面向对象的方式更新数据
与它没有特别的处理。你可以像之前使用 MySQL 扩展一样更新你的数据。但为了面向对象风格的考虑,我们展示了如何使用 mysqli_query() 函数来执行,如上述示例所示。实例化一个 MySQLi 对象,然后运行查询。
预处理语句
这里我们进入了一个非常有趣的章节,这是在 PHP OO 中首次使用 MySQLi 扩展引入的。预处理语句是在 MySQL 5.0 版本(动态 SQL)中引入的,以提高安全性和灵活性。它比常规语句有显著的性能提升。
那么实际上什么是预处理语句?预处理语句只不过是一个由 MySQL 服务器预先编译的常规查询,可以在以后调用。预处理语句减少了 SQL 注入的可能性,并且与一般的非预处理查询相比提供了更好的性能,因为它不需要在运行时执行不同的编译步骤。(记住,它已经编译过了?)
使用预处理语句的优点如下:
-
更好的性能
-
预防 SQL 注入
-
在处理 BLOB 时节省内存
但也有缺点!
-
如果你只为单个调用使用预处理语句,那么不会有性能提升。
-
使用预处理语句没有查询缓存。
-
如果语句没有显式关闭,则可能会发生内存泄漏。
-
并非所有语句都可以用作预处理语句。
预处理语句可以在准备查询时接受参数,其顺序与准备查询时指定的顺序相同。在本节中,我们将学习如何创建预处理语句,向它们传递值,并获取结果。
基本预处理语句
让我们使用 PHP 的原生 MySQLi 扩展来准备一个语句。在以下示例中,我们将创建一个预处理语句,执行它,并从中获取结果:
<?
$mysqli = new mysqli("localhost", "un" "pwd", "db");
if (mysqli_connect_errno()) {
echo("Failed to connect, the error message is : ".
mysqli_connect_error());
exit();
}
$stmt = $mysqli ->prepare("select name, pass from users
order by name");
$stmt->execute();
//$name=null;
$stmt->bind_result($name, $pass);
while ($stmt->fetch())
{
echo $name."<br/>";
}
?>
那么在上面的示例中我们实际上做了什么?
-
我们使用以下代码准备语句:
$stmt = $mysqli->prepare("select name, pass from users order by name"); -
然后我们执行了它:
$stmt->execute(); -
然后,我们用它与两个变量绑定,因为我们的查询中有两个变量:
$stmt->bind_result($name, $pass); -
最后我们使用以下方式获取结果:
$stmt->fetch()
每当我们调用 fetch() 时,绑定的变量会被填充值。因此,我们现在可以使用它们。
带变量的预处理语句
预处理语句的优势在于你可以使用变量与查询结合。首先,你可以在适当的位置放置一个 ? 符号来准备查询,然后你可以在准备之后传递值。让我们看看以下示例:
<?
$mysqli = new mysqli("localhost", "un" "pwd", "db");
if (mysqli_connect_errno()) {
echo("Failed to connect, the error message is : ".
mysqli_connect_error());
exit();
}
$stmt = $mysqli->prepare("select name, pass from users
where name=?");
$stmt->bind_param("s",$name); //binding name as string
$name = "tipu";
$stmt->execute();
$name=null;
$stmt->bind_result($name, $pass);
while ($r = $stmt->fetch())
{
echo $pass."<br/>";
}
?>
这里我们准备查询 "select name, pass from users where name=?",其中名称肯定是一个字符串类型的值。正如我们在前面的示例中使用 bind_results() 绑定参数以获取结果一样,这里我们必须使用 bind_params() 函数来绑定参数。除此之外,我们还需要提供绑定参数的数据类型。
MySQL 预处理语句支持四种类型的参数:
-
i,表示相应的变量是整数类型 -
d,表示相应的变量是双精度浮点数 -
s表示相应的变量具有字符串类型 -
b表示相应的变量是一个二进制大对象(blob),并且将以数据包的形式发送
由于我们的参数是字符串,我们使用了以下行来绑定参数:
$stmt->bind_param("s",$name);
在绑定变量之后,我们现在将值设置为 $name 并调用 execute() 函数。之后,我们像之前一样检索值。
使用预处理语句与 BLOB
预处理语句支持高效地处理 BLOB 或 二进制大对象。如果你使用预处理语句管理 BLOB,它将通过以数据包的形式发送数据来节省你更多的内存消耗。让我们看看我们如何存储 BLOB(在这种情况下,是一个图像文件)。
预处理语句支持使用 send_long_data() 函数分块发送数据。在下面的例子中,我们将使用这个函数来存储图像,尽管你可以像通常一样发送它们,除非你的数据超过了由 max_allowed_packet MySQL 配置变量定义的限制。
<?
$mysqli = new mysqli("localhost", "un" "pwd", "db");
if (mysqli_connect_errno()) {
echo("Failed to connect, the error message is : ".
mysqli_connect_error());
exit();
}
$stmt = $mysqli->prepare("insert into images value(NULL,?)");
$stmt->bind_param("b",$image);
$image = file_get_contents("signature.jpg");//fetching content of
//a file
$stmt->send_long_data(0,$image);
$stmt->execute();
?>
我们的表模式如下所示:
CREATE TABLE 'images' (
'id' int(11) NOT NULL auto_increment,
'image' mediumblob,
PRIMARY KEY ('id')
) ENGINE=MyISAM;
我们选择中等 BLOB 作为我们的数据类型,因为 blob 只能存储 65KB 的数据,而中等 BLOB 可以存储超过 16MB,长 BLOB 可以在其中存储超过 4GB 的数据。
现在,我们将使用图像再次在预处理语句中恢复这个 BLOB 数据:
<?
$mysqli = new mysqli("localhost", "username", "password", "test");
if (mysqli_connect_errno()) {
echo("Failed to connect, the error message is : ".
mysqli_connect_error());
exit();
}
$stmt = $mysqli->prepare("select image from images where id=?");
$stmt->bind_param("i",$id);
$id = $_GET['id'];
$stmt->execute();
$image=NULL;
$stmt->bind_result($image);
$stmt->fetch();
header("Content-type: image/jpeg");
echo $image;
?>
使用 MySQLi 和 PHP 执行存储过程
存储过程是 MySQL 5 中新增的另一个功能,它极大地减少了客户端查询的需求。使用 MySQLi 扩展,你可以在 MySQL 中执行存储过程。我们不会讨论存储过程,因为这超出了本书的范围。互联网上有几篇文章可以帮助你在 MySQL 中编写存储过程。你可以阅读这篇很棒的文章来获取关于高级 MySQL 功能的基本概念:dev.mysql.com/tech-resources/articles/mysql-storedprocedures.pdf
让我们创建一个小型的存储过程并使用 PHP 来运行它。这个存储过程可以接受一个输入并将该记录插入到表中:
DELIMITER $$;
DROP PROCEDURE IF EXISTS 'test'.'sp_create_user'$$
CREATE PROCEDURE 'sp_create_user'(IN uname VARCHAR(50))
BEGIN
INSERT INTO users(id,name) VALUES (null, uname);
END$$
DELIMITER ;$$
如果你在这个数据库中运行这个存储过程(使用 MySQL 查询构建器或其他任何东西),sp_create_user 过程将被创建。
注意
你可以使用 "Execute" 命令手动从 MySQL 客户端执行任何已存储的过程。例如,要执行上述存储过程,你必须使用 call sp_create_user(' username')。
现在,我们将使用 PHP 代码来运行这个存储过程。让我们看看。
<?
$mysqli = new mysqli("localhost", "username", "password", "test");
if (mysqli_connect_errno()) {
echo("Failed to connect, the error message is : ".
mysqli_connect_error());
exit();
}
$mysqli->query("call sp_create_user('hasin')");
?>
就这样!
PDO
PHP 5.1 中新增的另一个用于管理数据库的扩展是 PDO(尽管 PDO 在 PHP 5.0 中作为 PECL 扩展可用)。它包含了一组用于与不同数据库引擎工作的驱动程序。PDO 代表 PHP 数据对象。它是为了提供不同数据库引擎的轻量级接口而开发的。PDO 的一个非常好的特性是它像数据访问层一样工作,这样你就可以为所有数据库引擎使用相同的函数名。
您可以使用 DSN(数据源名称)字符串连接到不同的数据库。在以下示例中,我们将连接到 MySQL 数据库并检索一些数据。
<?php
$dsn = 'mysql:dbname=test;host=localhost;';
$user = 'user';
$password = 'password';
try {
$pdo = new PDO($dsn, $user, $password);
}
catch (PDOException $e)
{
echo 'Connection failed: ' . $e->getMessage();
}
$result = $pdo->query("select * from users");
foreach ($result as $row)
echo $row['name'];
?>
这相当方便,对吧?它只是通过 DSN(这里连接到 test 数据库)连接到 MySQL 服务器,然后执行查询。最后,我们显示结果。
如果我们连接到 SQLite 数据库会是什么样子呢?
<?php
$dsn = 'sqlite:abcd.db';
try
{
$pdo = new PDO($dsn);
$pdo->exec("CREATE TABLE users (id int, name VARCHAR)");
$pdo->exec("DELETE FROM users");
$pdo->exec("INSERT INTO users (name) VALUES('afif')");
$pdo->exec("INSERT INTO users (name) VALUES('tipu')");
$pdo->exec("INSERT INTO users (name) VALUES('robin')");
}
catch (PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
}
$result = $pdo->query("select * from users");
foreach ($result as $row)
echo $row['name'];
?>
看看代码中除了 DSN 之外没有变化。
您也可以在内存中创建 SQLite 数据库并在此处执行操作。让我们看一下以下代码:
<?php
$dsn = 'sqlite::memory:';
try {
$pdo = new PDO($dsn);
$pdo->exec("CREATE TABLE users (id int, name VARCHAR)");
$pdo->exec("DELETE FROM users");
$pdo->exec("INSERT INTO users (name) VALUES('afif')");
$pdo->exec("INSERT INTO users (name) VALUES('tipu')");
$pdo->exec("INSERT INTO users (name) VALUES('robin')");
}
catch (PDOException $e)
{
echo 'Connection failed: ' . $e->getMessage();
}
$result = $pdo->query("select * from users");
foreach ($result as $row)
echo $row['name'];
?>
我们只是在这里更改了 DSN。
不同数据库引擎的 DSN 设置
让我们看一下不同数据库引擎与 PDO 连接的 DSN 设置。支持的数据库驱动程序如下所示:
-
PDO_DBLIB 用于 FreeTDS/Microsoft SQL Server/Sybase
-
PDO_FIREBIRD 用于 Firebird/Interbase 6
-
PDO_INFORMIX 用于 IBM Informix Dynamic Server
-
PDO_MYSQL 用于 MySQL 3.x/4.x/5.x
-
PDO_OCI 用于 Oracle Call Interface
-
PDO_ODBC 用于 ODBC v3(IBM DB2、unixODBC 和 win32 ODBC)
-
PDO_PGSQL 用于 PostgreSQL
-
PDO_SQLITE 用于 SQLite 3 和 SQLite 2
让我们看一下这些示例驱动程序特定的 DSN 设置:
mssql:host=localhost;dbname=testdb
sybase:host=localhost;dbname=testdb
dblib:host=localhost;dbname=testdb
firebird:User=john;Password=mypass;Database=DATABASE.GDE;
DataSource=localhost;Port=3050
informix:host=host.domain.com; service=9800;database=common_db;
server=ids_server; protocol=onsoctcp;EnableScrollableCursors=1
mysql:host=localhost;port=3307;dbname=testdb
mysql:unix_socket=/tmp/mysql.sock;dbname=testdb
oci:mydb
oci:dbname=//localhost:1521/mydb
odbc:testdb
odbc:DRIVER={IBM DB2 ODBC
DRIVER};HOSTNAME=localhost;PORT=50000;DATABASE=SAMPLE;PROTOCOL=TCPIP;
UID=db2inst1;PWD=ibmdb2;
odbc:Driver={Microsoft Access Driver
(*.mdb)};Dbq=C:\\db.mdb;Uid=Admin
pgsql:dbname=example;user=nobody;password=change_me;host=localhost;
port=5432
sqlite:/opt/databases/mydb.sq3
sqlite::memory:
sqlite2:/opt/databases/mydb.sq2
sqlite2::memory:
使用 PDO 的预处理语句
使用 PDO 对您的数据库运行预处理语句。好处与之前相同。它通过解析和缓存服务器端查询来提高多次调用的性能,同时也消除了 SQL 注入的机会。
与我们之前在 MySQLi 的示例中看到的不同,PDO 预处理语句可以接受命名变量。
让我们看一下以下示例来理解这一点:
<?php
$dsn = 'mysql:dbname=test;host=localhost;';
$user = 'username';
$password = 'password';
try {
$pdo = new PDO($dsn, $user, $password);
} catch (PDOException $e)
{
echo 'Connection failed: ' . $e->getMessage();
}
$stmt = $pdo->prepare("select id from users where name=:name");
$name = "tipu";
$stmt->bindParam(":name",$name, PDO::PARAM_STR);
$stmt->execute();
$stmt->bindColumn("id",$id);
$stmt->fetch();
echo $id;
?>
但您也可以像这样运行示例:
<?php
$dsn = 'mysql:dbname=test;host=localhost;';
$user = 'username';
$password = 'password';
try {
$pdo = new PDO($dsn, $user, $password);
}
catch (PDOException $e)
{
echo 'Connection failed: ' . $e->getMessage();
}
$stmt = $pdo->prepare("select id from users where name=?");
$name = "tipu";
$stmt->bindParam(1,$name, PDO::PARAM_STR);
$stmt->execute();
$stmt->bindColumn("id",$id);
$stmt->fetch();
echo $id;
?>
与调用 bindParam() 不同,您可以使用以下类似的 bindValues():
$stmt->bindValue(1,"tipu", PDO::PARAM_STR);
调用存储过程
PDO 提供了一种简单的方法来调用存储过程。您只需通过 exec() 方法运行 "CALL SPNAME(PARAMS)" 即可:
$pdo->exec("CALL sp_create_user('david')");
其他有趣的函数
PDO 中还有其他几个有趣的函数可用。例如,看一下下面的列表:
-
fetchAll() -
fetchColumn() -
rowCount() -
setFetchMode()
fetchAll() 函数可以从结果集中检索所有记录。让我们看一下以下示例:
$stmt = $pdo->prepare("select * from users");
$stmt->execute();
echo "<pre>";
print_r($stmt->fetchAll());
echo "</pre>";
fetchColumn() 函数有助于在执行语句后从任何特定列中选择数据。让我们看一下:
$stmt = $pdo->prepare("select * from users");
$stmt->execute();
while ($name = $stmt->fetchColumn(1))
{
echo $name."<br/>";
}
rowCount() 在执行任何 UPDATE 或 DELETE 查询后返回受影响的行数。但您必须记住,它返回的是最新执行的查询影响的行数。
$stmt = $pdo->prepare("DELETE from users WHERE name='Anonymous'");
$stmt->execute();
echo $stmt->rowCount();
setFetchMode() 帮助您设置 PDO 预处理语句的检索模式。可用的值包括:
-
PDO::FETCH_NUM: 以数字索引数组的形式检索结果 -
PDO::FETCH_ASSOC: 以列名作为键检索行 -
PDO::FETCH_BOTH: 以上述两种方式检索 -
PDO::FETCH_OBJ: 将行作为对象检索,其中列名设置为属性
数据抽象层简介
数据抽象层(DALs)是为了提供统一的接口以与每个数据库引擎一起工作而开发的。它为每个数据库引擎提供类似的 API 进行独立操作。由于所有平台上的函数名称相似,因此它们更容易使用,更容易记忆,当然也使得代码更易于移植。为了让您了解 DAL 的必要性,让我解释一个常见的场景。
假设 Y 团队接到了一个大项目。他们的客户说他们将使用 MySQL。因此,Y 团队开发了应用程序,当交付时间到来时,客户要求团队提供对 PostgreSQL 的支持。他们将为此变更付费,但需要尽早完成。
Y 团队使用所有原生 MySQL 函数设计了应用程序。那么 Y 团队会怎么做?他们会重写一切以支持 PostgreSQL 吗?嗯,这是他们唯一的选择。但如果他们未来需要支持 MSSQL 呢?另一个重写?你能想象每次重构的成本吗?
为了避免这些灾难,就需要有 DAL,这样代码就可以保持不变,并且可以在任何时间更改以支持任何数据库,而无需进行任何重大更改。
有许多流行的库用于实现 PHP 的 DAL。以下是一些例子,如 ADOdb 和 PEAR::MDB2 非常流行。PEAR::DB 曾经非常流行,但其开发已经停止(blog.agoraproduction.com/index.php?/archives/42-PEARDB-is-DEPRECATED,-GOT-IT.html#extended)。
在本节中,我们将讨论 PEAR::MDB2 和 ADOdb。我们将使用它查看基本的数据库操作,并学习如何安装这些库以进行工作。
ADOdb
ADOdb 是由 John Lim 开发的一个非常好且流行的数据抽象层,并发布在 LGPL 下。这是 PHP 中最好的数据抽象层之一。您可以从adodb.sourceforge.net获取 ADOdb 的最新版本。
安装 ADOdb
没有 ADodb 的安装。它是一组类和常规脚本。所以您只需将存档提取到可以包含脚本的位置即可。让我们看一下以下图像,了解提取后的目录结构:

连接到不同的数据库
与 PDO 一样,您可以使用 ADOdb 连接到不同的数据库驱动程序。DSN 与 PDO 不同。让我们看一下支持的数据库列表及其 DSN 字符串。
ADOdb 支持常见的 DSN 格式,如下所示:
$driver://$username:$password@hostname/$database?options[=value]
那么,ADOdb 支持哪些可用的驱动程序呢?下面让我们看一下。这是一个从 ADOdb 手册中摘取的列表,供您理解:
| 名称 | 测试 | 数据库 | 预先条件 | 操作系统 |
|---|---|---|---|---|
| 访问 | B | Microsoft Access/Jet。您需要创建一个 ODBC DSN。 | ODBC | 仅限 Windows |
| ado | B | 通用 ADO,未针对特定数据库进行优化。允许无 DSN 连接。为了最佳性能,请使用 OLEDB 提供程序。这是所有 ado 驱动的基类。您可以在连接之前设置 $db->codePage。 |
ADO 或 OLEDB 提供程序 | 仅限 Windows |
| ado_access | B | 使用 ADO 的 Microsoft Access/Jet。允许无 DSN 连接。为了最佳性能,请使用 OLEDB 提供程序。 | ADO 或 OLEDB 提供程序 | 仅限 Windows |
| ado_mssql | B | 使用 ADO 的 Microsoft SQL Server。允许无 DSN 连接。为了最佳性能,请使用 OLEDB 提供程序。 | ADO 或 OLEDB 提供程序 | 仅限 Windows |
| db2 | C | 使用 PHP 的 db2 特定扩展以获得更好的性能。 | DB2 CLI/ODBC 接口 | Unix 和 Windows。需要 IBM DB2 Universal Database 客户端 |
| odbc_db2 | C | 使用通用的 ODBC 扩展连接到 DB2。 | DB2 CLI/ODBC 接口 | Unix 和 Windows。Unix 安装提示。我收到报告说在使用 CLI 接口时,在 Connect() 中必须将 $host 和 $database 参数颠倒过来。 |
| vfp | A | Microsoft Visual FoxPro。您需要创建一个 ODBC DSN。 | ODBC | 仅限 Windows |
| fbsql | C | FrontBase。 | ? | Unix 和 Windows |
| ibase | B | Interbase 6 或更早版本。一些用户报告说您可能需要使用此 $db->PConnect('localhost:c:/ibase/employee.gdb', "sysdba", "masterkey") 来连接。目前缺少 Affected_Rows。您可以在连接之前设置 $db->role,$db->dialect,$db->buffers 和 $db->charSet。 |
Interbase 客户端 | Unix 和 Windows |
| firebird | B | Firebird 版本的 interbase。 | Interbase 客户端 | Unix 和 Windows |
| borland_ibase | C | Borland 版本的 Interbase 6.5 或更高版本。非常遗憾分支不同。 | Interbase 客户端 | Unix 和 Windows |
| informix | C | 通用 informix 驱动程序。如果您使用的是 Informix 7.3 或更高版本,请使用此驱动程序。 | Informix 客户端 | Unix 和 Windows |
| informix72 | C | 不支持 SELECT FIRST 的 Informix 7.3 之前的数据库。 |
Informix 客户端 | Unix 和 Windows |
| ldap | C | LDAP 驱动程序。有关使用信息,请参阅此示例。 | LDAP 扩展 | ? |
| mssql | A | Microsoft SQL Server 7 及更高版本。也适用于 Microsoft SQL Server 2000。注意,此驱动程序中的日期格式有问题。例如,PHP MSSQL 扩展不返回 datetime 的秒数! | Mssql 客户端 | Unix 和 Windows。Unix 安装指南和另一个指南。 |
| mssqlpo | A | 可移植的 mssql 驱动程序。与上述 mssql 驱动程序相同,除了将 `' | ',连接运算符,转换为 +。对于从大多数其他使用 ' |
|
| mysql | A | 无事务支持的 MySQL。您也可以在连接之前设置 $db->clientFlags。 |
MySQL 客户端 | Unix 和 Windows |
| mysqli | B | 支持较新的 PHP5 MySQL API。 | MySQL 4.1+ 客户端 | Unix 和 Windows |
| mysqlt or maxsql | A | 具有事务支持的 MySQL。我们建议使用 ` | 作为连接运算符以获得最佳兼容性。可以通过以下方式运行 MySQL:mysqld --ansi或mysqld --sql-mode=PIPES_AS_CONCAT`。 |
|
| oci8 | A | Oracle 8/9。比 oracle 驱动程序具有更多功能(例如 Affected_Rows)。您可能需要在 Connect/PConnect 之前执行 putenv('ORACLE_HOME=...')。有两种连接方式:使用服务器 IP 和服务名称:PConnect('serverip:1521','scott','tiger','service') 或使用 TNSNAMES.ORA 或 ONAMES 或 HOSTNAMES 中的条目:PConnect(false, 'scott', 'tiger', $oraname)。自 2.31 版本起,我们直接支持 Oracle REF cursor 变量(请参阅 ExecuteCursor)。 |
Oracle 客户端 | Unix 和 Windows |
| oci805 | C | 支持 Oracle 8.0.5 的简化功能。SelectLimit 在 oci8 或 oci8po 驱动程序中效率不高。 |
Oracle 客户端 | Unix 和 Windows |
| oci8po | A | Oracle 8/9 可移植驱动程序。此驱动程序与 oci8 驱动程序几乎相同,除了(a)Prepare() 中的绑定变量使用 ? 约定,而不是 :bindvar,(b)字段名称使用更常见的 PHP 约定的小写名称。如果从其他数据库迁移很重要,请使用此驱动程序。否则,oci8 驱动程序提供更好的性能。 |
Oracle 客户端 | Unix 和 Windows |
| odbc | A | 通用 ODBC,未针对特定数据库进行优化。要连接,请使用 PConnect('DSN','user','pwd')。这是所有 ODBC 派生驱动程序的基础类。 |
ODBC | Unix 和 Windows。Unix 提示 |
| odbc_mssql | A | 使用 ODBC 连接到 MSSQL。 | ODBC | Unix 和 Windows |
| odbc_oracle | C | 使用 ODBC 连接到 Oracle。 | ODBC | Unix 和 Windows |
| odbtp | B | 通用 odbtp 驱动程序。Odbtp 是一种从其他操作系统访问 Windows ODBC 数据源的软件。 | odbtp | Unix 和 Windows |
| odbtp_unicode | C | 支持 Unicode 的 odbtp。 | odbtp | Unix 和 Windows |
| oracle | C | 实现旧的 Oracle 7 客户端 API。如果可能,请使用 oci8 驱动程序以获得更好的性能。 | Oracle 客户端 | Unix 和 Windows |
| netezza | C | Netezza 驱动程序。Netezza 基于 PostGREs 代码库。 | ? | ? |
| pdo | C | PHP5 的通用 PDO 驱动程序。 | PDO 扩展和特定数据库驱动程序 | Unix 和 Windows |
| postgres | A | 通用 PostgreSQL 驱动程序。目前与 postgres7 驱动程序相同。 | PostgreSQL 客户端 | Unix 和 Windows |
| postgres64 | A | 用于 PostgreSQL 6.4 及更早版本,这些版本不支持内部 LIMIT。 | PostgreSQL 客户端 | Unix 和 Windows |
| postgres7 | A | 支持 LIMIT 和其他版本 7 功能的 PostgreSQL。 | PostgreSQL 客户端 | Unix 和 Windows |
| postgres8 | A | 目前与 postgres7 相同。 | PostgreSQL 客户端 | Unix 和 Windows |
| sapdb | C | SAP DB。基于 ODBC 驱动程序,应该能够可靠地工作。 | SAP ODBC 客户端 | ? |
| sqlanywhere | C | Sybase SQL Anywhere。基于 ODBC 驱动程序,应该能够可靠地工作。 | SQL Anywhere ODBC 客户端 | ? |
| sqlite | B | SQLite。 | - | Unix 和 Windows |
| sqlitepo | B | 可移植 SQLite 驱动。这是因为关联模式在 SQLite 中不像其他驱动那样工作。具体来说,当选择(连接)多个表时,表名包含在“sqlite”驱动程序的关联键中。在“sqlitepo”驱动程序中,表名从返回的列名中去除。当这导致冲突时,第一个字段将优先。 | - | Unix 和 Windows |
| sybase | C | Sybase. | Sybase 客户端 | Unix 和 Windows |
使用 ADOdb 进行基本数据库操作
记得刚才看到的目录结构吗?现在我们将利用那些脚本。在本节中,我们将学习使用 ADOdb 进行基本数据库操作。让我们连接到 MySQL 并执行一个基本操作:
<?
include("adodb/adodb.inc.php");
$dsn = 'mysql://username:password@localhost/test?persist';
$conn = ADONewConnection($dsn);
$conn->setFetchMode(ADODB_FETCH_ASSOC);
$recordSet = $conn->Execute('select * from users');
if (!$recordSet)
print $conn->ErrorMsg(); //if any error is there
else
while (!$recordSet->EOF) {
echo $recordSet->fields['name'].'<BR>';
$recordSet->MoveNext();
}
?>
让我们看看一个替代的连接示例:
<?
include("adodb/adodb.inc.php");
$conn =ADONewConnection('mysql');//just the RDBMS type
$conn->connect("localhost","username","password","test");
//here comes the credentials
?>
插入、删除和更新记录
你可以使用ADONewConnection或ADOConnection对象的execute()方法执行任何 SQL 语句。所以这里没有什么新东西。但让我们看看我们如何插入、删除和更新一些记录,并跟踪成功或失败。
<?
include("adodb/adodb.inc.php");
$conn =ADONewConnection('mysql');
$conn->connect("localhost","user","password","test");
$conn->setFetchMode(ADODB_FETCH_ASSOC);
$res = $conn->execute("insert into users(name) values('test')");
echo $conn->Affected_Rows();
?>
因此,Affected_Rows为你提供了这些场景的结果。
提示
插入 ID
如果你正在寻找最新的插入 ID,你可以使用Insert_Id()函数。
执行预定义语句
ADOdb 提供了创建和执行预定义语句的简单 API。让我们看看以下示例,了解它是如何工作的:
<?
include("adodb/adodb.inc.php");
$conn =ADONewConnection('mysql');
$conn->connect("localhost","user","password","test") ;
$conn->setFetchMode(ADODB_FETCH_ASSOC);
$stmt = $conn->Prepare('insert into users(name) values (?)');
$conn->Execute($stmt,array((string) "afif"));
echo $conn->Affected_Rows();
?>
你可以用同样的方式检索记录。
MDB2
MDB2 是另一个在 PEAR 下开发的流行数据抽象库,它结合了 PEAR::DB 和 Metabase 的最佳特性。它提供了非常一致的 API、改进的性能和稳定的开发平台,优于 DB 和 MDB。MDB2 附带了一套优秀的文档。在本章中,我们当然不能涵盖 MDB2 支持的所有功能,但我们将介绍基本功能,以便你了解它是如何工作的。
安装 MDB2
安装 MDB2 需要安装一个有效的 PEAR 版本。因此,为了使用 MDB2,你必须确保 PEAR 已安装并能在你的机器上正常工作。如果你没有安装 PEAR,以下提示将对你有所帮助。
提示
安装 PEAR
前往pear.php.net/go-pear,并将页面保存为go-pear.php到你的硬盘上。现在在你的 shell 或命令提示符中执行命令php /path/to/go-pear.php,并按照那里的说明操作。如果询问你是否要安装 MDB2,请回答“是”。如果它要求修改你的php.ini文件,也请回答“是”。不用担心,它只会添加条目以使 PEAR 在你的当前包含路径中可用,所有其他设置都将保持不变。所以,你已经完成了。
如果你已经安装了 PEAR 但没有安装 MDB2,那么你可以立即安装它。打开你的 shell 或命令提示符,并执行以下命令:
pear install MDB2
pear install MDB2_Driver_$driver
其中 $driver 可以是任何类似 SQLite、PgSQL、MySQL、MYSQLi、oci8、MSSQL 和 ibase 等。例如,要安装 MySQL 驱动,你必须执行以下命令:
pear install MDB2_Driver_mysql
就这样。你已经完成了。
连接到数据库
使用 MDB2,你可以连接到不同的数据库引擎。MDB2 还有一个格式化的 DSN 字符串来连接。该 DSN 的格式如下所示:
phptype(dbsyntax)://username:password@protocol+hostspec/database?
option=value
但在这个 DSN 中也有一些变化。这些变化在此列出:
phptype://username:password@protocol+hostspec:110//usr/db_file.db
phptype://username:password@hostspec/database
phptype://username:password@hostspec
phptype://username@hostspec
phptype://hostspec/database
phptype://hostspec
phptype:///database
phptype:///database?option=value&anotheroption=anothervalue
支持的驱动程序(PHPtype)如下所示:
fbsql -> FrontBase
ibase -> InterBase / Firebird (requires PHP 5)
mssql -> Microsoft SQL Server (NOT for Sybase. Compile PHP --with-mssql)
mysql -> MySQL
mysqli -> MySQL (supports new authentication protocol) (requires PHP 5)
oci8 -> Oracle 7/8/9/10
pgsql -> PostgreSQL
querysim -> QuerySim
sqlite -> SQLite 2
现在,让我们连接到 MySQL:
<?php
set_include_path(get_include_path().";".
"C:/Program Files/PHP/pear;");
require_once 'MDB2.php';
$dsn = 'mysql://user:password@localhost/test';
$options = array('persistent' => true
);
$mdb2 = MDB2::factory($dsn, $options);
if (PEAR::isError($mdb2)) {
die($mdb2->getMessage());
}
// ...
$result = $mdb2->query("select * from users");
while ($row = $result->fetchRow(MDB2_FETCHMODE_ASSOC))
{
echo $row['name']."\n";
}
$mdb2->disconnect();
?>
执行预定义语句
你可以轻松地使用 MDB2 执行预定义语句。MDB2 提供了创建和执行预定义语句的灵活 API。在以下示例中,我们将执行两种类型的预定义语句。一种将仅执行一些插入/更新/删除查询,另一种将返回一些数据作为输出。
<?php
set_include_path(get_include_path().";".
"C:/Program Files/PHP/pear;");
require_once 'MDB2.php';
$dsn = 'mysql://user:password@localhost/test';
$options = array('persistent' => true
);
$mdb2 = MDB2::factory($dsn, $options);
if (PEAR::isError($mdb2)) {
die($mdb2->getMessage());
}
$stmt = $mdb2->Prepare("insert into users(name)
values(?)",array("text"),MDB2_PREPARE_MANIP);
//for DML statements, we should use MDB2_PREPARE_MANIP and For
//Reading we should use MDB2_PREPARE_RESULT
echo $stmt->execute("Mohiuddin");
$stmt = $mdb2->Prepare("select name from users where
id=?",array("integer"),array("text"));
$result = $stmt->execute(11);
if (PEAR::isError($result))
echo $result->getMessage();
while ($row = $result->fetchRow())
{
echo $row[0];
}
?>
现在如果我们想在多个字段中插入数据呢?例如,如果我们表中还有一个名为"age"的字段,我们需要传递如下数据:
$stmt = $mdb2->Prepare("insert into users(name,age)
values(?)",array("text","integer"),MDB2_PREPARE_MANIP);
echo $stmt->execute("Mohiuddin",2);
或者:
$stmt = $mdb2->Prepare("insert into users(name,age)
values(?)",array("text","integer"),MDB2_PREPARE_MANIP);
echo $stmt->execute(array("Mohiuddin",2));
因此,我们也可以使用executeMultiple()方法一次性插入多行:
$stmt = $mdb2->Prepare("insert into users(name,age) values(?)",array("text","integer"),MDB2_PREPARE_MANIP);
echo $stmt->executeMultiple(array(array("Mohiuddin",2),
array("another",3));
就这样。
ActiveRecord 简介
ActiveRecord 是一种设计模式,旨在以相当可读的方式解决数据访问问题。使用 ActiveRecord 设计模式,你可以像魔法一样操纵数据。在本节中,我们将介绍 PHP 中 ActiveRecord 实现的基本功能。
让我们看看 ActiveRecord 实际上是如何工作的。为此,我们将使用 ADOdb 的 Active Record 实现。Adodb 提供了一个名为Adodb_Active_Record的类,专门用于此。
让我们在数据库中创建一个具有以下结构的表:
CREATE TABLE 'users' (
'id' int(11) NOT NULL auto_increment,
'name' varchar(250),
'pass' varchar(32),
PRIMARY KEY ('id')
) ENGINE=MyISAM;
通过 ActiveRecord 创建新记录
现在,我们将在这个表中创建一个新的用户。看看以下代码:
<?
include("adodb/adodb.inc.php");
include('adodb/adodb-active-record.inc.php');
$conn =ADONewConnection('mysql');
$conn->connect("localhost","user","password","test") ;
ADODB_Active_Record::setDatabaseAdapter($conn);
class User extends ADODB_Active_Record {}
$user = new User();//a dynamic model to access the user table
$user->name = "Packt";
$user->pass = "Hello";
$user->save();//calling save() will internally save this
//record in table
?>
ActiveRecord 通过为你的数据库中的每个表暴露一个单独的对象,你可以执行不同的操作。让我们看看我们如何选择一些数据。
选择和更新数据
我们可以使用 ActiveRecord 轻松地加载和更改任何记录。让我们看看以下示例:
<?
include("adodb/adodb.inc.php");
include('adodb/adodb-active-record.inc.php');
$conn =ADONewConnection('mysql');
$conn->connect("localhost","user","password","test") ;
ADODB_Active_Record::setDatabaseAdapter($conn);
class User extends ADODB_Active_Record {}
$user = new User();
$user->load("id=10");//load the record where the id is 10
echo $user->name;
$user->name= "Afif Mohiuddin";//now update
$user->save();//and save the previously loaded record
?>
所以这相当简单。当你用任何表达式调用load()方法时,记录将被加载到对象本身中。然后你可以进行任何更改,最后保存它。ActiveRecord 与工作起来非常迷人。
摘要
你已经阅读了一章,专门介绍使用面向对象的方式完全访问数据库。还有很多其他有趣的项目,如 Propel(propel.phpdb.org/trac/)作为 PHP 开发者的对象关系映射库,Creole(creole.phpdb.org/trac/)作为 DAL,CodeIgniter 框架中的 ActiveRecord 库(www.codeigniter.com),等等。你有很多资源可以使用 PHP5 和面向对象风格来操纵数据库。
在下一章中,我们将学习如何在 PHP 中使用 XML。你会惊讶地发现,你可以使用纯 XML 文件作为常规重型数据库引擎的轻量级替代品。在此之前,祝大家编码愉快。
第八章. 使用面向对象的方法烹饪 XML
XML(可扩展 标记 语言)是存储多用途数据的重要格式。它也被称为通用数据格式,因为你可以借助渲染器表示任何内容,并正确地可视化数据。XML 最大的优点之一是它可以通过 XSLT 轻松地将一种数据形式转换为另一种形式。此外,XML 数据易于阅读。
PHP5 的一个巨大优势是它对操作 XML 的出色支持。PHP5 附带了一些新的 XML 扩展,可以轻松处理 XML。你有一个全新的SimpleXML API,可以以纯面向对象的方式读取 XML 文档。此外,你还有DOMDocument对象来解析和创建 XML 文档。在本章中,我们将学习这些 API,并学习如何使用 PHP 成功处理 XML。
XML 的构成
让我们看看一个常见的 XML 文档的结构,以防你对 XML 一无所知。如果你已经熟悉 XML,我们强烈推荐你在本章中学习,那么这不是为你准备的章节。
让我们看看以下示例,它表示一组电子邮件:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<emails>
<email>
<from>nowhere@notadomain.tld</from>
<to>unknown@unknown.tld</to>
<subject>there is no subject</subject>
<body>is it a body? oh ya</body>
</email>
</emails>
因此,你会发现 XML 文档在顶部确实有一个小的声明,它详细说明了文档的字符集。如果你正在存储 Unicode 文本,这很有用。在 XML 中,你必须像开始时一样关闭标签。(XML 比 HTML 严格,你必须遵循约定。)
让我们看看另一个例子,其中数据中包含一些特殊符号:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<emails>
<email>
<from>nowhere@notadomain.tld</from>
<to>unknown@unknown.tld</to>
<subject>there is no subject</subject>
<body><![CDATA[is it a body? oh ya, with some texts
& symbols]]></body>
</email>
</emails>
这意味着你必须用CDATA将包含特殊字符的所有字符串括起来。
再次强调,每个实体可能都有一些与之相关的属性。例如,考虑以下 XML,其中我们描述了学生的属性:
<student age= "17" class= "11" title= "Mr.">Ozniak</student>
在上面的例子中,这个student标签有三个属性——age、class和title。使用 PHP,我们也可以轻松地操作它们。在接下来的章节中,我们将学习如何解析 XML 文档,或者如何动态创建 XML 文档。
SimpleXML 简介
在 PHP4 中,有两种解析 XML 文档的方法,这些方法在 PHP5 中也是可用的。一种是通过 SAX(这是一个标准)解析文档,另一种是 DOM。但是使用 SAX 解析 XML 文档需要相当长的时间,编写代码也需要相当长的时间。
在 PHP5 中,引入了一个新的 API,可以轻松解析 XML 文档。这个 API 被命名为 SimpleXML API。使用 SimpleXML API,你可以将你的 XML 文档转换为数组。每个节点都将转换为易于解析的访问形式。
解析文档
在本节中,我们将学习如何使用 SimpleXML 解析基本的 XML 文档。让我们喘口气,开始吧。
<?
$str = <<< END
<emails>
<email>
<from>nowhere@notadomain.tld</from>
<to>unknown@unknown.tld</to>
<subject>there is no subject</subject>
<body><![CDATA[is it a body? oh ya, with some texts &
symbols]]></body>
</email>
</emails>
END;
$sxml = simplexml_load_string($str);
print_r($sxml);
?>
输出如下:
SimpleXMLElement Object
(
[email] => SimpleXMLElement Object
(
[from] => nowhere@notadomain.tld
[to] => unknown@unknown.tld
[subject] => there is no subject
[body] => SimpleXMLElement Object
(
)
)
)
那么,现在你可能想知道如何单独访问这些属性中的每一个。你可以像访问一个对象一样访问它们。例如,$sxml->email[0]返回第一个电子邮件对象。要访问此电子邮件下的from元素,你可以使用以下代码,例如:
echo $sxml->email[0]->from
因此,每个对象,除非有多个相同的对象,否则只需通过其名称即可访问。否则,你必须像访问集合一样访问它们。例如,如果你有多个元素,你可以使用foreach循环访问每个元素:
foreach ($sxml->email as $email)
echo $email->from;
访问属性
正如我们在上一个示例中看到的,XML 节点可能具有属性。还记得那个包含class、age和title的示例文档吗?现在你可以使用 SimpleXML API 轻松访问这些属性。让我们看看以下示例:
<?
$str = <<< END
<emails>
<email type="mime">
<from>nowhere@notadomain.tld</from>
<to>unknown@unknown.tld</to>
<subject>there is no subject</subject>
<body><![CDATA[is it a body? oh ya, with some texts &
symbols]]></body>
</email>
</emails>
END;
$sxml = simplexml_load_string($str);
foreach ($sxml->email as $email)
echo $email['type'];
?>
这将在输出窗口中显示文本mime。所以如果你仔细看,你会明白每个节点都可以像对象属性一样访问,所有属性都可以像数组键一样访问。SimpleXML 使 XML 解析变得非常有趣。
使用 SimpleXML 解析 Flickr 源
关于在咖啡里加一些牛奶和糖怎么样?到目前为止,我们已经学习了 SimpleXML API 是什么以及如何使用它。如果能看到一个实际例子会更好。在这个例子中,我们将解析 Flickr 源并显示图片。听起来很酷?让我们来做吧。
如果你感兴趣,想看看 Flickr 公共照片源的样子,这里就是内容。源数据是从www.flickr.com/services/feeds/photos_public.gne收集的:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<feed
>
<title>Everyone's photos</title>
<link rel="self"
href="http://www.flickr.com/services/feeds/photos_public.gne" />
<link rel="alternate" type="text/html"
href="http://www.flickr.com/photos/"/>
<id>tag:flickr.com,2005:/photos/public</id>
<icon>http://www.flickr.com/images/buddyicon.jpg</icon>
<subtitle></subtitle>
<updated>2007-07-18T12:44:52Z</updated>
<generator uri="http://www.flickr.com/">Flickr</generator>
<entry>
<title>A-lounge 9.07_6</title>
<link rel="alternate" type="text/html"
href="http://www.flickr.com/photos/dimitranova/845455130/"/>
<id>tag:flickr.com,2005:/photo/845455130</id>
<published>2007-07-18T12:44:52Z</published>
<updated>2007-07-18T12:44:52Z</updated>
<dc:date.Taken>2007-07-09T14:22:55-08:00</dc:date.Taken>
<content type="html"><p><a
href="http://www.flickr.com/people/dimitranova/"
>Dimitranova</a> posted a photo:</p>
<p><a
href="http://www.flickr.com/photos/dimitranova/845455130/
" title="A-lounge 9.07_6"><img src="
http://farm2.static.flickr.com/1285/845455130_dce61d101f_m.jpg
" width="180" height="240" alt="
A-lounge 9.07_6" /></a></p>
</content>
<author>
<name>Dimitranova</name>
<uri>http://www.flickr.com/people/dimitranova/</uri>
</author>
<link rel="license" type="text/html" href="deed.en-us" />
<link rel="enclosure" type="image/jpeg"
href="http://farm2.static.flickr.com/1285/
845455130_7ef3a3415d_o.jpg" />
</entry>
<entry>
<title>DSC00375</title>
<link rel="alternate" type="text/html"
href="http://www.flickr.com/photos/53395103@N00/845454986/"/>
<id>tag:flickr.com,2005:/photo/845454986</id>
<published>2007-07-18T12:44:50Z</published>
...
</entry>
</feed>
现在我们将提取每个条目的描述并显示出来。让我们玩得开心一些:
<?
$content =
file_get_contents(
"http://www.flickr.com/services/feeds/photos_public.gne ");
$sx = simplexml_load_string($content);
foreach ($sx->entry as $entry)
{
echo "<a href='{$entry->link['href']}'>".$entry->title."</a><br/>";
echo $entry->content."<br/>";
}
?>
这将创建以下输出。看看,SimpleXML 有多简单?以上脚本的输出如下所示:

使用 SimpleXML 管理 CDATA 部分
如我们之前所说,一些符号不能直接作为任何节点的值出现,除非你使用CDATA标签将其包围。例如,看看以下示例:
<?
$str = <<<EOT
<data>
<content>text & images </content>
</data>
EOT;
$s = simplexml_load_string($str);
?>
这将生成以下错误:
<br />
<b>Warning</b>: simplexml_load_string()
[<a href='function.simplexml-load-string'>
function.simplexml-load-string</a>]:
Entity: line 2: parser error : xmlParseEntityRef:
no name in <b>C:\OOP with PHP5\Codes\ch8\cdata.php</b>
on line <b>10</b><br /><br />
<b>Warning</b>: simplexml_load_string()
[<a href='function.simplexml-load-string'>
function.simplexml-load-string</a>]:
<content>text & images </content>
in <b>C:\OOP with PHP5\Codes\ch8\cdata.php</b>
on line <b>10</b><br /><br />
<b>Warning</b>: simplexml_load_string()
[<a href='function.simplexml-load-string'>
function.simplexml-load-string</a>]:
^ in <b>C:\OOP with PHP5\Codes\ch8\cdata.php</b>
on line <b>10</b><br />
为了避免这个问题,我们必须使用CDATA标签将其包围。让我们这样重写它:
<data>
<content><![CDATA[text & images ]]></content>
</data>
现在它将完美工作。而且你不需要做任何额外的工作来管理这个CDATA部分。
<?
$str = <<<EOT
<data>
<content><![CDATA[text & images ]]></content>
</data>
EOT;
$s = simplexml_load_string($str);
echo $s->content;//print "text & images"
?>
然而,在 PHP5.1 之前,你必须像下面这样加载这个部分:
$s = simplexml_load_string($str,null,LIBXML_NOCDATA);
XPath
SimpleXML 的另一个很好的补充是,你可以使用 XPath 进行查询。那么什么是 XPath 呢?它是一种表达式语言,帮助你使用格式化的输入来定位特定的节点。在本节中,我们将学习如何使用 SimpleXML 和 XPath 来定位 XML 文档的特定部分。让我们看看以下 XML:
<?xml version="1.0" encoding="utf-8"?>
<roles>
<task type="analysis">
<state name="new">
<assigned to="cto">
<action newstate="clarify" assignedto="pm">
<notify>pm</notify>
<notify>cto</notify>
</action>
</assigned>
</state>
<state name="clarify">
<assigned to="pm">
<action newstate="clarified" assignedto="pm">
<notify>cto</notify>
</action>
</assigned>
</state>
</task>
</roles>
这份文档简单地说明了分析任务的流程,然后告诉它在哪个状态下应该做什么。所以现在你想搜索当任务类型是analysis、分配给cto且当前状态是new时应该做什么。SimpleXML 使这变得非常简单。让我们看看以下代码:
<?
$str = <<< EOT
<roles>
<task type="analysis">
<state name="new">
<assigned to="cto">
<action newstate="clarify" assignedto="pm">
<notify>pm</notify>
<notify>cto</notify>
</action>
</assigned>
</state>
<state name="clarify">
<assigned to="pm">
<action newstate="clarified" assignedto="pm">
<notify>cto</notify>
</action>
</assigned>
</state>
</task>
</roles>
EOT;
$s = simplexml_load_string($str);
$node = $s->xpath("//task[@type='analysis']/state[@name='new']
/assigned[@to='cto']");
echo $node[0]->action[0]['newstate']."\n";
echo $node[0]->action[0]->notify[0];
?>
这将输出以下内容:
clarify
pm
然而,在编写 XPath 时,有一些事情需要记住。当你的 XPath 后面跟着/时,这意味着你应该保持 XML 文档的精确顺序。例如:
echo count($s->xpath("//state"));
这将输出2。
//state意味着从文档的任何地方获取状态节点。现在,如果你指定task//state,它将返回所有任务下的所有状态。例如,以下代码将输出3和3:
echo count($s->xpath("//notify"));
echo count($s->xpath("task//notify"));
现在假设你想在state节点下找到notify,紧随assigned之后,紧随action之后?你的 XPath 查询应该是//state/assigned/action/notify。
但如果你想要这样,它应该正好位于task节点下,该节点位于根节点下,它应该是/task/state/assigned/action/notify。
如果你需要匹配任何属性,则应将其匹配为[@AttributeName1='value'] [@AttributeName2='value']。如果你看到以下 XPath,它将对你很清晰:
//task[@type='analysis']/state[@name='new']/assigned[@to='cto']
DOM API
PHP 中的 SimpleXML 用于解析文档,但它不能创建任何 XML 文档。要动态创建 XML 文档,你必须使用随 PHP 5 捆绑的 DOM API。使用 DOM API,你还可以轻松创建页面抓取工具。
在本节中,我们将学习如何使用 DOM API 创建 XML 文档,然后我们将学习如何解析现有文档并修改它们。
在以下示例中,我们将创建一个基本的 HTML 文件:
<?
$doc = new DOMDocument("1.0","UTF-8");
$html = $doc->createElement("html");
$body = $doc->createElement("body");
$h1 = $doc->createElement("h1","OOP with PHP");
$body->appendChild($h1);
$html->appendChild($body);
$doc->appendChild($html);
echo $doc->saveHTML();
?>
这将生成以下代码:
<html>
<body>
<h1>OOP with PHP</h1>
</body>
</html>
这相当简单,对吧?
让我们再做一些练习:
<?
$doc = new DOMDocument("1.0","UTF-8");
$html = $doc->createElement("html");
$body = $doc->createElement("body");
$h1 = $doc->createElement("h1","OOP with PHP");
$h1->setAttribute("id","firsth1");
$p = $doc->createElement("p");
$p->appendChild($doc->createTextNode("Hi - how about some text?"));
$body->appendChild($h1);
$body->appendChild($p);
$html->appendChild($body);
$doc->appendChild($html);
echo $doc->saveHTML();
?>
这将生成以下代码。
<html><body>
<h1 id="firsth1">OOP with PHP</h1>
<p>Hi - how about some text?</p>
</body></html>
因此,你可以使用以下代码将 DOM 引擎生成的 XML 保存到你的文件系统中的一个文件中:
file_put_contents("c:/abc.xml", $doc->saveHTML());
修改现有文档
DOM API 不仅有助于轻松创建 XML 文档,还提供了轻松加载和修改现有文档的访问。以下 XML 示例,我们将加载我们几分钟前创建的文件,然后我们将更改第一个h1对象的标题测试:
<?php
$uri = 'c:/abc.xml';
$document = new DOMDocument();
$document->loadHTMLFile($uri);// load the content of this URL as HTML
$h1s = $document->getElementsByTagName("h1");//find all h1 elements
$newText = $document->createElement("h1","New Heading");//created a
//new h1 element
$h1s->item(0)->parentNode->insertBefore($newText,
$h1s->item(0));//insert before the existing h1 element
$h1s->item(0)->parentNode->removeChild($h1s->item(1));//remove the
//old h1 element
echo $document->saveHTML();//display the content as HTML
?>
以下是输出结果:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
"http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<h1>New Heading</h1>
<p>Hi - how about some text?</p>
</body></html>
其他有用函数
DOM 库中还有一些其他有用的函数。我们不会深入讨论它们,但它们包含在本节中,以提供一个简短的概述。
-
DomNode->setAttribute(): 帮助设置任何节点的属性 -
DomNode->hasChildNodes(): 如果 DOM 节点有子节点,则返回 true -
DomNode->replaceChild(): 用另一个节点替换任何子节点 -
DomNode->cloneNode(): 创建当前代码的深拷贝
摘要
在 PHP5 中,XML API 在 Web 应用程序开发中扮演着非常重要的角色,尤其是新的 SimpleXML API,它简化了解析过程。今天,XML 几乎是所有大型应用程序使用最多的数据格式之一。因此,熟悉 XML API 和相关技术无疑将帮助你更容易地设计健壮的基于 XML 的应用程序。
在下一章中,我们将学习 MVC 架构,并自己构建一个简洁的 MVC 框架。
第九章:用 MVC 构建更好的应用
在第四章中,我们学习了设计模式如何通过提供解决问题的常用方法来简化你的日常编程生活。在应用架构中,一种流行的设计模式是模型-视图-控制器(Model-View-Controller),也称为MVC。在 PHP 的快速应用开发(Rapid Application Development, RAD)中,MVC 框架扮演着至关重要的角色。如今,几个 MVC 框架已经引起了公众的兴趣,其中许多已经准备好用于企业级应用。例如,symfony框架已被用于开发 Yahoo 书签,CakePHP 正在重构 Mambo,CodeIgniter 被许多在网站上展示的大型应用所使用。还有像 Zend Framework 这样的流行 MVC 框架,它被 IBM 使用,也被用于开发 Magento 开源电子商务解决方案。
因此,如今,从头开始编写代码并进行微调已经过时了,如果你正在这样做,你应该真正避免这样做。在本章中,我们将讨论 MVC 框架的基本结构,然后介绍一些这些流行的框架。
什么是 MVC?
如其名所示,MVC 由三个组件组成。第一个是模型(Model),第二个是视图(View),第三个是控制器(Controller)。如果我们只是列出名称,这并没有什么意义。首先,模型是一个与数据库交互的对象。所有业务逻辑通常都写在模型中。控制器是一段代码,它接收用户输入,基于这些输入初始化模型和其他对象,并最终调用它们。最后,视图是一个组件,它使用模型帮助显示控制器生成的结果。
因此,为了良好的实践,你永远不应该在视图或控制器中实现任何业务逻辑。同样,你也不应该在模型中处理输出结果。而且,你永远不应该直接从控制器中产生任何输出(而是使用视图)。
在接下来的章节中,我们将创建一个非常小的 MVC。
项目规划
为了成功开发任何应用,你必须有一个明确的目标。每当一个应用架构强大、稳定、无懈可击时,你将获得大量用户使用你的应用。在本章中,我们将开发的 MVC 框架将成功解决以下问题:
-
脚本小
-
容易加载组件、库、辅助工具和模型
-
开发视图的优雅且灵活的语法
-
对流行数据库服务器的出色支持
-
不会占用大量资源
-
易于使用
-
容易与其他组件框架如 Pear、ezComponents 等集成。
-
支持缓存
-
类似于 RubyOnRails 的布局支持,便于设计你的 Web 应用
-
原生 gzip 压缩器用于 JavaScript
-
支持 Ajax
设计引导文件
引导文件是一个文件,它只为成功执行和集成控制器、模型和视图准备环境。基本上,引导文件初始化环境、路由器、对象加载器,并将所有输入参数传递给控制器。我们将设计一个引导文件,它将借助mod_rewrite接收成功请求 URL 的所有参数。
注意
mod_rewrite是一个 Apache 模块,它通过模式(正则表达式)帮助将请求重定向到另一个请求 URL。对于几乎每个设计的 Web 应用程序来说,这是一个必不可少的模块。如果你对它感兴趣并想了解更多,你可以访问:httpd.apache.org/docs/2.0/mod/mod_rewrite.html
要启用mod_rewrite,你可以按照以下细节操作。首先,打开httpd.conf并添加以下行:
LoadModule rewrite_module modules/mod_rewrite.so
<Directory />
Options FollowSymLinks
AllowOverride None
Order deny,allow
Deny from all
Satisfy all
</Directory>
我们必须在.htaccess文件中放置以下代码,并将其放置在我们的应用程序根目录内。
RewriteEngine on
RewriteCond $1 !^(index\.php|images|robots\.txt)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?$1
这段代码将仅仅将每个请求重定向到index.php,这将是我们的引导文件。这个引导文件将接收任何请求的 URL,并将其拆分为不同的部分,如控制器、操作和参数。例如,格式将是http://our_application/controller/action/param/param..../param。引导文件将借助路由器分析 URL,然后借助分发器调用控制器和操作,并传递所有参数。
下面是我们引导文件(index.php)的代码:
<?
include("core/ini.php");
initializer::initialize();
$router = loader::load("router");
dispatcher::dispatch($router);
?>
在上面的代码中,你可以看到一个名为loader的对象。这个对象的主要目的是为我们加载对象,但通过单例模式。这将帮助我们最小化加载。使用这个 loader,我们将通过单例模式加载一个名为router的对象。还有一个名为dispatcher的对象,它将最终借助路由器将 Web 请求分发出去。
让我们检查core/ini.php的代码,这是一个辅助工具,用于帮助轻松地从不同目录包含类文件。
<?
set_include_path(get_include_path().PATH_SEPARATOR."core/main");
function __autoload($object)
{
require_once("{$object}.php");
}
?>
下面是initializer文件(core/main/initializer.php)的内容:
<?
class initializer
{
public static function initialize()
{
set_include_path(get_include_path().PATH_SEPARATOR."core/main");
set_include_path(get_include_path().PATH_SEPARATOR.
"core/main/cache");
set_include_path(get_include_path().PATH_SEPARATOR."core/helpers");
set_include_path(get_include_path().PATH_SEPARATOR.
"core/libraries");
//set_include_path(get_include_path().PATH_SEPARATOR.
"app/controllers");
set_include_path(get_include_path().PATH_SEPARATOR."app/models");
set_include_path(get_include_path().PATH_SEPARATOR."app/views");
//include_once("core/config/config.php");
}
}
?>
如果你查看initializer文件的代码,你会发现它实际上只是扩展了包含路径。
下面是我们loader文件(core/main/loader.php)的代码,它将通过单例模式加载不同的组件。
<?
class loader
{
private static $loaded = array();
public static function load($object)
{
$valid = array( "library",
"view",
"model",
"helper",
"router",
"config",
"hook",
"cache",
"db");
if (!in_array($object,$valid))
{
$config = self::load("config");
if ("on"==$config->debug)
{
base::backtrace();
}
throw new Exception("Not a valid object '{$object}' to load");
}
if (empty(self::$loaded[$object])){
self::$loaded[$object]= new $object();
}
return self::$loaded[$object];
}
}
?>
Loader 使用另一个config文件(core/main/config.php),实际上是从config/configs.php文件下加载不同的configs:
<?
class config
{
private $config;
function __construct()
{
global $configs;
include_once("core/config/configs.php");
include_once("app/config/configs.php");
$this->config = $configs;
}
private function __get($var)
{
return $this->config[$var];
}
}
?>
如果你想知道我们的configs.php将如何看起来,下面是它的样子:
<?
$configs['debug']="on";
$configs['base_url']="http://localhost/orchid";
$configs['global_profile']=true;
$configs['allowed_url_chars'] = "/[^A-z0-9\/\^]/";
$configs['default_controller']="welcome";
?>
好吧,如果你查看loader.php的代码,会发现一个类似这样的部分:
$config = self::load("config");
if ("on"==$config->debug)
{
base::backtrace();
}
所以$config->debug实际上是通过config.php中的__get() 魔法方法返回$configs['debug']的值。
在 loader 中有一个名为base::backtrace()的方法。base是在core/libraries/base.php中声明的静态对象。它包含了一些在整个框架中使用的有用函数。这是在core/libraries/base.php中:
<?
class base{
public static function pr($array)
{
echo "<pre>";
print_r($array);
echo "</pre>";
}
public static function backtrace()
{
echo "<pre>";
debug_print_backtrace();
echo "</pre>";
}
public static function basePath()
{
return getcwd();
}
public static function baseUrl()
{
$conf = loader::load("config");
return $conf->base_url;
}
?>
因此,base::backtrace() 实际上打印 debug_backtrace 以便于跟踪异常。
到目前为止,我们还没有看到 router.php 和 dispatcher.php 的代码。路由器和调度器是整个应用的主要部分。以下是 router.php 的代码(core/main/router.php):
<?
class router
{
private $route;
private $controller;
private $action;
private $params;
public function __construct()
{
if(file_exists("app/config/routes.php")){
require_once("app/config/routes.php");
}
$path = array_keys($_GET);
$config = loader::load("config");
if (!isset($path[0]))
{
$default_controller = $config->default_controller;
if (!empty($default_controller))
$path[0] = $default_controller;
else
$path[0] = "index";
}
$route= $path[0];
$sanitzing_pattern = $config->allowed_url_chars;
$route = preg_replace($sanitzing_pattern, "", $route);
$route = str_replace("^","",$route);
$this->route = $route;
$routeParts = split( "/",$route);
$this->controller=$routeParts[0];
$this->action=isset($routeParts[1])? $routeParts[1]:"base";
array_shift($routeParts);
array_shift($routeParts);
$this->params=$routeParts;
/* match user defined routing pattern */
if (isset($routes)){
foreach ($routes as $_route)
{
$_pattern = "~{$_route[0]}~";
$_destination = $_route[1];
if (preg_match($_pattern,$route))
{
$newrouteparts = split("/",$_destination);
$this->controller = $newrouteparts[0];
$this->action = $newrouteparts[1];
}
}
}
}
public function getAction()
{
if (empty($this->action)) $this->action="main";
return $this->action;
}
public function getController()
{
return $this->controller;
}
public function getParams()
{
return $this->params;
}
}
?>
路由器实际上执行的操作是从请求 URL 中找到控制器、操作和参数。如果找不到控制器名称,它将使用默认控制器名称;如果默认控制器名称在 config 文件中找不到,它将使用 index 作为默认控制器。
在继续到调度器之前,我们必须看看视图引擎,这将用于模板引擎,这样任何从控制器都可以设置变量,例如 $this->view->set(varname, value)。之后,任何人都可以在我们的视图文件中以 $varname 的形式访问该变量。
接下来是视图引擎(core/main/view.php):
<?
class view
{
private $vars=array();
private $template;
public function set($key, $value)
{
$this->vars[$key]=$value;
}
public function getVars(&$controller=null)
{
if (!empty($controller)) $this->vars['app']=$controller;
return $this->vars;
}
public function setTemplate($template)
{
$this->template = $template;
}
public function getTemplate($controller=null)
{
if (empty($this->template)) return $controller;
return $this->template;
}
private function __get($var)
{
return loader::load($var);
}
}
?>
接下来是调度器,它是我们框架的核心部分(core/main/dispatcher.php):
<?
class dispatcher
{
public static function dispatch($router)
{
global $app;
//$cache = loader::load("cache");
ob_start();
$config = loader::load("config");
if ($config->global_profile) $start = microtime(true);
$controller = $router->getController();
$action = $router->getAction();
$params = $router->getParams();
if (count($params)>1){
if ("unittest"==$params[count($params)-1] ||
'1'==$_POST['unittest'])unittest::setUp();
}
$controllerfile = "app/controllers/{$controller}.php";
if (file_exists($controllerfile)){
require_once($controllerfile);
$app = new $controller();
$app->use_layout = true;
$app->setParams($params);
$app->$action();
unittest::tearDown();
ob_end_clean();
//manage view
ob_start();
$view = loader::load("view");
$viewvars = $view->getVars($app);
$uselayout = $config->use_layout;
if (!$app->use_layout) $uselayout=false;
$template = $view->getTemplate($action);
base::_loadTemplate($controller, $template,
$viewvars, $uselayout);
if (isset($start))
echo "<p>Total time for dispatching is :
".(microtime(true)-$start)." seconds.</p>";
$output = ob_get_clean();
//$cache->set("abcde",array
("content"=>base64_encode($output)));
echo $output;
}
else
throw new Exception("Controller not found");
}
}
?>
下面是调度器主要执行的操作(如上代码中高亮部分所示)。它接受一个路由对象作为参数,然后从路由器中找到控制器、操作和参数。如果控制器文件可用,它将加载该文件并初始化控制器。初始化后,它仅访问操作。
之后,调度器使用加载器初始化当前视图对象。由于它是通过 Singleton 来的,所以设置给它的所有变量仍然在作用域内。然后调度器将视图模板文件和变量传递给 base 中的 _loadTemplate 函数。
$uselayout 的目的是什么?它只是指示是否应该将布局文件附加到我们的模板上。当我们在实践中看到它时,这更有趣。
下面是 base::_loadTemplate() 函数:
public static function _loadTemplate($controller, $template,
$vars, $uselayout=false)
{
extract($vars);
if ($uselayout)
ob_start();
$templatefile ="app/views/{$controller}/{$template}.php";
if (file_exists($templatefile)){
include_once($templatefile);
}
else
{
throw new Exception("View '{$template}.php' is not found in
views/{$controller} directory.");
}
if ($uselayout) {
$layoutdata = ob_get_clean();
$layoutfilelocal = "app/views/{$controller}/{$controller}.php";
$layoutfileglobal = "app/views/layouts/{$controller}.php";
if (file_exists($layoutfilelocal))
include_once($layoutfilelocal);
else
include_once($layoutfileglobal);
}
}
如果您对这些文件的放置感到困惑,这里有一个目录结构来帮助您理解:

为什么还有像 jsm.php、benchmark.php、unittest.php、helper.php、model.php、library.php、cache.php 和 db.php 这样的其他文件?
-
这些文件将帮助我们理解以下部分:
-
jsm.php:帮助使用自动 gzip 压缩加载 JavaScript -
db.php:用于连接不同的数据库 -
library.php:帮助加载库文件 -
unittest.php:将帮助自动化单元测试 -
model.php:将帮助加载用于数据库访问的模型
现在我们来看看我们的 model 和 library 都在做什么。
接下来是 core/main/model.php:
<?
class model
{
private $loaded = array();
private function __get($model)
{
$model .="model";
$modelfile = "app/models/{$model}.php";
$config = loader::load("config");
if (file_exists($modelfile))
{
include_once($modelfile);
if (empty($this->loaded[$model]))
{
$this->loaded[$model]=new $model();
}
$modelobj = $this->loaded[$model];
if ($config->auto_model_association)
{
$this->associate($modelobj, $_REQUEST); //auto association
}
return $modelobj;
}
else
{
throw new Exception("Model {$model} is not found");
}
}
private function associate(&$obj, $array)
{
foreach ($array as $key=>$value)
{
if (property_exists($obj, $key))
{
$obj->$key = $value;
}
}
}
}
?>
每当表单提交时,我们希望在初始化模型后立即填充它。因此,我们保留了一个名为 auto_model_association 的配置变量。如果您将其设置为 true,模型将自动关联。
接下来是库加载器(core/main/library.php):
<?
class library{
private $loaded = array();
private function __get($lib)
{
if (empty($this->loaded[$lib]))
{
$libnamecore = "core/libraries/{$lib}.php";
$libnameapp = "app/libraries/{$lib}.php";
if (file_exists($libnamecore))
{
require_once($libnamecore);
$this->loaded[$lib]=new $lib();
}
else if(file_exists($libnameapp))
{
require_once($libnameapp);
$this->loaded[$lib]=new $lib();
}
else
{
throw new Exception("Library {$lib} not found.");
}
}
return $this->loaded[$lib];
}
}
?>
library.php 只帮助通过 Singleton 加载库。
现在我们将看到 JavaScript 加载器,它默认使用 gzip 压缩来提供每个库。如今,每个浏览器都支持 gzip 压缩,以加快任何对象的加载速度。我们还在我们的框架中内置了对 prototype、jQuery 和 script.aculo.us 的支持。
这里是 core/libraries/jsm.php:
<?
/**
* Javascript Manager
*
*/
class jsm
{
function loadPrototype()
{
$base = base::baseUrl();
echo "<script type='text/javascript'
src='{$base}/core/js/gzip.php?js=prototypec.js'>\n";
}
function loadScriptaculous()
{
$base = base::baseUrl();
echo "<script type='text/javascript'
src='{$base}/core/js/gzip.php?js=scriptaculousc.js'>\n";
}
function loadProtaculous()
{
$base = base::baseUrl();
echo "<script type='text/javascript'
src='{$base}/core/js/gzip.php?js=prototypec.js'>\n";
echo "<script type='text/javascript'
src='{$base}/core/js/gzip.php?js=scriptaculousc.js'>\n";
}
function loadJquery()
{
$base = base::baseUrl();
echo "<script type='text/javascript'
src='{$base}/core/js/gzip.php?js=jqueryc.js'>\n";
}
/**
* app specific libraries
*
* @param string $filename
*/
function loadScript($filename)
{
$base = base::baseUrl();
$script = $base."/app/js/{$filename}.js";
echo "<script type='text/javascript'
src='{$base}/core/js/gzip.php?js={$script}'>\n";
}
}
?>
如果你查看代码,你会发现它通过 gzip.php 加载每个 JavaScript 文件,实际上 gzip.php 负责压缩内容。所以这里是 gzip.php 的代码(core/js/gzip.php):
<?php
ob_start("ob_gzhandler");
header("Content-type: text/javascript; charset: UTF-8");
header("Cache-Control: must-revalidate");
$offset = 60 * 60 * 24 * 3;
$ExpStr = "Expires: " .
gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
header($ExpStr);
$js = $_GET['js'];
if (in_array($js,
array("prototypec.js","scriptaculousc.js","jqueryc.js")))
include(urldecode($_GET['js']));
?>
如果你还有其他库要加载,你可以修改这个库,并在以下行中添加它们。
if (in_array($js,
array("prototypec.js","scriptaculousc.js","jqueryc.js")))
最后,我们还有一个文件,它帮助我们在我们应用程序的开发过程中编写单元测试。unittest.php 负责这个,还有一个布尔配置标志:unit_test_enabled。
这里是 core/main/unittest.php:
<?
class unittest
{
private static $results = array();
private static $testmode = false;
public static function setUp()
{
$config = loader::load("config");
if ($config->unit_test_enabled){
self::$results = array();
self::$testmode = true;
}
}
public static function tearDown()
{
if (self::$testmode)
{
self::printTestResult();
self::$results = array();
self::$testmode = false;
die();
}
}
public static function printTestResult()
{
foreach (self::$results as $result)
{
echo $result."<hr/>";
}
}
public static function assertTrue($object)
{
if (!self::$testmode) return 0;
if (true==$object) $result = "passed";
self::saveResult(true, $object, $result);
}
public static function assertEqual($object, $constant)
{
if (!self::$testmode) return 0;
if ($object==$constant)
{
$result = 1;
}
self::saveResult($constant, $object, $result);
}
private static function getTrace()
{
$result = debug_backtrace();
$cnt = count($result);
$callerfile = $result[2]['file'];
$callermethod = $result[3]['function'];
$callerline = $result[2]['line'];
return array($callermethod, $callerline, $callerfile);
}
private static function saveResult($expected, $actual,
$result=false)
{
if (empty($actual)) $actual = "null/false";
if ("failed"==$result || empty($result))
$result = "<font color='red'><strong>failed</strong></font>";
else
$result = "<font color='green'><strong>passed</strong></font>";
$trace = self::getTrace();
$finalresult = "Test {$result} in Method:
<strong>{$trace[0]}</strong>. Line:
<strong>{$trace[1]}</strong>. File:
<strong>{$trace[2]}</strong>. <br/> Expected:
<strong>{$expected}</strong>, Actual:
<strong>{$actual}</strong>. ";
self::$results[] = $finalresult;
}
public static function assertArrayHasKey($key, array $array,
$message = '')
{
if (!self::$testmode) return 0;
if (array_key_exists($key, $array))
{
$result = 1;
self::saveResult("Array has a key named '{$key}'",
"Array has a key named '{$key}'", $result);
return ;
}
self::saveResult("Array has a key named '{$key}'",
"Array has not a key named '{$key}'", $result);
}
public static function assertArrayNotHasKey($key, array $array,
$message = '')
{
if (!self::$testmode) return 0;
if (!array_key_exists($key, $array))
{
$result = 1;
self::saveResult("Array has not a key named '{$key}'",
"Array has not a key named '{$key}'", $result);
return ;
}
self::saveResult("Array has not a key named '{$key}'",
"Array has a key named '{$key}'", $result);
}
public static function assertContains($needle, $haystack,
$message = '')
{
if (!self::$testmode) return 0;
if (in_array($needle,$haystack))
{
$result = 1;
self::saveResult("Array has a needle named '{$needle}'",
"Array has a needle named '{$needle}'", $result);
return ;
}
self::saveResult("Array has a needle named '{$needle}'",
"Array has not a needle named '{$needle}'", $result);
}
}
?>
我们必须保留内置的代码基准测试支持,以帮助分析。因此,我们有 benchmark.php (core/main/benchmark.php) 来执行它:
<?
class benchmark
{
private $times = array();
private $keys = array();
public function setMarker($key=null)
{
$this->keys[] = $key;
$this->times[] = microtime(true);
}
public function initiate()
{
$this->keys= array();
$this->times= array();
}
public function printReport()
{
$cnt = count($this->times);
$result = "";
for ($i=1; $i<$cnt; $i++)
{
$key1 = $this->keys[$i-1];
$key2 = $this->keys[$i];
$seconds = $this->times[$i]-$this->times[$i-1];
$result .= "For step '{$key1}' to '{$key2}' : {$seconds}
seconds.</br>";
}
$total = $this->times[$i-1]-$this->times[0];
$result .= "Total time : {$total} seconds.</br>";
echo $result;
}
}
?>
添加数据库支持
我们的框架必须有一个数据抽象层,以简化数据库操作。我们将为三个流行的数据库提供支持:SQLite、PostgreSQL 和 MySQL。以下是我们的数据抽象层代码在 core/main/db.php 中:
<?
include_once("dbdrivers/abstract.dbdriver.php");
class db
{
private $dbengine;
private $state = "development";
public function __construct()
{
$config = loader::load("config");
$dbengineinfo = $config->db;
if (!$dbengineinfo['usedb']==false)
{
$driver = $dbengineinfo[$this->state]['dbtype'].'driver';
include_once("dbdrivers/{$driver}.php");
$dbengine = new $driver($dbengineinfo[$this->state]);
$this->dbengine = $dbengine;
}
}
public function setDbState($state)
{
//must be 'development'/'production'/'test' or whatever
if (empty($this->dbengine)) return 0;
$config = loader::load("config");
$dbengineinfo = $config->db;
if (isset($dbengineinfo[$state]))
{
$this->state = $state;
}
else
{
throw new Exception("No such state in config filed called
['db']['{$state}']");
}
}
private function __call($method, $args)
{
if (empty($this->dbengine)) return 0;
if (!method_exists($this, $method))
return call_user_func_array(array($this->dbengine,
$method),$args);
}
/*private function __get($property)
{
if (property_exists($this->dbengine,$property))
return $this->dbengine->$property;
}*/
}
?>
它使用一个抽象驱动对象来确保驱动对象的可扩展性和一致性。将来,如果任何第三方开发者想要引入新的驱动,他必须在 core/main/dbdrivers/abstract.dbdriver.php 中扩展它:
<?
define ("FETCH_ASSOC",1);
define ("FETCH_ROW",2);
define ("FETCH_BOTH",3);
define ("FETCH_OBJECT",3);
abstract class abstractdbdriver
{
protected $connection;
protected $results = array();
protected $lasthash = "";
public function count()
{
return 0;
}
public function execute($sql)
{
return false;
}
private function prepQuery($sql)
{
return $sql;
}
public function escape($sql)
{
return $sql;
}
public function affectedRows()
{
return 0;
}
public function insertId()
{
return 0;
}
public function transBegin()
{
return false;
}
public function transCommit()
{
return false;
}
public function transRollback()
{
return false;
}
public function getRow($fetchmode = FETCH_ASSOC)
{
return array();
}
public function getRowAt($offset=null,$fetchmode = FETCH_ASSOC)
{
return array();
}
public function rewind()
{
return false;
}
public function getRows($start, $count, $fetchmode = FETCH_ASSOC)
{
return array();
}
}
?>
驾驶员
现在是难度最大的部分;驾驶员。让我们看看 SQLite 驱动文件 core/main/dbdrivers/sqlitedriver.php:
<?
class sqlitedriver extends abstractdbdriver
{
public function __construct($dbinfo)
{
if (isset($dbinfo['dbname']))
{
if (!$dbinfo['persistent'])
$this->connection =
sqlite_open($dbinfo['dbname'],0666,$errormessage);
else
$this->connection =
sqlite_popen($dbinfo['dbname'],0666,$errormessage);
if (!$this->connection)
{
throw new Exception($errormessage);
}
}
else
throw new Exception("You must supply database name for a
successful connection");
}
public function count()
{
$lastresult = $this->results[$this->lasthash];
//print_r($this->results);
$count = sqlite_num_rows($lastresult);
if (!$count) $count = 0;
return $count;
}
public function execute($sql)
{
$sql = $this->prepQuery($sql);
$parts = split(" ",trim($sql));
$type = strtolower($parts[0]);
$hash = md5($sql);
$this->lasthash = $hash;
if ("select"==$type)
{
if (isset($this->results[$hash]))
{
if (is_resource($this->results[$hash]))
return $this->results[$hash];
}
}
else if("update"==$type || "delete"==$type)
{
$this->results = array(); //clear the result cache
}
$this->results[$hash] = sqlite_query($sql,$this->connection);
}
private function prepQuery($sql)
{
return $sql;
}
public function escape($sql)
{
if (function_exists('sqlite_escape_string'))
{
return sqlite_escape_string($sql);
}
else
{
return addslashes($sql);
}
}
public function affectedRows()
{
return sqlite_changes($this->connection);
}
public function insertId()
{
return @sqlite_last_insert_rowid($this->connection);
}
public function transBegin()
{
$this->execute('BEGIN TRANSACTION');
}
public function transCommit()
{
$this->execute('COMMIT');
}
public function transRollback()
{
$this->execute('COMMIT');
}
public function getRow($fetchmode = FETCH_ASSOC)
{
$lastresult = $this->results[$this->lasthash];
if (FETCH_ASSOC == $fetchmode)
$row = sqlite_fetch_array($lastresult,SQLITE_ASSOC);
elseif (FETCH_ROW == $fetchmode)
$row = sqlite_fetch_array($lastresult, SQLITE_NUM);
elseif (FETCH_OBJECT == $fetchmode)
$row = sqlite_fetch_object($lastresult);
else
$row = sqlite_fetch_array($lastresult,SQLITE_BOTH);
return $row;
}
public function getRowAt($offset=null,$fetchmode = FETCH_ASSOC)
{
$lastresult = $this->results[$this->lasthash];
if (!empty($offset))
{
sqlite_seek($lastresult, $offset);
}
return $this->getRow($fetchmode);
}
public function rewind()
{
$lastresult = $this->results[$this->lasthash];
sqlite_rewind($lastresult);
}
public function getRows($start, $count, $fetchmode = FETCH_ASSOC)
{
$lastresult = $this->results[$this->lasthash];
sqlite_seek($lastresult, $start);
$rows = array();
for ($i=$start; $i<=($start+$count); $i++)
{
$rows[] = $this->getRow($fetchmode);
}
return $rows;
}
}
?>
如果你查看代码,你会发现我们只是在 abstractdbdriver.php 文件中实现了 abstractdbdriver 对象中描述的所有功能。
现在是 MySQL 驱动文件,core/main/dbdrivers/mysqldriver.php:
<?
class mysqldriver extends abstractdbdriver
{
public function __construct($dbinfo)
{
if (!empty($dbinfo['dbname']))
{
if ($dbinfo['persistent'])
$this->connection =
mysql_pconnect($dbinfo['dbhost'],$dbinfo['dbuser'],
$dbinfo['dbpwd']);
else
$this->connection =
mysql_connect($dbinfo['dbhost'],$dbinfo['dbuser'],
$dbinfo['dbpwd']);
mysql_select_db($dbinfo['dbname'],$this->connection);
}
else
throw new Exception("You must supply username, password,
hostname and database name for connecting to mysql");
}
public function execute($sql)
{
$sql = $this->prepQuery($sql);
$parts = split(" ",trim($sql));
$type = strtolower($parts[0]);
$hash = md5($sql);
$this->lasthash = $hash;
if ("select"==$type)
{
if (isset($this->results[$hash]))
{
if (is_resource($this->results[$hash]))
return $this->results[$hash];
}
}
else if("update"==$type || "delete"==$type)
{
$this->results = array(); //clear the result cache
}
$this->results[$hash] = mysql_query($sql,$this->connection);
}
public function count()
{
//print_r($this);
$lastresult = $this->results[$this->lasthash];
//print_r($this->results);
$count = mysql_num_rows($lastresult);
if (!$count) $count = 0;
return $count;
}
private function prepQuery($sql)
{
// "DELETE FROM TABLE" returns 0 affected rows.
// This hack modifies the query so that
// it returns the number of affected rows
if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql))
{
$sql = preg_replace("/^\s*DELETE\s+FROM\s+(\S+)\s*$/",
"DELETE FROM \\1 WHERE 1=1", $sql);
}
return $sql;
}
public function escape($sql)
{
if (function_exists('mysql_real_escape_string'))
{
return mysql_real_escape_string($sql, $this->conn_id);
}
elseif (function_exists('mysql_escape_string'))
{
return mysql_escape_string( $sql);
}
else
{
return addslashes($sql);
}
}
public function affectedRows()
{
return @mysql_affected_rows($this->connection);
}
public function insertId()
{
return @mysql_insert_id($this->connection);
}
public function transBegin()
{
$this->execute('SET AUTOCOMMIT=0');
$this->execute('START TRANSACTION'); // can also be BEGIN or
// BEGIN WORK
return TRUE;
}
public function transCommit()
{
$this->execute('COMMIT');
$this->execute('SET AUTOCOMMIT=1');
return TRUE;
}
public function transRollback()
{
$this->execute('ROLLBACK');
$this->execute('SET AUTOCOMMIT=1');
return TRUE;
}
public function getRow($fetchmode = FETCH_ASSOC)
{
$lastresult = $this->results[$this->lasthash];
if (FETCH_ASSOC == $fetchmode)
$row = mysql_fetch_assoc($lastresult);
elseif (FETCH_ROW == $fetchmode)
$row = mysql_fetch_row($lastresult);
elseif (FETCH_OBJECT == $fetchmode)
$row = mysql_fetch_object($lastresult);
else
$row = mysql_fetch_array($lastresult,MYSQL_BOTH);
return $row;
}
public function getRowAt($offset=null,$fetchmode = FETCH_ASSOC)
{
$lastresult = $this->results[$this->lasthash];
if (!empty($offset))
{
mysql_data_seek($lastresult, $offset);
}
return $this->getRow($fetchmode);
}
public function rewind()
{
$lastresult = $this->results[$this->lasthash];
mysql_data_seek($lastresult, 0);
}
public function getRows($start, $count, $fetchmode = FETCH_ASSOC)
{
$lastresult = $this->results[$this->lasthash];
mysql_data_seek($lastresult, $start);
$rows = array();
for ($i=$start; $i<=($start+$count); $i++)
{
$rows[] = $this->getRow($fetchmode);
}
return $rows;
}
function __destruct(){
foreach ($this->results as $result)
{
@mysql_free_result($result);
}
}
}
?>
最后,这里是 PostgreSQL 驱动,core/main/dbdrivers/postgresql.php:
<?
class pgsqldriver extends abstractdbdriver
{
public function __construct($dbinfo)
{
if (!empty($dbinfo['dbname']))
{
if ($dbinfo['persistent'])
$this->connection = pg_pconnect("host={$dbinfo['dbname']}
port=5432 dbname={$dbinfo['dbname']} user={$dbinfo['$dbuser']}
password={$dbinfo['dbpwd']}");
else
$this->connection = pg_connect("host={$dbinfo['dbname']}
port=5432 dbname={$dbinfo['dbname']} user={$dbinfo['$dbuser']}
password={$dbinfo['dbpwd']}");
}
else
throw new Exception("You must supply username, password,
hostname and database name for connecting to postgresql");
}
public function execute($sql)
{
$sql = $this->prepQuery($sql);
$parts = split(" ",trim($sql));
$type = strtolower($parts[0]);
$hash = md5($sql);
$this->lasthash = $hash;
if ("select"==$type)
{
if (isset($this->results[$hash]))
{
if (is_resource($this->results[$hash]))
return $this->results[$hash];
}
}
else if("update"==$type || "delete"==$type)
{
$this->results = array(); //clear the result cache
}
$this->results[$hash] = pg_query($this->connection,$sql);
}
public function count()
{
//print_r($this);
$lastresult = $this->results[$this->lasthash];
//print_r($this->results);
$count = pg_num_rows($lastresult);
if (!$count) $count = 0;
return $count;
}
private function prepQuery($sql)
{
// "DELETE FROM TABLE" returns 0 affected rows this hack modifies
// the query so that it returns the number of affected rows
if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql))
{
$sql = preg_replace("/^\s*DELETE\s+FROM\s+(\S+)\s*$/",
"DELETE FROM \\1 WHERE 1=1", $sql);
}
return $sql;
}
public function escape($sql)
{
if (function_exists('pg_escape_string'))
{
return pg_escape_string( $sql);
}
else
{
return addslashes($sql);
}
}
public function affectedRows()
{
return @pg_affected_rows($this->connection);
}
public function insertId($table=null, $column=null)
{
$_temp = $this->lasthash;
$lastresult = $this->results[$this->lasthash];
$this->execute("SELECT version() AS ver");
$row = $this->getRow();
$v = $row['server'];
$table = func_num_args() > 0 ? func_get_arg(0) : null;
$column = func_num_args() > 1 ? func_get_arg(1) : null;
if ($table == null && $v >= '8.1')
{
$sql='SELECT LASTVAL() as ins_id';
}
elseif ($table != null && $column != null && $v >= '8.0')
{
$sql = sprintf("SELECT pg_get_serial_sequence('%s','%s') as
seq", $table, $column);
$this->execte($sql);
$row = $this->getRow();
$sql = sprintf("SELECT CURRVAL('%s') as ins_id", $row['seq']);
}
elseif ($table != null)
{
// seq_name passed in table parameter
$sql = sprintf("SELECT CURRVAL('%s') as ins_id", $table);
}
else
{
return pg_last_oid($lastresult);
}
$this->execute($sql);
$row = $this->getRow();
$this->lasthash = $_temp;
return $row['ins_id'];
}
public function transBegin()
{
return @pg_exec($this->connection, "BEGIN");
return TRUE;
}
public function transCommit()
{
return @pg_exec($this->connection, "COMMIT");
return TRUE;
}
public function transRollback()
{
return @pg_exec($this->connection, "ROLLBACK");
return TRUE;
}
public function getRow($fetchmode = FETCH_ASSOC)
{
$lastresult = $this->results[$this->lasthash];
if (FETCH_ASSOC == $fetchmode)
$row = pg_fetch_assoc($lastresult);
elseif (FETCH_ROW == $fetchmode)
$row = pg_fetch_row($lastresult);
elseif (FETCH_OBJECT == $fetchmode)
$row = pg_fetch_object($lastresult);
else
$row = pg_fetch_array($lastresult,PGSQL_BOTH);
return $row;
}
public function getRowAt($offset=null,$fetchmode = FETCH_ASSOC)
{
$lastresult = $this->results[$this->lasthash];
if (!empty($offset))
{
pg_result_seek($lastresult, $offset);
}
return $this->getRow($fetchmode);
}
public function rewind()
{
$lastresult = $this->results[$this->lasthash];
pg_result_seek($lastresult, 0);
}
public function getRows($start, $count, $fetchmode = FETCH_ASSOC)
{
$lastresult = $this->results[$this->lasthash];
$rows = array();
for ($i=$start; $i<=($start+$count); $i++)
{
$rows[] = $this->getRowAt($i,$fetchmode);
}
return $rows;
}
function __destruct(){
foreach ($this->results as $result)
{
@pg_free_result($result);
}
}
}
?>
现在我们的框架已经完成。在接下来的章节中,我们将看到如何在这个框架上构建应用程序。
在我们的框架上构建应用程序
现在是精彩时刻。到目前为止,我们已经做了很多工作来简化在我们框架上开发应用程序。所以现在在本节中,我们将开发一个基本的博客应用程序,并讨论如何利用我们的框架。
对于那些不熟悉博客的人来说,它们只是基于网络的发布系统,人们可以写任何东西并发布。在这个应用程序中,我们将允许用户撰写文章、显示它们,并允许用户发布评论。
让我们创建一个名为 packtblog 的 MySQL 数据库,包含三个表;Users、Posts 和 Comments。以下是数据库模式:
Table: Posts
+---------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| title | varchar(250) | YES | | NULL | |
| content | text | YES | | NULL | |
| user_id | int(11) | YES | | NULL | |
| date | int(11) | YES | | NULL | |
+---------+--------------+------+-----+---------+----------------+
Table: Comments
+---------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| post_id | int(11) | YES | | NULL | |
| content | text | YES | | NULL | |
| date | int(11) | YES | | NULL | |
| author | varchar(250) | YES | | NULL | |
+---------+--------------+------+-----+---------+----------------+
Table: Users
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(100) | YES | | NULL | |
| fullname | varchar(250) | YES | | NULL | |
| email | varchar(250) | YES | | NULL | |
| password | varchar(32) | YES | | NULL | |
+----------+--------------+------+-----+---------+----------------+
认证控制器
让我们设计我们的主要控制器,其中用户将能够注册或登录到他们的系统。app/controllers/auth.php 文件中的代码如下:
<?
session_start();
class auth extends controller
{
public $use_layout = false;
function base()
{
}
public function login()
{
//$this->redirect("auth");
$this->view->set("message","");
if(!empty($_SESSION['userid']))
{
$this->redirect("blog","display");
}
else if (!empty($_POST))
{
$user = $this->model->user;
$userdata = $user->find(array("name"=>$user->name,
"password"=>md5($user->password)));
if (!$userdata)
{
//not found
$this->view->set("message","Wrong username and password");
}
else
{
$_SESSION['userid']=$userdata['id'];
$this->redirect("blog","display");
}
}
}
public function register()
{
if(!empty($_POST)){
$user = $this->model->user;
if (!$user->find(array("name"=>$user->name))){
$user->password = md5($user->password);
$user->insert();
}
}
}
}
?>
这里是认证控制器的视图:
app/views/auth/base.php
<h1>
Please <a href='<?=$base_url?>/auth/login'>login</a> or
<a href='<?=$base_url?>/auth/register'>register</a>
</h1>
这将显示以下屏幕:

app/views/auth/login.php
<h1>Please login</h1>
<font color="red"><?=$message;?></font><br/>
<form method="POST">
Username:<br/>
<input type="text" name="name"/><br/>
Password: <br/>
<input type="password" name="password" /><br/>
<input type="submit" name="Submit" value="Login" />
</form>
这将显示以下屏幕:

app/views/auth/register.php
<h1>Please register your account</h1><br/>
<form method="POST">
Your username: <br/>
<input type="text" name="name" /><br/>
Password: <br/>
<input type="password" name="password" /><br/>
Fullname: <br/>
<input type="text" name="fullname" /><br/>
Email: <br/>
<input type="text" name="email" /><br/>
<input type="submit" name="submit" value="Register"/>
</form>
这将显示以下屏幕:

接下来是处理博客操作的控制器
app/controllers/blog.php中的代码如下:
<?
session_start();
class blog extends controller
{
public function display()
{
$user = $_SESSION['userid'];
$posts = $this->model->post->find(array("user_id"=>$user),10);
if(!$posts)
{
$this->redirect("blog","write");
}
else
{
foreach ($posts as &$post)
{
$post['comments']=$this->model->comment->find
(array("post_id"=>$post['id']));
}
$this->view->set("posts",$posts);
}
}
public function post()
{
$postid= $this->params['0'];
if (count($_POST)>1)
{
$comment = $this->model->comment;
$comment->date = time();
$comment->post_id = $postid;
$comment->insert();
}
$post = $this->model->post->find(array("id"=>$postid));
if (!empty($postid))
{
$post[0]['comments'] = $this->model->comment->find
(array("post_id"=>$postid),100);
}
$this->view->set("message","");
$this->view->set("post",$post[0]);
//die($postid);
}
public function write()
{
$this->view->set("color","green");
if (!empty($_POST))
{
$post = $this->model->post;
$post->user_id=$_SESSION['userid'];
$post->date = time();
$post->insert();
$this->view->set("color","green");
$this->view->set("message","Successfully saved
your blog post");
}
}
}
?>
这里是我们的博客控制器的视图:
app/views/blog/display.php
<?
foreach ($posts as $post)
{
echo "<div id='post{$post['id']}' >";
echo "<b><a href='{$base_url}/blog/post/{$post['id']}'>
{$post['title']}</a></b><br/>";
echo "<p>".nl2br($post['content'])."</p>";
echo "Number of comments: ".(count($post['comments']));
echo "</div>";
}
?>
app/views/blog/post.php
<?
echo "<div id='post{$post['id']}' >";
echo "<b><a href='{$base_url}/blog/post/{$post['id']}'>
{$post['title']}</a></b><br/>";
echo "<p>".nl2br($post['content'])."</p>";
echo "Number of comments: ".(count($post['comments']));
echo "</div>";
foreach ($post['comments'] as $comment)
{
echo "<div style='padding:10px;margin-top:10px;
border:1px solid #cfcfcf;'>";
$time = date("Y-m-d",$comment['date']);
echo "Posted by {$comment['author']} at {$time}:<br/>";
echo "{$comment['content']}";
echo "</div>";
}
?>
<h2>Post a new comment</h2>
<font color="red"><?=$message;?></font><br/>
<form method="POST">
Name:<br/>
<input type="text" name="author"/><br/>
Comment: <br/>
<textarea rows="5" cols="60" name="content" ></textarea><br/>
<input type="submit" />
</form>
app/views/blog/write.php
<h1>Write a new blog post</h1>
<font color="<?=$color;?>"><?=$message;?></font><br/>
<form method="POST">
Title:<br/>
<input type="text" name="title"/><br/>
Content: <br/>
<textarea rows="5" cols="60" name="content" ></textarea><br/>
<input type="submit" value="save" />
</form>
这将显示以下表单:

最后但同样重要的是,这里是config文件。将其放置在app/config/configs.php或core/config/configs.php中:
<?
$configs['use_layout']=false;
$configs['unit_test_enabled']=true;
$configs['default_controller']="welcome";
$configs['global_profile']=true;
/* DB */
$configs['db']['usedb']="mysql";
$configs['db']['development']['dbname']="packtblog";
$configs['db']['development']['dbhost']="localhost";
$configs['db']['development']['dbuser']="root";
$configs['db']['development']['dbpwd']="root1234";
$configs['db']['development']['persistent']=true;
$configs['db']['development']['dbtype']="mysql";
?>
摘要
在 PHP 应用的快速发展中,框架扮演着非常重要的角色。这就是为什么今天市场上存在如此多的企业级框架,你有很多选择。在本章中,我们学习了如何构建框架,这也有助于理解对象加载、数据抽象层以及分离的重要性。最后,我们更深入地了解了应用程序是如何实现的。


浙公网安备 33010602011771号