Zend Framework
从理论上来说,PHP是一种通用的动态语言,它可以替代Perl实现通用的脚本,甚至可以创建客户端GUI程序(通过GTK+)。但是,在实际应用中,PHP在绝大多数情况下都被用来开发Web应用。即使在Java和.NET这样有软件业界巨头支持的重量级竞争对手面前,出身草莽的PHP也毫不逊色,尤其是在应用最为广泛的轻量级web开发领域,PHP一直牢牢占据着领先的位置。在这一领域参与竞争的其他语言,例如Python和Perl,虽然各具特色,但是仍然无法撼动PHP的地位。PHP是当之无愧的“Web开发第一语言”,而且似乎没有什么能对它构成挑战。随着web应用在软件界的地位越来越重要,PHP也逐渐从脚本小子手中的玩具,转变成重要的工业语言,并获得了IBM这样的巨人的支持。
但是,就在近两三年,一种新的Web开发解决方案的迅速崛起,震动了整个业界,让PHP开始感到王座不稳。这个解决方案,当然就是Ruby on Rails。
本文无意对PHP和Ruby这两种语言进行全面的比较(虽然这的确是一个非常有意思的话题)。无论读者对Ruby和ROR持什么看法,有一点却是不争的事实:Ruby作为一种新的语言,虽然极具特色,但是并没有得到业界的普遍接受;只有在ROR诞生以后,以其惊人的生产力征服了无数开发者,Ruby这才一飞冲天。也就是说,Ruby至于在很大程度上是依靠ROR这个框架,才能起迅速崛起。
对于感到严重威胁的PHP社区,如果要对Ruby进行有效的反击,这似乎是一条可行的道路:基于PHP语言,实现一个新的web开发框架,能够达到甚至超过ROR那样的生产力。这既是对Ruby的反击,其实也是PHP自身发展的必然结果。因为PHP在其发展过程中,早已经出现了数十个各种各样的框架,只是尚没有一个能具有ROR那样的生产力和影响力。
但是,实现这样一个框架,也面临着不少的问题:
 PHP语言本身能否完成这一任务?PHP原本只是用于快速解决web应用的简单脚本,虽然经过不断的发展,已经具有了许多高级的语言特征,但是是否能够实现象ROR那样精巧和强大的框架,能够达到ROR那样的生产力?
 PHP社群能否接受这样一个框架?PHP的使用者,大多数是脚本小子出身,习惯于快速、敏捷、直观的开发方式,对于重量级的框架,未必能够普遍认同。
无论这些问题的答案如何,重要的是,有人这么做了。Zend Framework就是这一思路的结果。
如上所述,Zend Framework就是这样一个完全基于PHP语言的、向ROR学习的、针对web应用开发的框架。与其它同类的框架相比,Zend Framework有两个特点让它显得与众不同:
 Zend Framework由Zend公司开发,因此它是一个“官方的”框架。众所周知,Zend公司是PHP编译器的维护者,也是Zend Optimizer、Zend Studio等一系列PHP相关产品的拥有者。由于这一关系,Zend Framework虽然没有被内置到PHP发行包中,但也算得上PHP的官方解决方案了。(顺便说句题外话,Zend公司最近发布了Zend Studio for Eclipse的beta版本,有兴趣的读者不妨尝试一下。)
 Zend Framework跟ROR实在太象了。和ROR一样,Zend Framework使用了同样的Front Controller模式,同样的Model/Controller/View模型,同样的ORM思路,甚至连命名规则都十分相似。当然了,Zend Framework跟ROR的不同之处还是很多的,至于底层的实现细节,肯定区别更大。但是,不管是有意还是无意(好吧,我承认“无意”绝对不可能做成这样),Zend Framework的实现思路和ROR是非常相似的。
