PHP+MySQL+AJAX Web开发给力起飞
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第一篇 学习与积累

第1章 PHP编程快速上手

第2章 MySQL数据库基本操作

第3章 PHP与AJAX

第4章 PHP与XML、WebService

第1章 PHP编程快速上手

我们无法确切地统计都有哪些公司在使用PHP来运行他们的商业级应用,但是如果你想进入一家全球知名的大企业,包括全球500强的企业,这个问题就简单了,十有八九他们用的就是PHP。

根据统计,目前PHP已被安装在超过2200万个域名的网站服务器上(数据来源:NetCraft),这使得PHP成为了网络上使用最为广泛的编程语言之一,超过40%的Web程序使用PHP来编写。怎么样?亲爱的读者朋友,这是一个受欢迎的家伙。

1.1 PHP简介与环境部署

PHP是Rasmus Lerdorf在1994年秋天构思出来的。最早的未发行版本用在了他自己的主页上,功能也只是用来和观看他的在线简历的人保持联系。第一个交付用户使用的版本是在1995年6月发行的,它仅被当做一个个人主页制作工具:包括一个只懂得很少几条宏指令的、非常简单的分析引擎和一组用于主页信息反馈的工具(一个留言簿、一个计数器和一些其他的东西)。这时,它还只是一个不起眼的小家伙。

1996年,Rasmus重写了整个解析器,并在4月取名为PHP/FI 2发布。FI来源于他写的另外一个HTML表单集成数据的软件包。他把个人主页工具和表单集成工具合并在一起,并加入了MySQL数据库的支持,这样就有了PHP/FI。此后,不起眼的小家伙便以一种令人惊异的速度传播开来,人们开始大量使用它编写程序。

1997年年中,PHP出现了一个重大的转折,这就是PHP的开发从Rasmus个人的爱好升级到一群程序员们有组织的工作。同年,这个解析器被Zeev Suraski和Andi Gutmans重写,通过这次全面的重写,大量PHP/FI的功能被移植到PHP中,并且成为了PHP 3的雏形。PHP项目从一个人的项目转变成一个有很多开发者的真正的世界性开源项目,到1998年年中时,已经有了大量的商业化产品,例如C2的StrongHold Web Server和RedHat Linux捆绑了PHP/FI解析器或PHP解析器。再也没有人敢说PHP是一个不起眼的小家伙了,在PHP 3发布之后,PHP的用户数量开始激增。

此后,PHP 4,PHP 5日趋完善,而PHP 6更是有大幅度的改进,不但添加了“命名空间(namespace)”,还对多字节的处理进行了更好的修订,同时废除了早期版本遗留的一些困惑初学者的功能(magic_quotes等)。

1.1.1 什么是PHP