这是一个典型的Zend Framework应用的目录:
/root
/application
/controllers
/models
/views
/filters
/helpers
/scripts
/library
/public
/images
/scripts
/styles
其中,library目录下存放的是Zend Framework所有的库文件,如果还有其他第三方的库文件,也可以存放在这里。Public目录是唯一一个可以通过web方式直接访问的目录,但它并不存放php文件,只存放CSS、JavaScript脚本、图片等静态文件。Application目录中存放着实现应用逻辑的所有PHP文件,它又拥有三个子目录:controllers、models、views。顾名思义,它们分别用于存放控制器/模型/视图的相应PHP文件。
Zend Framework实现了Front Controller模式,这意味着,所有的http请求都会被转发到同一个入口,然后再路由到相应的控制器。这个入口就是root根目录下的 index.php文件。Zend Framework通过URL Rewrite技术来实现这一点。在root根目录下,存在一个.htaccess文件,内容如下:
RewriteEngine on
RewriteRule .* index.php
php_flag magic_quotes_gpc off
php_flag register_globals off
熟悉apache的URL Rewrite技术的读者,很容易理解它的意义:将所有的请求转发到index.php。当然,对于图片之类的静态文件的请求不需要被转发,这可以通过在 public目录下放置另一个.htaccess来覆盖上级目录的Rewrite规则。
(习惯在IIS下运行PHP的读者可能会问,Zend Framework是否可以在IIS下运行呢?由于IIS不支持基于. htaccess的URL Rewrite,因此Zend Framework无法简单地在IIS下运行。但是,由于IIS支持基于HttpModule的URL Rewrite,因此理论上经过某些修改,是有可能让Zend Framework在IIS下运行的。有兴趣的读者可以自行尝试。)
Index.php的功能是将http请求转发到相关的控制器,另外,它还需要完成一些初始化时期的配置工作。它的内容如下:
<?php
error_reporting(E_ALL|E_STRICT);
date_default_timezone_set('Europe/London');
set_include_path('.' . PATH_SEPARATOR . './library'
. PATH_SEPARATOR . './application/models/'
. PATH_SEPARATOR . get_include_path());
include "Zend/Loader.php";
Zend_Loader::loadClass('Zend_Controller_Front');
// setup controller
$frontController = Zend_Controller_Front::getInstance();
$frontController->throwExceptions(true);
$frontController->setControllerDirectory('./application/controllers');
// run!
$frontController->dispatch();
?>
开始的几行,分别用于配置报错等级、设置时区、设置包含文件路径,以及载入相关文件。Zend_Loader::loadClass是一个静态方法,用于装载指定的类。在载入Zend_Controller_Front以后,程序建立了这个类的实例,通知它抛出所有的异常,并且设置了控制器所在的目录。最后,调用dispatch()方法,将http请求转发到相应的控制器。
在application/controllers目录下,存放着所有的控制器类。那么路由器是根据什么规则寻找相应的控制器呢?很简单,它按照一定的命名规范来进行匹配。下面是一个控制器文件的内容:
root/application/controllers/IndexController.php
<?php
class IndexController extends Zend_Controller_Action {
function indexAction() {
echo "<p>in IndexController::indexAction()</p>";
}
function addAction() {
echo "<p>in IndexController::addAction()</p>";
}
function editAction() {
echo "<p>in IndexController::editAction()</p>";
}
function deleteAction() {
echo "<p>in IndexController::deleteAction()</p>";
}
}
所有的控制器类都派生自Zend_Controller_Action。控制器名字必须大写字母开头,其他字母必须都为小写,最后加上 Controller结尾,并且存放在同名的php文件中。本例中的控制器是IndexController。(需要指出的是,“其他字母必须都为小写” 是一条很诡异的规则,这意味着CurrentUserController这样的名字是不合法的,无法被正确地路由。如果你这样做了,会是个很难发现的 bug。)
控制器拥有一个或多个行为(action),体现在代码上,行为是控制器类拥有的public方法,以小写字母开头,以Action结尾。在本例中,indexAction、deleteAction、addAction和editAction是IndexController的四个行为。
在浏览器中,可以通过“控制器/行为”的URL,来访问相应的行为。如上例,URL和行为的对应关系是:
URL Action
http://localhost/root/
IndexController::indexAction()
http://localhost/root/index
IndexController::indexAction()
http://localhost/root/index/add IndexController::addAction()
http://localhost/root/index/edit IndexController::editAction()
http://localhost/root/index/delete IndexController::deleteAction()
可以看到,indexAction是一个控制器的缺省行为:如果没有指定行为,路由器将把请求转发到indexAction方法。同样,IndexController是缺省的控制器:如果没有指定控制器,路由器将把请求转发到IndexController控制器。
在上例中,行为方法直接输出一行代码作为响应。在规范的应用中,与显示相关的代码应该存放在视图中。一个规范的控制器行为方法内容如下:
function indexAction() {
$view = new Zend_View();
$view->setScriptPath('./application/views/scripts’);
$view->title = "Hello world";
echo $view->render();
}
首先,创建了一个视图类(Zend_View)的实例,然后设置其路径,并对它的title属性赋值,最后调用render()方法,渲染相应的模板文件,将结果返回给浏览器。
同样的,视图根据特定的命名规范来匹配相应的模板文件。对于IndexController::indexAction()方法,视图会查找如下模板文件:root/application/views/scripts/index/index.phtml。后缀名为.phtml的模板文件事实上就是一个标准的PHP文件。(熟悉.rhtml后缀名的读者有何感觉?)在本例中,index.phtml的内容如下:
<html>
<head>
<title><?php echo $this->escape($this->title); ?></title>
</head>
<body>
<h1><?php echo $this->escape($this->title); ?></h1>
</body>
</html>
最后,我们终于又一次看到了亲切的“Hello world”。
Zend Framework使用Zend_Db_Table来实现类似于ActiveRecord的ORM功能。和任何ORM一样,首先需要为它配置数据库的相关信息(服务器,用户名,密码,数据库名,等等)。虽然这并不复杂,但本文不拟讨论这些细节问题。(Zend Framework在它的Roadmap中计划支持YAML!)一旦配置完成后,可以在/application/models子目录下创建和数据库表相对应的模型类。
举例来说,在数据库中存在表user,结构如下:
CREATE TABLE user (
id int(11) NOT NULL auto_increment,
name varchar(100) NOT NULL,
password varchar(100) NOT NULL,
PRIMARY KEY (id)
)
相应地,可以在models目录下创建一个模型:
root/application/models/User.php
<?php
class User extends Zend_Db_Table
{
protected $_name = 'user';
}
所有数据库相关的代码,都已经被封装在抽象类Zend_Db_Table中。有ROR经验的读者,可以和ROR中的模型对比一下:
Class User < ActiveRecord::Base
End
Zend Framework的模型仅仅多出了一行,就是把数据库表名赋予$_name属性。
下面来看看怎么使用这个User模型类。新的indexAction方法如下:
function indexAction() {
…
$user = new User();
$view->user = $user->fetchAll();
echo $view->render();
}
User类的fetchAll()返回一个数组,包含该表中所有的内容。这个数组被存放在$view的user属性。接下来,看看新的index.phtml:
…
<table>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
<?php foreach($this->user as $user) : ?>
<tr>
<td><?php echo $this->escape($user->id);?></td>
<td><?php echo $this->escape($user->name);?></td>
</tr>
<?php endforeach; ?>
</table>
…
这段代码遍历了$this->user,并将它的属性依次打印出来。
除了基本的MVC模型以外,Zend Framework还提供了一系列高级功能,下面是一个不全面的列表:
 Zend_Acl(access control list):实现了非常灵活的权限控制机制,通过对特定的角色和资源进行规则定义,可以方便地实现各种复杂的权限控制规则,并且权限可以按照角色的树型结构实现继承。
 Zend_Cache:提供了一种通用的数据缓存方式,可以将任何数据缓存到文件系统、数据库、内存,能够与Memcached和SQLite这样的第三方软件很好地集成。
 Zend_Log:提供了一种通用的log解决方案,支持多个log后端,支持log信息格式化,支持信息过滤。
 Zend_Pdf:完全用PHP语言实现的PDF文件操作引擎。
 Zend_Feed:封装了对RSS和ATOM的操作。
 Zend_Gdata:封装了对Google Data API的操作。
 Zend_Json:JSON是AJAX中必不可少的数据格式。Zend_Json封装了数据在PHP和JSON格式之间的转换操作。
 Zend_Rest:REST是越来越流行的Web Service协议,有逐渐凌驾传统的SOAP协议之势。Zend_Rest封装了对REST的操作,可以方便地实现REST的客户端或服务端应用。
 Zend_Search_Lucene:封装了对著名的全文检索引擎Lucene的操作。
Zend Framework在7月份发布1.0正式版,现在最新的版本为1.0.2。可以说,Zend Framework还是一个非常新的事物。但是在它漫长的测试期间,已经有不少人在尝试,甚至用于正式的项目,因为它的种种功能实在太让人馋涎欲滴了。
回到本文开头的两个问题,Zend Framework的前途将会如何呢?虽然作者深知揣测未来的风险,但是还是很愿意尝试回答这两个问题:第一,Zend Framework的出现,已经证明了PHP 5有能力实现大型的、复杂的框架,而由于PHP的编译器掌握在Zend公司手中,能够保证PHP语言会持续发展,以满足进一步的需求。第二,PHP本来就已经在逐步进入企业开发的领域,出现一个重量级的Framework是必然的事情。也许PHP未来的开发主力,将由经过短期培训的、偏向快速解决问题的初级程序员,变为经验丰富的、熟悉各种复杂的IT平台、侧重应用的安全性和健壮性的资深工程师。毕竟,只要经过不懈的努力,脚本小子也可能有成为大师的一天,对吗?
至于Zend Framework和ROR之间的竞争,现在没有人能够给出答案。也许到目前为止,Zend Framework还无法达到ROR 100%的生产力,但是它拥有更广大的用户基础,更成熟的语言,更短的学习曲线,类似C和Java的语法也更让人容易掌握。ROR中的DRY、“惯例重于配置”等原则,在Zend Framework中也得了很好的秉承和发扬。无论它是否能够有效地狙击ROR,毫无疑问的是,一旦Zend Framework被PHP社群广泛接受,PHP的生产力将会有极大的提高。