与GNU(GNU's Not Unix)一样,PHP也是一个递归缩写(PHP,Hypertext Preprocessor,超文本预处理器)。PHP是一种为创建HTML内容而设计的简单但是功能强大的语言。PHP主要运用在以下三个方面。

Web服务器端脚本

PHP最初被设计用于创建动态的Web内容,要生成HTML,需要一个PHP解释器和一个Web服务器来发送文档。PHP同时也能生成XML文档、图像、Flash、PDF文件等。

命令行脚本

PHP可以通过命令行方式运行,就像Perl,awk或xNix[0]注1:本书所指的Windows和xNix如无特别说明,Windows泛指Windows 98以上版本的系统,xNix泛指UNIX/Linux/BSD系统。 shell。可以使用命令行脚本来执行系统管理任务,例如备份和日志解析。

客户端GUI应用

通过使用PHP-GTK(http://gtk.php.net/),可以用PHP编写成熟的、跨平台的GUI应用程序。

看上去很强大是不是?强大的还在后面,PHP可以在所有主流操作系统上运行,从Windows,Mac OS,UNIX,Linux到FreeBSD,Solaris,你能想到的操作系统它都能游刃有余。所以,PHP也可以用在所有主流的Web服务器上。

PHP大量采用了C,Java和Perl语言的语法,并加入了各种PHP自己的特征。它与JavaScript等语言的区别就是PHP是在服务器端执行,客户机所看到的是其在服务器上运行的结果,这意味着如果想采用PHP,必须得有Web服务器支持。

PHP支持HTTP的身份认证、Cookie和GIF图像创建,其中一个最有代表性的特点是它的数据库层,可以使得编写基于数据库的网页变得非常简单。强大的PHP当然也支持目前所有的常见数据库。另外,PHP也支持与采用POP3,HTTP,SNMP,NNTP,IMAP等协议的服务进行网络编程。

心动了吧!喝杯咖啡,让我们先把这个强大的家伙部署起来。

1.1.2 安装PHP开发环境

PHP适用于很多操作系统和平台。下面介绍PHP在Windows Server 2008系统下以FastCGI方式运行的环境部署。

在Windows Server 2008下部署PHP所使用的软件环境:

Windows 2008简体中文企业版
Internet Information Service 7.0
PHP 5.3.2

01 安装IIS(Internet 信息服务,Internet Information Service),如果服务器上已经安装好了IIS,则此步可跳过。

打开“控制面板”→“管理工具”,运行“服务器管理器”,打开“添加角色向导”对话框,单击“添加角色”项,打开如图1-1所示的对话框。

图1-1 选择服务器角色

02 在对话框中勾选“Web服务器(IIS)”复选框并单击“下一步”按钮,略过IIS相关介绍,单击“下一步”按钮,选择角色服务,如图1-2所示。

图1-2 选择角色服务

03 单击“下一步”按钮再单击“安装”按钮。待其安装完毕后,打开“Internet信息服务(IIS)管理器”窗口,如图1-3所示。

图1-3 Internet信息服务(IIS)管理器

至此,IIS安装完毕。

04 接下来打开http://windows.php.net/,下载php-5.3.2-nts-Win32-VC9-x86.msi,如图1-4所示。

图1-4 下载PHP安装包

05 然后“以管理员方式运行”安装,到选择Web Server这一步时,选择IIS FastCGI单选项,如图1-5所示。

图1-5 选择Web Server类型

06 其他步骤可以一路单击Next按钮直到安装完成。

07 默认安装的IIS使用“C:\inetpub\wwwroot”作为本地站点的根目录,在其中新建名为info.php的文件,内容如下:

<?php
phpinfo();
?>

然后打开浏览器,输入http://localhost/info.php,出现如图1-6所示内容即表示安装顺利完成。

图1-6 查看phpinfo()函数运行结果,PHP顺利安装成功

1.1.3 部署PHP开发环境

经过前面的练习,相信大家已经能够让PHP运行起来了,但是可能有人会问,难道我们就用Windows系统自带的记事本编写程序,然后放到服务器上运行并查看结果吗?

非也!各位看官不要着急,马上就为大家介绍针对不同类型开发者的开发环境的搭建过程。

既然是才接触PHP的朋友,那么最好的选择就是市面上各种专门的IDE(Integrated Development Environment,集成开发环境),例如Zend Studio(http://www.zend.com/en/products/studio/downloads),Eclipse for PHP Developers(http://www.eclipse.org/downloads/),Nusphere PHPEd(http://www.nusphere.com/download.php.ide.htm),Komodo(http://www.activestate.com/komodo-ide/downloads),Waterproof PHPEdit(http://www.phpedit.com/en),PHP Designer(http://www.mpsoftware.dk/downloads.php)等。使用这些IDE的好处是显而易见的,如下所述。

● 支持项目管理

IDE的一个关键特性是它把一个PHP应用程序看做是一个项目,而不仅仅是一组文件。“项目”维护额外的信息,比如源码控制的配置、用于调试的数据库设置,以及某一关键目录所在位置等,能够更好地为开发者提供便利。

● 集成调试

使用这个功能,可以在编辑器中设置断点,当PHP解释程序执行到这个脚本时就会停止。从断点开始,就可以检查局部变量的值,在代码中诊断问题。

● 代码智能

PHP是一种非常遵守规则的编程语言,这意味着它遵循着简单的模式。这些模式不仅使代码易于编写,也使IDE在项目中检查代码变得很容易。此外,它们可以通过显示检查结果帮助你编写程序。例如,如果在项目中定义了一个名为MyClass的类,在键入关键词new时,IDE会立即提供一个包括MyClass作为选项的弹出窗口。当使用那种类型的对象时,IDE就会显示它的可用方法和实例变量。当开始键入一个函数命令时,IDE就会显示它的可用参数。公正地说,这是应该使用IDE而不是文本编辑器的首要原因。这种代码智能可以有效减少错误输入的类名、方法名和参数。

● 类视图

IDE中的代码智能引擎产生的另一个作用是IDE可以产生项目的类视图。系统不是显示文件,而是显示已经定义的不同的类,而不管它们所在的文件。当点击类时,编辑器访问相应文件并显示相应类、方法或者实例变量。这在大项目中是一种非常好的导航方法。

● 多语言支持

以上提到的每种IDE不仅支持PHP而且支持相关语言集:JavaScript,Structured Query Language(SQL,结构化查询语言),Hypertext Markup Language(HTML,超文本标记语言)和Cascading Style Sheets(CSS,级联样式表)。

● 源码控制

以上这些IDE都支持一些与源码控制系统的连接,允许随着时间维护项目中的文件版本。可以标记文件的特别版本为发布版本,这样在需要撤销改动的时候就可以恢复。在团队环境中使用源码控制系统是很关键的,即使对于个人使用也很重要。当磁盘瘫痪或者客户突然想要以前的版本而不是现在的版本的时候,一个好的源码控制系统就可以发挥作用了。大多数的IDE都支持Concurrent Version System(CVS)和Subversion,它们都是开源的版本控制系统。

● FTP/SFTP集成

与源码控制相关的一种功能是在服务器中对于最新的代码使用FTP。这比使用FTP客户机或者自己打包文件并发送给服务器然后再解包要容易许多。

● 数据库导航

一个非基本但十分有用的特性是数据库导航。使用这个特性,可以浏览应用程序访问的数据库、找到表格和字段名并返回查询结果。一些系统甚至可以自动生成一些数据库访问代码。

● 集成Web浏览器

一些IDE支持集成Web浏览器,可以直接导航到正在使用指定的附加参数编辑的页面,这个浏览器可以宿主在IDE内,也可以外部调用。

● 代码片段

在这些IDE中还有一个特性是对于整段和定制代码片段的支持。片段是完成小任务(比如在一些输入中运行正则表达式、连接到数据库和查询数据库)的小部分代码。这个功能可以在编写程序的时候尽量重用他人(或自己)的已有代码。

如果你已经是一位熟练的PHP程序员,对PHP本身的常用函数和正在开发的项目中的类了如指掌,同时不想因为修改一些局部内容就动辄开启Zend Studio这样的庞然大物,那么这个时候一个小巧好用的编辑器也许是你最佳的选择。

笔者最喜欢的编辑器是Editplus(http://www.editplus.com/download.html),好吧,我承认我是一个Editplus控。这个安装程序只有区区1MB多的软件真可谓是“麻雀虽小,五脏俱全”:

● 提供HTML,PHP,Java,C/C++,CSS,ASP,Perl,JavaScript和VBScript等语言的语法高亮突出显示和自动完成功能。同时,能够根据自定义语法文件支持其他程序语言。

● 内置Web浏览器。

● 集成FTP/SFTP支持。

● 支持列选择、括号匹配、代码折叠。

● 编辑大型文件,打开大型文件速度很快。

● 强大的搜索和替换功能,支持正则表达式的搜索和替换,支持在多文件中查找。

● 支持拼写检查。

● 支持按键录制和回放。

● 支持自定义热键和外部用户工具。

● 最新版本已经支持工程,能够简单地完成一些项目管理的工作。

图1-7是Editplus的运行截图,怎么样,是不是很强大?

图1-7 Editplus的运行截图

如果你是一位身兼程序员、页面设计师、用户体验分析师等诸多角色于一身的多面手,那么相信Dreamweaver(https://www.adobe.com/cfusion/tdrc/index.cfm?product=dreamweaver)一定是个很好的选择。Dreamweaver的主要功能有:

● 支持WYSIWYG(What You See Is What You Get,所见即所得)方式编辑页面,同时可以以代码视图编写程序。

● 支持HTML,XHTML,CSS,JavaScript,CFML(ColdFusion标记语言),Visual Basic,C#,JSP和PHP等语言的语法高亮、代码提示。

● 支持以项目方式管理站点和代码。

● 支持CVS,SVN等版本控制管理系统。

● 与Adobe其他软件的跨媒体一致性。

图1-8是Dreamweaver的截图。

图1-8 Dreamweaver的运行截图

事实上,究竟选择哪一种开发工具并没有成文的要求,在不同的时间段内也不是固定的。读者完全可以根据自己的实际情况和个人喜好来确定,当然,开发用的计算机的运行速度也是需要考虑在内的。

1.2 面向对象的PHP与MVC设计模式

什么是对象?先来看一个问题。

“这个世界是由什么组成的?”这个问题如果让不同的人来回答会得到不同的答案,如果是一个化学家,他也许会告诉你,“还用问吗,这个世界是由分子、原子、离子等的化学物质组成的”;如果是一个画家呢?他也许会告诉你,“这个世界是由不同的颜色所组成的”。呵呵,众说纷纭吧!但如果让一个分类学家来考虑问题就有趣得多了,他会告诉你,“这个世界是由不同类型的物与事所构成的”,好!作为面向对象的程序员来说,我们要站在分类学家的角度去考虑问题!是的,这个世界是由动物、植物等组成的。动物又分为单细胞动物、多细胞动物、哺乳动物等,哺乳动物又分为人、大象、老虎……就这样分下去了!

现在,站在抽象的角度,我们给“类”下个定义吧——站在抽象的角度,回答“什么是人类?”首先让我们来看看人类所具有的一些特征,这个特征包括属性(一些参数,数值)以及方法(一些行为,他能干什么)。每个人都有身高、体重、年龄、血型等一些属性。人都会劳动、人都会直立行走、人都会用自己的头脑去创造工具等这些方法!人之所以能区别于其他类型的动物,是因为每个人都具有人这个群体的属性与方法。“人类”只是一个抽象的概念,它仅仅是一个概念,它是不存在的实体!但是所有具备“人类”这个群体的属性与方法的对象都叫人!这个对象“人”是实际存在的实体!每个人都是人这个群体中的一个对象。老虎为什么不是人?因为它不具备人这个群体的属性与方法,老虎不会直立行走,不会使用工具等,所以老虎不是人!

由此可见,类描述了一组有相同特性(属性)和相同行为(方法)的对象。在程序中,类实际上就是数据类型。例如:整数,小数等。整数也有一组特性和行为。面向过程的语言与面向对象的语言的区别就在于,面向过程的语言不允许程序员自己定义数据类型,而只能使用程序中内置的数据类型。而为了模拟真实世界,为了更好地解决问题,往往需要创建解决问题所必需的数据类型。面向对象编程为我们提供了解决方案。

面向对象编程可以进行更干净的设计和更轻松的代码维护,大大提高了代码的可重用性。PHP支持面向对象编程中的很多实用特性,并且PHP 5引入了一个全新的、功能更为强大的面向对象的实现,使得类和对象更有用。

面向对象编程诠释了数据及操作数据的代码之间的基本联系。作为数据和功能代码的集合,对象是程序开发和代码重用的基本单元。对象是类的实例,对象所带有的数据被称为对象的属性(Property),对象所带有的功能函数被称为对象的方法(Method)。当定义一个类时,可以定义类的属性名称并写出类方法的相应代码。

使用了类封装的方法以后,一个类提供特定的类方法给其他要使用这个类的代码来调用,外部的代码不能直接访问对象的数据。这样使得调试和维护代码变得更加容易——改变一个类内部的功能代码后可以不需要改变调用这个类的代码。

继承(Inheritance)则是通过声明“类似”于已存在的类来定义一个新的类的过程,但新的类可以有其自己特有的属性或方法。已存在的类称为父类或基类(Super class或Base class),新定义的类被称为子类(Subclass)或派生类(Derived class)。继承是一种不需要复制粘贴就可以重用父类代码的方法。任何对父类的改进或修改都会自动传递给子类。

面向对象的编程语言必须支持多态性,多态性是指不同的类对同一操作可以有不同的行为。实际上多态性更应被理解为是“行为”的特性,在PHP中只有类的成员函数可以是多态的。不同的对象所包含的“行为”操作是不同的,一旦“行为”作用的对象确定下来,操作才可以和一系列特定的“行为”联系起来。

1.2.1 PHP中的面向对象编程

类与对象

在PHP中,通过使用“public/private/protected”关键词声明类的属性;使用“function”关键词声明类的方法,就像定义一个函数一样。

大多数类都包含一个称为构造函数的特殊方法,其作用是当创建一个对象时,PHP解释器将自动调用该类的构造函数来完成一些操作,例如设置属性的初始值或创建该对象所需的其他对象等。声明构造函数与声明类的其他方法相同,但其名称必须是__construct()这是PHP 5中的要求。在PHP 4中则要求构造函数的名称必须与类的名称相同。PHP 5为了保持与PHP 4兼容,如果一个类中没有名为__construct()的方法,PHP解释器将搜索一个与类名称相同的方法。

同样,PHP 5也支持析构函数,其作用是在销毁一个类之前执行一些“最后”的操作或完成一些功能,这些操作或功能通常在所有对该类的引用都被重置或超出作用域时自动发生。与构造函数的名称类似,析构函数的名称必须是__destruct(),不同的是,析构函数不能带有任何参数。

类的定义指明了其包含的信息和对外的接口,但是要使用它,必须将其实例化为对象。

在PHP 5中,可以使用clone关键词创建一个已有对象的副本,副本与原对象具有相同类的拷贝,而且具有相同的属性值,如果要对复制的过程做其他操作,可以在基类中创建一个__clone()方法,在其中定义所需的确切行为。

使用类的属性和方法

在一个类中,存在一个特殊的内部指针——$this,可以使用这个变量指代这个类自身来进行操作。

按照封装的原则,从类的外部直接访问类的属性是不合适的,解决的办法是使用__get()和__set()函数。__get()函数的作用是返回该属性的值,其带有一个参数:属性的名称。__set()函数的作用是设置属性的值,其带有两个参数:属性的名称和值。__get()和__set()函数的用法如例1-1所示。

实例演练

例1-1 __get()和__set()函数

function__get($name) {
   return $this->$name;
}
function __set($name, $value) {
   $this->$name = $value;
}

在实际应用中,__get()和__set()函数的内容可能要比例1-1要复杂一些,例如可以在__get()函数中增加一些表现层的代码,可以在__set()函数中增加对数据进行检查的代码等。

与C#和Java等强类型语言不同,PHP 5中不能通过定义不同参数的同名函数来实现方法的重载(Overload),而需要使用__call()函数来实现,如例1-2所示。

实例演练

例1-2 PHP中方法的重载

public function __call($method, $p) {
   echo "Invoking $method()<br />";
   if($method == 'display') {
           if(is_object($p[0])) {
                   //若$p[0]是一个对象,则调用displayobject()函数
                   $this->displayObject($p[0]);
           } else {
                 if(is_array($p[0])) {
                         //若$p[0]是一个数组,则调用displayArray()函数
                         $this->displayArray($p[0]);
                 } else {
                         //若$p[0]既不是对象也不是数组,调用display Scalar()函数
                         $this->displayScalar($p[0]);
                         }
                 }
           }
}
public function displayObject($p)
{
       echo "传入的是对象,内容如下:<br />";
       print_r($p);
       echo "<hr>";
}
public function displayArray($p)
{
       echo "传入的是数组,内容如下:<br />";
       print_r($p);
       echo "<hr>";
}
public function displayScalar($p)
{
       echo "传入的是单独变量,内容如下:<br />" .$p;
       echo "<hr>";
}

使用以上方式的运行结果如图1-9所示。

图1-9 PHP中使用__call()函数实现方法的重载

继承

在PHP中实现类与类之间的继承关系,应使用extends关键词,如例1-3所示。

实例演练

例1-3 类NEW_CLASS继承自CLASSNAME

class NEW_CLASS extends CLASSNAME {
   private $property2;
   function __construct() {
          echo 'NEW_CLASS\'s constructor.<br />';
   }
   public function output() {
          echo '$property='.$this->property;
          echo '<br />';
          echo '$property2='.$this->property2;
   }
   function __get($name) {
          return $this->$name;
   }
   function __set($name, $value) {
          $this->$name = $value;
   }
}

类NEW_CLASS继承自CLASSNAME,若子类NEW_CLASS含有其自己的构造函数,则其在实例化对象时将执行自身的构造函数;反之将调用父类CLASSNAME的构造函数。析构函数也与此类似,子类NEW_CLASS含有其自己的析构函数,则其在实例化对象时将执行自身的析构函数;反之将调用父类CLASSNAME的析构函数。可以使用以下代码测试:

$newobj = new NEW_CLASS;
$newobj->__set('property', 'new value');
$newobj->__set('property2', 'another value');
$newobj->output();

结果如图1-10所示。

图1-10 子类和父类的继承关系

父类和子类之间继承的内容(包括属性和方法)可以使用“public/private/protected”关键词修饰:

● 被指定为public的内容,在类的外部可见,也可以被当前类的子类继承。

● 被指定为private的内容,在类的外部不可见,也不能被继承。

● 被指定为protected的内容,在类的外部不可见,可以被继承。

● 被继承的内容不指明修饰关键词的情况下,默认为public。

若子类与其父类具有相同名称的属性或方法,则子类中的属性和方法比父类中的优先级高,将会覆盖(override)父类中的属性和方法。此时访问类的属性或调用类的方法将访问子类的属性或调用子类的方法子类NEW-CLASS和父类CLASSNAME的构造函数与析构函数即属于这种情况的一个特例。。在这种情况下要访问父类中被覆盖的方法,可以使用以下方式:

parent::method();

而要调用当前类自身的方法,可以使用:

self::method();

若要禁止父类的方法在子类中被覆盖,可以使用final关键词,例如若将类CLASSNAME中的test()方法修改为:

final public function test() {
       echo $this->property.'<br />';
}

则其将不能在类NEW_CLASS中被覆盖,否则PHP解释器会报错并提示“Fatal error:Cannot override final method”。

final关键词也可以被用来禁止一个类被继承。

接口与多重继承

部分面向对象的语言支持多重继承,如C++和Smalltalk等,但是PHP并不支持。即在PHP中,每个类都只能继承自一个父类,但是一个父类可以有多个子类。

接口(Interface)提供了定义一个类所遵循的规则的途径。接口提供了类方法的原型和常量。任何实现(Implement)该接口的类必须提供接口中所有方法的具体实现。接口可以看做多重继承问题的一个解决方法。定义一个接口的代码如例1-4所示。

实例演练

例1-4 定义一个接口

interface INTERFACENAME {
   function if_func();
}

接口中定义的方法不能包含任何函数内容,在形式上更类似于一个方法的声明(Declare)。

实现了若干个接口的类,同时可以继承自某一个类,如例1-5所示。

实例演练

例1-5 类MIX_CLASS继承自类CLASSNAME并实现了接口INTERFACENAME

class MIX_CLASS extends CLASSNAME implements INTERFACENAME {
   function if_func() {
         echo 'This function is from MIX_CLASS';
   }
}

执行以下代码的结果如图1-11所示。

$mixclass=new MIX_CLASS;
$mixclass->if_func();

图1-11 调用类MIX_CLASS的if_func()方法

类MIX_CLASS,CLASSNAME和接口INTERFACENAME之间的继承关系如图1-12所示。

图1-12 类MIX_CLASS,CLASSNAME和接口INTERFACENAME之间的继承关系图

抽象类

PHP提供了一种机制,让一个类这种特定的方法在子类中必须实现,而这些方法在父类中没有实现(仅仅做了“声明”),这时可以使用称为抽象类方法(Abstract Method)的方式。另外,一个类中只要有一个方法被定义为抽象方法,则该类就必须使用“abstract”关键词定义为抽象类(Abstract Class)。定义一个类并继承自另一个抽象类的代码如例1-6所示。

实例演练

例1-6 类SUB_CLASS继承自抽象类ABSTRACTCLASS

abstract class ABSTRACTCLASS {
   abstract function ab_func();
}
class SUB_CLASS extends ABSTRACTCLASS {
   function ab_func() {
         echo 'This function is from ABSTRACT class.';
   }
}

执行以下代码的结果如图1-13所示。

$subclass=new SUB_CLASS;
$subclass->ab_func();

图1-13 调用类SUB_CLASS的ab_func()方法

需要注意,抽象类不能被直接实例化,在PHP中不能为其提供一个默认的实现。

自省机制

自省(Introspection)机制被用来让程序检查对象的特性,例如名称、父类、属性和访问等。利用自省机制可以编写对任何类或对象进行操作的代码,在编写代码时不需要知道类中定义了哪些属性和方法,但在运行时可以得到这些信息。

确定一个类是否存在可以使用class_exists()函数,该函数有一个字符串参数并返回一个布尔值。使用get_declared_classes()函数则将得到一个包含所有当前已定义的类的数组。可以使用如例1-7所示的两种方式来检查某个类是否存在。

实例演练

例1-7 检查某个类THECLASS是否已被定义的两种方法

//方法1:使用class_exists()函数
$classexists = class_exists('THECLASS');
//方法2:使用in_array()函数和get_declared_classes()函数
$classexists = in_array('THECLASS', get_declared_classes());

类似的,可以使用函数get_class_methods()来得到一个类中的所有方法,使用函数get_class_vars()来得到一个类中的所有属性。这两个函数都使用类名作为参数,返回一个数组。函数get_parent_class()用来返回一个类的父类的名称。

检查一个变量是否为对象,可以使用is_object()函数,返回值为布尔型。要得知一个对象所属的类名,可以使用get_class()函数。检查一个类是否存在某方法,可以使用method_exists()函数:

method_exists($newobj,'output');

调用一个未定义的类方法会触发一个运行时异常(Runtime Exception)。与函数get_class_vars()类似,get_object_vars()返回一个对象的所有属性组成的数组。

执行例1-8所示的代码得到的结果如图1-14所示。

实例演练

例1-8 测试类和对象自省机制的代码

print_r(get_class_methods('NEW_CLASS'));
echo "<hr>";
print_r(get_class_vars('NEW_CLASS'));
echo "<hr>";
var_dump(is_object($newobj));
echo "<hr>";
var_dump(get_class($newobj));
echo "<hr>";
var_dump(method_exists($newobj, 'output'));
echo "<hr>";
print_r(get_object_vars($newobj));

图1-14 例1-8的运行结果

检查一个对象是否是某一个类的实例,可以使用instanceof关键词,以检查一个对象是否是某特定的类的实现,或是否是从某个类继承过来或是否实现了某个接口。

PHP 5引入了类的类型检查机制,通常情况下向一个函数传递一个参数的时候,不能传递该参数的类型;而使用类类型检查,可以指定必须传入的参数的类类型,如果传入的参数的类类型不是指定的类型,将产生一个Fatal Error错误。

__autoload(),__tostring(),__sleep()和__wakeup()方法

事实上,__autoload()方法不是一个类方法,而是一个单独的函数,可以在任何类声明之外声明这个函数。如果实现了这个函数,其将在实例化一个还未声明的类前自动调用。__autoload()方法的主要用途是尝试包括或要求任何用来初始化所需类的文件,例1-9的作用是将在任何类实例化的时候将一个与该类同名的PHP程序文件包含进来。

实例演练

例 1-9 __autoload()方法示例

function __autoload($classname) {
   include_once './include/classes/'.$classname.'.php';
}

如果在类中实现了__tostring()方法,则其返回的所有内容都可以作为字符串输出,一般用来输出一些类和对象的内部信息。

__sleep()和__wakeup()方法用于在对象序列化(Serialize)和反序列化(Unserialize)的时候做一些操作。__sleep()方法在一个对象被序列化之前调用,它被用来执行一些必要的清理工作以保存对象的状态,如关闭数据库连接、写入未保存的持久性数据等。__sleep()方法将返回一个数组,其中包含那些需要被写入字节流的数据成员的名称,如果其返回空数组,则说明没有任何数据被写入字节流。

__wakeup()方法则与此相反,它是在一个对象被反序列化的时候调用,此时程序将根据字节流中的信息重建此对象。该方法将执行一些必需的操作,如重新连接数据库或其他一些初始化工作等。

实际上这几个方法的具体函数体都是由程序员实现的,PHP本身对此没有什么苛刻的硬性规定。

1.2.2 PHP中的命名空间

从PHP 5.3版本开始,新增了一个重要特性:命名空间(Namespace),主要用于解决类和方法的命名冲突。使用namespace关键字给一段代码命名后,这段代码外部的脚本必须使用操作符“::”加上命名空间的名称来引用这个代码块,引用静态的类成员也是用相同的方法。在命名空间内代码不需要声明命名空间,它本身就是默认的。使用命名空间可以使代码变得更加紧凑和可读。

那么是否可以建立分层的(即嵌套的)命名空间呢?答案是不可以。但可以在命名空间名称后加上冒号,即可再次调用在名称中不包含冒号的变量、函数和类。命名空间允许存在冒号,只要不是第一个字符和最后一个字符或接着另一个冒号即可。命名空间的名称中的冒号对于PHP来说没有任何意义,但可以用来区分逻辑上的区块,因为命名空间能够很好地说明代码中的层级关系。

在PHP 5.3的早期版本中,使用import关键字来导入命名空间和类,而在最新的版本中则使用use关键字。一个use语句的有效范围是从它被定义开始直到文件的结尾,可以在全局范围内任何地方使用它,也可以在多个文件中使用相同的命名空间。但是一个文件只应该包含一个命名空间(这一行为可能会在最终版本中被改变,也可能用package来替换namespace关键字)。尽管不能导入一个函数或者常量,但是仍然可以使用一些前缀来从命名空间中访问它们。新增的as关键字用于导入命名空间或类后为其设置一个别名。例1-10使用命名空间定义了一个类,例1-11则描述了一些常见的调用方式。

实例演练

例1-10 使用命名空间定义一个类

<?
/** classes/my/foo/MyClass.php */
namespace my::foo;
//定义一个类
class MyClass {}
// 当然,也可以定义函数和常量
function myFunc() { }
const MY_CONST = 'foo';
?>

实例演练

例1-11 一些常见的调用方式

<?
/** test.php */
include 'classes/my/foo/MyClass.php';
// 可以随时通过完整的名称来访问一个类
$foo = new my::foo::MyClass();
// 还可以使用use语句来导入一个命名空间
use my::foo;
// 然后,通过foo来引用 my::foo这个命名空间
$foo = new foo::MyClass();
// 也可以只导入一个类
use my::foo::MyClass;
$foo = new MyClass;
// 可以为命名空间或者命名空间中的类创建别名
use my::foo as MyFoo;
use my::foo::MyClass as MyFooClass;
$foo = new MyFoo::MyClass();
$foo = new MyFooClass();
// 注意,下面的两种写法是等价的:
use my::foo;
use my::foo as foo;
// 也可以用同样的方法来访问函数和常量
my::foo::myFunc();
myFoo::myFunc();
my::foo::MY_CONST;
myFoo::MY_CONST;
?>

如果在函数或常量名前面使用“::”前缀,则这些函数或常量将会被从当前的引用规则中独立出来,即所谓的“全局命名空间”(The Global Namespace),例如:

function foo() {  ... }

这种函数可以使用foo()方式执行,也可以使用::foo()方式执行。

使用命名空间需要避免在类名中使用PHP的保留字,例如以下代码:

<?
/** classes/my/form/element/static.php */
class MyFormElementStatic {}
?>

在使用命名空间后,会变成这样:

<?
/** classes/my/form/element/static.php */
namespace my::form::element;
class Static {}
?>

这是因为static是一个PHP保留关键字,所以这段代码最终会导致一个错误。

从PHP 5.3版本开始,__autoload()函数将使用类的全路径名称,这意味着如果已经使用了__autoload(),就要对它做一下适当的修改,例如:

<?
/** test.php */
function __autoload($className)
{
require 'classes/'.str_replace('::', DIRECTORY_SEPARATOR,$className).'.php';
}
$foo = new my::foo::MyClass();
?>

同样的,使用get_class(),get_parent_class()及其他类似函数的时候也要注意,同样会返回一个类的全路径名称。

随着命名空间的引入,PHP从5.3版本开始引入了一个新的常量__NAMESPACE__,它包含了当前引用的命名空间的名字。例如可以使用如例1-12所示的方式来实现一个SPL风格的类自动加载器。

实例演练

例1-12 使用__NAMESPACE__常量实现SPL风格的类自动加载器

<?
/** classes/my/core/classloader.php */
namespace my::core;
function classLoader($className)
{
require 'classes/'.str_replace('::', DIRECTORY_SEPARATOR,$className).'.php';
}
spl_autoload_register(__NAMESPACE__.'::classLoader');
?>

使用命名空间后,PHP会首先参照当前的命名空间来解析一个元素,例如:

<?
namespace my::foo;
…
mysql_connect();
…
::mysql_connect();
?>

当调用mysql_connect()的时候,PHP会首先到my::foo下面去找这个函数,如果找到了就调用它,若没找到才再去调用PHP内部集成的mysql_connect()函数,而其他命名空间中定义的mysql_ connect()函数是无法访问到的。而使用::bar()这种写法则可以访问到全局命名空间中的函数,它可能是PHP的内部函数,也可能是用户自定义的。

又例如:

<?
namespace my::foo;
…
core::bar();
…
::core::bar();
?>

在core::bar()这种情况下,PHP会首先尝试调用my::foo::core下面的函数bar(),如果没有找到,则尝试调用my::foo命名空间下的类core(如果存在的话)的静态方法bar(),最后尝试调用PHP的内置类core的bar()方法。

而在::core::bar()这种情况下,PHP会首先尝试调用core命名空间下的函数bar(),然后尝试调用全局范围内类core的静态方法bar()。

1.2.3 MVC模式

“设计模式”这个术语最初被用于建筑学领域。Christopher Alexander在他1977年的著作“A Pattern Language:Towns/Building/Construction”里描述了一些常见的建筑学设计问题,并解释了如何用这些已有的、著名的模式集合来开始全新的有效的设计。Alexander的观点被很好地转化到软件开发上来,用原有的组件来构造新的解决方案。设计模式不仅代表着更快开发健壮软件的有用方法,而且还提供了以友好的术语封装大型理念的方法。

所有的设计模式都有一些常用的特性:一个标识(a name)、一个问题陈述(a problem statement)和一个解决方案(a solution):

1.一个设计模式的标识是很重要的,因为它会让其他的程序员不用进行太深入的学习就能立刻理解你的代码的目的(至少通过这个标识,程序员会很熟悉这个模式)。

2.问题描述用来说明这个模式被应用的领域。

3.解决方案描述了这个模型的执行。一个好的设计模式的论述应该覆盖使用这个模型的优点和缺点。

一个模式是解决特定问题的有效方法。设计模式不是在项目中直接包含和使用的代码库,而是一个用来组织代码的模板或方式。事实上,一个代码库和一个设计模式在应用上有很多不同。

MVC模式(Model-View-Controller)是时下流行的Web应用架构,它在1970年由Trygve M.H.Reenskaug在施乐公司的研究中心提出。其最早的参考例程代码用SmallTalk80写成,最初用来解决应用软件中用户图形界面的交互问题。

不像其他的设计模式,MVC模式不是一个注重于讲述一种可以直接设计编码的类结构的模式。在概念上,MVC模式的定义是Model,View及Controller三者之间的桥梁。Controller以及View都依赖于Model,因为View和Controller都需要向Model发送数据请求。数据通过Controller输入到系统中,并最终通过View显示结果。更具体地说,Controller处理每一个外部的HTTP请求,而View将产生HTTP响应。

图1-15示意了MVC模式的基本流程。

图1-15 MVC模式的基本流程

● Model

Model封装了系统的应用数据、应用流程和业务逻辑,它可能是应用中最主要的逻辑处理部分。Model没有任何关于界面风格的表达,也不处理任何HTTP请求。

● View

View主要处理所有界面表现的问题。View从Model中提取数据,格式化为HTML或者XML页面的内容,输出到用户的客户端。

直接通过View中的表单来调用Model中的方法去修改数据是不合适的,数据的更新修改方法只能借助Controller来调用。View只能以数据的只读方式调用Model中的方法,不能对数据进行修改。

View在通常情况下的表现形式是模板系统,它使用一些包括了特殊标记的模板文件(例如HTML格式),View被执行时,这些特殊标记就被Model里的对应数据替换并显示出来。以这种方式运作的最典型的例子就是Smarty——一个典型的PHP模板系统。

● Controller

Controller主要用来解释HTTP请求和响应、控制操作和行为、控制程序流程、接收输入,并把它们传递给Model和View。

Controller包括前端控制器(Front Controllers)和应用控制器(Application Controller)。前端控制器有助于集中控制应用流于一点。集中化可以帮助了解一个复杂的系统是怎样运行的,以及提供一个可以插入全局代码的空间。前端控制器对于集中控制的系统是很好的选择。前端控制器经常把控制委托给应用控制器,应用控制器是对不同的请求执行的具体操作,它是MVC中Controller的真正核心所在。

在应用了MVC模式的实际Web应用中,Model-View-Controller序列图的结构如图1-16所示。

图1-16 实际Web应用中的MVC序列图

下面给出的由多个程序文件构成的例子是一个典型的MVC模式的Web应用——“书籍库存”。本例用到的数据库表books的结构如表1-1所示。

实例演练

表1-1 MVC模式的例子——“书籍库存”用到的数据库表books的结构

生成数据表并插入示例数据的SQL语句如例1-13所示。

例1-13 生成数据表并插入示例数据的SQL语句

CREATE TABLE IF NOT EXISTS 'books' (
  'BOOKID' int(11) NOT NULL auto_increment,
  'BOOKNAME' varchar(255) NOT NULL default '',
  'UNITPRICE' varchar(255) NOT NULL default '',
  'UNITSINSTOCK' varchar(255) NOT NULL default '',
  'DISCONTINUED' varchar(255) NOT NULL default '',
  PRIMARY KEY  ('BOOKID')
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=12 ;
INSERT INTO 'books' ('BOOKID', 'BOOKNAME', 'UNITPRICE', 'UNITSINSTOCK',
'DISCONTINUED') VALUES
(1, 'PHP和MYSQL WEB开发 (原书第3版)', '78', '43', '0'),
(2, 'PHP5权威编程', '90', '546', '0'),
(3, '搜索引擎优化高级编程(PHP版)', '48', '45', '0'),
(4, '精通正则表达式--基于.NET/ASP/PHP/JSP/JAVASCRIPT', '52', '66', '0'),
(5, 'AJAX与PHP基础教程', '39', '54', '0'),
(6, 'PHP网络编程技术与实践', '58', '33', '1'),
(7, 'PHP 5高级应用开发实践', '55', '67', '0'),
(8, 'AJAX与PHP WEB开发', '39', '23', '0'),
(9, 'WIRELESS WEB DEVELOPMENT WITH PHP AND WAP', '486', '0', '1'),
(10, 'PHP 5 IN PRACTICE中文版', '49', '11', '0'),
(11, 'PHP IN A NUTSHELL', '341', '0', '1');

例1-14是本例中操作数据库的类DataAccess的定义(DataAccess.php)。

例1-14 操作数据库的类DataAccess

<?
class DataAccess {
   var $db;
   var $query;
   function __construct($host, $user, $pass, $db) {
          //构造函数,用于初始化数据库连接
          $this->db=mysql_pconnect($host, $user, $pass);
          if(mysql_get_server_info($this->db) > '4.1') {
                 @mysql_query("SET character_set_connection='utf8',character_set_results='utf8', character_set_client=binary", $this->db);
                 @mysql_query("SET names 'utf8'", $this->db);
                 if(mysql_get_server_info($this->db) > '5.0.1') {
                         @mysql_query("SET sql_mode=''", $this->db);
                 }
          }
          mysql_select_db($db,$this->db);
   }
   function fetch($sql) {
          //执行$sql表示的SQL语句,取得返回结果
          $this->query = mysql_unbuffered_query($sql, $this->db);
   }
   function numRows() {
          //获取本次操作影响的数据行数
          $nums = mysql_num_rows($this->query);
          return $nums;
   }
   function getRow() {
          //执行SQL语句,取得一行数据
          if($row = mysql_fetch_array($this->query, MYSQL_ASSOC)) {
                  return $row;
          } else {
                  return false;
          }
   }
}
?>

例1-15是本例中Model部分的代码(BookModel.php),用来取得书籍列表、单本书籍的信息、当前书籍列表每页显示的书的数量等。

例1-15 “书籍库存”的Model部分

<?
class BookModel {
   var $dao;
   function __construct(&$dao) {
          $this->dao=& $dao;
   }
   function listBooks($start = 0, $rows = 10) {
          //从数据库中取得若干条书籍信息数据(默认10条)
          $this->dao->fetch("SELECT * FROM books LIMIT ".$start.",".$rows);
   }
   function getNums($start = 0, $rows = 10) {
          $nums = $this->dao->numRows("SELECT * FROM books LIMIT".$start.", ".$rows);
          if($nums) {
                 return $nums;
          } else {
                 return false;
          }
   }
   function listBook($id) {
          //从数据库中取得某一本书的信息
          $this->dao->fetch("SELECT * FROM books WHERE BOOKID='".$id."'");
   }
   function getBook() {
          if($book = $this->dao->getRow()) {
                 return $book;
          } else {
                 return false;
          }
   }
}
?>

例1-16是本例中View部分的代码(BookView.php),显示页面的头、尾信息,显示书籍列表的HTML内容以及列出单本书籍的信息等。

例1-16 “书籍库存”的View部分

<?
class BookView {
   var $model;
   var $output;
   function BookView(&$model) {
          $this->model = &$model;
   }
   function header() {
          $this->output = <<<HEAD
<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>书籍列表</title>
<style>
body { font-size: 13.75px; font-family: verdana }
td { font-size: 13.75px; font-family: verdana }
.title { font-size: 15.75px; font-weight: bold; font-family: verdana }
.heading {
font-size: 13.75px; font-weight: bold;
font-family: verdana; background-color: #f7f8f9 }
.nav { background-color: #f7f8f9 }
</style>
</head>
<body>
<div align="center" class="title">书籍列表</div>
HEAD;
          $this->output .= "\n<div align=\"center\"><a href=\"".$_SERVER['PHP_SELF']."\">首页</a></div>\n";
   }
   function footer() {
          $this->output .= "</body>\n</html>";
   }
   function bookItem($id = 1) {
          $this->model->listBook($id);
          while($book = $this->model->getBook()) {
                 $this->output .= "<p><b>名称</b>:".$book['BOOKNAME']."</ p > < p > < b >价格< / b > : " .$ b o o k [ ' U N I T P R I C E ' ] ." < / p > < p > < b >数量< / b>:".$book['UNITSINSTOCK']."</p>";
                 if ( $this->$book['DISCONTINUED'] == 1 ) {
                        $this->output .= "<p>此书籍已停售.</p>";
                 }
          }
  }
  function bookTable($rownum = 0) {
         $rowsperpage = '4';
         $this->model->listBooks($rownum, $rowsperpage);
         $this->output .= "<table width=\"600\" align=\"center\">\n<tr>\n<td class=\"heading\">名称</td>\n<td class=\"heading\">价格</td>\n</ tr>\n";
         while($book = $this->model->getBook()) {
                $this->output.= "<tr>\n<td><a href=\"".$_SERVER['PHP_S ELF']."?view=book&id=".$book['BOOKID']."\">".$book['BOOKNAME']."</a></ td><td>".$book['UNITPRICE']."</td>\n</tr>\n";
         }
         $this->output .= "<tr class=\"nav\">\n";
         if($rownum >= $rowsperpage) {
                $this->output .= "<td><a href=\"".$_SERVER['PHP_ SELF']."?view=table&rownum=".($rownum - $rowsperpage)."\"><< 前一页</a></ td>";
         } else {
                $this->output .= "<td>&nbsp;</td>";
         }
         if($this->model->getNums() == $rowsperpage) {
                $this->output.="<td><a href=\"".$_SERVER['PHP_ SELF']."?view=table&rownum=".($rownum+$rowsperpage)."\">后一页 >></a></ td>";
         } else {
                $this->output .= "<td>&nbsp;</td>\n";
         }
         $this->output .= "</tr>\n</table>\n";
  }
  function display() {
         return $this->output;
  }
}
?>

例1-17是本例中的Controller部分(BookController.php),根据GET方式的参数执行不同的业务逻辑。

例1-17 “书籍库存”的Controller部分

<?
class BookController extends BookView {
   function BookController(&$model, $getvars = null) {
          BookView::BookView($model);
          $this->header();
          switch($getvars['view']) {
                 case "book":
                        $this->bookItem($getvars['id']); break;
                 default:
                        if(empty($getvars['rownum'])) {
                               $this->bookTable();
                        } else {
                               $this->bookTable($getvars['rownum']);
                        }
                        break;
          }
          $this->footer();
   }
}
?>

最后的例1-18是首页程序index.php代码,也是整个程序的入口,负责实例化对象并调用Controller执行相应的操作。

例1-18 “书籍库存”的首页入口程序

<?
require_once('lib/DataAccess.php');
require_once('lib/BookModel.php');
require_once('lib/BookView.php');
require_once('lib/BookController.php');
$dao = &new DataAccess('数据库服务器地址', '用户名', '密码', '数据库名');
$BookModel = &new BookModel($dao);
$BookController = &new BookController($BookModel, $_GET);
echo $BookController->display();
?>

“书籍库存”的执行结果如图1-17所示。

图1-17 “书籍库存”的执行结果

单击“后一页”链接的结果如图1-18所示,注意浏览器地址栏的URL:index.php?view=table&rownum=4。

图1-18 翻页后的效果

单击某一本书书名链接的结果如图1-19所示,同样注意地址栏的URL:index.php?view=book&id=2。

图1-19 查看某一本书的详细信息

1.3 PHP与正则表达式

正则表达式(Regular Expression),也常被简称为“正则”,这一概念最早由美国数学家Stephen Kleene于1956年在论文《神经网事件的表示法》中引入,用来描述他称为“正则集的代数”的表达式,因此采用“正则表达式”这个术语。后来UNIX的主要发明人Ken Thompson将其用于UNIX系统中的文本实用程序:sed和grep。在最近的半个世纪中,正则表达式逐渐从模糊深奥的数学概念发展为在各类工具和软件包中应用的主要功能。尽管数十年来很多UNIX工具都支持正则表达式,但仅仅是近十年来,它才在大部分Windows开发者工具包中得到体现。

1.3.1 正则表达式基础知识

正则表达式究竟是什么呢?正则表达式是一种语言,它可以明确描述文本字符串中的模式。除了简单描述这些模式之外,正则表达式引擎通常可用于遍历匹配,并使用模式作为分隔符来将字符串解析为子字符串,或以智能方式替换文本或重新设置文本格式。正则表达式为解决与文本处理有关的许多常见任务提供了有效而简捷的方式。在讨论正则表达式时,通常以正则表达式匹配(或不匹配)的文本为基础分析正则表达式。

基于文本的编辑器和搜索工具中的一个重要部分,正则表达式可以让用户通过使用一系列的特殊字符构建匹配模式,然后把匹配模式与数据文件、程序输入以及Web页面的表单输入等目标对象进行比较,根据比较对象中是否包含匹配模式,从而执行相应的程序。

简单表达式

最简单的正则表达式即文字字符串。特定的字符串可通过文字本身加以描述;像“foo”这样的正则表达式模式可精确匹配输入的字符串foo。在本例中,也将匹配如下输入:“The food was quite tasty”,如果希望精确匹配,这可能不是预期的结果。

当然,使用正则表达式匹配等于它自身的精确字符串是没有价值的实现,不能体现正则表达式的真正作用。假如不查找foo,而是查找以字母f开头的所有单词,或所有3个字母的单词,那该怎么办?目前,这超出了文字字符串的合理范围。我们需要更加深入地研究正则表达式。表1-2是一个文字表达式示例及一些匹配的输入。

表1-2 文字表达式示例和匹配的输入

限定符

限定符提供了一种简单方法,用于指定在模式中允许特定字符或字符集自身重复出现的次数。有3个非显式限定符:

1.*,描述“出现0或多次”。

2.+,描述“出现1或多次”。

3.?,描述“出现0或1次”。

限定符始终引用限定符前(左边)的模式,通常是单个字符,除非使用括号创建模式组。

表1-3是一些模式示例及匹配的输入。

表1-3 限定符示例和匹配的输入

除了指定给定模式准确出现0或1次之外,“?”限定符还可强制模式或子模式匹配数目最少的字符(如果匹配输入字符串中的多个字符)。

除了非显式限定符(一般叫做限定符,但为区别于下一组,所以称非显式限定符)之外,还有显式限定符。在模式出现次数方面,限定符的概念非常模糊。使用显式限定符则可准确指定数字、范围或数字集。显式限定符位于所应用的模式的后边,这一点与正则限定符一样。显式限定符使用花括号“{}”及其中的数字值表示模式出现次数的上下限。例如,“x{5}”将准确匹配5个“x”字符“xxxxx”,如果仅指定一个数字,则表示次数上限;如果数字后跟一个逗号,如“x{5,}”,表示匹配任何出现次数大于4的“x”字符。表1-4是一些模式示例及匹配的输入。

表1-4 显式限定符示例和匹配的输入

元字符

在正则表达式中,有一种意义特殊的构造,即元字符。目前已知的元字符有很多,如“*”、“?”、“+”和“{}”字符。其他字符在正则表达式语言中都有特殊的含义,这些字符包括:“$”、“^”、“.”、“[]”、“()”、“|”和“\”。

● “.”(句点或点)元字符是最简单但最常用的一个字符。它可匹配任何单字符,如果要指定某些模式可包含任意组合的字符,使用句点非常有用,但一定要在特定长度范围内。此外,我们知道表达式将对包含在较长字符串中的所有模式进行匹配,假如只需要精确匹配模式,又该怎么办?这在验证方案中经常出现,例如,要确保用户输入的邮政编码或电话号码的格式正确。使用“^”元字符可指定字符串(或行)的开始,使用“$”元字符可指定字符串(或行)的结束。通过将这些字符添加到模式的开始和结束处,可强制模式仅匹配精确匹配的输入字符串,如果“^”元字符用在方括号“[]”指定的字符类的开头,也有特殊的含义。

● “\”(反斜杠)元字符既可根据特殊含义“转义”字符,也可指定预定义集合元字符的实例。为了在正则表达式中包括文字样式的元字符,必须使用反斜杠进行“转义”。例如,如果要匹配以“c:\”开始的字符串,可使用“^c:\\”。注意,要使用“^”元字符指出字符串必须以此模式作为开始,然后用反斜杠元字符转义文字反斜杠。

● “|”元字符用于交替指定,特别用于在模式中指定“此或彼”。例如,“a|b”将匹配包含“a”或“b”的任何输入内容,这与字符类“[ab]”非常类似。

括号“()”用于给模式分组。它允许使用限定符让一个完整模式出现多次。为了便于阅读,或分开匹配特定的输入部分,可能允许分析或重新设置格式。

表1-5是一些模式示例及匹配的输入。

表1-5 元字符示例和匹配的输入

字符类

字符类是正则表达式中的“迷你”语言,在方括号“[]”中定义。最简单的字符类只不过是括号中的一个字符表,如“[aeiou]”。在表达式中使用字符类时,可在模式的此位置使用其中任何一个字符(但只能使用一个字符,除非使用了限定符)。请注意,不能使用字符类定义单词或模式,只能定义单个字符。

要指定任何数值数字,可以使用字符类“[0123456789]”。但是,由于这样使用字符不大方便,所以要通过在括号中使用连字符“-”来定义字符的范围。连字符在字符类中有特殊的含义(不是在正则表达式中,因此,准确地说它不能叫正则表达式元字符),且仅在连字符不是第一个字符时,连字符才在字符类中有特殊含义。要使用连字符指定任何数值数字,可以使用“[0-9]”。小写字母也一样,可以使用“[a-z]”,大写字母可以使用“[A-Z]”。连字符定义的范围取决于使用的字符集。因此,字符在(例如)ASCII或Unicode表中出现的顺序确定了在范围中包括的字符,如果需要在范围中包括连字符,将它指定为第一个字符。另请注意,正则表达式元字符在字符类中不做特殊处理,所以这些元字符不需要转义。考虑到字符类是与其他正则表达式语言分开的一种语言,因此字符类有自己的规则和语法。

如果使用字符“^”作为字符类的第一个字符来否定此类,也可以匹配字符类成员以外的任何字符。因此,要匹配任何非元字符,可以使用字符类“[^aAeEiIoOuU]”。注意,如果要否定连字符,应将连字符作为字符类的第二个字符,如“[^-]”。“^”在字符类中的作用与它在正则表达式模式中的作用完全不同。表1-6是一些模式示例及匹配的输入。

表1-6 字符类示例和匹配的输入

预定义的集合元字符

使用以上的模式可以完成很多工作。但是,要使用“[0-9]”表示模式中的每个数值数字,或使用“[0-9a-zA-Z]”表示任何字母数字字符,还有一段相当漫长的过程。为了减轻处理这些常用但冗长模式的痛苦,事先定义了预定义元字符集合。正则表达式的不同实现定义了不同的预定义元字符集合,这些预定义元字符的标准语法是,在反斜杠“\”后跟一个或多个字符。多数预定义元字符只有一个字符,它们的使用很容易,是冗长字符类的理想替代字符。以下是两个示例:“\d”匹配所有数值数字,“\w”匹配所有单词字符(字母数字加下划线)。例外情况是一些特定字符代码匹配,此时必须指定所匹配字符的地址,如“\u000D”将匹配Unicode回车符。表1-7是一些最常用的字符类及其等效的元字符示例及匹配的输入。

表1-7 常用的字符类及其等效的元字符示例及匹配的输入

1.3.2 在PHP中使用正则表达式

PHP支持两种风格的正则表达式语法:POSIX和Perl,可以通过编译PCRE(Perl Compatible Regular Expression,Perl兼容正则表达式)库来使用Perl风格。Perl风格的正则表达式包括非贪婪模式匹配、断言、条件子模式以及其他许多POSIX风格正则表达式语法所不支持的特性。

在PHP中可用于POSIX风格的正则表达式的函数如表1-8所示。

表1-8 PHP中可用于POSIX风格正则表达式的函数

可用于Perl风格的正则表达式的相关函数如表1-9所示。

表1-9 Perl风格的正则表达式可以使用的相关函数

量词Quantifier

量词可以指定某个特定模式出现的次数。当指定某个模式应当出现的次数时,可以指定硬性数量(如某个字符应该出现三次),也可以指定软性数量(如某个字符至少应该出现一次,可以重复任意次)。

简单量词及其含义如表1-10所示。

表1-10 简单量词及其含义

除了简单量词外,还有三种量词:贪婪的、惰性的和支配性的。

贪婪量词先测试整个字符串是否一个匹配;如果没有发现匹配,将去掉字符串中的最后一个字符,并再次尝试,如果仍然没有发现匹配,那么将再次去掉最后一个字符,此过程会一直重复直到发现一个匹配或字符串不包括任何字符为止。

惰性量词先测试字符串的第一个字符是否一个匹配;如果单独一个字符不够,则读入下一个字符,组成两个字符的字符串,如果还是没有发现匹配,则继续向字符串中添加字符直到发现一个匹配或整个字符串都测试完毕也没有匹配。惰性量词与贪婪量词的工作方式正好相反。

支配量词只尝试匹配整个字符串,如果整个字符串不能产生匹配则不做进一步测试。

表1-11列出了以上三种量词及其含义。

表1-11 贪婪量词、惰性量词和支配量词及其含义

子模式Sub-Pattern

子模式由圆括号定界,可以嵌套。将模式中的一部分标记为子模式可以:

1.将多选一的分支局部化。

2.将子模式设定为捕获子模式(如同以前定义的)。当整个模式匹配时,目标字符串中匹配了子模式的部分会通过pcre_exec()的ovector参数传递回调用者。左圆括号从左到右计数(从1开始)以取得捕获子模式的数目。

事实上,圆括号并不是总能起到我们期望的作用。在没有捕捉要求的情况下,很多时候需要用到队列子模式,如果一个左圆括号后面跟着问号的话,子模式就不会执行捕捉,而且也不会计算在后续的捕捉子模式的数量。例如,“the white queen”匹配格式“((?:red|white)(king|queen))”捕捉到的子字符串就是“white queen”和“queen”,并被编号为1和2。可捕捉的子字符串最多可以编号到99,所有包含可捕捉的和不可捕捉的子模式最多可以编号到200。

作为一种便捷的速记,如果在不可捕捉子模式的开始要求提供一些可选项,则可选项可能会出现在“?”和“:”之间。例如“(?i:saturday|sunday)”和“(?:(?i)saturday|sunday)”匹配了相同的字符串。因为选择支很难从左改到右,而且选项直到子模式实现之后才会重置。在一个支干里面的选项设置不会影响到子模式的分支,所以以上的格式匹配“saturday”,也匹配“sunday”。

重复Repetition

重复是被数量词所指定的,后面能接续以下任何条目:

● 一个单独的字符,包括转义字符。

● 元字符“.”。

● 一个字符类。

● 一个逆向引用。

● 一个带括号的子模式(除去断言以外)。

一般的重复数量词通过大括号里的两个用逗号隔开的数字,来指定匹配的对象的最小和最大值。数字必须小于65536,并且第一位必须小于或者等于第二位数字。例如,“z{2,4}”匹配了“zz”、“zzz”或“zzzz”。其本身的终止大括号不是一个特殊字符,如果第二位数被省略但存在逗号,则表明此数量词没有上限;如果第二位数和逗号都被省略的话,数量词则指明了确切的匹配次数。因此,“[aeiou]{3,}”匹配至少3个连续的元音,但当“\d{8}”匹配恰好8个数字的时候,则有可能匹配更多的。单独的左大括号出现在不能使用数量词或者不匹配数量词语法的场合,这种情况下被作为文本字符看待。

为简便起见(也是为了保持兼容性),以下三种数量词有单字符缩写的写法:

● {0,}等价于*。

● {1,}等价于+。

● {0,1}等价于?。

使用无上限数量词导致无匹配结果的子模式可能会产生一个无限循环,例如“(a?)*”。早期版本的Perl和PCRE在执行这种模式时会报错,但是因为使用的场合越来越多,这种模式现在也被接受,不过当子模式的重复导致事实上无法匹配任何字符时,循环将被强制终止。

默认情况下,数量词是“贪婪的”,这意味着将匹配尽可能多的内容(直到达到被允许的次数上限)而使得模式剩下的部分匹配失败。这种方式出现问题的一个传统的例子就是C语言程序中的注释内容。这些注释内容出现在“/*”和“*/”之间,在注释内容中也可能会出现独立的“*”或“/”。当使用模式“/\*.*\*/”来匹配以下字符串中的C语言注释内容将会失败:

/* first command */ not comment /* second comment */

这是因为“.*”的贪婪性导致匹配了整个字符串。

然而,当一个数量词后紧跟一个问号则会阻止其贪婪性,并代之以匹配可能次数的最小值,因此,“/\*.*?\*/”能够正确匹配C语言的注释内容。这些数量词代表的含义不是被改变的内容,而是希望匹配的数量。由于其有两种用法,有时它将会连续出现两次,例如“\d??\d”将优先匹配一个数字,但当这是剩余模式匹配中的唯一方式时其也可以匹配两个数字。

当选项“PCRE_UNGREEDY”(该选项在Perl中不可用)被开启时,数量词将默认不具有“贪婪性”,但是也可以在单独的数量词后紧跟一个问号“?”使其具有“贪婪性”。换言之,它改变了数量词的默认行为。

量词后紧跟一个加号“+”表明此量词具有“侵占性”,它“吃掉”了尽可能多的字符但并不返回剩下的模式中匹配的部分。因此,“.*abc”匹配“aabc”,但“.*+abc”并不匹配,因为“.*+”“吃掉”了整个字符串。

当一个括号子模式被一个大于1或存在最大值上限的最小重复值所量化后,此编译模式将需要更多的存储空间,其大小与这个最大值或最小值的大小成比例。

如果一个模式以“.*”或“.{0,}”开始并且选项“PCRE_DOTALL”(等价于Perl中的“/s”参数)被开启,则意味着允许“.”匹配新行,同时此模式将被隐式标记。由于接下来的任何内容将对子字符串中每一个字符位置进行比对,则在第一个位置以后在任何位置重试全体匹配将没有任何意义。

当一个捕捉子模式重复出现时,捕捉到的值是最后一次出现时匹配的子串。例如“(tweedle[dume]{3}\s*)+”在匹配“tweedledum tweedledee”时捕捉到的子串是“tweedledee”。但是,当存在嵌套的捕捉子模式时,捕捉到的结果将是之前出现的子模式匹配的值。例如“/(a|(b))+/”在匹配“aba”时,第二次捕捉的子串是“b”。

逆向引用Back References

在一个字符类之外,如果先前已有多个未闭合的左括号,则在反斜线“\”后紧跟一个大于0的数字(或更多的数字)表示在此模式中对于之前的捕捉子模式的逆向引用。

但是,如果斜线后的十进制数字小于10,也被认为是一个逆向引用,并在当整个模式中没有足够多的未闭合左括号时会导致错误。换句话说,在数字小于10时,被参考的括号不必完全闭合至整个引用的左侧。

一个逆向引用在当前字符串中匹配任何实际上匹配到的捕捉子模式,而不是任何匹配子模式的内容本身,因此模式“(sens|respons)e and\1ibility”匹配“sense and sensibility”和“response and responsibility”,不匹配“sense and responsibility”,如果逆向引用中启用了大小写匹配,则字母的大小写与之有关。例如,“((?i)rah)\s+\1”匹配“rah rah”和“RAH RAH”,不匹配“RAH rah”,即使最初的捕捉子模式不匹配大小写。

逆向引用最多能够捕获99个子模式。

断言Assertions

如果一些时候在模式中能做到“若这里是下一个,那么匹配这里”这样的功能是很有用的,这常常被用在拆分字符串中。在这种情况下可以使用前向断言(Lookahead Assertion)来确保在分隔符之后有更多的数据(因为没有匹配,所以阻止被返回)。与此类似,后向断言(Lookbehind Assertion)检查前面的文字。

前向和后向各有两种形式:正的(Positive)和负的(Negative)。正的前/后向表示“下一个/前面的文本必须如此”,负的前/后向则表示“下一个/前面的文本必须不是这样”。表1-12列出了在Perl风格的正则表达式中可以使用的4种断言结构。

表1-12 Perl风格的正则表达式中可以使用的前向和后向断言

单词From作为一行的起始来指明一条新消息的开始,因此可以通过在文本中查找From的位置将mailbox拆分为消息:

$messages = preg_split('/(?=^From)/m', $mailbox);

负后向的一个例子是过滤出引号中的文本。例如:

$string = <<<END
statement = 'Nothing is easier than to deceive one\'s self'.
END;
$pattern = <<<END
'
(
.*?
(?<! \\\\)
)
'
END;
preg_match("($pattern)x", $string, $match);
var_dump($match);

在Perl中限制了后向断言只能用于固定长度的表达式上,即表达式不能包含量词,并且若使用选择符“|”,则所有的选择都必须是相同长度。Perl风格的正则表达式也禁止在后向断言中使用量词,但是允许不同长度的选择。

条件子模式Conditional Sub-Patterns

条件子模式也称为条件表达式(Conditional Expressions)。它根据一个断言的结果或一个之前的捕捉子模式是否匹配,使得一个匹配过程有条件地遵循一个子模式或从一个二选一的子模式中进行选择。条件子模式的两种可能形式为:

(?(condition)yes-pattern)
(?(condition)yes-pattern|no-pattern)

如果条件condition被满足,则使用yes-pattern模式,否则使用no-pattern模式(如果给出)。若在子模式中出现超过两个可选项,将导致编译期错误。

条件condition可以是以下其中的一种:逆向引用或前向/后向匹配(Lookahead and Lookbehind Match)。要引用一个之前匹配的字符串,要求condition是从1到99中的数字。只有逆向引用被匹配时,条件子模式才能使用其中的模式,如果condition不是逆向引用,则它必须是正或负的前向或后向断言。

如果在一对小括号中的字符是一系列数字,则当此数字表示的捕捉子模式在先前被匹配时条件被满足。以下模式可以被分为三个部分:

( \( )? [^()]+ (?(1) \) )

第一部分匹配一个可选的左小括号,如果这个左小括号出现,则被设置为第一个被捕捉到的子串。第二部分匹配了一个或多个非括号的字符。第三部分是一个条件子模式测试了第一对括号是否匹配,如果被匹配字符串以一个左小括号开始则条件成立并且执行yes-pattern模式,同时需要一个右小括号来关闭它。

条件子模式是一种正的前向断言,匹配一个可选的单字母后的非字母序列。换句话说,它测试被匹配字符串中是否存在至少一个字母,如果存在一个字母,字符串继续匹配第一个可选项,其他情况则匹配第二个可选项。

1.4 边学边练:简易网络聊天室

话说有一天,冯大刚导演和葛优秀大爷两位故交在网上一个聊天室里相遇了,老朋友偶遇,那肯定要好好聊聊啦。聊天界面如图1-20所示。

图1-20 冯导与葛大爷相遇在聊天室

(以下略去二位的聊天内容若干字……)

为了能让冯导和葛大爷在这个简易的聊天室里多待一会儿,聊天室的开发者提供了以下功能:

1.使用数据库保存用户名、密码、聊天记录等信息。

2.使用AJAX方式刷新页面,更新用户聊天内容。

3.使用正则表达式对聊天内容进行敏感词语过滤。

4.以SOAP方式对外提供WebService,站外用户通过验证身份(使用系统聊天用户的账号密码)后可以调用API。API包括:验证身份、发消息、获取最新10条聊天记录、查看在线用户列表等。

5.整个程序使用MVC模式。

除了第4点功能冯导和葛大爷无法直接使用以外,他们二位在登录进这个聊天室后就已经在使用其他各项功能了。那么这个简易的聊天室是怎么实现的呢?

对于这样一个简易的聊天室,其数据库包含members和messages两个表,表结构如表1-13和表1-14所示。

表1-13 表members的结构

表1-14 表messages的结构

设计好数据库后,使用前面介绍的MVC模式构建整个系统框架,该聊天室的目录和文件结构如表1-15所示。

表1-15 聊天室系统的目录和文件结构

接下来了解一下用户类的程序,其定义于module/member.php中,获取在线用户列表、注册、登录和注销等操作的代码如下:

<?php
class member {
   ……
/**
 * 获取在线用户列表
 */
public function getOnlineUsers() {
       //从数据库中取得所有在线用户(即用户状态status列为1的数据)
$rs = base::query ( "SELECT * FROM 'members' WHERE 'status'=1" );
$users = array ();
while ( $_ = base::fetch ( $rs ) ) {
       $users [] = $_;
}
return $users;
}
/**
 * 登录
 */
public function login($email, $password) {
if ($_ = $this->authlogin ( $email, $password )) {
       //用户登录成功后更新Session和数据库信息
       $_SESSION ['uid'] = $this->uid = $_ ['uid'];
       base::query ( "UPDATE 'members' SET 'status'=1,)
       'lastlogin'='" .TIME ."' WHERE 'uid'='$_[uid]'" );
       return true;
} else {
       return false;
}
}
/**
 * 注销
 */
public function logout() {
       //用户注销后清除Session和数据库信息
       unset ( $_SESSION ['uid'] );
       if ($this->uid) {
              base::query ( "UPDATE 'members' SET 'status'=0 WHERE 'uid'='{$this->uid}'" );
              $this->uid = 0;
       }
}
/**
 * 注册
 */
public function register($user) {
       //验证输入数据合法性
       $error = array ();
       preg_match ( '/^([\.a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$/', $user ['email'] ) || $error [] = '邮箱格式错误!';
       mysql_num_rows ( base::query ( "SELECT uid FROM 'members' WHERE'email'='$user[email]'" ) ) == 0 || $error [] = '该E-Mail地址已经存在 ';
       preg_match ( '/^.{2,30}$/', $user ['nickname'] ) || $error [] ='用户长度为2到20位!';
       preg_match ( '/^.{6,20}$/', $user ['password'] ) || $error [] ='密码长度为6到20位!';
       $user ['password'] == $user ['checkpass'] || $error [] = '两次输入密码不一致!';
       if (count ( $error )) {
              return $error;
       } else {
              $password = md5 ( $user ['password'] );
              //向数据库插入一条新的数据,写入用户信息
              base::query ( "INSERT INTO 'members'('email','nickname',' password') VALUES('$user[email]','$user[nickname]','$password')" );
              $this->login ( $user ['email'], $user ['password'] );
              return true;
       }
       }
}
?>

对于敏感词语的过滤,主要通过module/message.php中的post()方法完成,其代码如下:

public function post($msg) {
   $uid = member::instance ()->getUid ();
   $msg = trim ( $msg );
   /**
    * 过滤不良词汇
    */
   $censors = trim ( file_get_contents ( 'censors.txt' ) );
   $censors = str_replace ( "\r\n", "\n", $censors );
   $censors = explode ( "\n", $censors );
   array_filter ( $censors );
   array_walk ( $censors, 'preg_quote' );
   $censors = join ( '|', $censors );
   $msg = preg_replace ( "/($censors)/", "****", $msg );
   if (strlen ( $msg )) {
          $msg = str_replace ( "\n", "\n<br/>", $msg );
          base::query ( "INSERT INTO 'messages'('uid','message','datelin e') VALUE('$uid','$msg','" .TIME ."')" );
          return true;
   }
   return false;
   }